Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/20260903010000-drop-agent-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@truefoundry/trueforge": patch
---

Drop unused `agent.metadata`; remote identity is stored in `external_id`.
7 changes: 2 additions & 5 deletions packages/trueforge/src/db/agentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
* Implementations: PostgresAgentStore and SqliteAgentStore.
*/
import { AgentSpecSchema, type AgentSpec } from '@truefoundry/trueforge-core/agent-session';
import type { AgentMetadata } from '../schemas/agentMetadata';
import type { ResourceName } from '../schemas/common';

export interface AgentRecord {
id: string;
tenant_id: string;
name: ResourceName;
manifest: AgentSpec;
metadata: AgentMetadata;
external_id: string | null;
/** ISO-8601 UTC instant. */
created_at: string;
Expand All @@ -40,14 +38,13 @@ export interface CreateAgentInput {
}

/**
* Patch an existing agent by immutable id. At least one of `manifest`, `metadata`, or `external_id` is required.
* Patch an existing agent by immutable id. At least one of `manifest` or `external_id` is required.
* Provided fields replace the stored column; omitted fields are left unchanged.
*/
export interface UpdateAgentInput {
tenant_id: string;
id: string;
manifest?: AgentSpec;
metadata?: AgentMetadata;
external_id?: string | null;
}

Expand Down Expand Up @@ -87,7 +84,7 @@ export interface IAgentStore<TTransaction = never> {
getAgent(input: GetAgentInput, transaction?: TTransaction): Promise<AgentRecord | undefined>;
/** Inserts a new agent with a generated ULID. Throws AgentNameConflictError or AgentExternalIdConflictError on unique clash. */
createAgent(input: CreateAgentInput, transaction?: TTransaction): Promise<AgentRecord>;
/** Patches `manifest`, `metadata`, and/or `external_id`. Throws AgentExternalIdConflictError on unique clash. Returns undefined if missing. */
/** Patches `manifest` and/or `external_id`. Throws AgentExternalIdConflictError on unique clash. Returns undefined if missing. */
updateAgent(input: UpdateAgentInput, transaction?: TTransaction): Promise<AgentRecord | undefined>;
/** Deletes by immutable id. Idempotent if already missing. */
deleteAgent(input: DeleteAgentInput, transaction?: TTransaction): Promise<void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Kysely, Selectable, Transaction } from 'kysely';
import { EMPTY_AGENT_METADATA } from '../../../schemas/agentMetadata';
import { newId } from '../../../utils/id';
import {
AgentExternalIdConflictError,
Expand All @@ -23,7 +22,6 @@ function toRecord(row: Selectable<AgentTable>): AgentRecord {
tenant_id: row.tenant_id,
name: row.name,
manifest: parseStoredAgentSpec(row.manifest),
metadata: row.metadata,
external_id: row.external_id,
created_at: row.created_at.toISOString(),
updated_at: row.updated_at.toISOString(),
Expand Down Expand Up @@ -83,7 +81,6 @@ export class PostgresAgentStore implements IAgentStore<Transaction<Database>> {
tenant_id: input.tenant_id,
name: input.name,
manifest: json(input.manifest),
metadata: json(EMPTY_AGENT_METADATA),
external_id: input.external_id,
created_at: now(),
updated_at: now(),
Expand All @@ -105,16 +102,15 @@ export class PostgresAgentStore implements IAgentStore<Transaction<Database>> {
}

async updateAgent(input: UpdateAgentInput, transaction?: Transaction<Database>): Promise<AgentRecord | undefined> {
if (input.manifest === undefined && input.metadata === undefined && input.external_id === undefined) {
throw new Error('updateAgent requires manifest, metadata, and/or external_id');
if (input.manifest === undefined && input.external_id === undefined) {
throw new Error('updateAgent requires manifest and/or external_id');
}
const db = transaction ?? this.#db;
try {
const row = await db
.updateTable('agent')
.set({
...(input.manifest === undefined ? {} : { manifest: json(input.manifest) }),
...(input.metadata === undefined ? {} : { metadata: json(input.metadata) }),
...(input.external_id === undefined ? {} : { external_id: input.external_id }),
updated_at: now(),
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { sql, type Kysely } from 'kysely';

/**
* Drop agent.metadata — identity lives in external_id; column was never on the public Agent API.
*/
export async function up(db: Kysely<unknown>): Promise<void> {
await sql`SET LOCAL lock_timeout = '5s'`.execute(db);
await sql`ALTER TABLE agent DROP COLUMN IF EXISTS metadata`.execute(db);
}

export async function down(db: Kysely<unknown>): Promise<void> {
await sql`SET LOCAL lock_timeout = '5s'`.execute(db);
await sql`
ALTER TABLE agent
ADD COLUMN metadata jsonb NOT NULL DEFAULT '{}'::jsonb
`.execute(db);
}
3 changes: 0 additions & 3 deletions packages/trueforge/src/db/postgres/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import type {
} from '@truefoundry/trueforge-core/core';
import type { CurrentContextUsage } from '@truefoundry/trueforge-core/core/runtime/contextUsage';
import type { ColumnType, Generated, JSONColumnType } from 'kysely';
import type { AgentMetadata } from '../../schemas/agentMetadata';
import type { McpServerManifest } from '../../schemas/mcpServer';
import type { ModelProviderManifest } from '../../schemas/modelProvider';
import type { SandboxBuildMetadata, SandboxBuildStatus, SandboxProviderManifest } from '../../schemas/sandboxProvider';
Expand Down Expand Up @@ -376,8 +375,6 @@ export interface AgentTable {
name: string;
/** AgentSpec document; replaced whole on every upsert */
manifest: JSONColumnType<AgentSpec, AgentSpec, AgentSpec>;
/** `agent.metadata` jsonb; default `{}` for existing rows */
metadata: JSONColumnType<AgentMetadata, AgentMetadata, AgentMetadata>;
external_id: string | null;
created_at: Date;
updated_at: Date;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { AgentSpec } from '@truefoundry/trueforge-core/agent-session';
import type { ExpressionBuilder, Kysely, Transaction } from 'kysely';
import { EMPTY_AGENT_METADATA, type AgentMetadata } from '../../../schemas/agentMetadata';
import { newId } from '../../../utils/id';
import {
AgentExternalIdConflictError,
Expand All @@ -24,7 +23,6 @@ function recordColumns(eb: ExpressionBuilder<Database, 'agent'>) {
'tenant_id' as const,
'name' as const,
jsonText<AgentSpec>(eb.ref('manifest')).as('manifest'),
jsonText<AgentMetadata>(eb.ref('metadata')).as('metadata'),
'external_id' as const,
'created_at' as const,
'updated_at' as const,
Expand All @@ -36,7 +34,6 @@ function toRecord(row: {
tenant_id: string;
name: AgentRecord['name'];
manifest: AgentSpec;
metadata: AgentMetadata;
external_id: string | null;
created_at: string;
updated_at: string;
Expand Down Expand Up @@ -85,7 +82,6 @@ export class SqliteAgentStore implements IAgentStore<Transaction<Database>> {
tenant_id: input.tenant_id,
name: input.name,
manifest: jsonbBind(input.manifest),
metadata: jsonbBind(EMPTY_AGENT_METADATA),
external_id: input.external_id,
created_at: timestamp,
updated_at: timestamp,
Expand All @@ -108,16 +104,15 @@ export class SqliteAgentStore implements IAgentStore<Transaction<Database>> {
}

async updateAgent(input: UpdateAgentInput, transaction?: Transaction<Database>): Promise<AgentRecord | undefined> {
if (input.manifest === undefined && input.metadata === undefined && input.external_id === undefined) {
throw new Error('updateAgent requires manifest, metadata, and/or external_id');
if (input.manifest === undefined && input.external_id === undefined) {
throw new Error('updateAgent requires manifest and/or external_id');
}
const db = transaction ?? this.#db;
try {
const row = await db
.updateTable('agent')
.set({
...(input.manifest === undefined ? {} : { manifest: jsonbBind(input.manifest) }),
...(input.metadata === undefined ? {} : { metadata: jsonbBind(input.metadata) }),
...(input.external_id === undefined ? {} : { external_id: input.external_id }),
updated_at: nowIso(),
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { type Kysely, sql } from 'kysely';
import { AGENT_EXTERNAL_ID_UQ } from '../../indexes';

/**
* Drop agent.metadata. Rebuild: ADD/DROP COLUMN cannot take DEFAULT (jsonb(...)) / STRICT drop.
* Keeps external_id + partial unique index from 20260902_000002.
* DROP TABLE agent needs FKs off (`schedule` REFERENCES it).
* PRAGMA foreign_keys is a no-op inside a txn.
*/
export async function up(db: Kysely<unknown>): Promise<void> {
await sql`PRAGMA foreign_keys = OFF`.execute(db);
try {
await db.transaction().execute(async trx => {
await sql`
CREATE TABLE agent_new (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
manifest BLOB NOT NULL,
external_id TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (id),
UNIQUE (tenant_id, name)
) STRICT
`.execute(trx);

await sql`
INSERT INTO agent_new (
id,
tenant_id,
name,
manifest,
external_id,
created_at,
updated_at
)
SELECT
id,
tenant_id,
name,
manifest,
external_id,
created_at,
updated_at
FROM agent
`.execute(trx);

await sql`DROP TABLE agent`.execute(trx);
await sql`ALTER TABLE agent_new RENAME TO agent`.execute(trx);
await sql`
CREATE UNIQUE INDEX ${sql.raw(AGENT_EXTERNAL_ID_UQ)}
ON agent (tenant_id, external_id)
WHERE external_id IS NOT NULL
`.execute(trx);
});
} finally {
await sql`PRAGMA foreign_keys = ON`.execute(db);
}
}

export async function down(db: Kysely<unknown>): Promise<void> {
await sql`PRAGMA foreign_keys = OFF`.execute(db);
try {
await db.transaction().execute(async trx => {
await sql`DROP INDEX IF EXISTS ${sql.raw(AGENT_EXTERNAL_ID_UQ)}`.execute(trx);
await sql`
CREATE TABLE agent_old (
id TEXT NOT NULL,
tenant_id TEXT NOT NULL,
name TEXT NOT NULL,
manifest BLOB NOT NULL,
metadata BLOB NOT NULL DEFAULT (jsonb('{}')),
external_id TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (id),
UNIQUE (tenant_id, name)
) STRICT
`.execute(trx);

await sql`
INSERT INTO agent_old (
id,
tenant_id,
name,
manifest,
metadata,
external_id,
created_at,
updated_at
)
SELECT
id,
tenant_id,
name,
manifest,
jsonb('{}'),
external_id,
created_at,
updated_at
FROM agent
`.execute(trx);

await sql`DROP TABLE agent`.execute(trx);
await sql`ALTER TABLE agent_old RENAME TO agent`.execute(trx);
await sql`
CREATE UNIQUE INDEX ${sql.raw(AGENT_EXTERNAL_ID_UQ)}
ON agent (tenant_id, external_id)
WHERE external_id IS NOT NULL
`.execute(trx);
});
} finally {
await sql`PRAGMA foreign_keys = ON`.execute(db);
}
}
3 changes: 0 additions & 3 deletions packages/trueforge/src/db/sqlite/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import type {
} from '@truefoundry/trueforge-core/core';
import type { CurrentContextUsage } from '@truefoundry/trueforge-core/core/runtime/contextUsage';
import type { ColumnType, Generated, JSONColumnType } from 'kysely';
import type { AgentMetadata } from '../../schemas/agentMetadata';
import type { McpServerManifest } from '../../schemas/mcpServer';
import type { ModelProviderManifest } from '../../schemas/modelProvider';
import type { SandboxBuildMetadata, SandboxBuildStatus, SandboxProviderManifest } from '../../schemas/sandboxProvider';
Expand Down Expand Up @@ -222,8 +221,6 @@ export interface AgentTable {
name: string;
/** AgentSpec document; replaced whole on every upsert */
manifest: JsonbColumn<AgentSpec>;
/** `agent.metadata` jsonb; default `{}` for existing rows */
metadata: JsonbColumn<AgentMetadata>;
external_id: string | null;
created_at: string;
updated_at: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/trueforge/src/schemas/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const CreateAgentRequestSchema = z
.strict()
.openapi('CreateAgentRequest');

/** PUT body: full manifest replacement only (metadata is store-internal, not on the wire). */
/** PUT body: full manifest replacement only. */
export const UpdateAgentRequestSchema = z
.object({
manifest: AgentSpecSchema,
Expand Down
7 changes: 0 additions & 7 deletions packages/trueforge/src/schemas/agentMetadata.ts

This file was deleted.

47 changes: 1 addition & 46 deletions packages/trueforge/tests/db/agentStoreContractSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void {
expect(created.name).toBe('research');
expect(created.id.length).toBeGreaterThan(0);
expect(created.manifest).toEqual(manifest());
expect(created.metadata).toEqual({});
expect(created.external_id).toBeNull();
expect(created.created_at).toMatch(ISO_UTC);
expect(created.updated_at).toBe(created.created_at);
Expand All @@ -49,7 +48,7 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void {
expect(await store.getAgent({ tenant_id: TENANT, name: 'missing' })).toBeUndefined();
});

it('updateAgent by id replaces manifest but keeps id, name, metadata, and created_at', async () => {
it('updateAgent by id replaces manifest but keeps id, name, and created_at', async () => {
const store = getStore();
const created = await store.createAgent({
tenant_id: TENANT,
Expand All @@ -70,7 +69,6 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void {
id: created.id,
name: 'research',
manifest: replacement,
metadata: {},
created_at: created.created_at,
}),
);
Expand All @@ -83,49 +81,6 @@ export function runAgentStoreContractSuite(getStore: () => IAgentStore): void {
expect(await store.getAgent({ tenant_id: TENANT, name: 'research' })).toEqual(updated);
});

it('updateAgent can patch metadata without changing manifest', async () => {
const store = getStore();
const created = await store.createAgent({
tenant_id: TENANT,
name: 'research',
manifest: manifest(),
external_id: null,
});

const updated = await store.updateAgent({
tenant_id: TENANT,
id: created.id,
metadata: {},
});

expect(updated).toEqual(
expect.objectContaining({
id: created.id,
name: 'research',
manifest: created.manifest,
metadata: {},
created_at: created.created_at,
}),
);
expect(updated).toBeDefined();
if (updated === undefined) {
throw new Error('expected updateAgent to return a record');
}
expect(Date.parse(updated.updated_at)).toBeGreaterThanOrEqual(Date.parse(created.updated_at));
expect(await store.getAgent({ tenant_id: TENANT, id: created.id })).toEqual(updated);
});

it('updateAgent returns undefined for unknown ids when patching metadata', async () => {
const store = getStore();
expect(
await store.updateAgent({
tenant_id: TENANT,
id: 'missing',
metadata: {},
}),
).toBeUndefined();
});

it('updateAgent returns undefined for unknown ids', async () => {
const store = getStore();
expect(
Expand Down
Loading