From 88103c55cdeb97027d85216d534c9c50d576b4ad Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Fri, 18 Sep 2026 13:57:03 +0530 Subject: [PATCH 01/23] add table for session inbound events --- .../trueforge-core/src/agent-session/index.ts | 8 + .../src/agent-session/schemas/sendEvent.ts | 17 ++ .../src/agent-session/store/ISessionStore.ts | 68 ++++++ .../store/InMemorySessionStore.ts | 103 +++++++++ .../agent-session/store/SessionStoreErrors.ts | 12 ++ .../agent-session/store/storeContractSuite.ts | 201 ++++++++++++++++++ .../20260918_000001_session_inbound_events.ts | 37 ++++ .../session-store/PostgresSessionStore.ts | 23 ++ .../session-store/queries/inboundEvents.ts | 121 +++++++++++ packages/trueforge/src/db/postgres/types.ts | 14 ++ packages/trueforge/src/db/sqlite/client.ts | 1 + .../20260918_000001_session_inbound_events.ts | 30 +++ .../session-store/SqliteSessionStore.ts | 23 ++ .../session-store/queries/inboundEvents.ts | 122 +++++++++++ packages/trueforge/src/db/sqlite/types.ts | 15 ++ 15 files changed, 795 insertions(+) create mode 100644 packages/trueforge-core/src/agent-session/schemas/sendEvent.ts create mode 100644 packages/trueforge/src/db/postgres/migrations/20260918_000001_session_inbound_events.ts create mode 100644 packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts create mode 100644 packages/trueforge/src/db/sqlite/migrations/20260918_000001_session_inbound_events.ts create mode 100644 packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts diff --git a/packages/trueforge-core/src/agent-session/index.ts b/packages/trueforge-core/src/agent-session/index.ts index 3b9b5d66c..82c599435 100644 --- a/packages/trueforge-core/src/agent-session/index.ts +++ b/packages/trueforge-core/src/agent-session/index.ts @@ -21,6 +21,9 @@ export { } from './schemas/turn'; export type { TerminalTurnState, Turn, TurnInputItem, TurnMetrics, TurnState } from './schemas/turn'; +export { SendTurnEventItemSchema } from './schemas/sendEvent'; +export type { SendTurnEventItem } from './schemas/sendEvent'; + export { SessionMetadataSchema, SessionMetricsSchema, @@ -80,16 +83,20 @@ export type { GetSessionInput, GetTurnInput, ISessionStore, + InsertSessionInboundEventsInput, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, + ListUnconsumedSessionInboundEventsInput, + MarkSessionInboundEventsConsumedInput, NewThreadInit, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, + SessionInboundEventRecord, TurnContextAppend, TurnRecordWithoutSnapshot, UpdateSessionInput, @@ -100,6 +107,7 @@ export { PreviousTurnRunningError, SessionAlreadyExistsError, SessionExternalIdConflictError, + SessionInboundEventAlreadyExistsError, SessionNotFoundError, SessionStoreConflictError, SessionStoreInvariantError, diff --git a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts new file mode 100644 index 000000000..7a1bd08c6 --- /dev/null +++ b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts @@ -0,0 +1,17 @@ +/** + * Inbound send-event payloads for tip HITL (client → harness), distinct from the + * stream log ({@link PersistedTurnEvent} / session_event). + * + * Public send is session-scoped (`POST …/sessions/{id}/events`); `turn_id` is a + * required body field (not path) and is stored on `session_inbound_events`. + * v1 union is tip-only; approval policies may relax `turn_id` to optional/null later. + * `user.message` stays on createTurn / steer. + */ +import { z } from '@hono/zod-openapi'; +import { UserToolApprovalMessageSchema, UserToolResponseMessageSchema } from '../../core/events/schema'; + +export const SendTurnEventItemSchema = z + .discriminatedUnion('type', [UserToolApprovalMessageSchema, UserToolResponseMessageSchema]) + .openapi('SendTurnEventItem'); + +export type SendTurnEventItem = z.infer; diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index b11a7ca1d..a665f2cd6 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -11,6 +11,7 @@ import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord } from '../models/TurnRecord'; import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; +import type { SendTurnEventItem } from '../schemas/sendEvent'; import type { SessionMetadata } from '../schemas/session'; import type { CancellationReason, TerminalTurnState } from '../schemas/turn'; @@ -170,6 +171,49 @@ export interface AppendToEventsInput { events: PersistedTurnEvent[]; } +/** One durable inbound send-event row (tip HITL and/or session-scoped). */ +export interface SessionInboundEventRecord { + event_id: string; + /** Tip id when tip-scoped; null for session-only (e.g. future policies). */ + turn_id: string | null; + /** Validated {@link SendTurnEventItem} body (widens when policy lands). */ + payload: SendTurnEventItem; + /** ISO-8601; copied from insert input. Ordering uses `event_id`. */ + created_at: string; +} + +export interface InsertSessionInboundEventsInput { + session_id: string; + /** + * Caller mints `event_id` (monotonic ULID) — same contract as session_event. + * Empty array is a no-op. `turn_id` is required for v1 tip HITL (HTTP body + * field on session send-event); relax to optional/null when session-scoped + * policies land. + */ + events: Array<{ + turn_id: string; + event_id: string; + payload: SendTurnEventItem; + created_at: string; + }>; +} + +export interface ListUnconsumedSessionInboundEventsInput { + session_id: string; + /** + * `undefined` (omit) — all unconsumed for the session. + * `string` — unconsumed for that turn only. + * `null` — unconsumed session-scoped rows only (`turn_id` IS NULL; empty until + * policies allow null inserts). + */ + turn_id?: string | null; +} + +export interface MarkSessionInboundEventsConsumedInput { + session_id: string; + event_ids: string[]; +} + export interface AddThreadsInput { session_id: string; turn_id: string; @@ -360,6 +404,30 @@ export interface ISessionStore< */ appendToEvents(input: AppendToEventsInput): Promise; + /** + * Durable inbound send-event inbox for the session. v1 requires `turn_id` on + * every row (tip HITL). Column stays nullable for later session-scoped + * policies. Missing session → {@link SessionNotFoundError}; unknown turn → + * {@link TurnNotFoundError}. Duplicate `event_id` → + * {@link SessionInboundEventAlreadyExistsError}. + */ + insertSessionInboundEvents(input: InsertSessionInboundEventsInput): Promise; + + /** + * Unconsumed inbox rows, ordered by monotonic `event_id` ascending. + * See {@link ListUnconsumedSessionInboundEventsInput.turn_id} for filtering. + * Missing session → {@link SessionNotFoundError}. + */ + listUnconsumedSessionInboundEvents( + input: ListUnconsumedSessionInboundEventsInput, + ): Promise; + + /** + * Marks inbox rows consumed. Already-consumed or unknown ids are ignored. + * Empty `event_ids` is a no-op. Missing session → {@link SessionNotFoundError}. + */ + markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise; + /** Adds thread snapshots to the turn (sub-agent spawns). */ addThreads(input: AddThreadsInput): Promise; diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index a2e114b61..c4b1961a1 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -4,6 +4,7 @@ import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord, TurnSnapshot } from '../models/TurnRecord'; import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; +import type { SendTurnEventItem } from '../schemas/sendEvent'; import type { TerminalTurnState } from '../schemas/turn'; import { assertCreateTurnThreadDelta } from './assertCreateTurnThreadDelta'; import type { @@ -18,17 +19,21 @@ import type { GetSessionByExternalIdInput, GetSessionInput, GetTurnInput, + InsertSessionInboundEventsInput, ISessionStore, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, + ListUnconsumedSessionInboundEventsInput, + MarkSessionInboundEventsConsumedInput, NewThreadInit, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, + SessionInboundEventRecord, TurnContextAppend, TurnRecordWithoutSnapshot, UpdateSessionInput, @@ -45,6 +50,7 @@ import { PreviousTurnRunningError, SessionAlreadyExistsError, SessionExternalIdConflictError, + SessionInboundEventAlreadyExistsError, SessionNotFoundError, SessionStoreInvariantError, TurnAlreadyExistsError, @@ -56,6 +62,14 @@ import { type StoredEvent = PersistedTurnEvent; +interface StoredInboundEvent { + event_id: string; + turn_id: string | null; + payload: SendTurnEventItem; + created_at: string; + consumed: boolean; +} + interface StoredSession { record: SessionRecord; turnIds: string[]; @@ -170,6 +184,8 @@ export class InMemorySessionStore< private readonly sessions = new Map>(); private readonly turns = new Map>(); private readonly events = new Map(); + /** session_id → inbound send-event inbox */ + private readonly inboundEvents = new Map(); async createSession(input: CreateSessionInput): Promise { const key = sessionKey(input.session_id); @@ -219,6 +235,7 @@ export class InMemorySessionStore< this.turns.delete(tKey); this.events.delete(tKey); } + this.inboundEvents.delete(sessionKey(input.session_id)); this.sessions.delete(sKey); } @@ -485,6 +502,83 @@ export class InMemorySessionStore< return; } + async insertSessionInboundEvents(input: InsertSessionInboundEventsInput): Promise { + if (input.events.length === 0) { + return; + } + this.requireSession(input.session_id); + for (const event of input.events) { + this.requireTurn(input.session_id, event.turn_id); + } + const sKey = sessionKey(input.session_id); + let list = this.inboundEvents.get(sKey); + if (!list) { + list = []; + this.inboundEvents.set(sKey, list); + } + const existing = new Set(list.map(row => row.event_id)); + for (const event of input.events) { + if (existing.has(event.event_id)) { + throw new SessionInboundEventAlreadyExistsError(input.session_id, event.event_id); + } + } + for (const event of input.events) { + list.push({ + event_id: event.event_id, + turn_id: event.turn_id, + payload: deepCopy(event.payload), + created_at: event.created_at, + consumed: false, + }); + existing.add(event.event_id); + } + } + + async listUnconsumedSessionInboundEvents( + input: ListUnconsumedSessionInboundEventsInput, + ): Promise { + this.requireSession(input.session_id); + const list = this.inboundEvents.get(sessionKey(input.session_id)) ?? []; + return list + .filter(row => { + if (row.consumed) { + return false; + } + if (input.turn_id === undefined) { + return true; + } + if (input.turn_id === null) { + return row.turn_id === null; + } + return row.turn_id === input.turn_id; + }) + .slice() + .sort((a, b) => (a.event_id < b.event_id ? -1 : a.event_id > b.event_id ? 1 : 0)) + .map(row => ({ + event_id: row.event_id, + turn_id: row.turn_id, + payload: deepCopy(row.payload), + created_at: row.created_at, + })); + } + + async markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise { + if (input.event_ids.length === 0) { + return; + } + this.requireSession(input.session_id); + const list = this.inboundEvents.get(sessionKey(input.session_id)); + if (!list) { + return; + } + const wanted = new Set(input.event_ids); + for (const row of list) { + if (wanted.has(row.event_id)) { + row.consumed = true; + } + } + } + /** Cost from turn metrics when present; duration is completed_at − created_at, floored at 0. */ private addTerminalSessionMetrics(sessionId: string, created_at: Date, state: TerminalTurnState): void { const stored = this.sessions.get(sessionKey(sessionId)); @@ -499,7 +593,16 @@ export class InMemorySessionStore< stored.record.metrics.total_duration_ms += elapsed_ms > 0 ? Math.trunc(elapsed_ms) : 0; } + private requireSession(sessionId: string): StoredSession { + const stored = this.sessions.get(sessionKey(sessionId)); + if (!stored) { + throw new SessionNotFoundError(sessionId); + } + return stored; + } + private requireTurn(sessionId: string, turnId: string): TurnRecord { + this.requireSession(sessionId); const turn = this.turns.get(turnKey({ session_id: sessionId, turn_id: turnId })); if (!turn) { throw new TurnNotFoundError(turnId); diff --git a/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts b/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts index 89b31ee90..b3829cac6 100644 --- a/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts +++ b/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts @@ -74,6 +74,18 @@ export class TurnAlreadyExistsError extends SessionStoreConflictError { } } +export class SessionInboundEventAlreadyExistsError extends SessionStoreConflictError { + readonly session_id: string; + readonly event_id: string; + + constructor(session_id: string, event_id: string, options?: ErrorOptions) { + super(`Session inbound event already exists: ${session_id}/${event_id}`, options); + this.name = 'SessionInboundEventAlreadyExistsError'; + this.session_id = session_id; + this.event_id = event_id; + } +} + export class PreviousTurnRunningError extends SessionStoreConflictError { readonly previous_turn_id: string; diff --git a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts index 6c3cd36ac..d629c20aa 100644 --- a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts +++ b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts @@ -8,6 +8,7 @@ import { decodeSessionEventPageToken } from '../../../src/agent-session/store/Se import { PreviousTurnRunningError, SessionExternalIdConflictError, + SessionInboundEventAlreadyExistsError, SessionNotFoundError, SessionStoreConflictError, SessionStoreInvariantError, @@ -685,6 +686,27 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { order: undefined, }), ).rejects.toBeInstanceOf(TurnNotFoundError); + await expect( + store.insertSessionInboundEvents({ + session_id: sessionId, + events: [ + { + event_id: newEventId(), + turn_id: 'turn-1', + payload: { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + created_at: new Date().toISOString(), + }, + ], + }), + ).rejects.toBeInstanceOf(SessionNotFoundError); + await expect(store.listUnconsumedSessionInboundEvents({ session_id: sessionId })).rejects.toBeInstanceOf( + SessionNotFoundError, + ); await expect( store.listSessionEvents({ session_id: sessionId, @@ -2206,6 +2228,185 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { expect(data.map(e => e.id)).toEqual([created.id, model.id]); }); + it('session_inbound_events: insert, list unconsumed, mark consumed, duplicate id', async () => { + const store = createStore(); + await seedSession(store); + await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); + + const earlier = { + event_id: '01AAAAAAAAAAAAAAAAAAAAAAAA', + turn_id: 'turn-1', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' as const }, + }, + created_at: new Date().toISOString(), + }; + const later = { + event_id: '01BBBBBBBBBBBBBBBBBBBBBBBB', + turn_id: 'turn-1', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-2', + approval: { status: 'deny' as const, reason: 'nope' }, + }, + created_at: new Date().toISOString(), + }; + + await store.insertSessionInboundEvents({ + session_id: sessionId, + events: [later, earlier], + }); + + let pending = await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: 'turn-1', + }); + expect(pending.map(e => e.event_id)).toEqual([earlier.event_id, later.event_id]); + expect(pending[0]?.payload).toEqual(earlier.payload); + expect(pending[0]?.turn_id).toBe('turn-1'); + + await store.markSessionInboundEventsConsumed({ + session_id: sessionId, + event_ids: [earlier.event_id], + }); + pending = await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: 'turn-1', + }); + expect(pending.map(e => e.event_id)).toEqual([later.event_id]); + + await expect( + store.insertSessionInboundEvents({ + session_id: sessionId, + events: [later], + }), + ).rejects.toBeInstanceOf(SessionInboundEventAlreadyExistsError); + + // Tip-scoped inbox writes are not fenced on running — still allowed after terminal tip + // (paused tips will need this; OWN-16 can tighten to paused/running only). + await finishTurn(store, 'turn-1'); + const afterDone = { + event_id: '01CCCCCCCCCCCCCCCCCCCCCCCC', + turn_id: 'turn-1', + payload: { + type: 'user.tool_response' as const, + thread_id: 'main', + tool_call_id: 'tc-3', + content: 'client result', + }, + created_at: new Date().toISOString(), + }; + await store.insertSessionInboundEvents({ + session_id: sessionId, + events: [afterDone], + }); + pending = await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: 'turn-1', + }); + expect(pending.map(e => e.event_id)).toEqual([later.event_id, afterDone.event_id]); + }); + + it('session_inbound_events: list filter turn_id string | null | omitted', async () => { + const store = createStore(); + await seedSession(store); + await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-a' })); + await finishTurn(store, 'turn-a'); + await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-b', previous_turn_id: 'turn-a' })); + + const forA = { + event_id: '01AAAAAAAAAAAAAAAAAAAAAAAA', + turn_id: 'turn-a', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-a', + approval: { status: 'allow' as const }, + }, + created_at: new Date().toISOString(), + }; + const forB = { + event_id: '01BBBBBBBBBBBBBBBBBBBBBBBB', + turn_id: 'turn-b', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-b', + approval: { status: 'allow' as const }, + }, + created_at: new Date().toISOString(), + }; + + await store.insertSessionInboundEvents({ + session_id: sessionId, + events: [forB, forA], + }); + + // string — that turn only + expect( + ( + await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: 'turn-a', + }) + ).map(e => e.event_id), + ).toEqual([forA.event_id]); + expect( + ( + await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: 'turn-b', + }) + ).map(e => e.event_id), + ).toEqual([forB.event_id]); + + // null — session-scoped only (v1 insert always sets turn_id; no such rows yet) + expect( + ( + await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: null, + }) + ).map(e => e.event_id), + ).toEqual([]); + + // omitted — all unconsumed, ordered by event_id + expect((await store.listUnconsumedSessionInboundEvents({ session_id: sessionId })).map(e => e.event_id)).toEqual([ + forA.event_id, + forB.event_id, + ]); + }); + + it('session_inbound_events cascade away with deleteSession', async () => { + const store = createStore(); + await seedSession(store); + await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); + await store.insertSessionInboundEvents({ + session_id: sessionId, + events: [ + { + event_id: newEventId(), + turn_id: 'turn-1', + payload: { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-x', + approval: { status: 'allow' }, + }, + created_at: new Date().toISOString(), + }, + ], + }); + await store.deleteSession({ tenant_id: tenant, session_id: sessionId }); + await expect(store.listUnconsumedSessionInboundEvents({ session_id: sessionId })).rejects.toBeInstanceOf( + SessionNotFoundError, + ); + }); + it('add/remove threads and append/overwrite context', async () => { const store = createStore(); await seedSession(store); diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000001_session_inbound_events.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000001_session_inbound_events.ts new file mode 100644 index 000000000..1bf4f1833 --- /dev/null +++ b/packages/trueforge/src/db/postgres/migrations/20260918_000001_session_inbound_events.ts @@ -0,0 +1,37 @@ +import { sql, type Kysely } from 'kysely'; + +/** + * Session inbound send-event inbox (tip HITL + future session-scoped payloads). + * `turn_id` column nullable (v1 insert always sets it; null reserved for session-only policies later). + */ +export async function up(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + + await db.schema + .createTable('session_inbound_events') + .addColumn('session_id', 'text', col => col.notNull()) + .addColumn('event_id', 'text', col => col.notNull()) + .addColumn('turn_id', 'text') + .addColumn('payload', 'jsonb', col => col.notNull()) + .addColumn('consumed', 'boolean', col => col.notNull().defaultTo(false)) + .addColumn('created_at', 'timestamptz', col => col.notNull()) + .addPrimaryKeyConstraint('session_inbound_events_pkey', ['session_id', 'event_id']) + .execute(); + + await db.schema + .alterTable('session_inbound_events') + .addForeignKeyConstraint('session_inbound_events_session_fkey', ['session_id'], 'session', ['session_id']) + .onDelete('cascade') + .execute(); + + await sql` + CREATE INDEX session_inbound_events_unconsumed_idx + ON session_inbound_events (session_id, turn_id, event_id) + WHERE consumed = false + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`SET LOCAL lock_timeout = '5s'`.execute(db); + await db.schema.dropTable('session_inbound_events').ifExists().cascade().execute(); +} diff --git a/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts b/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts index a44502cc7..aadb410dc 100644 --- a/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts +++ b/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts @@ -21,16 +21,20 @@ import type { GetSessionByExternalIdInput, GetSessionInput, GetTurnInput, + InsertSessionInboundEventsInput, ISessionStore, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, + ListUnconsumedSessionInboundEventsInput, + MarkSessionInboundEventsConsumedInput, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, + SessionInboundEventRecord, TurnRecordWithoutSnapshot, UpdateSessionInput, UpdateTurnStateInput, @@ -52,6 +56,11 @@ import { listSessionEvents as listSessionEventsQuery, listTurnEvents as listTurnEventsQuery, } from './queries/events'; +import { + insertSessionInboundEvents as insertSessionInboundEventsQuery, + listUnconsumedSessionInboundEvents as listUnconsumedSessionInboundEventsQuery, + markSessionInboundEventsConsumed as markSessionInboundEventsConsumedQuery, +} from './queries/inboundEvents'; import { createSession as createSessionQuery, deleteSession as deleteSessionQuery, @@ -215,6 +224,20 @@ export class PostgresSessionStore implements ISessionStore { + return insertSessionInboundEventsQuery(this.db, input); + } + + listUnconsumedSessionInboundEvents( + input: ListUnconsumedSessionInboundEventsInput, + ): Promise { + return listUnconsumedSessionInboundEventsQuery(this.db, input); + } + + markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise { + return markSessionInboundEventsConsumedQuery(this.db, input); + } + addThreads(input: AddThreadsInput): Promise { return addThreadsQuery(this.db, input); } diff --git a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts new file mode 100644 index 000000000..e00a7cbfc --- /dev/null +++ b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts @@ -0,0 +1,121 @@ +import type { + InsertSessionInboundEventsInput, + ListUnconsumedSessionInboundEventsInput, + MarkSessionInboundEventsConsumedInput, + SessionInboundEventRecord, +} from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; +import { + SessionInboundEventAlreadyExistsError, + SessionNotFoundError, + TurnNotFoundError, +} from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; +import type { Kysely } from 'kysely'; +import { sql } from 'kysely'; +import { isUniqueViolation } from '../../client'; +import { jsonUnknown } from '../../sqlExpressions'; +import type { Database } from '../../types'; + +async function requireSession(db: Kysely, sessionId: string): Promise { + const row = await db + .selectFrom('session') + .select('session_id') + .where('session_id', '=', sessionId) + .executeTakeFirst(); + if (!row) { + throw new SessionNotFoundError(sessionId); + } +} + +async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { + const row = await db + .selectFrom('turn') + .select('turn_id') + .where('session_id', '=', sessionId) + .where('turn_id', '=', turnId) + .executeTakeFirst(); + if (!row) { + throw new TurnNotFoundError(turnId); + } +} + +export async function insertSessionInboundEvents( + db: Kysely, + input: InsertSessionInboundEventsInput, +): Promise { + if (input.events.length === 0) { + return; + } + await requireSession(db, input.session_id); + for (const event of input.events) { + await requireTurn(db, input.session_id, event.turn_id); + } + + try { + await db + .insertInto('session_inbound_events') + .values( + input.events.map(event => ({ + session_id: input.session_id, + event_id: event.event_id, + turn_id: event.turn_id, + payload: jsonUnknown(event.payload), + consumed: false, + created_at: sql`${event.created_at}::timestamptz`, + })), + ) + .execute(); + } catch (error) { + if (isUniqueViolation(error)) { + const first = input.events[0]; + throw new SessionInboundEventAlreadyExistsError(input.session_id, first?.event_id ?? '', { + cause: error, + }); + } + throw error; + } +} + +export async function listUnconsumedSessionInboundEvents( + db: Kysely, + input: ListUnconsumedSessionInboundEventsInput, +): Promise { + await requireSession(db, input.session_id); + + let query = db + .selectFrom('session_inbound_events') + .select(['event_id', 'turn_id', 'payload', 'created_at']) + .where('session_id', '=', input.session_id) + .where('consumed', '=', false); + + if (input.turn_id === null) { + query = query.where('turn_id', 'is', null); + } else if (input.turn_id !== undefined) { + query = query.where('turn_id', '=', input.turn_id); + } + + const rows = await query.orderBy('event_id', 'asc').execute(); + + return rows.map(row => ({ + event_id: row.event_id, + turn_id: row.turn_id, + payload: row.payload as SessionInboundEventRecord['payload'], + created_at: new Date(row.created_at).toISOString(), + })); +} + +export async function markSessionInboundEventsConsumed( + db: Kysely, + input: MarkSessionInboundEventsConsumedInput, +): Promise { + if (input.event_ids.length === 0) { + return; + } + await requireSession(db, input.session_id); + + await db + .updateTable('session_inbound_events') + .set({ consumed: true }) + .where('session_id', '=', input.session_id) + .where('event_id', 'in', input.event_ids) + .execute(); +} diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index cdc6bcd64..b3e3e6bee 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -247,6 +247,19 @@ export interface SessionEventTable { created_at: Date; } +/** + * Session inbound send-event inbox (tip HITL + future session-scoped payloads). + * PRIMARY KEY (session_id, event_id). `turn_id` nullable. + */ +export interface SessionInboundEventsTable { + session_id: string; + event_id: string; + turn_id: string | null; + payload: ColumnType; + consumed: boolean; + created_at: Date; +} + /** * pure immutable CONTENT; no state → no checkpoint field * PRIMARY KEY (session_id, thread_id, append_id) @@ -518,6 +531,7 @@ export interface Database { turn: TurnTable; turn_thread: TurnThreadTable; session_event: SessionEventTable; + session_inbound_events: SessionInboundEventsTable; thread_context_log: ThreadContextLogTable; thread_capability_state: ThreadCapabilityStateTable; model_provider: ModelProviderTable; diff --git a/packages/trueforge/src/db/sqlite/client.ts b/packages/trueforge/src/db/sqlite/client.ts index 8c29149aa..ce6dc9407 100644 --- a/packages/trueforge/src/db/sqlite/client.ts +++ b/packages/trueforge/src/db/sqlite/client.ts @@ -141,6 +141,7 @@ const JSON_RESULT_COLUMNS = new Set([ 'turn_state', 'thread_checkpoint', 'event', + 'payload', 'manifest', 'metadata', 'build_metadata', diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000001_session_inbound_events.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000001_session_inbound_events.ts new file mode 100644 index 000000000..e7fc8ddcc --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260918_000001_session_inbound_events.ts @@ -0,0 +1,30 @@ +import { type Kysely, sql } from 'kysely'; + +/** + * Session inbound send-event inbox (tip HITL + future session-scoped payloads). + * `turn_id` column nullable (v1 insert always sets it; null reserved for session-only policies later). + */ +export async function up(db: Kysely): Promise { + await sql` + CREATE TABLE session_inbound_events ( + session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE, + event_id TEXT NOT NULL, + turn_id TEXT, + payload BLOB NOT NULL, + consumed INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + PRIMARY KEY (session_id, event_id) + ) STRICT + `.execute(db); + + await sql` + CREATE INDEX session_inbound_events_unconsumed_idx + ON session_inbound_events (session_id, turn_id, event_id) + WHERE consumed = 0 + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX IF EXISTS session_inbound_events_unconsumed_idx`.execute(db); + await sql`DROP TABLE IF EXISTS session_inbound_events`.execute(db); +} diff --git a/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts b/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts index 61425e954..faaab7f81 100644 --- a/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts +++ b/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts @@ -14,16 +14,20 @@ import type { GetSessionByExternalIdInput, GetSessionInput, GetTurnInput, + InsertSessionInboundEventsInput, ISessionStore, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, + ListUnconsumedSessionInboundEventsInput, + MarkSessionInboundEventsConsumedInput, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, + SessionInboundEventRecord, TurnRecordWithoutSnapshot, UpdateSessionInput, UpdateTurnStateInput, @@ -40,6 +44,11 @@ import { listSessionEvents as listSessionEventsQuery, listTurnEvents as listTurnEventsQuery, } from './queries/events'; +import { + insertSessionInboundEvents as insertSessionInboundEventsQuery, + listUnconsumedSessionInboundEvents as listUnconsumedSessionInboundEventsQuery, + markSessionInboundEventsConsumed as markSessionInboundEventsConsumedQuery, +} from './queries/inboundEvents'; import { createSession as createSessionQuery, deleteSession as deleteSessionQuery, @@ -190,6 +199,20 @@ export class SqliteSessionStore implements ISessionStore { + return insertSessionInboundEventsQuery(this.db, input); + } + + listUnconsumedSessionInboundEvents( + input: ListUnconsumedSessionInboundEventsInput, + ): Promise { + return listUnconsumedSessionInboundEventsQuery(this.db, input); + } + + markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise { + return markSessionInboundEventsConsumedQuery(this.db, input); + } + addThreads(input: AddThreadsInput): Promise { return addThreadsQuery(this.db, input); } diff --git a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts new file mode 100644 index 000000000..7e4abbce2 --- /dev/null +++ b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts @@ -0,0 +1,122 @@ +import type { + InsertSessionInboundEventsInput, + ListUnconsumedSessionInboundEventsInput, + MarkSessionInboundEventsConsumedInput, + SessionInboundEventRecord, +} from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; +import { + SessionInboundEventAlreadyExistsError, + SessionNotFoundError, + TurnNotFoundError, +} from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; +import type { JsonValue } from '@truefoundry/trueforge-core/core'; +import type { Kysely } from 'kysely'; +import { sql } from 'kysely'; +import { isUniqueViolation } from '../../client'; +import { jsonbBind, jsonText } from '../../sqlExpressions'; +import type { Database } from '../../types'; + +async function requireSession(db: Kysely, sessionId: string): Promise { + const row = await db + .selectFrom('session') + .select('session_id') + .where('session_id', '=', sessionId) + .executeTakeFirst(); + if (!row) { + throw new SessionNotFoundError(sessionId); + } +} + +async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { + const row = await db + .selectFrom('turn') + .select('turn_id') + .where('session_id', '=', sessionId) + .where('turn_id', '=', turnId) + .executeTakeFirst(); + if (!row) { + throw new TurnNotFoundError(turnId); + } +} + +export async function insertSessionInboundEvents( + db: Kysely, + input: InsertSessionInboundEventsInput, +): Promise { + if (input.events.length === 0) { + return; + } + await requireSession(db, input.session_id); + for (const event of input.events) { + await requireTurn(db, input.session_id, event.turn_id); + } + + try { + await db + .insertInto('session_inbound_events') + .values( + input.events.map(event => ({ + session_id: input.session_id, + event_id: event.event_id, + turn_id: event.turn_id, + payload: jsonbBind(event.payload), + consumed: 0, + created_at: event.created_at, + })), + ) + .execute(); + } catch (error) { + if (isUniqueViolation(error)) { + const first = input.events[0]; + throw new SessionInboundEventAlreadyExistsError(input.session_id, first?.event_id ?? '', { + cause: error, + }); + } + throw error; + } +} + +export async function listUnconsumedSessionInboundEvents( + db: Kysely, + input: ListUnconsumedSessionInboundEventsInput, +): Promise { + await requireSession(db, input.session_id); + + let query = db + .selectFrom('session_inbound_events') + .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) + .where('session_id', '=', input.session_id) + .where('consumed', '=', 0); + + if (input.turn_id === null) { + query = query.where('turn_id', 'is', null); + } else if (input.turn_id !== undefined) { + query = query.where('turn_id', '=', input.turn_id); + } + + const rows = await query.orderBy('event_id', 'asc').execute(); + + return rows.map(row => ({ + event_id: row.event_id, + turn_id: row.turn_id, + payload: row.payload as SessionInboundEventRecord['payload'], + created_at: row.created_at, + })); +} + +export async function markSessionInboundEventsConsumed( + db: Kysely, + input: MarkSessionInboundEventsConsumedInput, +): Promise { + if (input.event_ids.length === 0) { + return; + } + await requireSession(db, input.session_id); + + await db + .updateTable('session_inbound_events') + .set({ consumed: 1 }) + .where('session_id', '=', input.session_id) + .where('event_id', 'in', input.event_ids) + .execute(); +} diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index c4340877b..4de0b2ae9 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -145,6 +145,20 @@ export interface SessionEventTable { created_at: string; } +/** + * Session inbound send-event inbox (tip HITL + future session-scoped payloads). + * PRIMARY KEY (session_id, event_id). `turn_id` nullable. + * `consumed` is INTEGER 0/1 (STRICT has no boolean). + */ +export interface SessionInboundEventsTable { + session_id: string; + event_id: string; + turn_id: string | null; + payload: ColumnType; + consumed: number; + created_at: string; +} + /** * Pure immutable content; no state → no checkpoint field. * PRIMARY KEY (append_id) AUTOINCREMENT @@ -338,6 +352,7 @@ export interface Database { turn_thread: TurnThreadTable; turn_thread_context: TurnThreadContextTable; session_event: SessionEventTable; + session_inbound_events: SessionInboundEventsTable; thread_context_log: ThreadContextLogTable; thread_capability_state: ThreadCapabilityStateTable; model_provider: ModelProviderTable; From f54105b7dd0ed93e54656d4a884c2e5efcc4466a Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Fri, 18 Sep 2026 14:02:37 +0530 Subject: [PATCH 02/23] rename migrations --- ...nbound_events.ts => 20260918_000002_session_inbound_events.ts} | 0 ...nbound_events.ts => 20260918_000002_session_inbound_events.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/trueforge/src/db/postgres/migrations/{20260918_000001_session_inbound_events.ts => 20260918_000002_session_inbound_events.ts} (100%) rename packages/trueforge/src/db/sqlite/migrations/{20260918_000001_session_inbound_events.ts => 20260918_000002_session_inbound_events.ts} (100%) diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000001_session_inbound_events.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000002_session_inbound_events.ts similarity index 100% rename from packages/trueforge/src/db/postgres/migrations/20260918_000001_session_inbound_events.ts rename to packages/trueforge/src/db/postgres/migrations/20260918_000002_session_inbound_events.ts diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000001_session_inbound_events.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_session_inbound_events.ts similarity index 100% rename from packages/trueforge/src/db/sqlite/migrations/20260918_000001_session_inbound_events.ts rename to packages/trueforge/src/db/sqlite/migrations/20260918_000002_session_inbound_events.ts From 0efbc391fbe564e2234ff05db54955871319229f Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Fri, 18 Sep 2026 14:54:57 +0530 Subject: [PATCH 03/23] address comments --- .changeset/pre/session-inbound-events.md | 6 + .../src/agent-session/store/ISessionStore.ts | 15 +- .../store/InMemorySessionStore.ts | 7 +- .../agent-session/store/storeContractSuite.ts | 154 ++++++++++++++---- .../session-store/queries/inboundEvents.ts | 62 +++++-- packages/trueforge/src/db/postgres/types.ts | 3 +- .../trueforge/src/db/sessionInboundEvents.ts | 28 ++++ .../session-store/queries/inboundEvents.ts | 62 +++++-- packages/trueforge/src/db/sqlite/types.ts | 3 +- 9 files changed, 265 insertions(+), 75 deletions(-) create mode 100644 .changeset/pre/session-inbound-events.md create mode 100644 packages/trueforge/src/db/sessionInboundEvents.ts diff --git a/.changeset/pre/session-inbound-events.md b/.changeset/pre/session-inbound-events.md new file mode 100644 index 000000000..de1b7ec2d --- /dev/null +++ b/.changeset/pre/session-inbound-events.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge-core": patch +"@truefoundry/trueforge": patch +--- + +Add `session_inbound_events` store API for durable tip HITL send-event inbox (insert / list unconsumed / mark consumed), with Postgres and SQLite migrations. diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index a665f2cd6..1cdad94b6 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -201,12 +201,13 @@ export interface InsertSessionInboundEventsInput { export interface ListUnconsumedSessionInboundEventsInput { session_id: string; /** - * `undefined` (omit) — all unconsumed for the session. - * `string` — unconsumed for that turn only. - * `null` — unconsumed session-scoped rows only (`turn_id` IS NULL; empty until - * policies allow null inserts). + * Three-way filter — pass the key explicitly (do not omit): + * - `undefined` — all unconsumed for the session + * - `string` — unconsumed for that turn only + * - `null` — session-scoped rows only (`turn_id` IS NULL; empty until + * policies allow null inserts) */ - turn_id?: string | null; + turn_id: string | null | undefined; } export interface MarkSessionInboundEventsConsumedInput { @@ -407,7 +408,9 @@ export interface ISessionStore< /** * Durable inbound send-event inbox for the session. v1 requires `turn_id` on * every row (tip HITL). Column stays nullable for later session-scoped - * policies. Missing session → {@link SessionNotFoundError}; unknown turn → + * policies. Tip must be non-terminal (v1: `running`; `paused` when that + * status lands) — terminal tip → {@link TurnNotRunningError}. Missing + * session → {@link SessionNotFoundError}; unknown turn → * {@link TurnNotFoundError}. Duplicate `event_id` → * {@link SessionInboundEventAlreadyExistsError}. */ diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index c4b1961a1..cecd27060 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -507,8 +507,8 @@ export class InMemorySessionStore< return; } this.requireSession(input.session_id); - for (const event of input.events) { - this.requireTurn(input.session_id, event.turn_id); + for (const turnId of new Set(input.events.map(event => event.turn_id))) { + this.requireRunningTurn(input.session_id, turnId); } const sKey = sessionKey(input.session_id); let list = this.inboundEvents.get(sKey); @@ -521,6 +521,7 @@ export class InMemorySessionStore< if (existing.has(event.event_id)) { throw new SessionInboundEventAlreadyExistsError(input.session_id, event.event_id); } + existing.add(event.event_id); } for (const event of input.events) { list.push({ @@ -530,7 +531,6 @@ export class InMemorySessionStore< created_at: event.created_at, consumed: false, }); - existing.add(event.event_id); } } @@ -602,7 +602,6 @@ export class InMemorySessionStore< } private requireTurn(sessionId: string, turnId: string): TurnRecord { - this.requireSession(sessionId); const turn = this.turns.get(turnKey({ session_id: sessionId, turn_id: turnId })); if (!turn) { throw new TurnNotFoundError(turnId); diff --git a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts index d629c20aa..84e217f8f 100644 --- a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts +++ b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts @@ -8,7 +8,6 @@ import { decodeSessionEventPageToken } from '../../../src/agent-session/store/Se import { PreviousTurnRunningError, SessionExternalIdConflictError, - SessionInboundEventAlreadyExistsError, SessionNotFoundError, SessionStoreConflictError, SessionStoreInvariantError, @@ -704,9 +703,9 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ], }), ).rejects.toBeInstanceOf(SessionNotFoundError); - await expect(store.listUnconsumedSessionInboundEvents({ session_id: sessionId })).rejects.toBeInstanceOf( - SessionNotFoundError, - ); + await expect( + store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: undefined }), + ).rejects.toBeInstanceOf(SessionNotFoundError); await expect( store.listSessionEvents({ session_id: sessionId, @@ -2234,7 +2233,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); const earlier = { - event_id: '01AAAAAAAAAAAAAAAAAAAAAAAA', + event_id: 'evt-a', turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, @@ -2245,7 +2244,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { created_at: new Date().toISOString(), }; const later = { - event_id: '01BBBBBBBBBBBBBBBBBBBBBBBB', + event_id: 'evt-b', turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, @@ -2284,42 +2283,112 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { session_id: sessionId, events: [later], }), - ).rejects.toBeInstanceOf(SessionInboundEventAlreadyExistsError); + ).rejects.toMatchObject({ + name: 'SessionInboundEventAlreadyExistsError', + event_id: later.event_id, + }); - // Tip-scoped inbox writes are not fenced on running — still allowed after terminal tip - // (paused tips will need this; OWN-16 can tighten to paused/running only). - await finishTurn(store, 'turn-1'); - const afterDone = { - event_id: '01CCCCCCCCCCCCCCCCCCCCCCCC', + // Later id in the batch collides — error must name that id. + const fresh = { + event_id: 'evt-fresh', turn_id: 'turn-1', payload: { - type: 'user.tool_response' as const, + type: 'user.tool_approval' as const, thread_id: 'main', - tool_call_id: 'tc-3', - content: 'client result', + tool_call_id: 'tc-fresh', + approval: { status: 'allow' as const }, }, created_at: new Date().toISOString(), }; - await store.insertSessionInboundEvents({ - session_id: sessionId, - events: [afterDone], + await expect( + store.insertSessionInboundEvents({ + session_id: sessionId, + events: [fresh, later], + }), + ).rejects.toMatchObject({ + name: 'SessionInboundEventAlreadyExistsError', + event_id: later.event_id, }); - pending = await store.listUnconsumedSessionInboundEvents({ - session_id: sessionId, - turn_id: 'turn-1', + expect( + (await store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( + e => e.event_id, + ), + ).toEqual([later.event_id]); + + const dupId = 'evt-dup'; + await expect( + store.insertSessionInboundEvents({ + session_id: sessionId, + events: [ + { + event_id: dupId, + turn_id: 'turn-1', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-dup', + approval: { status: 'allow' as const }, + }, + created_at: new Date().toISOString(), + }, + { + event_id: dupId, + turn_id: 'turn-1', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-dup-2', + approval: { status: 'deny' as const, reason: 'dup' }, + }, + created_at: new Date().toISOString(), + }, + ], + }), + ).rejects.toMatchObject({ + name: 'SessionInboundEventAlreadyExistsError', + event_id: dupId, }); - expect(pending.map(e => e.event_id)).toEqual([later.event_id, afterDone.event_id]); + // Failed batch must not leave a partial row (SQL PK is all-or-nothing). + expect( + (await store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( + e => e.event_id, + ), + ).toEqual([later.event_id]); + + // Terminal tip rejects inbox writes. + await finishTurn(store, 'turn-1'); + await expect( + store.insertSessionInboundEvents({ + session_id: sessionId, + events: [ + { + event_id: 'evt-after-done', + turn_id: 'turn-1', + payload: { + type: 'user.tool_response' as const, + thread_id: 'main', + tool_call_id: 'tc-3', + content: 'client result', + }, + created_at: new Date().toISOString(), + }, + ], + }), + ).rejects.toBeInstanceOf(TurnNotRunningError); + expect( + (await store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( + e => e.event_id, + ), + ).toEqual([later.event_id]); }); - it('session_inbound_events: list filter turn_id string | null | omitted', async () => { + it('session_inbound_events: list filter turn_id string | null | undefined', async () => { const store = createStore(); await seedSession(store); await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-a' })); - await finishTurn(store, 'turn-a'); - await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-b', previous_turn_id: 'turn-a' })); const forA = { - event_id: '01AAAAAAAAAAAAAAAAAAAAAAAA', + event_id: 'evt-a', turn_id: 'turn-a', payload: { type: 'user.tool_approval' as const, @@ -2329,8 +2398,18 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }, created_at: new Date().toISOString(), }; + await store.insertSessionInboundEvents({ + session_id: sessionId, + events: [forA], + }); + + await finishTurn(store, 'turn-a'); + await store.createTurn( + makeCreateTurnInput({ sessionId, turnId: 'turn-b', previousTurnId: 'turn-a', firstTurnId: 'turn-a' }), + ); + const forB = { - event_id: '01BBBBBBBBBBBBBBBBBBBBBBBB', + event_id: 'evt-b', turn_id: 'turn-b', payload: { type: 'user.tool_approval' as const, @@ -2343,7 +2422,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await store.insertSessionInboundEvents({ session_id: sessionId, - events: [forB, forA], + events: [forB], }); // string — that turn only @@ -2375,10 +2454,15 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ).toEqual([]); // omitted — all unconsumed, ordered by event_id - expect((await store.listUnconsumedSessionInboundEvents({ session_id: sessionId })).map(e => e.event_id)).toEqual([ - forA.event_id, - forB.event_id, - ]); + // undefined — all unconsumed, ordered by event_id + expect( + ( + await store.listUnconsumedSessionInboundEvents({ + session_id: sessionId, + turn_id: undefined, + }) + ).map(e => e.event_id), + ).toEqual([forA.event_id, forB.event_id]); }); it('session_inbound_events cascade away with deleteSession', async () => { @@ -2402,9 +2486,9 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ], }); await store.deleteSession({ tenant_id: tenant, session_id: sessionId }); - await expect(store.listUnconsumedSessionInboundEvents({ session_id: sessionId })).rejects.toBeInstanceOf( - SessionNotFoundError, - ); + await expect( + store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: undefined }), + ).rejects.toBeInstanceOf(SessionNotFoundError); }); it('add/remove threads and append/overwrite context', async () => { diff --git a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts index e00a7cbfc..14b53a718 100644 --- a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts @@ -8,11 +8,13 @@ import { SessionInboundEventAlreadyExistsError, SessionNotFoundError, TurnNotFoundError, + TurnNotRunningError, } from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; import type { Kysely } from 'kysely'; import { sql } from 'kysely'; +import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../sessionInboundEvents'; import { isUniqueViolation } from '../../client'; -import { jsonUnknown } from '../../sqlExpressions'; +import { json } from '../../sqlExpressions'; import type { Database } from '../../types'; async function requireSession(db: Kysely, sessionId: string): Promise { @@ -26,18 +28,47 @@ async function requireSession(db: Kysely, sessionId: string): Promise< } } -async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { - const row = await db +/** Exists + non-terminal (v1: `running` only; `paused` will be allowed when that status lands). */ +async function requireTurns(db: Kysely, sessionId: string, turnIds: string[]): Promise { + if (turnIds.length === 0) { + return; + } + const rows = await db .selectFrom('turn') - .select('turn_id') + .select(['turn_id', 'state']) .where('session_id', '=', sessionId) - .where('turn_id', '=', turnId) - .executeTakeFirst(); - if (!row) { - throw new TurnNotFoundError(turnId); + .where('turn_id', 'in', turnIds) + .execute(); + const byId = new Map(rows.map(row => [row.turn_id, row])); + for (const turnId of turnIds) { + const row = byId.get(turnId); + if (!row) { + throw new TurnNotFoundError(turnId); + } + if (row.state.status !== 'running') { + throw new TurnNotRunningError(turnId, row.state); + } } } +async function resolveCollidingEventId( + db: Kysely, + sessionId: string, + events: InsertSessionInboundEventsInput['events'], +): Promise { + const ids = [...new Set(events.map(e => e.event_id))]; + if (ids.length === 0) { + return ''; + } + const rows = await db + .selectFrom('session_inbound_events') + .select('event_id') + .where('session_id', '=', sessionId) + .where('event_id', 'in', ids) + .execute(); + return firstCollidingEventId(events, new Set(rows.map(r => r.event_id))); +} + export async function insertSessionInboundEvents( db: Kysely, input: InsertSessionInboundEventsInput, @@ -46,8 +77,11 @@ export async function insertSessionInboundEvents( return; } await requireSession(db, input.session_id); - for (const event of input.events) { - await requireTurn(db, input.session_id, event.turn_id); + await requireTurns(db, input.session_id, [...new Set(input.events.map(event => event.turn_id))]); + + const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); + if (duplicateInBatch !== undefined) { + throw new SessionInboundEventAlreadyExistsError(input.session_id, duplicateInBatch); } try { @@ -58,7 +92,7 @@ export async function insertSessionInboundEvents( session_id: input.session_id, event_id: event.event_id, turn_id: event.turn_id, - payload: jsonUnknown(event.payload), + payload: json(event.payload), consumed: false, created_at: sql`${event.created_at}::timestamptz`, })), @@ -66,8 +100,8 @@ export async function insertSessionInboundEvents( .execute(); } catch (error) { if (isUniqueViolation(error)) { - const first = input.events[0]; - throw new SessionInboundEventAlreadyExistsError(input.session_id, first?.event_id ?? '', { + const eventId = await resolveCollidingEventId(db, input.session_id, input.events); + throw new SessionInboundEventAlreadyExistsError(input.session_id, eventId, { cause: error, }); } @@ -98,7 +132,7 @@ export async function listUnconsumedSessionInboundEvents( return rows.map(row => ({ event_id: row.event_id, turn_id: row.turn_id, - payload: row.payload as SessionInboundEventRecord['payload'], + payload: row.payload, created_at: new Date(row.created_at).toISOString(), })); } diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index b3e3e6bee..39de1d2a6 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -6,6 +6,7 @@ import type { AgentSpec, CreatedBySubject, PersistedTurnEvent, + SendTurnEventItem, SessionMetadata, SessionMetrics, SessionSource, @@ -255,7 +256,7 @@ export interface SessionInboundEventsTable { session_id: string; event_id: string; turn_id: string | null; - payload: ColumnType; + payload: JSONColumnType; consumed: boolean; created_at: Date; } diff --git a/packages/trueforge/src/db/sessionInboundEvents.ts b/packages/trueforge/src/db/sessionInboundEvents.ts new file mode 100644 index 000000000..4a0eedf6a --- /dev/null +++ b/packages/trueforge/src/db/sessionInboundEvents.ts @@ -0,0 +1,28 @@ +import type { InsertSessionInboundEventsInput } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; + +type InboundInsertEvent = InsertSessionInboundEventsInput['events'][number]; + +/** First repeated `event_id` in the batch (input order), if any. */ +export function firstDuplicateEventIdInBatch( + events: ReadonlyArray>, +): string | undefined { + const seen = new Set(); + for (const event of events) { + if (seen.has(event.event_id)) { + return event.event_id; + } + seen.add(event.event_id); + } + return undefined; +} + +/** + * After a unique/PK violation, pick the colliding id: first input `event_id` + * that already exists. + */ +export function firstCollidingEventId( + events: ReadonlyArray>, + existingEventIds: ReadonlySet, +): string { + return events.find(e => existingEventIds.has(e.event_id))?.event_id ?? events[0]?.event_id ?? ''; +} diff --git a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts index 7e4abbce2..4ad71899f 100644 --- a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts @@ -1,3 +1,4 @@ +import type { SendTurnEventItem, TurnState } from '@truefoundry/trueforge-core/agent-session'; import type { InsertSessionInboundEventsInput, ListUnconsumedSessionInboundEventsInput, @@ -8,10 +9,11 @@ import { SessionInboundEventAlreadyExistsError, SessionNotFoundError, TurnNotFoundError, + TurnNotRunningError, } from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; -import type { JsonValue } from '@truefoundry/trueforge-core/core'; import type { Kysely } from 'kysely'; import { sql } from 'kysely'; +import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../sessionInboundEvents'; import { isUniqueViolation } from '../../client'; import { jsonbBind, jsonText } from '../../sqlExpressions'; import type { Database } from '../../types'; @@ -27,18 +29,47 @@ async function requireSession(db: Kysely, sessionId: string): Promise< } } -async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { - const row = await db +/** Exists + non-terminal (v1: `running` only; `paused` will be allowed when that status lands). */ +async function requireTurns(db: Kysely, sessionId: string, turnIds: string[]): Promise { + if (turnIds.length === 0) { + return; + } + const rows = await db .selectFrom('turn') - .select('turn_id') + .select(['turn_id', jsonText(sql.ref('state')).as('state')]) .where('session_id', '=', sessionId) - .where('turn_id', '=', turnId) - .executeTakeFirst(); - if (!row) { - throw new TurnNotFoundError(turnId); + .where('turn_id', 'in', turnIds) + .execute(); + const byId = new Map(rows.map(row => [row.turn_id, row])); + for (const turnId of turnIds) { + const row = byId.get(turnId); + if (!row) { + throw new TurnNotFoundError(turnId); + } + if (row.state.status !== 'running') { + throw new TurnNotRunningError(turnId, row.state); + } } } +async function resolveCollidingEventId( + db: Kysely, + sessionId: string, + events: InsertSessionInboundEventsInput['events'], +): Promise { + const ids = [...new Set(events.map(e => e.event_id))]; + if (ids.length === 0) { + return ''; + } + const rows = await db + .selectFrom('session_inbound_events') + .select('event_id') + .where('session_id', '=', sessionId) + .where('event_id', 'in', ids) + .execute(); + return firstCollidingEventId(events, new Set(rows.map(r => r.event_id))); +} + export async function insertSessionInboundEvents( db: Kysely, input: InsertSessionInboundEventsInput, @@ -47,8 +78,11 @@ export async function insertSessionInboundEvents( return; } await requireSession(db, input.session_id); - for (const event of input.events) { - await requireTurn(db, input.session_id, event.turn_id); + await requireTurns(db, input.session_id, [...new Set(input.events.map(event => event.turn_id))]); + + const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); + if (duplicateInBatch !== undefined) { + throw new SessionInboundEventAlreadyExistsError(input.session_id, duplicateInBatch); } try { @@ -67,8 +101,8 @@ export async function insertSessionInboundEvents( .execute(); } catch (error) { if (isUniqueViolation(error)) { - const first = input.events[0]; - throw new SessionInboundEventAlreadyExistsError(input.session_id, first?.event_id ?? '', { + const eventId = await resolveCollidingEventId(db, input.session_id, input.events); + throw new SessionInboundEventAlreadyExistsError(input.session_id, eventId, { cause: error, }); } @@ -84,7 +118,7 @@ export async function listUnconsumedSessionInboundEvents( let query = db .selectFrom('session_inbound_events') - .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) + .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) .where('session_id', '=', input.session_id) .where('consumed', '=', 0); @@ -99,7 +133,7 @@ export async function listUnconsumedSessionInboundEvents( return rows.map(row => ({ event_id: row.event_id, turn_id: row.turn_id, - payload: row.payload as SessionInboundEventRecord['payload'], + payload: row.payload, created_at: row.created_at, })); } diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 4de0b2ae9..e38332c33 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -9,6 +9,7 @@ import type { AgentSpec, CreatedBySubject, PersistedTurnEvent, + SendTurnEventItem, SessionMetadata, SessionMetrics, SessionSource, @@ -154,7 +155,7 @@ export interface SessionInboundEventsTable { session_id: string; event_id: string; turn_id: string | null; - payload: ColumnType; + payload: JsonbColumn; consumed: number; created_at: string; } From 0a7e0c122f7be7c1ad9c5acee340e24febb82ec3 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Fri, 18 Sep 2026 14:58:18 +0530 Subject: [PATCH 04/23] simplify to single turn per request --- .../src/agent-session/schemas/sendEvent.ts | 6 ++-- .../src/agent-session/store/ISessionStore.ts | 19 ++++++------ .../store/InMemorySessionStore.ts | 6 ++-- .../agent-session/store/storeContractSuite.ts | 19 ++++++------ .../session-store/queries/inboundEvents.ts | 29 +++++++------------ .../session-store/queries/inboundEvents.ts | 29 +++++++------------ 6 files changed, 46 insertions(+), 62 deletions(-) diff --git a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts index 7a1bd08c6..2467383ef 100644 --- a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts +++ b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts @@ -2,9 +2,9 @@ * Inbound send-event payloads for tip HITL (client → harness), distinct from the * stream log ({@link PersistedTurnEvent} / session_event). * - * Public send is session-scoped (`POST …/sessions/{id}/events`); `turn_id` is a - * required body field (not path) and is stored on `session_inbound_events`. - * v1 union is tip-only; approval policies may relax `turn_id` to optional/null later. + * Public send is session-scoped (`POST …/sessions/{id}/events`) with required + * body `turn_id` (one batch → one tip) plus `SendTurnEventItem`s; rows stamp that + * tip id. v1 union is tip-only; approval policies may relax `turn_id` later. * `user.message` stays on createTurn / steer. */ import { z } from '@hono/zod-openapi'; diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index 1cdad94b6..c5c29de41 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -184,14 +184,16 @@ export interface SessionInboundEventRecord { export interface InsertSessionInboundEventsInput { session_id: string; + /** + * Tip that receives this batch (v1 required). One send = one tip; stamp every + * row with this id. Relax to optional/null when session-scoped policies land. + */ + turn_id: string; /** * Caller mints `event_id` (monotonic ULID) — same contract as session_event. - * Empty array is a no-op. `turn_id` is required for v1 tip HITL (HTTP body - * field on session send-event); relax to optional/null when session-scoped - * policies land. + * Empty array is a no-op. */ events: Array<{ - turn_id: string; event_id: string; payload: SendTurnEventItem; created_at: string; @@ -406,11 +408,10 @@ export interface ISessionStore< appendToEvents(input: AppendToEventsInput): Promise; /** - * Durable inbound send-event inbox for the session. v1 requires `turn_id` on - * every row (tip HITL). Column stays nullable for later session-scoped - * policies. Tip must be non-terminal (v1: `running`; `paused` when that - * status lands) — terminal tip → {@link TurnNotRunningError}. Missing - * session → {@link SessionNotFoundError}; unknown turn → + * Durable inbound send-event inbox for the session. Column stays nullable for later + * session-scoped policies. Tip must be non-terminal (v1: `running`; `paused` + * when that status lands) — terminal tip → {@link TurnNotRunningError}. + * Missing session → {@link SessionNotFoundError}; unknown turn → * {@link TurnNotFoundError}. Duplicate `event_id` → * {@link SessionInboundEventAlreadyExistsError}. */ diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index cecd27060..4f61e2259 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -507,9 +507,7 @@ export class InMemorySessionStore< return; } this.requireSession(input.session_id); - for (const turnId of new Set(input.events.map(event => event.turn_id))) { - this.requireRunningTurn(input.session_id, turnId); - } + this.requireRunningTurn(input.session_id, input.turn_id); const sKey = sessionKey(input.session_id); let list = this.inboundEvents.get(sKey); if (!list) { @@ -526,7 +524,7 @@ export class InMemorySessionStore< for (const event of input.events) { list.push({ event_id: event.event_id, - turn_id: event.turn_id, + turn_id: input.turn_id, payload: deepCopy(event.payload), created_at: event.created_at, consumed: false, diff --git a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts index 84e217f8f..b264ff0e6 100644 --- a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts +++ b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts @@ -688,10 +688,10 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await expect( store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [ { event_id: newEventId(), - turn_id: 'turn-1', payload: { type: 'user.tool_approval', thread_id: 'main', @@ -2234,7 +2234,6 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { const earlier = { event_id: 'evt-a', - turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2245,7 +2244,6 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }; const later = { event_id: 'evt-b', - turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2257,6 +2255,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [later, earlier], }); @@ -2281,6 +2280,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await expect( store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [later], }), ).rejects.toMatchObject({ @@ -2291,7 +2291,6 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { // Later id in the batch collides — error must name that id. const fresh = { event_id: 'evt-fresh', - turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2303,6 +2302,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await expect( store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [fresh, later], }), ).rejects.toMatchObject({ @@ -2319,10 +2319,10 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await expect( store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [ { event_id: dupId, - turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2333,7 +2333,6 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }, { event_id: dupId, - turn_id: 'turn-1', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2360,10 +2359,10 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await expect( store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [ { event_id: 'evt-after-done', - turn_id: 'turn-1', payload: { type: 'user.tool_response' as const, thread_id: 'main', @@ -2389,7 +2388,6 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { const forA = { event_id: 'evt-a', - turn_id: 'turn-a', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2400,6 +2398,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }; await store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-a', events: [forA], }); @@ -2410,7 +2409,6 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { const forB = { event_id: 'evt-b', - turn_id: 'turn-b', payload: { type: 'user.tool_approval' as const, thread_id: 'main', @@ -2422,6 +2420,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-b', events: [forB], }); @@ -2471,10 +2470,10 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); await store.insertSessionInboundEvents({ session_id: sessionId, + turn_id: 'turn-1', events: [ { event_id: newEventId(), - turn_id: 'turn-1', payload: { type: 'user.tool_approval', thread_id: 'main', diff --git a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts index 14b53a718..da3a6f1a3 100644 --- a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts @@ -29,25 +29,18 @@ async function requireSession(db: Kysely, sessionId: string): Promise< } /** Exists + non-terminal (v1: `running` only; `paused` will be allowed when that status lands). */ -async function requireTurns(db: Kysely, sessionId: string, turnIds: string[]): Promise { - if (turnIds.length === 0) { - return; - } - const rows = await db +async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { + const row = await db .selectFrom('turn') .select(['turn_id', 'state']) .where('session_id', '=', sessionId) - .where('turn_id', 'in', turnIds) - .execute(); - const byId = new Map(rows.map(row => [row.turn_id, row])); - for (const turnId of turnIds) { - const row = byId.get(turnId); - if (!row) { - throw new TurnNotFoundError(turnId); - } - if (row.state.status !== 'running') { - throw new TurnNotRunningError(turnId, row.state); - } + .where('turn_id', '=', turnId) + .executeTakeFirst(); + if (!row) { + throw new TurnNotFoundError(turnId); + } + if (row.state.status !== 'running') { + throw new TurnNotRunningError(turnId, row.state); } } @@ -77,7 +70,7 @@ export async function insertSessionInboundEvents( return; } await requireSession(db, input.session_id); - await requireTurns(db, input.session_id, [...new Set(input.events.map(event => event.turn_id))]); + await requireTurn(db, input.session_id, input.turn_id); const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); if (duplicateInBatch !== undefined) { @@ -91,7 +84,7 @@ export async function insertSessionInboundEvents( input.events.map(event => ({ session_id: input.session_id, event_id: event.event_id, - turn_id: event.turn_id, + turn_id: input.turn_id, payload: json(event.payload), consumed: false, created_at: sql`${event.created_at}::timestamptz`, diff --git a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts index 4ad71899f..089eca221 100644 --- a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts @@ -30,25 +30,18 @@ async function requireSession(db: Kysely, sessionId: string): Promise< } /** Exists + non-terminal (v1: `running` only; `paused` will be allowed when that status lands). */ -async function requireTurns(db: Kysely, sessionId: string, turnIds: string[]): Promise { - if (turnIds.length === 0) { - return; - } - const rows = await db +async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { + const row = await db .selectFrom('turn') .select(['turn_id', jsonText(sql.ref('state')).as('state')]) .where('session_id', '=', sessionId) - .where('turn_id', 'in', turnIds) - .execute(); - const byId = new Map(rows.map(row => [row.turn_id, row])); - for (const turnId of turnIds) { - const row = byId.get(turnId); - if (!row) { - throw new TurnNotFoundError(turnId); - } - if (row.state.status !== 'running') { - throw new TurnNotRunningError(turnId, row.state); - } + .where('turn_id', '=', turnId) + .executeTakeFirst(); + if (!row) { + throw new TurnNotFoundError(turnId); + } + if (row.state.status !== 'running') { + throw new TurnNotRunningError(turnId, row.state); } } @@ -78,7 +71,7 @@ export async function insertSessionInboundEvents( return; } await requireSession(db, input.session_id); - await requireTurns(db, input.session_id, [...new Set(input.events.map(event => event.turn_id))]); + await requireTurn(db, input.session_id, input.turn_id); const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); if (duplicateInBatch !== undefined) { @@ -92,7 +85,7 @@ export async function insertSessionInboundEvents( input.events.map(event => ({ session_id: input.session_id, event_id: event.event_id, - turn_id: event.turn_id, + turn_id: input.turn_id, payload: jsonbBind(event.payload), consumed: 0, created_at: event.created_at, From 78506cfee4c64d72cb06c242b4542a34ecd64fb0 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Fri, 18 Sep 2026 15:00:11 +0530 Subject: [PATCH 05/23] lint --- packages/trueforge/src/db/sessionInboundEvents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/trueforge/src/db/sessionInboundEvents.ts b/packages/trueforge/src/db/sessionInboundEvents.ts index 4a0eedf6a..2e598acf5 100644 --- a/packages/trueforge/src/db/sessionInboundEvents.ts +++ b/packages/trueforge/src/db/sessionInboundEvents.ts @@ -4,7 +4,7 @@ type InboundInsertEvent = InsertSessionInboundEventsInput['events'][number]; /** First repeated `event_id` in the batch (input order), if any. */ export function firstDuplicateEventIdInBatch( - events: ReadonlyArray>, + events: readonly Pick[], ): string | undefined { const seen = new Set(); for (const event of events) { @@ -21,7 +21,7 @@ export function firstDuplicateEventIdInBatch( * that already exists. */ export function firstCollidingEventId( - events: ReadonlyArray>, + events: readonly Pick[], existingEventIds: ReadonlySet, ): string { return events.find(e => existingEventIds.has(e.event_id))?.event_id ?? events[0]?.event_id ?? ''; From d611bfbef6f885426bfab2800a18150be5b3efab Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Fri, 18 Sep 2026 17:09:09 +0530 Subject: [PATCH 06/23] rename event --- .changeset/{pre => }/session-inbound-events.md | 0 packages/trueforge-core/src/agent-session/index.ts | 4 ++-- .../trueforge-core/src/agent-session/schemas/sendEvent.ts | 8 ++++---- .../src/agent-session/store/ISessionStore.ts | 8 ++++---- .../src/agent-session/store/InMemorySessionStore.ts | 4 ++-- packages/trueforge/src/db/postgres/types.ts | 4 ++-- .../src/db/sqlite/session-store/queries/inboundEvents.ts | 4 ++-- packages/trueforge/src/db/sqlite/types.ts | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) rename .changeset/{pre => }/session-inbound-events.md (100%) diff --git a/.changeset/pre/session-inbound-events.md b/.changeset/session-inbound-events.md similarity index 100% rename from .changeset/pre/session-inbound-events.md rename to .changeset/session-inbound-events.md diff --git a/packages/trueforge-core/src/agent-session/index.ts b/packages/trueforge-core/src/agent-session/index.ts index 82c599435..d8b492690 100644 --- a/packages/trueforge-core/src/agent-session/index.ts +++ b/packages/trueforge-core/src/agent-session/index.ts @@ -21,8 +21,8 @@ export { } from './schemas/turn'; export type { TerminalTurnState, Turn, TurnInputItem, TurnMetrics, TurnState } from './schemas/turn'; -export { SendTurnEventItemSchema } from './schemas/sendEvent'; -export type { SendTurnEventItem } from './schemas/sendEvent'; +export { SessionInboundEventItemSchema } from './schemas/sendEvent'; +export type { SessionInboundEventItem } from './schemas/sendEvent'; export { SessionMetadataSchema, diff --git a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts index 2467383ef..f25a46587 100644 --- a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts +++ b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts @@ -3,15 +3,15 @@ * stream log ({@link PersistedTurnEvent} / session_event). * * Public send is session-scoped (`POST …/sessions/{id}/events`) with required - * body `turn_id` (one batch → one tip) plus `SendTurnEventItem`s; rows stamp that + * body `turn_id` (one batch → one tip) plus `SessionInboundEventItem`s; rows stamp that * tip id. v1 union is tip-only; approval policies may relax `turn_id` later. * `user.message` stays on createTurn / steer. */ import { z } from '@hono/zod-openapi'; import { UserToolApprovalMessageSchema, UserToolResponseMessageSchema } from '../../core/events/schema'; -export const SendTurnEventItemSchema = z +export const SessionInboundEventItemSchema = z .discriminatedUnion('type', [UserToolApprovalMessageSchema, UserToolResponseMessageSchema]) - .openapi('SendTurnEventItem'); + .openapi('SessionInboundEventItem'); -export type SendTurnEventItem = z.infer; +export type SessionInboundEventItem = z.infer; diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index c5c29de41..69b74aa2f 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -11,7 +11,7 @@ import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord } from '../models/TurnRecord'; import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { SendTurnEventItem } from '../schemas/sendEvent'; +import type { SessionInboundEventItem } from '../schemas/sendEvent'; import type { SessionMetadata } from '../schemas/session'; import type { CancellationReason, TerminalTurnState } from '../schemas/turn'; @@ -176,8 +176,8 @@ export interface SessionInboundEventRecord { event_id: string; /** Tip id when tip-scoped; null for session-only (e.g. future policies). */ turn_id: string | null; - /** Validated {@link SendTurnEventItem} body (widens when policy lands). */ - payload: SendTurnEventItem; + /** Validated {@link SessionInboundEventItem} body (widens when policy lands). */ + payload: SessionInboundEventItem; /** ISO-8601; copied from insert input. Ordering uses `event_id`. */ created_at: string; } @@ -195,7 +195,7 @@ export interface InsertSessionInboundEventsInput { */ events: Array<{ event_id: string; - payload: SendTurnEventItem; + payload: SessionInboundEventItem; created_at: string; }>; } diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index 4f61e2259..7684ec873 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -4,7 +4,7 @@ import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord, TurnSnapshot } from '../models/TurnRecord'; import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { SendTurnEventItem } from '../schemas/sendEvent'; +import type { SessionInboundEventItem } from '../schemas/sendEvent'; import type { TerminalTurnState } from '../schemas/turn'; import { assertCreateTurnThreadDelta } from './assertCreateTurnThreadDelta'; import type { @@ -65,7 +65,7 @@ type StoredEvent = PersistedTurnEvent; interface StoredInboundEvent { event_id: string; turn_id: string | null; - payload: SendTurnEventItem; + payload: SessionInboundEventItem; created_at: string; consumed: boolean; } diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 4ced65b29..342f6f376 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -6,7 +6,7 @@ import type { AgentSpec, CreatedBySubject, PersistedTurnEvent, - SendTurnEventItem, + SessionInboundEventItem, SessionMetadata, SessionMetrics, SessionSource, @@ -258,7 +258,7 @@ export interface SessionInboundEventsTable { session_id: string; event_id: string; turn_id: string | null; - payload: JSONColumnType; + payload: JSONColumnType; consumed: boolean; created_at: Date; } diff --git a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts index 089eca221..108a965bf 100644 --- a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts @@ -1,4 +1,4 @@ -import type { SendTurnEventItem, TurnState } from '@truefoundry/trueforge-core/agent-session'; +import type { SessionInboundEventItem, TurnState } from '@truefoundry/trueforge-core/agent-session'; import type { InsertSessionInboundEventsInput, ListUnconsumedSessionInboundEventsInput, @@ -111,7 +111,7 @@ export async function listUnconsumedSessionInboundEvents( let query = db .selectFrom('session_inbound_events') - .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) + .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) .where('session_id', '=', input.session_id) .where('consumed', '=', 0); diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index 8b5f6915e..befe4af55 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -9,7 +9,7 @@ import type { AgentSpec, CreatedBySubject, PersistedTurnEvent, - SendTurnEventItem, + SessionInboundEventItem, SessionMetadata, SessionMetrics, SessionSource, @@ -157,7 +157,7 @@ export interface SessionInboundEventsTable { session_id: string; event_id: string; turn_id: string | null; - payload: JsonbColumn; + payload: JsonbColumn; consumed: number; created_at: string; } From 9fe00ff3ab5efa224bebad0617e98aaca596782c Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Tue, 22 Sep 2026 14:52:28 +0530 Subject: [PATCH 07/23] rename to turn inbound events --- .changeset/session-inbound-events.md | 6 -- .changeset/turn-inbound-events.md | 6 ++ .../trueforge-core/src/agent-session/index.ts | 14 ++-- .../src/agent-session/schemas/sendEvent.ts | 12 ++- .../src/agent-session/store/ISessionStore.ts | 60 ++++++--------- .../store/InMemorySessionStore.ts | 53 ++++++-------- .../agent-session/store/SessionStoreErrors.ts | 10 ++- .../agent-session/store/storeContractSuite.ts | 73 +++++++------------ ...=> 20260918_000002_turn_inbound_events.ts} | 20 ++--- .../session-store/PostgresSessionStore.ts | 28 ++++--- .../session-store/queries/inboundEvents.ts | 62 ++++++++-------- packages/trueforge/src/db/postgres/types.ts | 14 ++-- .../20260918_000002_session_inbound_events.ts | 30 -------- .../20260918_000002_turn_inbound_events.ts | 30 ++++++++ .../session-store/SqliteSessionStore.ts | 28 ++++--- .../session-store/queries/inboundEvents.ts | 66 ++++++++--------- packages/trueforge/src/db/sqlite/types.ts | 14 ++-- ...nInboundEvents.ts => turnInboundEvents.ts} | 4 +- 18 files changed, 238 insertions(+), 292 deletions(-) delete mode 100644 .changeset/session-inbound-events.md create mode 100644 .changeset/turn-inbound-events.md rename packages/trueforge/src/db/postgres/migrations/{20260918_000002_session_inbound_events.ts => 20260918_000002_turn_inbound_events.ts} (52%) delete mode 100644 packages/trueforge/src/db/sqlite/migrations/20260918_000002_session_inbound_events.ts create mode 100644 packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts rename packages/trueforge/src/db/{sessionInboundEvents.ts => turnInboundEvents.ts} (79%) diff --git a/.changeset/session-inbound-events.md b/.changeset/session-inbound-events.md deleted file mode 100644 index de1b7ec2d..000000000 --- a/.changeset/session-inbound-events.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@truefoundry/trueforge-core": patch -"@truefoundry/trueforge": patch ---- - -Add `session_inbound_events` store API for durable tip HITL send-event inbox (insert / list unconsumed / mark consumed), with Postgres and SQLite migrations. diff --git a/.changeset/turn-inbound-events.md b/.changeset/turn-inbound-events.md new file mode 100644 index 000000000..e7c335ab2 --- /dev/null +++ b/.changeset/turn-inbound-events.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge-core": patch +"@truefoundry/trueforge": patch +--- + +Add `turn_inbound_events` store API for durable tip HITL send-event inbox (insert / list unconsumed / mark consumed), with Postgres and SQLite migrations. diff --git a/packages/trueforge-core/src/agent-session/index.ts b/packages/trueforge-core/src/agent-session/index.ts index d8b492690..ece6662ae 100644 --- a/packages/trueforge-core/src/agent-session/index.ts +++ b/packages/trueforge-core/src/agent-session/index.ts @@ -21,8 +21,8 @@ export { } from './schemas/turn'; export type { TerminalTurnState, Turn, TurnInputItem, TurnMetrics, TurnState } from './schemas/turn'; -export { SessionInboundEventItemSchema } from './schemas/sendEvent'; -export type { SessionInboundEventItem } from './schemas/sendEvent'; +export { TurnInboundEventItemSchema } from './schemas/sendEvent'; +export type { TurnInboundEventItem } from './schemas/sendEvent'; export { SessionMetadataSchema, @@ -83,21 +83,21 @@ export type { GetSessionInput, GetTurnInput, ISessionStore, - InsertSessionInboundEventsInput, + InsertTurnInboundEventsInput, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, - ListUnconsumedSessionInboundEventsInput, - MarkSessionInboundEventsConsumedInput, + ListUnconsumedTurnInboundEventsInput, + MarkTurnInboundEventsConsumedInput, NewThreadInit, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, - SessionInboundEventRecord, TurnContextAppend, + TurnInboundEventRecord, TurnRecordWithoutSnapshot, UpdateSessionInput, UpdateTurnStateInput, @@ -107,12 +107,12 @@ export { PreviousTurnRunningError, SessionAlreadyExistsError, SessionExternalIdConflictError, - SessionInboundEventAlreadyExistsError, SessionNotFoundError, SessionStoreConflictError, SessionStoreInvariantError, SessionStoreNotFoundError, TurnAlreadyExistsError, + TurnInboundEventAlreadyExistsError, TurnNotFoundError, TurnNotRunningError, } from './store/SessionStoreErrors'; diff --git a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts index f25a46587..ff83878f4 100644 --- a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts +++ b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts @@ -2,16 +2,14 @@ * Inbound send-event payloads for tip HITL (client → harness), distinct from the * stream log ({@link PersistedTurnEvent} / session_event). * - * Public send is session-scoped (`POST …/sessions/{id}/events`) with required - * body `turn_id` (one batch → one tip) plus `SessionInboundEventItem`s; rows stamp that - * tip id. v1 union is tip-only; approval policies may relax `turn_id` later. - * `user.message` stays on createTurn / steer. + * Public create is turn-scoped (`POST …/sessions/{id}/turns/{turn_id}/events`) + * with `TurnInboundEventItem`s. `user.message` stays on createTurn / steer. */ import { z } from '@hono/zod-openapi'; import { UserToolApprovalMessageSchema, UserToolResponseMessageSchema } from '../../core/events/schema'; -export const SessionInboundEventItemSchema = z +export const TurnInboundEventItemSchema = z .discriminatedUnion('type', [UserToolApprovalMessageSchema, UserToolResponseMessageSchema]) - .openapi('SessionInboundEventItem'); + .openapi('TurnInboundEventItem'); -export type SessionInboundEventItem = z.infer; +export type TurnInboundEventItem = z.infer; diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index 69b74aa2f..886a84844 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -11,7 +11,7 @@ import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord } from '../models/TurnRecord'; import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { SessionInboundEventItem } from '../schemas/sendEvent'; +import type { TurnInboundEventItem } from '../schemas/sendEvent'; import type { SessionMetadata } from '../schemas/session'; import type { CancellationReason, TerminalTurnState } from '../schemas/turn'; @@ -171,23 +171,19 @@ export interface AppendToEventsInput { events: PersistedTurnEvent[]; } -/** One durable inbound send-event row (tip HITL and/or session-scoped). */ -export interface SessionInboundEventRecord { +/** One durable inbound send-event row for a tip. */ +export interface TurnInboundEventRecord { event_id: string; - /** Tip id when tip-scoped; null for session-only (e.g. future policies). */ - turn_id: string | null; - /** Validated {@link SessionInboundEventItem} body (widens when policy lands). */ - payload: SessionInboundEventItem; + turn_id: string; + /** Validated {@link TurnInboundEventItem} body. */ + payload: TurnInboundEventItem; /** ISO-8601; copied from insert input. Ordering uses `event_id`. */ created_at: string; } -export interface InsertSessionInboundEventsInput { +export interface InsertTurnInboundEventsInput { session_id: string; - /** - * Tip that receives this batch (v1 required). One send = one tip; stamp every - * row with this id. Relax to optional/null when session-scoped policies land. - */ + /** Tip that receives this batch. One send = one tip; stamp every row with this id. */ turn_id: string; /** * Caller mints `event_id` (monotonic ULID) — same contract as session_event. @@ -195,25 +191,19 @@ export interface InsertSessionInboundEventsInput { */ events: Array<{ event_id: string; - payload: SessionInboundEventItem; + payload: TurnInboundEventItem; created_at: string; }>; } -export interface ListUnconsumedSessionInboundEventsInput { +export interface ListUnconsumedTurnInboundEventsInput { session_id: string; - /** - * Three-way filter — pass the key explicitly (do not omit): - * - `undefined` — all unconsumed for the session - * - `string` — unconsumed for that turn only - * - `null` — session-scoped rows only (`turn_id` IS NULL; empty until - * policies allow null inserts) - */ - turn_id: string | null | undefined; + turn_id: string; } -export interface MarkSessionInboundEventsConsumedInput { +export interface MarkTurnInboundEventsConsumedInput { session_id: string; + turn_id: string; event_ids: string[]; } @@ -408,29 +398,25 @@ export interface ISessionStore< appendToEvents(input: AppendToEventsInput): Promise; /** - * Durable inbound send-event inbox for the session. Column stays nullable for later - * session-scoped policies. Tip must be non-terminal (v1: `running`; `paused` - * when that status lands) — terminal tip → {@link TurnNotRunningError}. - * Missing session → {@link SessionNotFoundError}; unknown turn → - * {@link TurnNotFoundError}. Duplicate `event_id` → - * {@link SessionInboundEventAlreadyExistsError}. + * Durable inbound send-event inbox for a tip. Tip must be non-terminal + * (v1: `running`; `paused` when that status lands) — terminal tip → + * {@link TurnNotRunningError}. Missing session → {@link SessionNotFoundError}; + * unknown turn → {@link TurnNotFoundError}. Duplicate `event_id` on that tip → + * {@link TurnInboundEventAlreadyExistsError}. */ - insertSessionInboundEvents(input: InsertSessionInboundEventsInput): Promise; + insertTurnInboundEvents(input: InsertTurnInboundEventsInput): Promise; /** - * Unconsumed inbox rows, ordered by monotonic `event_id` ascending. - * See {@link ListUnconsumedSessionInboundEventsInput.turn_id} for filtering. + * Unconsumed inbox rows for a tip, ordered by monotonic `event_id` ascending. * Missing session → {@link SessionNotFoundError}. */ - listUnconsumedSessionInboundEvents( - input: ListUnconsumedSessionInboundEventsInput, - ): Promise; + listUnconsumedTurnInboundEvents(input: ListUnconsumedTurnInboundEventsInput): Promise; /** - * Marks inbox rows consumed. Already-consumed or unknown ids are ignored. + * Marks inbox rows consumed for a tip. Already-consumed or unknown ids are ignored. * Empty `event_ids` is a no-op. Missing session → {@link SessionNotFoundError}. */ - markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise; + markTurnInboundEventsConsumed(input: MarkTurnInboundEventsConsumedInput): Promise; /** Adds thread snapshots to the turn (sub-agent spawns). */ addThreads(input: AddThreadsInput): Promise; diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index 7684ec873..f50eca4b2 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -4,7 +4,7 @@ import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord, TurnSnapshot } from '../models/TurnRecord'; import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { SessionInboundEventItem } from '../schemas/sendEvent'; +import type { TurnInboundEventItem } from '../schemas/sendEvent'; import type { TerminalTurnState } from '../schemas/turn'; import { assertCreateTurnThreadDelta } from './assertCreateTurnThreadDelta'; import type { @@ -19,22 +19,22 @@ import type { GetSessionByExternalIdInput, GetSessionInput, GetTurnInput, - InsertSessionInboundEventsInput, + InsertTurnInboundEventsInput, ISessionStore, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, - ListUnconsumedSessionInboundEventsInput, - MarkSessionInboundEventsConsumedInput, + ListUnconsumedTurnInboundEventsInput, + MarkTurnInboundEventsConsumedInput, NewThreadInit, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, - SessionInboundEventRecord, TurnContextAppend, + TurnInboundEventRecord, TurnRecordWithoutSnapshot, UpdateSessionInput, UpdateTurnStateInput, @@ -50,10 +50,10 @@ import { PreviousTurnRunningError, SessionAlreadyExistsError, SessionExternalIdConflictError, - SessionInboundEventAlreadyExistsError, SessionNotFoundError, SessionStoreInvariantError, TurnAlreadyExistsError, + TurnInboundEventAlreadyExistsError, TurnNotFoundError, TurnNotRunningError, } from './SessionStoreErrors'; @@ -64,8 +64,8 @@ type StoredEvent = PersistedTurnEvent; interface StoredInboundEvent { event_id: string; - turn_id: string | null; - payload: SessionInboundEventItem; + turn_id: string; + payload: TurnInboundEventItem; created_at: string; consumed: boolean; } @@ -234,8 +234,8 @@ export class InMemorySessionStore< const tKey = turnKey({ session_id: input.session_id, turn_id: turnId }); this.turns.delete(tKey); this.events.delete(tKey); + this.inboundEvents.delete(tKey); } - this.inboundEvents.delete(sessionKey(input.session_id)); this.sessions.delete(sKey); } @@ -502,22 +502,22 @@ export class InMemorySessionStore< return; } - async insertSessionInboundEvents(input: InsertSessionInboundEventsInput): Promise { + async insertTurnInboundEvents(input: InsertTurnInboundEventsInput): Promise { if (input.events.length === 0) { return; } this.requireSession(input.session_id); this.requireRunningTurn(input.session_id, input.turn_id); - const sKey = sessionKey(input.session_id); - let list = this.inboundEvents.get(sKey); + const tKey = turnKey(input); + let list = this.inboundEvents.get(tKey); if (!list) { list = []; - this.inboundEvents.set(sKey, list); + this.inboundEvents.set(tKey, list); } const existing = new Set(list.map(row => row.event_id)); for (const event of input.events) { if (existing.has(event.event_id)) { - throw new SessionInboundEventAlreadyExistsError(input.session_id, event.event_id); + throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, event.event_id); } existing.add(event.event_id); } @@ -532,24 +532,13 @@ export class InMemorySessionStore< } } - async listUnconsumedSessionInboundEvents( - input: ListUnconsumedSessionInboundEventsInput, - ): Promise { + async listUnconsumedTurnInboundEvents( + input: ListUnconsumedTurnInboundEventsInput, + ): Promise { this.requireSession(input.session_id); - const list = this.inboundEvents.get(sessionKey(input.session_id)) ?? []; + const list = this.inboundEvents.get(turnKey(input)) ?? []; return list - .filter(row => { - if (row.consumed) { - return false; - } - if (input.turn_id === undefined) { - return true; - } - if (input.turn_id === null) { - return row.turn_id === null; - } - return row.turn_id === input.turn_id; - }) + .filter(row => !row.consumed) .slice() .sort((a, b) => (a.event_id < b.event_id ? -1 : a.event_id > b.event_id ? 1 : 0)) .map(row => ({ @@ -560,12 +549,12 @@ export class InMemorySessionStore< })); } - async markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise { + async markTurnInboundEventsConsumed(input: MarkTurnInboundEventsConsumedInput): Promise { if (input.event_ids.length === 0) { return; } this.requireSession(input.session_id); - const list = this.inboundEvents.get(sessionKey(input.session_id)); + const list = this.inboundEvents.get(turnKey(input)); if (!list) { return; } diff --git a/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts b/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts index b3829cac6..d1420305b 100644 --- a/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts +++ b/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts @@ -74,14 +74,16 @@ export class TurnAlreadyExistsError extends SessionStoreConflictError { } } -export class SessionInboundEventAlreadyExistsError extends SessionStoreConflictError { +export class TurnInboundEventAlreadyExistsError extends SessionStoreConflictError { readonly session_id: string; + readonly turn_id: string; readonly event_id: string; - constructor(session_id: string, event_id: string, options?: ErrorOptions) { - super(`Session inbound event already exists: ${session_id}/${event_id}`, options); - this.name = 'SessionInboundEventAlreadyExistsError'; + constructor(session_id: string, turn_id: string, event_id: string, options?: ErrorOptions) { + super(`Turn inbound event already exists: ${session_id}/${turn_id}/${event_id}`, options); + this.name = 'TurnInboundEventAlreadyExistsError'; this.session_id = session_id; + this.turn_id = turn_id; this.event_id = event_id; } } diff --git a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts index 1cc25afc5..aecc9c5ae 100644 --- a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts +++ b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts @@ -688,7 +688,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }), ).rejects.toBeInstanceOf(TurnNotFoundError); await expect( - store.insertSessionInboundEvents({ + store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [ @@ -706,7 +706,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }), ).rejects.toBeInstanceOf(SessionNotFoundError); await expect( - store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: undefined }), + store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1' }), ).rejects.toBeInstanceOf(SessionNotFoundError); await expect( store.listSessionEvents({ @@ -2229,7 +2229,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { expect(data.map(e => e.id)).toEqual([created.id, model.id]); }); - it('session_inbound_events: insert, list unconsumed, mark consumed, duplicate id', async () => { + it('turn_inbound_events: insert, list unconsumed, mark consumed, duplicate id', async () => { const store = createStore(); await seedSession(store); await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); @@ -2255,13 +2255,13 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { created_at: new Date().toISOString(), }; - await store.insertSessionInboundEvents({ + await store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [later, earlier], }); - let pending = await store.listUnconsumedSessionInboundEvents({ + let pending = await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', }); @@ -2269,24 +2269,25 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { expect(pending[0]?.payload).toEqual(earlier.payload); expect(pending[0]?.turn_id).toBe('turn-1'); - await store.markSessionInboundEventsConsumed({ + await store.markTurnInboundEventsConsumed({ session_id: sessionId, + turn_id: 'turn-1', event_ids: [earlier.event_id], }); - pending = await store.listUnconsumedSessionInboundEvents({ + pending = await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', }); expect(pending.map(e => e.event_id)).toEqual([later.event_id]); await expect( - store.insertSessionInboundEvents({ + store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [later], }), ).rejects.toMatchObject({ - name: 'SessionInboundEventAlreadyExistsError', + name: 'TurnInboundEventAlreadyExistsError', event_id: later.event_id, }); @@ -2302,24 +2303,24 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { created_at: new Date().toISOString(), }; await expect( - store.insertSessionInboundEvents({ + store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [fresh, later], }), ).rejects.toMatchObject({ - name: 'SessionInboundEventAlreadyExistsError', + name: 'TurnInboundEventAlreadyExistsError', event_id: later.event_id, }); expect( - (await store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( + (await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( e => e.event_id, ), ).toEqual([later.event_id]); const dupId = 'evt-dup'; await expect( - store.insertSessionInboundEvents({ + store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [ @@ -2346,12 +2347,12 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ], }), ).rejects.toMatchObject({ - name: 'SessionInboundEventAlreadyExistsError', + name: 'TurnInboundEventAlreadyExistsError', event_id: dupId, }); // Failed batch must not leave a partial row (SQL PK is all-or-nothing). expect( - (await store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( + (await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( e => e.event_id, ), ).toEqual([later.event_id]); @@ -2359,7 +2360,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { // Terminal tip rejects inbox writes. await finishTurn(store, 'turn-1'); await expect( - store.insertSessionInboundEvents({ + store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [ @@ -2377,13 +2378,13 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }), ).rejects.toBeInstanceOf(TurnNotRunningError); expect( - (await store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( + (await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1' })).map( e => e.event_id, ), ).toEqual([later.event_id]); }); - it('session_inbound_events: list filter turn_id string | null | undefined', async () => { + it('turn_inbound_events: list is turn-scoped', async () => { const store = createStore(); await seedSession(store); await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-a' })); @@ -2398,7 +2399,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }, created_at: new Date().toISOString(), }; - await store.insertSessionInboundEvents({ + await store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-a', events: [forA], @@ -2420,16 +2421,15 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { created_at: new Date().toISOString(), }; - await store.insertSessionInboundEvents({ + await store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-b', events: [forB], }); - // string — that turn only expect( ( - await store.listUnconsumedSessionInboundEvents({ + await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-a', }) @@ -2437,40 +2437,19 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ).toEqual([forA.event_id]); expect( ( - await store.listUnconsumedSessionInboundEvents({ + await store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-b', }) ).map(e => e.event_id), ).toEqual([forB.event_id]); - - // null — session-scoped only (v1 insert always sets turn_id; no such rows yet) - expect( - ( - await store.listUnconsumedSessionInboundEvents({ - session_id: sessionId, - turn_id: null, - }) - ).map(e => e.event_id), - ).toEqual([]); - - // omitted — all unconsumed, ordered by event_id - // undefined — all unconsumed, ordered by event_id - expect( - ( - await store.listUnconsumedSessionInboundEvents({ - session_id: sessionId, - turn_id: undefined, - }) - ).map(e => e.event_id), - ).toEqual([forA.event_id, forB.event_id]); }); - it('session_inbound_events cascade away with deleteSession', async () => { + it('turn_inbound_events cascade away with deleteSession', async () => { const store = createStore(); await seedSession(store); await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); - await store.insertSessionInboundEvents({ + await store.insertTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1', events: [ @@ -2488,7 +2467,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { }); await store.deleteSession({ tenant_id: tenant, session_id: sessionId }); await expect( - store.listUnconsumedSessionInboundEvents({ session_id: sessionId, turn_id: undefined }), + store.listUnconsumedTurnInboundEvents({ session_id: sessionId, turn_id: 'turn-1' }), ).rejects.toBeInstanceOf(SessionNotFoundError); }); diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000002_session_inbound_events.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts similarity index 52% rename from packages/trueforge/src/db/postgres/migrations/20260918_000002_session_inbound_events.ts rename to packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts index 1bf4f1833..54af1de25 100644 --- a/packages/trueforge/src/db/postgres/migrations/20260918_000002_session_inbound_events.ts +++ b/packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts @@ -1,37 +1,37 @@ import { sql, type Kysely } from 'kysely'; /** - * Session inbound send-event inbox (tip HITL + future session-scoped payloads). - * `turn_id` column nullable (v1 insert always sets it; null reserved for session-only policies later). + * Turn-scoped inbound send-event inbox (tip HITL / policies). + * `turn_id` is required — every row belongs to a tip. */ export async function up(db: Kysely): Promise { await sql`SET LOCAL lock_timeout = '5s'`.execute(db); await db.schema - .createTable('session_inbound_events') + .createTable('turn_inbound_events') .addColumn('session_id', 'text', col => col.notNull()) + .addColumn('turn_id', 'text', col => col.notNull()) .addColumn('event_id', 'text', col => col.notNull()) - .addColumn('turn_id', 'text') .addColumn('payload', 'jsonb', col => col.notNull()) .addColumn('consumed', 'boolean', col => col.notNull().defaultTo(false)) .addColumn('created_at', 'timestamptz', col => col.notNull()) - .addPrimaryKeyConstraint('session_inbound_events_pkey', ['session_id', 'event_id']) + .addPrimaryKeyConstraint('turn_inbound_events_pkey', ['session_id', 'turn_id', 'event_id']) .execute(); await db.schema - .alterTable('session_inbound_events') - .addForeignKeyConstraint('session_inbound_events_session_fkey', ['session_id'], 'session', ['session_id']) + .alterTable('turn_inbound_events') + .addForeignKeyConstraint('turn_inbound_events_session_fkey', ['session_id'], 'session', ['session_id']) .onDelete('cascade') .execute(); await sql` - CREATE INDEX session_inbound_events_unconsumed_idx - ON session_inbound_events (session_id, turn_id, event_id) + CREATE INDEX turn_inbound_events_unconsumed_idx + ON turn_inbound_events (session_id, turn_id, event_id) WHERE consumed = false `.execute(db); } export async function down(db: Kysely): Promise { await sql`SET LOCAL lock_timeout = '5s'`.execute(db); - await db.schema.dropTable('session_inbound_events').ifExists().cascade().execute(); + await db.schema.dropTable('turn_inbound_events').ifExists().cascade().execute(); } diff --git a/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts b/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts index 8a254caa1..737847194 100644 --- a/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts +++ b/packages/trueforge/src/db/postgres/session-store/PostgresSessionStore.ts @@ -21,20 +21,20 @@ import type { GetSessionByExternalIdInput, GetSessionInput, GetTurnInput, - InsertSessionInboundEventsInput, + InsertTurnInboundEventsInput, ISessionStore, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, - ListUnconsumedSessionInboundEventsInput, - MarkSessionInboundEventsConsumedInput, + ListUnconsumedTurnInboundEventsInput, + MarkTurnInboundEventsConsumedInput, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, - SessionInboundEventRecord, + TurnInboundEventRecord, TurnRecordWithoutSnapshot, UpdateSessionInput, UpdateTurnStateInput, @@ -57,9 +57,9 @@ import { listTurnEvents as listTurnEventsQuery, } from './queries/events'; import { - insertSessionInboundEvents as insertSessionInboundEventsQuery, - listUnconsumedSessionInboundEvents as listUnconsumedSessionInboundEventsQuery, - markSessionInboundEventsConsumed as markSessionInboundEventsConsumedQuery, + insertTurnInboundEvents as insertTurnInboundEventsQuery, + listUnconsumedTurnInboundEvents as listUnconsumedTurnInboundEventsQuery, + markTurnInboundEventsConsumed as markTurnInboundEventsConsumedQuery, } from './queries/inboundEvents'; import { createSession as createSessionQuery, @@ -225,18 +225,16 @@ export class PostgresSessionStore implements ISessionStore { - return insertSessionInboundEventsQuery(this.db, input); + insertTurnInboundEvents(input: InsertTurnInboundEventsInput): Promise { + return insertTurnInboundEventsQuery(this.db, input); } - listUnconsumedSessionInboundEvents( - input: ListUnconsumedSessionInboundEventsInput, - ): Promise { - return listUnconsumedSessionInboundEventsQuery(this.db, input); + listUnconsumedTurnInboundEvents(input: ListUnconsumedTurnInboundEventsInput): Promise { + return listUnconsumedTurnInboundEventsQuery(this.db, input); } - markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise { - return markSessionInboundEventsConsumedQuery(this.db, input); + markTurnInboundEventsConsumed(input: MarkTurnInboundEventsConsumedInput): Promise { + return markTurnInboundEventsConsumedQuery(this.db, input); } addThreads(input: AddThreadsInput): Promise { diff --git a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts index da3a6f1a3..043127b6a 100644 --- a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts @@ -1,18 +1,18 @@ import type { - InsertSessionInboundEventsInput, - ListUnconsumedSessionInboundEventsInput, - MarkSessionInboundEventsConsumedInput, - SessionInboundEventRecord, + InsertTurnInboundEventsInput, + ListUnconsumedTurnInboundEventsInput, + MarkTurnInboundEventsConsumedInput, + TurnInboundEventRecord, } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; import { - SessionInboundEventAlreadyExistsError, SessionNotFoundError, + TurnInboundEventAlreadyExistsError, TurnNotFoundError, TurnNotRunningError, } from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; import type { Kysely } from 'kysely'; import { sql } from 'kysely'; -import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../sessionInboundEvents'; +import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../turnInboundEvents'; import { isUniqueViolation } from '../../client'; import { json } from '../../sqlExpressions'; import type { Database } from '../../types'; @@ -47,24 +47,26 @@ async function requireTurn(db: Kysely, sessionId: string, turnId: stri async function resolveCollidingEventId( db: Kysely, sessionId: string, - events: InsertSessionInboundEventsInput['events'], + turnId: string, + events: InsertTurnInboundEventsInput['events'], ): Promise { const ids = [...new Set(events.map(e => e.event_id))]; if (ids.length === 0) { return ''; } const rows = await db - .selectFrom('session_inbound_events') + .selectFrom('turn_inbound_events') .select('event_id') .where('session_id', '=', sessionId) + .where('turn_id', '=', turnId) .where('event_id', 'in', ids) .execute(); return firstCollidingEventId(events, new Set(rows.map(r => r.event_id))); } -export async function insertSessionInboundEvents( +export async function insertTurnInboundEvents( db: Kysely, - input: InsertSessionInboundEventsInput, + input: InsertTurnInboundEventsInput, ): Promise { if (input.events.length === 0) { return; @@ -74,17 +76,17 @@ export async function insertSessionInboundEvents( const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); if (duplicateInBatch !== undefined) { - throw new SessionInboundEventAlreadyExistsError(input.session_id, duplicateInBatch); + throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, duplicateInBatch); } try { await db - .insertInto('session_inbound_events') + .insertInto('turn_inbound_events') .values( input.events.map(event => ({ session_id: input.session_id, - event_id: event.event_id, turn_id: input.turn_id, + event_id: event.event_id, payload: json(event.payload), consumed: false, created_at: sql`${event.created_at}::timestamptz`, @@ -93,8 +95,8 @@ export async function insertSessionInboundEvents( .execute(); } catch (error) { if (isUniqueViolation(error)) { - const eventId = await resolveCollidingEventId(db, input.session_id, input.events); - throw new SessionInboundEventAlreadyExistsError(input.session_id, eventId, { + const eventId = await resolveCollidingEventId(db, input.session_id, input.turn_id, input.events); + throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, eventId, { cause: error, }); } @@ -102,25 +104,20 @@ export async function insertSessionInboundEvents( } } -export async function listUnconsumedSessionInboundEvents( +export async function listUnconsumedTurnInboundEvents( db: Kysely, - input: ListUnconsumedSessionInboundEventsInput, -): Promise { + input: ListUnconsumedTurnInboundEventsInput, +): Promise { await requireSession(db, input.session_id); - let query = db - .selectFrom('session_inbound_events') + const rows = await db + .selectFrom('turn_inbound_events') .select(['event_id', 'turn_id', 'payload', 'created_at']) .where('session_id', '=', input.session_id) - .where('consumed', '=', false); - - if (input.turn_id === null) { - query = query.where('turn_id', 'is', null); - } else if (input.turn_id !== undefined) { - query = query.where('turn_id', '=', input.turn_id); - } - - const rows = await query.orderBy('event_id', 'asc').execute(); + .where('turn_id', '=', input.turn_id) + .where('consumed', '=', false) + .orderBy('event_id', 'asc') + .execute(); return rows.map(row => ({ event_id: row.event_id, @@ -130,9 +127,9 @@ export async function listUnconsumedSessionInboundEvents( })); } -export async function markSessionInboundEventsConsumed( +export async function markTurnInboundEventsConsumed( db: Kysely, - input: MarkSessionInboundEventsConsumedInput, + input: MarkTurnInboundEventsConsumedInput, ): Promise { if (input.event_ids.length === 0) { return; @@ -140,9 +137,10 @@ export async function markSessionInboundEventsConsumed( await requireSession(db, input.session_id); await db - .updateTable('session_inbound_events') + .updateTable('turn_inbound_events') .set({ consumed: true }) .where('session_id', '=', input.session_id) + .where('turn_id', '=', input.turn_id) .where('event_id', 'in', input.event_ids) .execute(); } diff --git a/packages/trueforge/src/db/postgres/types.ts b/packages/trueforge/src/db/postgres/types.ts index 342f6f376..c964b39e2 100644 --- a/packages/trueforge/src/db/postgres/types.ts +++ b/packages/trueforge/src/db/postgres/types.ts @@ -6,10 +6,10 @@ import type { AgentSpec, CreatedBySubject, PersistedTurnEvent, - SessionInboundEventItem, SessionMetadata, SessionMetrics, SessionSource, + TurnInboundEventItem, TurnInputItem, TurnState, } from '@truefoundry/trueforge-core/agent-session'; @@ -251,14 +251,14 @@ export interface SessionEventTable { } /** - * Session inbound send-event inbox (tip HITL + future session-scoped payloads). - * PRIMARY KEY (session_id, event_id). `turn_id` nullable. + * Turn-scoped inbound send-event inbox. + * PRIMARY KEY (session_id, turn_id, event_id). */ -export interface SessionInboundEventsTable { +export interface TurnInboundEventsTable { session_id: string; + turn_id: string; event_id: string; - turn_id: string | null; - payload: JSONColumnType; + payload: JSONColumnType; consumed: boolean; created_at: Date; } @@ -534,7 +534,7 @@ export interface Database { turn: TurnTable; turn_thread: TurnThreadTable; session_event: SessionEventTable; - session_inbound_events: SessionInboundEventsTable; + turn_inbound_events: TurnInboundEventsTable; thread_context_log: ThreadContextLogTable; thread_capability_state: ThreadCapabilityStateTable; model_provider: ModelProviderTable; diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_session_inbound_events.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_session_inbound_events.ts deleted file mode 100644 index e7fc8ddcc..000000000 --- a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_session_inbound_events.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { type Kysely, sql } from 'kysely'; - -/** - * Session inbound send-event inbox (tip HITL + future session-scoped payloads). - * `turn_id` column nullable (v1 insert always sets it; null reserved for session-only policies later). - */ -export async function up(db: Kysely): Promise { - await sql` - CREATE TABLE session_inbound_events ( - session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE, - event_id TEXT NOT NULL, - turn_id TEXT, - payload BLOB NOT NULL, - consumed INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - PRIMARY KEY (session_id, event_id) - ) STRICT - `.execute(db); - - await sql` - CREATE INDEX session_inbound_events_unconsumed_idx - ON session_inbound_events (session_id, turn_id, event_id) - WHERE consumed = 0 - `.execute(db); -} - -export async function down(db: Kysely): Promise { - await sql`DROP INDEX IF EXISTS session_inbound_events_unconsumed_idx`.execute(db); - await sql`DROP TABLE IF EXISTS session_inbound_events`.execute(db); -} diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts new file mode 100644 index 000000000..d1d6d2055 --- /dev/null +++ b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts @@ -0,0 +1,30 @@ +import { type Kysely, sql } from 'kysely'; + +/** + * Turn-scoped inbound send-event inbox (tip HITL / policies). + * `turn_id` is required — every row belongs to a tip. + */ +export async function up(db: Kysely): Promise { + await sql` + CREATE TABLE turn_inbound_events ( + session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE, + turn_id TEXT NOT NULL, + event_id TEXT NOT NULL, + payload BLOB NOT NULL, + consumed INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + PRIMARY KEY (session_id, turn_id, event_id) + ) STRICT + `.execute(db); + + await sql` + CREATE INDEX turn_inbound_events_unconsumed_idx + ON turn_inbound_events (session_id, turn_id, event_id) + WHERE consumed = 0 + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`DROP INDEX IF EXISTS turn_inbound_events_unconsumed_idx`.execute(db); + await sql`DROP TABLE IF EXISTS turn_inbound_events`.execute(db); +} diff --git a/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts b/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts index e4efa84b3..8f430d0e4 100644 --- a/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts +++ b/packages/trueforge/src/db/sqlite/session-store/SqliteSessionStore.ts @@ -14,20 +14,20 @@ import type { GetSessionByExternalIdInput, GetSessionInput, GetTurnInput, - InsertSessionInboundEventsInput, + InsertTurnInboundEventsInput, ISessionStore, ListSessionEventsInput, ListSessionsInput, ListTurnEventsInput, ListTurnsInput, - ListUnconsumedSessionInboundEventsInput, - MarkSessionInboundEventsConsumedInput, + ListUnconsumedTurnInboundEventsInput, + MarkTurnInboundEventsConsumedInput, OverwriteThreadContextInput, PatchMCPServersInput, PatchSandboxInfoInput, PatchThreadCapabilityStateInput, RemoveThreadsInput, - SessionInboundEventRecord, + TurnInboundEventRecord, TurnRecordWithoutSnapshot, UpdateSessionInput, UpdateTurnStateInput, @@ -45,9 +45,9 @@ import { listTurnEvents as listTurnEventsQuery, } from './queries/events'; import { - insertSessionInboundEvents as insertSessionInboundEventsQuery, - listUnconsumedSessionInboundEvents as listUnconsumedSessionInboundEventsQuery, - markSessionInboundEventsConsumed as markSessionInboundEventsConsumedQuery, + insertTurnInboundEvents as insertTurnInboundEventsQuery, + listUnconsumedTurnInboundEvents as listUnconsumedTurnInboundEventsQuery, + markTurnInboundEventsConsumed as markTurnInboundEventsConsumedQuery, } from './queries/inboundEvents'; import { createSession as createSessionQuery, @@ -200,18 +200,16 @@ export class SqliteSessionStore implements ISessionStore { - return insertSessionInboundEventsQuery(this.db, input); + insertTurnInboundEvents(input: InsertTurnInboundEventsInput): Promise { + return insertTurnInboundEventsQuery(this.db, input); } - listUnconsumedSessionInboundEvents( - input: ListUnconsumedSessionInboundEventsInput, - ): Promise { - return listUnconsumedSessionInboundEventsQuery(this.db, input); + listUnconsumedTurnInboundEvents(input: ListUnconsumedTurnInboundEventsInput): Promise { + return listUnconsumedTurnInboundEventsQuery(this.db, input); } - markSessionInboundEventsConsumed(input: MarkSessionInboundEventsConsumedInput): Promise { - return markSessionInboundEventsConsumedQuery(this.db, input); + markTurnInboundEventsConsumed(input: MarkTurnInboundEventsConsumedInput): Promise { + return markTurnInboundEventsConsumedQuery(this.db, input); } addThreads(input: AddThreadsInput): Promise { diff --git a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts index 108a965bf..4ac8f8075 100644 --- a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts @@ -1,19 +1,19 @@ -import type { SessionInboundEventItem, TurnState } from '@truefoundry/trueforge-core/agent-session'; +import type { TurnInboundEventItem, TurnState } from '@truefoundry/trueforge-core/agent-session'; import type { - InsertSessionInboundEventsInput, - ListUnconsumedSessionInboundEventsInput, - MarkSessionInboundEventsConsumedInput, - SessionInboundEventRecord, + InsertTurnInboundEventsInput, + ListUnconsumedTurnInboundEventsInput, + MarkTurnInboundEventsConsumedInput, + TurnInboundEventRecord, } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; import { - SessionInboundEventAlreadyExistsError, SessionNotFoundError, + TurnInboundEventAlreadyExistsError, TurnNotFoundError, TurnNotRunningError, } from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; import type { Kysely } from 'kysely'; import { sql } from 'kysely'; -import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../sessionInboundEvents'; +import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../turnInboundEvents'; import { isUniqueViolation } from '../../client'; import { jsonbBind, jsonText } from '../../sqlExpressions'; import type { Database } from '../../types'; @@ -48,24 +48,26 @@ async function requireTurn(db: Kysely, sessionId: string, turnId: stri async function resolveCollidingEventId( db: Kysely, sessionId: string, - events: InsertSessionInboundEventsInput['events'], + turnId: string, + events: InsertTurnInboundEventsInput['events'], ): Promise { const ids = [...new Set(events.map(e => e.event_id))]; if (ids.length === 0) { return ''; } const rows = await db - .selectFrom('session_inbound_events') + .selectFrom('turn_inbound_events') .select('event_id') .where('session_id', '=', sessionId) + .where('turn_id', '=', turnId) .where('event_id', 'in', ids) .execute(); return firstCollidingEventId(events, new Set(rows.map(r => r.event_id))); } -export async function insertSessionInboundEvents( +export async function insertTurnInboundEvents( db: Kysely, - input: InsertSessionInboundEventsInput, + input: InsertTurnInboundEventsInput, ): Promise { if (input.events.length === 0) { return; @@ -75,17 +77,17 @@ export async function insertSessionInboundEvents( const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); if (duplicateInBatch !== undefined) { - throw new SessionInboundEventAlreadyExistsError(input.session_id, duplicateInBatch); + throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, duplicateInBatch); } try { await db - .insertInto('session_inbound_events') + .insertInto('turn_inbound_events') .values( input.events.map(event => ({ session_id: input.session_id, - event_id: event.event_id, turn_id: input.turn_id, + event_id: event.event_id, payload: jsonbBind(event.payload), consumed: 0, created_at: event.created_at, @@ -94,8 +96,8 @@ export async function insertSessionInboundEvents( .execute(); } catch (error) { if (isUniqueViolation(error)) { - const eventId = await resolveCollidingEventId(db, input.session_id, input.events); - throw new SessionInboundEventAlreadyExistsError(input.session_id, eventId, { + const eventId = await resolveCollidingEventId(db, input.session_id, input.turn_id, input.events); + throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, eventId, { cause: error, }); } @@ -103,25 +105,20 @@ export async function insertSessionInboundEvents( } } -export async function listUnconsumedSessionInboundEvents( +export async function listUnconsumedTurnInboundEvents( db: Kysely, - input: ListUnconsumedSessionInboundEventsInput, -): Promise { + input: ListUnconsumedTurnInboundEventsInput, +): Promise { await requireSession(db, input.session_id); - let query = db - .selectFrom('session_inbound_events') - .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) + const rows = await db + .selectFrom('turn_inbound_events') + .select(['event_id', 'turn_id', 'created_at', jsonText(sql.ref('payload')).as('payload')]) .where('session_id', '=', input.session_id) - .where('consumed', '=', 0); - - if (input.turn_id === null) { - query = query.where('turn_id', 'is', null); - } else if (input.turn_id !== undefined) { - query = query.where('turn_id', '=', input.turn_id); - } - - const rows = await query.orderBy('event_id', 'asc').execute(); + .where('turn_id', '=', input.turn_id) + .where('consumed', '=', 0) + .orderBy('event_id', 'asc') + .execute(); return rows.map(row => ({ event_id: row.event_id, @@ -131,9 +128,9 @@ export async function listUnconsumedSessionInboundEvents( })); } -export async function markSessionInboundEventsConsumed( +export async function markTurnInboundEventsConsumed( db: Kysely, - input: MarkSessionInboundEventsConsumedInput, + input: MarkTurnInboundEventsConsumedInput, ): Promise { if (input.event_ids.length === 0) { return; @@ -141,9 +138,10 @@ export async function markSessionInboundEventsConsumed( await requireSession(db, input.session_id); await db - .updateTable('session_inbound_events') + .updateTable('turn_inbound_events') .set({ consumed: 1 }) .where('session_id', '=', input.session_id) + .where('turn_id', '=', input.turn_id) .where('event_id', 'in', input.event_ids) .execute(); } diff --git a/packages/trueforge/src/db/sqlite/types.ts b/packages/trueforge/src/db/sqlite/types.ts index befe4af55..f8576b8d8 100644 --- a/packages/trueforge/src/db/sqlite/types.ts +++ b/packages/trueforge/src/db/sqlite/types.ts @@ -9,10 +9,10 @@ import type { AgentSpec, CreatedBySubject, PersistedTurnEvent, - SessionInboundEventItem, SessionMetadata, SessionMetrics, SessionSource, + TurnInboundEventItem, TurnInputItem, TurnState, } from '@truefoundry/trueforge-core/agent-session'; @@ -149,15 +149,15 @@ export interface SessionEventTable { } /** - * Session inbound send-event inbox (tip HITL + future session-scoped payloads). - * PRIMARY KEY (session_id, event_id). `turn_id` nullable. + * Turn-scoped inbound send-event inbox. + * PRIMARY KEY (session_id, turn_id, event_id). * `consumed` is INTEGER 0/1 (STRICT has no boolean). */ -export interface SessionInboundEventsTable { +export interface TurnInboundEventsTable { session_id: string; + turn_id: string; event_id: string; - turn_id: string | null; - payload: JsonbColumn; + payload: JsonbColumn; consumed: number; created_at: string; } @@ -355,7 +355,7 @@ export interface Database { turn_thread: TurnThreadTable; turn_thread_context: TurnThreadContextTable; session_event: SessionEventTable; - session_inbound_events: SessionInboundEventsTable; + turn_inbound_events: TurnInboundEventsTable; thread_context_log: ThreadContextLogTable; thread_capability_state: ThreadCapabilityStateTable; model_provider: ModelProviderTable; diff --git a/packages/trueforge/src/db/sessionInboundEvents.ts b/packages/trueforge/src/db/turnInboundEvents.ts similarity index 79% rename from packages/trueforge/src/db/sessionInboundEvents.ts rename to packages/trueforge/src/db/turnInboundEvents.ts index 2e598acf5..ac21c3fad 100644 --- a/packages/trueforge/src/db/sessionInboundEvents.ts +++ b/packages/trueforge/src/db/turnInboundEvents.ts @@ -1,6 +1,6 @@ -import type { InsertSessionInboundEventsInput } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; +import type { InsertTurnInboundEventsInput } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; -type InboundInsertEvent = InsertSessionInboundEventsInput['events'][number]; +type InboundInsertEvent = InsertTurnInboundEventsInput['events'][number]; /** First repeated `event_id` in the batch (input order), if any. */ export function firstDuplicateEventIdInBatch( From 37196649508f59bc38a8de0ad63c160fef9a3d10 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Tue, 22 Sep 2026 14:59:43 +0530 Subject: [PATCH 08/23] address comments --- .../trueforge-core/src/agent-session/index.ts | 2 +- .../src/agent-session/store/ISessionStore.ts | 2 +- .../store/InMemorySessionStore.ts | 8 +- .../agent-session/store/SessionStoreErrors.ts | 14 ++-- .../agent-session/store/storeContractSuite.ts | 52 ++++++++++++- .../session-store/queries/inboundEvents.ts | 74 +++++++++--------- .../20260918_000002_turn_inbound_events.ts | 41 +++++----- .../session-store/queries/inboundEvents.ts | 76 +++++++++---------- 8 files changed, 162 insertions(+), 107 deletions(-) diff --git a/packages/trueforge-core/src/agent-session/index.ts b/packages/trueforge-core/src/agent-session/index.ts index ece6662ae..436503a76 100644 --- a/packages/trueforge-core/src/agent-session/index.ts +++ b/packages/trueforge-core/src/agent-session/index.ts @@ -112,7 +112,7 @@ export { SessionStoreInvariantError, SessionStoreNotFoundError, TurnAlreadyExistsError, - TurnInboundEventAlreadyExistsError, + TurnEventAlreadyExistsError, TurnNotFoundError, TurnNotRunningError, } from './store/SessionStoreErrors'; diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index 886a84844..ce039b508 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -402,7 +402,7 @@ export interface ISessionStore< * (v1: `running`; `paused` when that status lands) — terminal tip → * {@link TurnNotRunningError}. Missing session → {@link SessionNotFoundError}; * unknown turn → {@link TurnNotFoundError}. Duplicate `event_id` on that tip → - * {@link TurnInboundEventAlreadyExistsError}. + * {@link TurnEventAlreadyExistsError}. */ insertTurnInboundEvents(input: InsertTurnInboundEventsInput): Promise; diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index f50eca4b2..a537c8463 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -53,7 +53,7 @@ import { SessionNotFoundError, SessionStoreInvariantError, TurnAlreadyExistsError, - TurnInboundEventAlreadyExistsError, + TurnEventAlreadyExistsError, TurnNotFoundError, TurnNotRunningError, } from './SessionStoreErrors'; @@ -517,7 +517,11 @@ export class InMemorySessionStore< const existing = new Set(list.map(row => row.event_id)); for (const event of input.events) { if (existing.has(event.event_id)) { - throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, event.event_id); + throw new TurnEventAlreadyExistsError({ + session_id: input.session_id, + turn_id: input.turn_id, + event_id: event.event_id, + }); } existing.add(event.event_id); } diff --git a/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts b/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts index d1420305b..3d6b8d3cb 100644 --- a/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts +++ b/packages/trueforge-core/src/agent-session/store/SessionStoreErrors.ts @@ -74,17 +74,17 @@ export class TurnAlreadyExistsError extends SessionStoreConflictError { } } -export class TurnInboundEventAlreadyExistsError extends SessionStoreConflictError { +export class TurnEventAlreadyExistsError extends SessionStoreConflictError { readonly session_id: string; readonly turn_id: string; readonly event_id: string; - constructor(session_id: string, turn_id: string, event_id: string, options?: ErrorOptions) { - super(`Turn inbound event already exists: ${session_id}/${turn_id}/${event_id}`, options); - this.name = 'TurnInboundEventAlreadyExistsError'; - this.session_id = session_id; - this.turn_id = turn_id; - this.event_id = event_id; + constructor(input: { session_id: string; turn_id: string; event_id: string }, options?: ErrorOptions) { + super(`Turn event already exists: ${input.session_id}/${input.turn_id}/${input.event_id}`, options); + this.name = 'TurnEventAlreadyExistsError'; + this.session_id = input.session_id; + this.turn_id = input.turn_id; + this.event_id = input.event_id; } } diff --git a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts index aecc9c5ae..402f98f26 100644 --- a/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts +++ b/packages/trueforge-core/tests/agent-session/store/storeContractSuite.ts @@ -2287,7 +2287,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { events: [later], }), ).rejects.toMatchObject({ - name: 'TurnInboundEventAlreadyExistsError', + name: 'TurnEventAlreadyExistsError', event_id: later.event_id, }); @@ -2309,7 +2309,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { events: [fresh, later], }), ).rejects.toMatchObject({ - name: 'TurnInboundEventAlreadyExistsError', + name: 'TurnEventAlreadyExistsError', event_id: later.event_id, }); expect( @@ -2347,7 +2347,7 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ], }), ).rejects.toMatchObject({ - name: 'TurnInboundEventAlreadyExistsError', + name: 'TurnEventAlreadyExistsError', event_id: dupId, }); // Failed batch must not leave a partial row (SQL PK is all-or-nothing). @@ -2384,6 +2384,52 @@ export function runStoreContractSuite(createStore: () => ISessionStore) { ).toEqual([later.event_id]); }); + it('concurrent freeze x insertTurnInboundEvents: terminal tip rejects or insert wins cleanly', async () => { + const store = createStore(); + await seedSession(store); + await store.createTurn(makeCreateTurnInput({ sessionId, turnId: 'turn-1' })); + const cancelledState = makeCancelledTurnState(CancellationReason.CancelledForNextTurn); + const inbound = { + event_id: 'evt-race', + payload: { + type: 'user.tool_approval' as const, + thread_id: 'main', + tool_call_id: 'tc-race', + approval: { status: 'allow' as const }, + }, + created_at: new Date().toISOString(), + }; + + const results = await Promise.allSettled([ + store.freezeAndGetTurn({ + session_id: sessionId, + turn_id: 'turn-1', + reason: CancellationReason.CancelledForNextTurn, + turn_done_event: makeTurnDoneEvent(cancelledState), + }), + store.insertTurnInboundEvents({ + session_id: sessionId, + turn_id: 'turn-1', + events: [inbound], + }), + ]); + + const insertResult = results[1]; + const pending = await store.listUnconsumedTurnInboundEvents({ + session_id: sessionId, + turn_id: 'turn-1', + }); + if (insertResult?.status === 'fulfilled') { + expect(pending.map(e => e.event_id)).toEqual([inbound.event_id]); + } else { + expect(insertResult?.reason).toBeInstanceOf(TurnNotRunningError); + expect(pending).toEqual([]); + } + + const turn = await store.getTurn({ session_id: sessionId, turn_id: 'turn-1' }); + expect(mustGet(turn).state.status).toBe('cancelled'); + }); + it('turn_inbound_events: list is turn-scoped', async () => { const store = createStore(); await seedSession(store); diff --git a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts index 043127b6a..9bd4e00da 100644 --- a/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/postgres/session-store/queries/inboundEvents.ts @@ -6,9 +6,7 @@ import type { } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; import { SessionNotFoundError, - TurnInboundEventAlreadyExistsError, - TurnNotFoundError, - TurnNotRunningError, + TurnEventAlreadyExistsError, } from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; import type { Kysely } from 'kysely'; import { sql } from 'kysely'; @@ -16,6 +14,7 @@ import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../tu import { isUniqueViolation } from '../../client'; import { json } from '../../sqlExpressions'; import type { Database } from '../../types'; +import { assertTurnRunning, type TurnKeys } from './turns'; async function requireSession(db: Kysely, sessionId: string): Promise { const row = await db @@ -28,22 +27,6 @@ async function requireSession(db: Kysely, sessionId: string): Promise< } } -/** Exists + non-terminal (v1: `running` only; `paused` will be allowed when that status lands). */ -async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { - const row = await db - .selectFrom('turn') - .select(['turn_id', 'state']) - .where('session_id', '=', sessionId) - .where('turn_id', '=', turnId) - .executeTakeFirst(); - if (!row) { - throw new TurnNotFoundError(turnId); - } - if (row.state.status !== 'running') { - throw new TurnNotRunningError(turnId, row.state); - } -} - async function resolveCollidingEventId( db: Kysely, sessionId: string, @@ -72,33 +55,50 @@ export async function insertTurnInboundEvents( return; } await requireSession(db, input.session_id); - await requireTurn(db, input.session_id, input.turn_id); const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); if (duplicateInBatch !== undefined) { - throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, duplicateInBatch); + throw new TurnEventAlreadyExistsError({ + session_id: input.session_id, + turn_id: input.turn_id, + event_id: duplicateInBatch, + }); } + const keys: TurnKeys = { + session_id: input.session_id, + turn_id: input.turn_id, + }; + try { - await db - .insertInto('turn_inbound_events') - .values( - input.events.map(event => ({ - session_id: input.session_id, - turn_id: input.turn_id, - event_id: event.event_id, - payload: json(event.payload), - consumed: false, - created_at: sql`${event.created_at}::timestamptz`, - })), - ) - .execute(); + // Ensure the turn does not stop before we insert events. + await db.transaction().execute(async trx => { + await assertTurnRunning(trx, keys); + await trx + .insertInto('turn_inbound_events') + .values( + input.events.map(event => ({ + session_id: input.session_id, + turn_id: input.turn_id, + event_id: event.event_id, + payload: json(event.payload), + consumed: false, + created_at: sql`${event.created_at}::timestamptz`, + })), + ) + .execute(); + }); } catch (error) { if (isUniqueViolation(error)) { const eventId = await resolveCollidingEventId(db, input.session_id, input.turn_id, input.events); - throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, eventId, { - cause: error, - }); + throw new TurnEventAlreadyExistsError( + { + session_id: input.session_id, + turn_id: input.turn_id, + event_id: eventId, + }, + { cause: error }, + ); } throw error; } diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts index d1d6d2055..4ccd16009 100644 --- a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts +++ b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts @@ -3,28 +3,33 @@ import { type Kysely, sql } from 'kysely'; /** * Turn-scoped inbound send-event inbox (tip HITL / policies). * `turn_id` is required — every row belongs to a tip. + * Kysely does not wrap SQLite migrations — keep CREATE TABLE + INDEX atomic. */ export async function up(db: Kysely): Promise { - await sql` - CREATE TABLE turn_inbound_events ( - session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE, - turn_id TEXT NOT NULL, - event_id TEXT NOT NULL, - payload BLOB NOT NULL, - consumed INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - PRIMARY KEY (session_id, turn_id, event_id) - ) STRICT - `.execute(db); + await db.transaction().execute(async trx => { + await sql` + CREATE TABLE turn_inbound_events ( + session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE, + turn_id TEXT NOT NULL, + event_id TEXT NOT NULL, + payload BLOB NOT NULL, + consumed INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL, + PRIMARY KEY (session_id, turn_id, event_id) + ) STRICT + `.execute(trx); - await sql` - CREATE INDEX turn_inbound_events_unconsumed_idx - ON turn_inbound_events (session_id, turn_id, event_id) - WHERE consumed = 0 - `.execute(db); + await sql` + CREATE INDEX turn_inbound_events_unconsumed_idx + ON turn_inbound_events (session_id, turn_id, event_id) + WHERE consumed = 0 + `.execute(trx); + }); } export async function down(db: Kysely): Promise { - await sql`DROP INDEX IF EXISTS turn_inbound_events_unconsumed_idx`.execute(db); - await sql`DROP TABLE IF EXISTS turn_inbound_events`.execute(db); + await db.transaction().execute(async trx => { + await sql`DROP INDEX IF EXISTS turn_inbound_events_unconsumed_idx`.execute(trx); + await sql`DROP TABLE IF EXISTS turn_inbound_events`.execute(trx); + }); } diff --git a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts index 4ac8f8075..409319239 100644 --- a/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts +++ b/packages/trueforge/src/db/sqlite/session-store/queries/inboundEvents.ts @@ -1,4 +1,4 @@ -import type { TurnInboundEventItem, TurnState } from '@truefoundry/trueforge-core/agent-session'; +import type { TurnInboundEventItem } from '@truefoundry/trueforge-core/agent-session'; import type { InsertTurnInboundEventsInput, ListUnconsumedTurnInboundEventsInput, @@ -7,9 +7,7 @@ import type { } from '@truefoundry/trueforge-core/agent-session/store/ISessionStore'; import { SessionNotFoundError, - TurnInboundEventAlreadyExistsError, - TurnNotFoundError, - TurnNotRunningError, + TurnEventAlreadyExistsError, } from '@truefoundry/trueforge-core/agent-session/store/SessionStoreErrors'; import type { Kysely } from 'kysely'; import { sql } from 'kysely'; @@ -17,6 +15,7 @@ import { firstCollidingEventId, firstDuplicateEventIdInBatch } from '../../../tu import { isUniqueViolation } from '../../client'; import { jsonbBind, jsonText } from '../../sqlExpressions'; import type { Database } from '../../types'; +import { assertTurnRunning, type TurnKeys } from './turns'; async function requireSession(db: Kysely, sessionId: string): Promise { const row = await db @@ -29,22 +28,6 @@ async function requireSession(db: Kysely, sessionId: string): Promise< } } -/** Exists + non-terminal (v1: `running` only; `paused` will be allowed when that status lands). */ -async function requireTurn(db: Kysely, sessionId: string, turnId: string): Promise { - const row = await db - .selectFrom('turn') - .select(['turn_id', jsonText(sql.ref('state')).as('state')]) - .where('session_id', '=', sessionId) - .where('turn_id', '=', turnId) - .executeTakeFirst(); - if (!row) { - throw new TurnNotFoundError(turnId); - } - if (row.state.status !== 'running') { - throw new TurnNotRunningError(turnId, row.state); - } -} - async function resolveCollidingEventId( db: Kysely, sessionId: string, @@ -73,33 +56,50 @@ export async function insertTurnInboundEvents( return; } await requireSession(db, input.session_id); - await requireTurn(db, input.session_id, input.turn_id); const duplicateInBatch = firstDuplicateEventIdInBatch(input.events); if (duplicateInBatch !== undefined) { - throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, duplicateInBatch); + throw new TurnEventAlreadyExistsError({ + session_id: input.session_id, + turn_id: input.turn_id, + event_id: duplicateInBatch, + }); } + const keys: TurnKeys = { + session_id: input.session_id, + turn_id: input.turn_id, + }; + try { - await db - .insertInto('turn_inbound_events') - .values( - input.events.map(event => ({ - session_id: input.session_id, - turn_id: input.turn_id, - event_id: event.event_id, - payload: jsonbBind(event.payload), - consumed: 0, - created_at: event.created_at, - })), - ) - .execute(); + // Ensure the turn does not stop before we insert events. + await db.transaction().execute(async trx => { + await assertTurnRunning(trx, keys); + await trx + .insertInto('turn_inbound_events') + .values( + input.events.map(event => ({ + session_id: input.session_id, + turn_id: input.turn_id, + event_id: event.event_id, + payload: jsonbBind(event.payload), + consumed: 0, + created_at: event.created_at, + })), + ) + .execute(); + }); } catch (error) { if (isUniqueViolation(error)) { const eventId = await resolveCollidingEventId(db, input.session_id, input.turn_id, input.events); - throw new TurnInboundEventAlreadyExistsError(input.session_id, input.turn_id, eventId, { - cause: error, - }); + throw new TurnEventAlreadyExistsError( + { + session_id: input.session_id, + turn_id: input.turn_id, + event_id: eventId, + }, + { cause: error }, + ); } throw error; } From 929afc8ec6b3f5a6b94d9be7c248a307a769f59f Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 09:24:31 +0530 Subject: [PATCH 09/23] rename schemas --- .../src/agent-session/schemas/events.ts | 8 ++++++++ .../src/agent-session/schemas/sendEvent.ts | 15 -------------- .../trueforge-core/src/core/events/schema.ts | 20 +++++++++++++++++++ 3 files changed, 28 insertions(+), 15 deletions(-) delete mode 100644 packages/trueforge-core/src/agent-session/schemas/sendEvent.ts diff --git a/packages/trueforge-core/src/agent-session/schemas/events.ts b/packages/trueforge-core/src/agent-session/schemas/events.ts index 370f209c2..2cec37611 100644 --- a/packages/trueforge-core/src/agent-session/schemas/events.ts +++ b/packages/trueforge-core/src/agent-session/schemas/events.ts @@ -18,6 +18,8 @@ import { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalMsgSchema, + UserToolResponseMsgSchema, } from '../../core/events/schema'; import { TurnInputItemSchema, @@ -139,3 +141,9 @@ export interface SessionEventItem { turn_id: string; event: PersistedTurnEvent; } + +export const TurnInboundEventItemSchema = z + .discriminatedUnion('type', [UserToolApprovalMsgSchema, UserToolResponseMsgSchema]) + .openapi('TurnInboundEventItem'); + +export type TurnInboundEventItem = z.infer; diff --git a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts b/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts deleted file mode 100644 index ff83878f4..000000000 --- a/packages/trueforge-core/src/agent-session/schemas/sendEvent.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Inbound send-event payloads for tip HITL (client → harness), distinct from the - * stream log ({@link PersistedTurnEvent} / session_event). - * - * Public create is turn-scoped (`POST …/sessions/{id}/turns/{turn_id}/events`) - * with `TurnInboundEventItem`s. `user.message` stays on createTurn / steer. - */ -import { z } from '@hono/zod-openapi'; -import { UserToolApprovalMessageSchema, UserToolResponseMessageSchema } from '../../core/events/schema'; - -export const TurnInboundEventItemSchema = z - .discriminatedUnion('type', [UserToolApprovalMessageSchema, UserToolResponseMessageSchema]) - .openapi('TurnInboundEventItem'); - -export type TurnInboundEventItem = z.infer; diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index bdced8eda..b985cdfc4 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -78,6 +78,7 @@ export const ApprovalDecisionSchema = z .discriminatedUnion('status', [AgentApprovalDecisionAllowSchema, AgentApprovalDecisionDenySchema]) .openapi('ApprovalDecision'); +// TODO: Remove this after we have migrated to the new schema. export const UserToolApprovalMessageSchema = z .object({ type: z.literal(EventType.USER_TOOL_APPROVAL).describe('Client resume after tool.approval_required.'), @@ -87,6 +88,16 @@ export const UserToolApprovalMessageSchema = z }) .openapi('UserToolApprovalEvent'); + export const UserToolApprovalMsgSchema = z + .object({ + type: z.literal(EventType.USER_TOOL_APPROVAL).describe('Client resume after tool.approval_required.'), + thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), + tool_call_id: z.string().min(1, 'tool_call_id is required').describe('Tool call id being approved or denied.'), + approval: ApprovalDecisionSchema, + }) + .openapi('UserToolApprovalMessage'); + +// TODO: Remove this after we have migrated to the new schema. export const UserToolResponseMessageSchema = z .object({ type: z.literal(EventType.USER_TOOL_RESPONSE).describe('Client resume after tool.response_required.'), @@ -96,6 +107,15 @@ export const UserToolResponseMessageSchema = z }) .openapi('UserToolResponseEvent'); +export const UserToolResponseMsgSchema = z + .object({ + type: z.literal(EventType.USER_TOOL_RESPONSE).describe('Client resume after tool.response_required.'), + thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), + tool_call_id: z.string().min(1, 'tool_call_id is required').describe('Tool call id receiving the client response.'), + content: z.string().min(1, 'content cannot be empty').describe('Client-side tool result content.'), + }) + .openapi('UserToolResponseMessage'); + export const TextContentPartSchema = z .object({ type: z.literal('text').describe('Text content part.'), From 8e49f1320fe6cd39d4babed018050bbeb0b97569 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 09:27:25 +0530 Subject: [PATCH 10/23] fix --- packages/trueforge-core/src/agent-session/index.ts | 5 ++--- .../trueforge-core/src/agent-session/store/ISessionStore.ts | 3 +-- .../src/agent-session/store/InMemorySessionStore.ts | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/trueforge-core/src/agent-session/index.ts b/packages/trueforge-core/src/agent-session/index.ts index 436503a76..5d599a7cf 100644 --- a/packages/trueforge-core/src/agent-session/index.ts +++ b/packages/trueforge-core/src/agent-session/index.ts @@ -21,9 +21,6 @@ export { } from './schemas/turn'; export type { TerminalTurnState, Turn, TurnInputItem, TurnMetrics, TurnState } from './schemas/turn'; -export { TurnInboundEventItemSchema } from './schemas/sendEvent'; -export type { TurnInboundEventItem } from './schemas/sendEvent'; - export { SessionMetadataSchema, SessionMetricsSchema, @@ -50,6 +47,7 @@ export { SessionEventSchema, TurnCreatedEventSchema, TurnDoneEventSchema, + TurnInboundEventItemSchema, TurnUpdateEventSchema, } from './schemas/events'; export type { @@ -58,6 +56,7 @@ export type { SessionEventItem, TurnCreatedEvent, TurnDoneEvent, + TurnInboundEventItem, TurnUpdateEvent, } from './schemas/events'; diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index ce039b508..b586c9698 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -9,9 +9,8 @@ import type { CurrentContextUsage } from '../../core/runtime/contextUsage'; import type { SandboxInfo } from '../../core/sandbox/Sandbox'; import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord } from '../models/TurnRecord'; -import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; +import type { PersistedTurnEvent, SessionEventItem, TurnInboundEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { TurnInboundEventItem } from '../schemas/sendEvent'; import type { SessionMetadata } from '../schemas/session'; import type { CancellationReason, TerminalTurnState } from '../schemas/turn'; diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index a537c8463..e36b1a150 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -2,9 +2,8 @@ import type { AgentThreadSnapshot } from '../../core/runtime/AgentThread.types'; import { getEmptyCurrentContextUsage } from '../../core/runtime/contextUsage'; import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord, TurnSnapshot } from '../models/TurnRecord'; -import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; +import type { PersistedTurnEvent, SessionEventItem, TurnInboundEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { TurnInboundEventItem } from '../schemas/sendEvent'; import type { TerminalTurnState } from '../schemas/turn'; import { assertCreateTurnThreadDelta } from './assertCreateTurnThreadDelta'; import type { From 64c003860f0bcd6e5cebdfaf68490f2c2f1c39e6 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 09:29:21 +0530 Subject: [PATCH 11/23] lint fix --- packages/trueforge-core/src/core/events/schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index b985cdfc4..8b50564ae 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -88,7 +88,7 @@ export const UserToolApprovalMessageSchema = z }) .openapi('UserToolApprovalEvent'); - export const UserToolApprovalMsgSchema = z +export const UserToolApprovalMsgSchema = z .object({ type: z.literal(EventType.USER_TOOL_APPROVAL).describe('Client resume after tool.approval_required.'), thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), From f237631975a4329447b273b9facec82b00eeebf2 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 09:37:32 +0530 Subject: [PATCH 12/23] move --- .../trueforge-core/src/agent-session/schemas/events.ts | 8 -------- packages/trueforge-core/src/agent-session/schemas/turn.ts | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/trueforge-core/src/agent-session/schemas/events.ts b/packages/trueforge-core/src/agent-session/schemas/events.ts index 2cec37611..370f209c2 100644 --- a/packages/trueforge-core/src/agent-session/schemas/events.ts +++ b/packages/trueforge-core/src/agent-session/schemas/events.ts @@ -18,8 +18,6 @@ import { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, - UserToolApprovalMsgSchema, - UserToolResponseMsgSchema, } from '../../core/events/schema'; import { TurnInputItemSchema, @@ -141,9 +139,3 @@ export interface SessionEventItem { turn_id: string; event: PersistedTurnEvent; } - -export const TurnInboundEventItemSchema = z - .discriminatedUnion('type', [UserToolApprovalMsgSchema, UserToolResponseMsgSchema]) - .openapi('TurnInboundEventItem'); - -export type TurnInboundEventItem = z.infer; diff --git a/packages/trueforge-core/src/agent-session/schemas/turn.ts b/packages/trueforge-core/src/agent-session/schemas/turn.ts index 59ec0b9a0..ce4e4119d 100644 --- a/packages/trueforge-core/src/agent-session/schemas/turn.ts +++ b/packages/trueforge-core/src/agent-session/schemas/turn.ts @@ -5,7 +5,9 @@ import { AgentInputUserMessageSchema, ModelMessageEventSchema, UserToolApprovalMessageSchema, + UserToolApprovalMsgSchema, UserToolResponseMessageSchema, + UserToolResponseMsgSchema, } from '../../core/events/schema'; export enum CancellationReason { @@ -176,8 +178,13 @@ export const CreateTurnRequestSchema = z }) .openapi('CreateTurnRequest'); +export const TurnInboundEventItemSchema = z + .discriminatedUnion('type', [UserToolApprovalMsgSchema, UserToolResponseMsgSchema]) + .openapi('TurnInboundEventItem'); + export type Turn = z.infer; export type TurnInputItem = z.infer; export type TurnState = z.infer; export type TerminalTurnState = Exclude; export type TurnMetrics = z.infer; +export type TurnInboundEventItem = z.infer; From 1a665f577028ba9c6ba38ab9af777699350227e2 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 09:39:02 +0530 Subject: [PATCH 13/23] move --- .../trueforge-core/src/agent-session/store/ISessionStore.ts | 4 ++-- .../src/agent-session/store/InMemorySessionStore.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts index b586c9698..c2e700df3 100644 --- a/packages/trueforge-core/src/agent-session/store/ISessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/ISessionStore.ts @@ -9,10 +9,10 @@ import type { CurrentContextUsage } from '../../core/runtime/contextUsage'; import type { SandboxInfo } from '../../core/sandbox/Sandbox'; import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord } from '../models/TurnRecord'; -import type { PersistedTurnEvent, SessionEventItem, TurnInboundEventItem } from '../schemas/events'; +import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; import type { SessionMetadata } from '../schemas/session'; -import type { CancellationReason, TerminalTurnState } from '../schemas/turn'; +import type { CancellationReason, TerminalTurnState, TurnInboundEventItem } from '../schemas/turn'; /** * Caller-supplied fields for creating a session; the store owns timestamps and tip state. diff --git a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts index e36b1a150..c84b39f58 100644 --- a/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts +++ b/packages/trueforge-core/src/agent-session/store/InMemorySessionStore.ts @@ -2,9 +2,9 @@ import type { AgentThreadSnapshot } from '../../core/runtime/AgentThread.types'; import { getEmptyCurrentContextUsage } from '../../core/runtime/contextUsage'; import type { SessionRecord } from '../models/SessionRecord'; import type { TurnRecord, TurnSnapshot } from '../models/TurnRecord'; -import type { PersistedTurnEvent, SessionEventItem, TurnInboundEventItem } from '../schemas/events'; +import type { PersistedTurnEvent, SessionEventItem } from '../schemas/events'; import type { TokenPagination } from '../schemas/pagination'; -import type { TerminalTurnState } from '../schemas/turn'; +import type { TerminalTurnState, TurnInboundEventItem } from '../schemas/turn'; import { assertCreateTurnThreadDelta } from './assertCreateTurnThreadDelta'; import type { AddThreadsInput, From 12f3163430d3cfa828364511b92d0f27ed13cb52 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 10:02:53 +0530 Subject: [PATCH 14/23] fix --- .github/fern/openapi/openapi.json | 12 ++++++------ docs/openapi.json | 12 ++++++------ .../trueforge-core/src/agent-session/index.ts | 12 +++++++++--- .../src/agent-session/schemas/turn.ts | 4 +--- .../trueforge-core/src/core/events/schema.ts | 19 ------------------- .../src/api/types/TurnInputItem.ts | 5 ++++- ...valEvent.ts => UserToolApprovalMessage.ts} | 2 +- ...nseEvent.ts => UserToolResponseMessage.ts} | 2 +- packages/trueforge-sdk/src/api/types/index.ts | 4 ++-- .../src/serialization/types/TurnInputItem.ts | 8 ++++---- ...valEvent.ts => UserToolApprovalMessage.ts} | 8 ++++---- ...nseEvent.ts => UserToolResponseMessage.ts} | 8 ++++---- .../src/serialization/types/index.ts | 4 ++-- .../src/trueforge_sdk/__init__.py | 12 ++++++------ .../src/trueforge_sdk/types/__init__.py | 12 ++++++------ .../trueforge_sdk/types/turn_input_item.py | 6 +++--- ...event.py => user_tool_approval_message.py} | 2 +- ...event.py => user_tool_response_message.py} | 2 +- 18 files changed, 61 insertions(+), 73 deletions(-) rename packages/trueforge-sdk/src/api/types/{UserToolApprovalEvent.ts => UserToolApprovalMessage.ts} (88%) rename packages/trueforge-sdk/src/api/types/{UserToolResponseEvent.ts => UserToolResponseMessage.ts} (88%) rename packages/trueforge-sdk/src/serialization/types/{UserToolApprovalEvent.ts => UserToolApprovalMessage.ts} (78%) rename packages/trueforge-sdk/src/serialization/types/{UserToolResponseEvent.ts => UserToolResponseMessage.ts} (77%) rename python/trueforge_sdk/src/trueforge_sdk/types/{user_tool_approval_event.py => user_tool_approval_message.py} (94%) rename python/trueforge_sdk/src/trueforge_sdk/types/{user_tool_response_event.py => user_tool_response_message.py} (94%) diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 3cbb63504..114708b55 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -5232,8 +5232,8 @@ "discriminator": { "mapping": { "user.message": "#/components/schemas/UserMessage", - "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", - "user.tool_response": "#/components/schemas/UserToolResponseEvent" + "user.tool_approval": "#/components/schemas/UserToolApprovalMessage", + "user.tool_response": "#/components/schemas/UserToolResponseMessage" }, "propertyName": "type" }, @@ -5242,10 +5242,10 @@ "$ref": "#/components/schemas/UserMessage" }, { - "$ref": "#/components/schemas/UserToolApprovalEvent" + "$ref": "#/components/schemas/UserToolApprovalMessage" }, { - "$ref": "#/components/schemas/UserToolResponseEvent" + "$ref": "#/components/schemas/UserToolResponseMessage" } ] }, @@ -5764,7 +5764,7 @@ } ] }, - "UserToolApprovalEvent": { + "UserToolApprovalMessage": { "properties": { "approval": { "$ref": "#/components/schemas/ApprovalDecision" @@ -5795,7 +5795,7 @@ ], "type": "object" }, - "UserToolResponseEvent": { + "UserToolResponseMessage": { "properties": { "content": { "description": "Client-side tool result content.", diff --git a/docs/openapi.json b/docs/openapi.json index 3cbb63504..114708b55 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -5232,8 +5232,8 @@ "discriminator": { "mapping": { "user.message": "#/components/schemas/UserMessage", - "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", - "user.tool_response": "#/components/schemas/UserToolResponseEvent" + "user.tool_approval": "#/components/schemas/UserToolApprovalMessage", + "user.tool_response": "#/components/schemas/UserToolResponseMessage" }, "propertyName": "type" }, @@ -5242,10 +5242,10 @@ "$ref": "#/components/schemas/UserMessage" }, { - "$ref": "#/components/schemas/UserToolApprovalEvent" + "$ref": "#/components/schemas/UserToolApprovalMessage" }, { - "$ref": "#/components/schemas/UserToolResponseEvent" + "$ref": "#/components/schemas/UserToolResponseMessage" } ] }, @@ -5764,7 +5764,7 @@ } ] }, - "UserToolApprovalEvent": { + "UserToolApprovalMessage": { "properties": { "approval": { "$ref": "#/components/schemas/ApprovalDecision" @@ -5795,7 +5795,7 @@ ], "type": "object" }, - "UserToolResponseEvent": { + "UserToolResponseMessage": { "properties": { "content": { "description": "Client-side tool result content.", diff --git a/packages/trueforge-core/src/agent-session/index.ts b/packages/trueforge-core/src/agent-session/index.ts index 5d599a7cf..3e1e832b1 100644 --- a/packages/trueforge-core/src/agent-session/index.ts +++ b/packages/trueforge-core/src/agent-session/index.ts @@ -9,6 +9,7 @@ export type { AgentSpec, Skill } from './schemas/agentSpec'; export { CancellationReason, CreateTurnRequestSchema, + TurnInboundEventItemSchema, TurnInputItemSchema, TurnMetricsSchema, TurnSchema, @@ -19,7 +20,14 @@ export { TurnStateRunningSchema, TurnStateSchema, } from './schemas/turn'; -export type { TerminalTurnState, Turn, TurnInputItem, TurnMetrics, TurnState } from './schemas/turn'; +export type { + TerminalTurnState, + Turn, + TurnInboundEventItem, + TurnInputItem, + TurnMetrics, + TurnState, +} from './schemas/turn'; export { SessionMetadataSchema, @@ -47,7 +55,6 @@ export { SessionEventSchema, TurnCreatedEventSchema, TurnDoneEventSchema, - TurnInboundEventItemSchema, TurnUpdateEventSchema, } from './schemas/events'; export type { @@ -56,7 +63,6 @@ export type { SessionEventItem, TurnCreatedEvent, TurnDoneEvent, - TurnInboundEventItem, TurnUpdateEvent, } from './schemas/events'; diff --git a/packages/trueforge-core/src/agent-session/schemas/turn.ts b/packages/trueforge-core/src/agent-session/schemas/turn.ts index ce4e4119d..3c08a46db 100644 --- a/packages/trueforge-core/src/agent-session/schemas/turn.ts +++ b/packages/trueforge-core/src/agent-session/schemas/turn.ts @@ -5,9 +5,7 @@ import { AgentInputUserMessageSchema, ModelMessageEventSchema, UserToolApprovalMessageSchema, - UserToolApprovalMsgSchema, UserToolResponseMessageSchema, - UserToolResponseMsgSchema, } from '../../core/events/schema'; export enum CancellationReason { @@ -179,7 +177,7 @@ export const CreateTurnRequestSchema = z .openapi('CreateTurnRequest'); export const TurnInboundEventItemSchema = z - .discriminatedUnion('type', [UserToolApprovalMsgSchema, UserToolResponseMsgSchema]) + .discriminatedUnion('type', [UserToolApprovalMessageSchema, UserToolResponseMessageSchema]) .openapi('TurnInboundEventItem'); export type Turn = z.infer; diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index 8b50564ae..872347bcc 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -80,15 +80,6 @@ export const ApprovalDecisionSchema = z // TODO: Remove this after we have migrated to the new schema. export const UserToolApprovalMessageSchema = z - .object({ - type: z.literal(EventType.USER_TOOL_APPROVAL).describe('Client resume after tool.approval_required.'), - thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), - tool_call_id: z.string().min(1, 'tool_call_id is required').describe('Tool call id being approved or denied.'), - approval: ApprovalDecisionSchema, - }) - .openapi('UserToolApprovalEvent'); - -export const UserToolApprovalMsgSchema = z .object({ type: z.literal(EventType.USER_TOOL_APPROVAL).describe('Client resume after tool.approval_required.'), thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), @@ -97,17 +88,7 @@ export const UserToolApprovalMsgSchema = z }) .openapi('UserToolApprovalMessage'); -// TODO: Remove this after we have migrated to the new schema. export const UserToolResponseMessageSchema = z - .object({ - type: z.literal(EventType.USER_TOOL_RESPONSE).describe('Client resume after tool.response_required.'), - thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), - tool_call_id: z.string().min(1, 'tool_call_id is required').describe('Tool call id receiving the client response.'), - content: z.string().min(1, 'content cannot be empty').describe('Client-side tool result content.'), - }) - .openapi('UserToolResponseEvent'); - -export const UserToolResponseMsgSchema = z .object({ type: z.literal(EventType.USER_TOOL_RESPONSE).describe('Client resume after tool.response_required.'), thread_id: z.string().min(1, 'thread_id is required').describe('Thread that owns the pending tool call.'), diff --git a/packages/trueforge-sdk/src/api/types/TurnInputItem.ts b/packages/trueforge-sdk/src/api/types/TurnInputItem.ts index 24c1cb953..831d85fb8 100644 --- a/packages/trueforge-sdk/src/api/types/TurnInputItem.ts +++ b/packages/trueforge-sdk/src/api/types/TurnInputItem.ts @@ -2,4 +2,7 @@ import type * as TrueForge from "../index.js"; -export type TurnInputItem = TrueForge.UserMessage | TrueForge.UserToolApprovalEvent | TrueForge.UserToolResponseEvent; +export type TurnInputItem = + | TrueForge.UserMessage + | TrueForge.UserToolApprovalMessage + | TrueForge.UserToolResponseMessage; diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts similarity index 88% rename from packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts rename to packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts index 432dde181..a9c9f3226 100644 --- a/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts @@ -2,7 +2,7 @@ import type * as TrueForge from "../index.js"; -export interface UserToolApprovalEvent { +export interface UserToolApprovalMessage { approval: TrueForge.ApprovalDecision; /** Thread that owns the pending tool call. */ threadId: string; diff --git a/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts similarity index 88% rename from packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts rename to packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts index d86330196..b1bc4d80a 100644 --- a/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts +++ b/packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts @@ -1,6 +1,6 @@ // This file was auto-generated by Fern from our API Definition. -export interface UserToolResponseEvent { +export interface UserToolResponseMessage { /** Client-side tool result content. */ content: string; /** Thread that owns the pending tool call. */ diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index 3c8a2dcf3..b51454d87 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -235,8 +235,8 @@ export * from "./TurnUpdateStateRunning.js"; export * from "./UserMessage.js"; export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; -export * from "./UserToolApprovalEvent.js"; -export * from "./UserToolResponseEvent.js"; +export * from "./UserToolApprovalMessage.js"; +export * from "./UserToolResponseMessage.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; export * from "./WebSearchProviderManifest.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/TurnInputItem.ts b/packages/trueforge-sdk/src/serialization/types/TurnInputItem.ts index 36becb445..54f376cf8 100644 --- a/packages/trueforge-sdk/src/serialization/types/TurnInputItem.ts +++ b/packages/trueforge-sdk/src/serialization/types/TurnInputItem.ts @@ -4,12 +4,12 @@ import type * as TrueForge from "../../api/index.js"; import * as core from "../../core/index.js"; import type * as serializers from "../index.js"; import { UserMessage } from "./UserMessage.js"; -import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; -import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; +import { UserToolApprovalMessage } from "./UserToolApprovalMessage.js"; +import { UserToolResponseMessage } from "./UserToolResponseMessage.js"; export const TurnInputItem: core.serialization.Schema = - core.serialization.undiscriminatedUnion([UserMessage, UserToolApprovalEvent, UserToolResponseEvent]); + core.serialization.undiscriminatedUnion([UserMessage, UserToolApprovalMessage, UserToolResponseMessage]); export declare namespace TurnInputItem { - export type Raw = UserMessage.Raw | UserToolApprovalEvent.Raw | UserToolResponseEvent.Raw; + export type Raw = UserMessage.Raw | UserToolApprovalMessage.Raw | UserToolResponseMessage.Raw; } diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts similarity index 78% rename from packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts rename to packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts index 18f86bbbf..5aa1ac58b 100644 --- a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts @@ -5,9 +5,9 @@ import * as core from "../../core/index.js"; import type * as serializers from "../index.js"; import { ApprovalDecision } from "./ApprovalDecision.js"; -export const UserToolApprovalEvent: core.serialization.ObjectSchema< - serializers.UserToolApprovalEvent.Raw, - TrueForge.UserToolApprovalEvent +export const UserToolApprovalMessage: core.serialization.ObjectSchema< + serializers.UserToolApprovalMessage.Raw, + TrueForge.UserToolApprovalMessage > = core.serialization.object({ approval: ApprovalDecision, threadId: core.serialization.property("thread_id", core.serialization.string()), @@ -15,7 +15,7 @@ export const UserToolApprovalEvent: core.serialization.ObjectSchema< type: core.serialization.stringLiteral("user.tool_approval"), }); -export declare namespace UserToolApprovalEvent { +export declare namespace UserToolApprovalMessage { export interface Raw { approval: ApprovalDecision.Raw; thread_id: string; diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts similarity index 77% rename from packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts rename to packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts index 8a878654a..4849207ef 100644 --- a/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts @@ -4,9 +4,9 @@ import type * as TrueForge from "../../api/index.js"; import * as core from "../../core/index.js"; import type * as serializers from "../index.js"; -export const UserToolResponseEvent: core.serialization.ObjectSchema< - serializers.UserToolResponseEvent.Raw, - TrueForge.UserToolResponseEvent +export const UserToolResponseMessage: core.serialization.ObjectSchema< + serializers.UserToolResponseMessage.Raw, + TrueForge.UserToolResponseMessage > = core.serialization.object({ content: core.serialization.string(), threadId: core.serialization.property("thread_id", core.serialization.string()), @@ -14,7 +14,7 @@ export const UserToolResponseEvent: core.serialization.ObjectSchema< type: core.serialization.stringLiteral("user.tool_response"), }); -export declare namespace UserToolResponseEvent { +export declare namespace UserToolResponseMessage { export interface Raw { content: string; thread_id: string; diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index 3c8a2dcf3..b51454d87 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -235,8 +235,8 @@ export * from "./TurnUpdateStateRunning.js"; export * from "./UserMessage.js"; export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; -export * from "./UserToolApprovalEvent.js"; -export * from "./UserToolResponseEvent.js"; +export * from "./UserToolApprovalMessage.js"; +export * from "./UserToolResponseMessage.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; export * from "./WebSearchProviderManifest.js"; diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index 9b49e63fa..700b078bc 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -244,8 +244,8 @@ UserMessage, UserMessageContent, UserMessageContentItem, - UserToolApprovalEvent, - UserToolResponseEvent, + UserToolApprovalMessage, + UserToolResponseMessage, WebSearchCapability, WebSearchConfig, WebSearchProviderManifest, @@ -523,8 +523,8 @@ "UserMessage": ".types", "UserMessageContent": ".types", "UserMessageContentItem": ".types", - "UserToolApprovalEvent": ".types", - "UserToolResponseEvent": ".types", + "UserToolApprovalMessage": ".types", + "UserToolResponseMessage": ".types", "WebSearchCapability": ".types", "WebSearchConfig": ".types", "WebSearchProviderManifest": ".types", @@ -819,8 +819,8 @@ def __dir__(): "UserMessage", "UserMessageContent", "UserMessageContentItem", - "UserToolApprovalEvent", - "UserToolResponseEvent", + "UserToolApprovalMessage", + "UserToolResponseMessage", "WebSearchCapability", "WebSearchConfig", "WebSearchProviderManifest", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index 07d9b1918..f1fb9e3ef 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -243,8 +243,8 @@ from .user_message import UserMessage from .user_message_content import UserMessageContent from .user_message_content_item import UserMessageContentItem - from .user_tool_approval_event import UserToolApprovalEvent - from .user_tool_response_event import UserToolResponseEvent + from .user_tool_approval_message import UserToolApprovalMessage + from .user_tool_response_message import UserToolResponseMessage from .web_search_capability import WebSearchCapability from .web_search_config import WebSearchConfig from .web_search_provider_manifest import WebSearchProviderManifest @@ -487,8 +487,8 @@ "UserMessage": ".user_message", "UserMessageContent": ".user_message_content", "UserMessageContentItem": ".user_message_content_item", - "UserToolApprovalEvent": ".user_tool_approval_event", - "UserToolResponseEvent": ".user_tool_response_event", + "UserToolApprovalMessage": ".user_tool_approval_message", + "UserToolResponseMessage": ".user_tool_response_message", "WebSearchCapability": ".web_search_capability", "WebSearchConfig": ".web_search_config", "WebSearchProviderManifest": ".web_search_provider_manifest", @@ -755,8 +755,8 @@ def __dir__(): "UserMessage", "UserMessageContent", "UserMessageContentItem", - "UserToolApprovalEvent", - "UserToolResponseEvent", + "UserToolApprovalMessage", + "UserToolResponseMessage", "WebSearchCapability", "WebSearchConfig", "WebSearchProviderManifest", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py index 9e1c299ac..ba6523d22 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_input_item.py @@ -3,7 +3,7 @@ import typing from .user_message import UserMessage -from .user_tool_approval_event import UserToolApprovalEvent -from .user_tool_response_event import UserToolResponseEvent +from .user_tool_approval_message import UserToolApprovalMessage +from .user_tool_response_message import UserToolResponseMessage -TurnInputItem = typing.Union[UserMessage, UserToolApprovalEvent, UserToolResponseEvent] +TurnInputItem = typing.Union[UserMessage, UserToolApprovalMessage, UserToolResponseMessage] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py similarity index 94% rename from python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py rename to python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py index 79939dc2c..07a39d669 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py @@ -8,7 +8,7 @@ from .approval_decision import ApprovalDecision -class UserToolApprovalEvent(UncheckedBaseModel): +class UserToolApprovalMessage(UncheckedBaseModel): approval: ApprovalDecision thread_id: str = pydantic.Field() """ diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py similarity index 94% rename from python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py rename to python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py index 0fa843bf1..a7e919420 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py @@ -7,7 +7,7 @@ from ..core.unchecked_base_model import UncheckedBaseModel -class UserToolResponseEvent(UncheckedBaseModel): +class UserToolResponseMessage(UncheckedBaseModel): content: str = pydantic.Field() """ Client-side tool result content. From d87b09d937873c91054dab8f1725bc44fd30e3a7 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Tue, 22 Sep 2026 15:28:35 +0530 Subject: [PATCH 15/23] add approval policy schema --- .changeset/user-tool-approval-policy-event.md | 6 ++++ .../trueforge-core/src/core/events/schema.ts | 30 +++++++++++++++++++ packages/trueforge-core/src/core/index.ts | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 .changeset/user-tool-approval-policy-event.md diff --git a/.changeset/user-tool-approval-policy-event.md b/.changeset/user-tool-approval-policy-event.md new file mode 100644 index 000000000..97d861d6b --- /dev/null +++ b/.changeset/user-tool-approval-policy-event.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge-core": patch +"@truefoundry/trueforge": patch +--- + +Add `user.tool_approval_policy` send-event schema (`allow_session`, optional ISO `expire_at`). Send-only like `user.tool_approval` / `user.tool_response` — not on the durable stream. diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index 872347bcc..d36fe5721 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -40,6 +40,7 @@ export const EventType = { TOOL_RESPONSE_REQUIRED: 'tool.response_required', USER_TOOL_APPROVAL: 'user.tool_approval', USER_TOOL_RESPONSE: 'user.tool_response', + USER_TOOL_APPROVAL_POLICY: 'user.tool_approval_policy', USER_MESSAGE: 'user.message', } as const; @@ -97,6 +98,33 @@ export const UserToolResponseMessageSchema = z }) .openapi('UserToolResponseMessage'); + export const ToolApprovalPolicyAllowSessionSchema = z + .object({ + type: z.literal('allow_session').describe('Allow matching tool calls for the rest of this session.'), + expire_at: z + .string() + .optional() + .describe('ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session.'), + }) + .openapi('ToolApprovalPolicyAllowSession'); + +export const ToolApprovalPolicyItemSchema = z + .object({ + server: z.string().min(1, 'server is required').describe('MCP server name the tool belongs to.'), + tool_name: z.string().min(1, 'tool_name is required').describe('Tool name this policy applies to.'), + action: z.discriminatedUnion('type', [ToolApprovalPolicyAllowSessionSchema]), + }) + .openapi('ToolApprovalPolicyItem'); + +export const UserToolApprovalPolicyMessageSchema = z + .object({ + type: z + .literal(EventType.USER_TOOL_APPROVAL_POLICY) + .describe('Sticky allow-session policy for matching tools (optional expiry).'), + policies: z.array(ToolApprovalPolicyItemSchema).min(1).describe('One or more (server, tool_name) policy entries.'), + }) + .openapi('UserToolApprovalPolicyMessage'); + export const TextContentPartSchema = z .object({ type: z.literal('text').describe('Text content part.'), @@ -374,6 +402,8 @@ export type AgentInfo = z.infer; export type ApprovalDecision = z.infer; export type UserToolApprovalMessage = z.infer; export type UserToolResponseMessage = z.infer; +export type ToolApprovalPolicyItem = z.infer; +export type UserToolApprovalPolicyMessage = z.infer; export type AgentApprovalDecisionMessage = z.infer; export type InputTokensBreakdown = z.infer; export type ModelMessageUsage = z.infer; diff --git a/packages/trueforge-core/src/core/index.ts b/packages/trueforge-core/src/core/index.ts index 1cff0d22d..c9056a503 100644 --- a/packages/trueforge-core/src/core/index.ts +++ b/packages/trueforge-core/src/core/index.ts @@ -114,6 +114,7 @@ export { ToolResponseEventSchema, ToolResponseRequiredEventSchema, UserToolApprovalMessageSchema, + UserToolApprovalPolicyMessageSchema, UserToolResponseMessageSchema, newEventId, } from './events/schema'; @@ -124,6 +125,7 @@ export type { MCPServerInitInfo, ThreadDoneEvent, ThreadOverwriteContextEvent, + UserToolApprovalPolicyMessage, } from './events/schema'; export { CompletionUsageSchema } from './llm/LLMTypes'; export type { CompletionUsage } from './llm/LLMTypes'; From 8d0e3729e293b170541ed21fddfb8882f322ddd2 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 09:39:54 +0530 Subject: [PATCH 16/23] add to inbound event --- packages/trueforge-core/src/agent-session/schemas/turn.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/trueforge-core/src/agent-session/schemas/turn.ts b/packages/trueforge-core/src/agent-session/schemas/turn.ts index 3c08a46db..dd6d6dec6 100644 --- a/packages/trueforge-core/src/agent-session/schemas/turn.ts +++ b/packages/trueforge-core/src/agent-session/schemas/turn.ts @@ -5,6 +5,7 @@ import { AgentInputUserMessageSchema, ModelMessageEventSchema, UserToolApprovalMessageSchema, + UserToolApprovalPolicyMessageSchema, UserToolResponseMessageSchema, } from '../../core/events/schema'; @@ -177,7 +178,11 @@ export const CreateTurnRequestSchema = z .openapi('CreateTurnRequest'); export const TurnInboundEventItemSchema = z - .discriminatedUnion('type', [UserToolApprovalMessageSchema, UserToolResponseMessageSchema]) + .discriminatedUnion('type', [ + UserToolApprovalMessageSchema, + UserToolResponseMessageSchema, + UserToolApprovalPolicyMessageSchema, + ]) .openapi('TurnInboundEventItem'); export type Turn = z.infer; From 900a4dd7a0b185bc3d6dd81a2c264f2baacdfb49 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Wed, 23 Sep 2026 10:13:36 +0530 Subject: [PATCH 17/23] add POST events API and add events to session stream --- .changeset/turn-inbound-events-api.md | 6 + .../src/agent-session/schemas/events.ts | 6 + .../trueforge-core/src/core/events/schema.ts | 32 +++- packages/trueforge-core/src/core/index.ts | 6 + packages/trueforge/src/apis/turns.ts | 53 ++++++ packages/trueforge/src/routes/turnRoutes.ts | 50 +++++- packages/trueforge/src/schemas/events.ts | 34 ++++ .../tests/unit/apis/sessionHttp.test.ts | 162 ++++++++++++++++++ .../trueforge/tests/unit/apis/turns.test.ts | 35 ++++ 9 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 .changeset/turn-inbound-events-api.md diff --git a/.changeset/turn-inbound-events-api.md b/.changeset/turn-inbound-events-api.md new file mode 100644 index 000000000..909824e72 --- /dev/null +++ b/.changeset/turn-inbound-events-api.md @@ -0,0 +1,6 @@ +--- +"@truefoundry/trueforge": patch +"@truefoundry/trueforge-core": patch +--- + +Add `POST /sessions/{session_id}/turns/{turn_id}/events` to persist inbound tip HITL / policy events. Response items use the same `UserToolApprovalEvent` / `UserToolResponseEvent` / `UserToolApprovalPolicyEvent` shapes as `SessionEvent` and the turn SSE stream. diff --git a/packages/trueforge-core/src/agent-session/schemas/events.ts b/packages/trueforge-core/src/agent-session/schemas/events.ts index 370f209c2..0a098f602 100644 --- a/packages/trueforge-core/src/agent-session/schemas/events.ts +++ b/packages/trueforge-core/src/agent-session/schemas/events.ts @@ -18,6 +18,9 @@ import { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolApprovalPolicyEventSchema, + UserToolResponseEventSchema, } from '../../core/events/schema'; import { TurnInputItemSchema, @@ -111,6 +114,9 @@ export const SessionEventSchema = z SandboxCreatedEventSchema, ToolApprovalRequiredEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, ]) .openapi('SessionEvent'); diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index d36fe5721..fbb9deed2 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -98,7 +98,7 @@ export const UserToolResponseMessageSchema = z }) .openapi('UserToolResponseMessage'); - export const ToolApprovalPolicyAllowSessionSchema = z +export const ToolApprovalPolicyAllowSessionSchema = z .object({ type: z.literal('allow_session').describe('Allow matching tool calls for the rest of this session.'), expire_at: z @@ -125,6 +125,33 @@ export const UserToolApprovalPolicyMessageSchema = z }) .openapi('UserToolApprovalPolicyMessage'); +/** Durable / SSE form of {@link UserToolApprovalMessageSchema}. */ +export const UserToolApprovalEventSchema = z + .object({ + ...UserToolApprovalMessageSchema.shape, + id: EventIdSchema, + created_at: z.string().describe('ISO 8601 event timestamp.'), + }) + .openapi('UserToolApprovalEvent'); + +/** Durable / SSE form of {@link UserToolResponseMessageSchema}. */ +export const UserToolResponseEventSchema = z + .object({ + ...UserToolResponseMessageSchema.shape, + id: EventIdSchema, + created_at: z.string().describe('ISO 8601 event timestamp.'), + }) + .openapi('UserToolResponseEvent'); + +/** Durable / SSE form of {@link UserToolApprovalPolicyMessageSchema}. */ +export const UserToolApprovalPolicyEventSchema = z + .object({ + ...UserToolApprovalPolicyMessageSchema.shape, + id: EventIdSchema, + created_at: z.string().describe('ISO 8601 event timestamp.'), + }) + .openapi('UserToolApprovalPolicyEvent'); + export const TextContentPartSchema = z .object({ type: z.literal('text').describe('Text content part.'), @@ -404,6 +431,9 @@ export type UserToolApprovalMessage = z.infer; export type ToolApprovalPolicyItem = z.infer; export type UserToolApprovalPolicyMessage = z.infer; +export type UserToolApprovalEvent = z.infer; +export type UserToolResponseEvent = z.infer; +export type UserToolApprovalPolicyEvent = z.infer; export type AgentApprovalDecisionMessage = z.infer; export type InputTokensBreakdown = z.infer; export type ModelMessageUsage = z.infer; diff --git a/packages/trueforge-core/src/core/index.ts b/packages/trueforge-core/src/core/index.ts index c9056a503..2a6fdf465 100644 --- a/packages/trueforge-core/src/core/index.ts +++ b/packages/trueforge-core/src/core/index.ts @@ -113,8 +113,11 @@ export { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, UserToolApprovalMessageSchema, + UserToolApprovalPolicyEventSchema, UserToolApprovalPolicyMessageSchema, + UserToolResponseEventSchema, UserToolResponseMessageSchema, newEventId, } from './events/schema'; @@ -125,7 +128,10 @@ export type { MCPServerInitInfo, ThreadDoneEvent, ThreadOverwriteContextEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, UserToolApprovalPolicyMessage, + UserToolResponseEvent, } from './events/schema'; export { CompletionUsageSchema } from './llm/LLMTypes'; export type { CompletionUsage } from './llm/LLMTypes'; diff --git a/packages/trueforge/src/apis/turns.ts b/packages/trueforge/src/apis/turns.ts index 4ffc8994c..a25f162a1 100644 --- a/packages/trueforge/src/apis/turns.ts +++ b/packages/trueforge/src/apis/turns.ts @@ -22,6 +22,7 @@ import { isAgentInputUserMessage, isFileContentPart, McpConnectionError, + newEventId, rawSandboxId, redisKey, SandboxError, @@ -42,6 +43,7 @@ import type { ISkillStore } from '../db/skillStore'; import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { createAndExecuteTurnRoute, + createTurnInboundEventRoute, downloadSandboxFileRoute, getTurnRoute, listTurnEventsRoute, @@ -923,12 +925,63 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { }); }; + /** Persist-only inbox write; apply / wake is follow-up work. */ + const createTurnInboundEventHandler: RouteHandler = async c => { + const { session_id: sessionId, turn_id: turnId } = c.req.valid('param'); + const body = c.req.valid('json'); + const requestContext = deps.resolveRequestContext(c); + const session = await deps.sessions.get({ + tenant_id: requestContext.tenant_id, + session_id: sessionId, + }); + if (!session) { + return c.json({ error: { message: `Session not found: ${sessionId}` } }, 404); + } + if ( + !isSessionOwner({ + subject_id: requestContext.subject.id, + created_by_subject: session.record.created_by_subject, + }) + ) { + return c.json({ error: { message: FORBIDDEN_SESSION_ACCESS } }, 403); + } + + const createdAt = new Date().toISOString(); + const events = body.events.map(payload => { + const id = newEventId(); + return { + event_id: id, + payload, + created_at: createdAt, + created: { ...payload, id, created_at: createdAt }, + }; + }); + + try { + await deps.sessionStore.insertTurnInboundEvents({ + session_id: sessionId, + turn_id: turnId, + events: events.map(({ event_id, payload, created_at }) => ({ event_id, payload, created_at })), + }); + return c.json({ data: events.map(e => e.created) }, 201); + } catch (error) { + if (error instanceof SessionStoreNotFoundError) { + return c.json({ error: { message: error.message } }, 404); + } + if (error instanceof SessionStoreConflictError) { + return c.json({ error: { message: error.message } }, 409); + } + throw error; + } + }; + const router = new OpenAPIHono(); router.openapi(createAndExecuteTurnRoute, createAndExecuteTurnHandler); router.openapi(listTurnsRoute, listTurnsHandler); router.openapi(getTurnRoute, getTurnHandler); router.openapi(downloadSandboxFileRoute, downloadSandboxFileHandler); router.openapi(listTurnEventsRoute, listTurnEventsHandler); + router.openapi(createTurnInboundEventRoute, createTurnInboundEventHandler); router.openapi(subscribeTurnRoute, subscribeTurnHandler); return router; } diff --git a/packages/trueforge/src/routes/turnRoutes.ts b/packages/trueforge/src/routes/turnRoutes.ts index 14f9b9398..641d2b2ca 100644 --- a/packages/trueforge/src/routes/turnRoutes.ts +++ b/packages/trueforge/src/routes/turnRoutes.ts @@ -5,7 +5,11 @@ */ import { createRoute, z } from '@hono/zod-openapi'; import { RequestErrorResponseSchema } from '../schemas/errors'; -import { TurnStreamingEventSchema } from '../schemas/events'; +import { + CreateTurnInboundEventRequestSchema, + CreateTurnInboundEventResponseSchema, + TurnStreamingEventSchema, +} from '../schemas/events'; import { CreateTurnRequestSchema, DownloadSandboxFileRequestQuerySchema, @@ -168,6 +172,50 @@ export const listTurnEventsRoute = createRoute({ }, }); +/** + * Create inbound tip HITL / policy events in `turn_inbound_events`. + * Apply / wake / auto-continue land in follow-up work — this route only inserts. + */ +export const createTurnInboundEventRoute = createRoute({ + method: 'post', + path: '/{session_id}/turns/{turn_id}/events', + tags: [OpenApiTag.AGENT_SESSIONS], + summary: 'Create turn inbound events', + description: + 'Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step.', + 'x-fern-sdk-group-name': ['sessions'], + 'x-fern-sdk-method-name': 'create_turn_inbound_event', + request: { + params: TurnIdParamsSchema, + body: { + content: { 'application/json': { schema: CreateTurnInboundEventRequestSchema } }, + required: true, + }, + }, + responses: { + 201: { + content: { 'application/json': { schema: CreateTurnInboundEventResponseSchema } }, + description: 'Events created in the turn inbox.', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid request body.', + }, + 403: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Caller is not the session creator.', + }, + 404: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Session or turn not found.', + }, + 409: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Turn is terminal, or an event id already exists.', + }, + }, +}); + export const createAndExecuteTurnRoute = createRoute({ method: 'post', path: '/{session_id}/turns', diff --git a/packages/trueforge/src/schemas/events.ts b/packages/trueforge/src/schemas/events.ts index 4b702a6e1..2717cecd1 100644 --- a/packages/trueforge/src/schemas/events.ts +++ b/packages/trueforge/src/schemas/events.ts @@ -9,6 +9,7 @@ import { TokenPaginationSchema, TurnCreatedEventSchema, TurnDoneEventSchema, + TurnInboundEventItemSchema, TurnUpdateEventSchema, } from '@truefoundry/trueforge-core/agent-session'; import { @@ -22,12 +23,42 @@ import { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolApprovalPolicyEventSchema, + UserToolResponseEventSchema, } from '@truefoundry/trueforge-core/core'; import { EVENTS_PAGE_LIMIT } from './common'; export type { TurnCreatedEvent } from '@truefoundry/trueforge-core/agent-session'; export { EventType }; +/** Client → harness inbound events (tip HITL / sticky policy). Persisted to the turn inbox. */ +export const CreateTurnInboundEventRequestSchema = z + .object({ + events: z + .array(TurnInboundEventItemSchema) + .min(1) + .describe('One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).'), + }) + .openapi('CreateTurnInboundEventRequest'); + +/** Same shapes as the durable stream / {@link SessionEvent} members for these types. */ +export const CreatedTurnInboundEventSchema = z + .discriminatedUnion('type', [ + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, + ]) + .openapi('CreatedTurnInboundEvent'); + +export const CreateTurnInboundEventResponseSchema = z + .object({ + data: z + .array(CreatedTurnInboundEventSchema) + .describe('Created inbox events with server-minted `id` and `created_at`, in request order.'), + }) + .openapi('CreateTurnInboundEventResponse'); + /** Live SSE stream for session turns — content events, deltas and lifecycle. */ export const TurnStreamingEventSchema = z .discriminatedUnion('type', [ @@ -41,6 +72,9 @@ export const TurnStreamingEventSchema = z SandboxCreatedEventSchema, ToolApprovalRequiredEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, TurnCreatedEventSchema, TurnUpdateEventSchema, TurnDoneEventSchema, diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 5e64789f2..496a50ac8 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -3,14 +3,21 @@ import { AgentSpecSchema, Sessions } from '@truefoundry/trueforge-core/agent-ses import { RequestReplyRouter } from '@truefoundry/trueforge-core/request-reply'; import { createClient } from 'redis'; import { createLogger } from 'winston'; +import { + makeCreateTurnInput, + makeDoneTurnState, + makeTurnDoneEvent, +} from '../../../../trueforge-core/tests/agent-session/testHelpers'; import { createInternalMetricsRouter } from '../../../src/apis/sessionMetrics'; import { createInternalSessionsRouter, createSessionsRouter, type SessionsRouterDeps, } from '../../../src/apis/sessions'; +import { createTurnsRouter } from '../../../src/apis/turns'; import { TrueForgeAuthorizer, type Authorizer } from '../../../src/auth/authorizer'; import { STANDALONE_REQUEST_CONTEXT } from '../../../src/auth/identity'; +import { McpServerWithAuthStore } from '../../../src/db/McpServerWithAuthStore'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; import { SqliteAgentStore } from '../../../src/db/sqlite/agent-store/SqliteAgentStore'; import { createSqliteDb } from '../../../src/db/sqlite/client'; @@ -20,8 +27,10 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionMetricsStore } from '../../../src/db/sqlite/session-metrics/SqliteSessionMetricsStore'; import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; +import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; +import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription/index.js'; import { ListSessionsResponseSchema } from '../../../src/schemas/session'; import { GetSessionMetricsChartDataResponseSchema, @@ -108,6 +117,30 @@ describe('sessions HTTP agent binding', () => { sessionDeps = deps; app = new OpenAPIHono(); app.route('/', createSessionsRouter(deps)); + const tokenStore = new SqliteOAuthTokenStore(db); + app.route( + '/', + createTurnsRouter({ + sessions, + sessionStore, + activeTurns: deps.activeTurns, + resolveModelProviderStore: () => modelProviderStore, + resolveMcpServerStore: () => + new McpServerWithAuthStore({ + store: mcpServerStore, + tokenStore, + clientName: 'test-client', + }), + resolveSkillStore: () => skillStore, + resolveAgentStore: () => agentStore, + eventSubscriptions: new EventSubscriptionRegistry(undefined), + resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, + logger: deps.logger, + resolveRequestContext: deps.resolveRequestContext, + authorizer: deps.authorizer, + }), + ); app.route('/api/internal/sessions', createInternalSessionsRouter(deps)); app.route( '/api/internal/metrics', @@ -423,10 +456,139 @@ describe('sessions HTTP agent binding', () => { expect(eventsForbidden.status).toBe(403); expect(await eventsForbidden.json()).toEqual(forbiddenBody); + const sendEventsForbidden = await app.request( + '/other-user-session/turns/any-turn/events', + jsonInit('POST', { + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + ); + expect(sendEventsForbidden.status).toBe(403); + expect(await sendEventsForbidden.json()).toEqual(forbiddenBody); + const allowed = await app.request(`/${json.data.id}`); expect(allowed.status).toBe(200); }); + it('POST /sessions/{id}/turns/{turn_id}/events persists inbox rows for a running tip', async () => { + const created = await app.request('/', jsonInit('POST', { agent: { spec: inlineSpec } })); + expect(created.status).toBe(201); + const { data: session } = (await created.json()) as { data: { id: string } }; + await sessionStore.createTurn(makeCreateTurnInput({ sessionId: session.id, turnId: 'tip-1' })); + + const res = await app.request( + `/${session.id}/turns/tip-1/events`, + jsonInit('POST', { + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + { + type: 'user.tool_approval_policy', + policies: [ + { + server: 'github', + tool_name: 'create_issue', + action: { type: 'allow_session' }, + }, + ], + }, + ], + }), + ); + expect(res.status).toBe(201); + const body = (await res.json()) as { + data: Array<{ + id: string; + created_at: string; + type: string; + thread_id?: string; + tool_call_id?: string; + approval?: { status: string }; + policies?: unknown[]; + }>; + }; + expect(body.data).toHaveLength(2); + expect(body.data[0]).toMatchObject({ + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }); + expect(body.data[0]?.id).toEqual(expect.any(String)); + expect(body.data[0]?.created_at).toEqual(expect.any(String)); + expect(body.data[1]).toMatchObject({ + type: 'user.tool_approval_policy', + policies: [ + { + server: 'github', + tool_name: 'create_issue', + action: { type: 'allow_session' }, + }, + ], + }); + + const pending = await sessionStore.listUnconsumedTurnInboundEvents({ + session_id: session.id, + turn_id: 'tip-1', + }); + expect(pending.map(e => e.event_id)).toEqual(body.data.map(e => e.id)); + expect(pending.map(e => e.payload.type)).toEqual(['user.tool_approval', 'user.tool_approval_policy']); + }); + + it('POST /sessions/{id}/turns/{turn_id}/events returns 404 for unknown tip and 409 for terminal tip', async () => { + const created = await app.request('/', jsonInit('POST', { agent: { spec: inlineSpec } })); + expect(created.status).toBe(201); + const { data: session } = (await created.json()) as { data: { id: string } }; + await sessionStore.createTurn(makeCreateTurnInput({ sessionId: session.id, turnId: 'tip-done' })); + const doneState = makeDoneTurnState(); + await sessionStore.updateTurnState({ + session_id: session.id, + turn_id: 'tip-done', + state: doneState, + turn_done_event: makeTurnDoneEvent(doneState), + }); + + const missing = await app.request( + `/${session.id}/turns/no-such-tip/events`, + jsonInit('POST', { + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + ); + expect(missing.status).toBe(404); + + const terminal = await app.request( + `/${session.id}/turns/tip-done/events`, + jsonInit('POST', { + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + ); + expect(terminal.status).toBe(409); + }); + it('rejects PATCH agent on a named session', async () => { const agent = await agentStore.createAgent({ tenant_id: 'default', diff --git a/packages/trueforge/tests/unit/apis/turns.test.ts b/packages/trueforge/tests/unit/apis/turns.test.ts index b8bdc9be2..db85a17a8 100644 --- a/packages/trueforge/tests/unit/apis/turns.test.ts +++ b/packages/trueforge/tests/unit/apis/turns.test.ts @@ -107,6 +107,23 @@ describe('turns', () => { expect(eventsResponse.status).toBe(403); expect(await eventsResponse.json()).toEqual(forbiddenAccess); + const createEventsResponse = await app.request('/s1/turns/any-turn/events', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + }); + expect(createEventsResponse.status).toBe(403); + expect(await createEventsResponse.json()).toEqual(forbiddenAccess); + const subscribeResponse = await app.request('/s1/turns/any-turn/subscribe'); expect(subscribeResponse.status).toBe(403); expect(await subscribeResponse.json()).toEqual(forbiddenAccess); @@ -193,6 +210,24 @@ describe('turns', () => { }) ).status, ).toBe(403); + expect( + ( + await app.request('/managed-session/turns/missing/events', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + }) + ).status, + ).toBe(403); }); }); From e8f80f1cd17333c9f115bbcdd2d7ad7d7c973ee0 Mon Sep 17 00:00:00 2001 From: "trueforge-dev-bot[bot]" Date: Wed, 23 Sep 2026 04:46:31 +0000 Subject: [PATCH 18/23] Regenerate OpenAPI document and SDKs --- ...60923044631-regenerate-sdk-from-openapi.md | 5 + .github/fern/openapi/openapi.json | 393 +++++++++++++++++- docs/openapi.json | 393 +++++++++++++++++- packages/trueforge-sdk/reference.md | 88 ++++ .../api/resources/sessions/client/Client.ts | 152 +++++++ .../requests/CreateTurnInboundEventRequest.ts | 21 + .../sessions/client/requests/index.ts | 1 + .../types/CreateTurnInboundEventResponse.ts | 8 + .../src/api/types/CreatedTurnInboundEvent.ts | 8 + .../src/api/types/SessionEvent.ts | 5 +- .../types/ToolApprovalPolicyAllowSession.ts | 7 + .../src/api/types/ToolApprovalPolicyItem.ts | 11 + .../src/api/types/TurnInboundEventItem.ts | 8 + .../src/api/types/TurnStreamingEvent.ts | 5 +- .../src/api/types/UserToolApprovalEvent.ts | 16 + .../api/types/UserToolApprovalPolicyEvent.ts | 13 + .../types/UserToolApprovalPolicyMessage.ts | 9 + .../src/api/types/UserToolResponseEvent.ts | 15 + packages/trueforge-sdk/src/api/types/index.ts | 9 + .../requests/CreateTurnInboundEventRequest.ts | 19 + .../sessions/client/requests/index.ts | 1 + .../types/CreateTurnInboundEventResponse.ts | 19 + .../types/CreatedTurnInboundEvent.ts | 21 + .../src/serialization/types/SessionEvent.ts | 11 +- .../types/ToolApprovalPolicyAllowSession.ts | 20 + .../types/ToolApprovalPolicyItem.ts | 23 + .../types/TurnInboundEventItem.ts | 21 + .../serialization/types/TurnStreamingEvent.ts | 11 +- .../types/UserToolApprovalEvent.ts | 29 ++ .../types/UserToolApprovalPolicyEvent.ts | 25 ++ .../types/UserToolApprovalPolicyMessage.ts | 21 + .../types/UserToolResponseEvent.ts | 28 ++ .../src/serialization/types/index.ts | 9 + .../trueforge-sdk/tests/wire/sessions.test.ts | 239 +++++++++++ python/trueforge_sdk/reference.md | 99 +++++ .../src/trueforge_sdk/__init__.py | 27 ++ .../src/trueforge_sdk/sessions/client.py | 120 ++++++ .../src/trueforge_sdk/sessions/raw_client.py | 217 ++++++++++ .../src/trueforge_sdk/types/__init__.py | 27 ++ .../create_turn_inbound_event_response.py | 23 + .../types/created_turn_inbound_event.py | 9 + .../src/trueforge_sdk/types/session_event.py | 6 + .../tool_approval_policy_allow_session.py | 24 ++ .../types/tool_approval_policy_item.py | 29 ++ .../types/turn_inbound_event_item.py | 9 + .../types/turn_streaming_event.py | 6 + .../types/user_tool_approval_event.py | 41 ++ .../types/user_tool_approval_policy_event.py | 35 ++ .../user_tool_approval_policy_message.py | 25 ++ .../types/user_tool_response_event.py | 44 ++ 50 files changed, 2397 insertions(+), 8 deletions(-) create mode 100644 .changeset/20260923044631-regenerate-sdk-from-openapi.md create mode 100644 packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts create mode 100644 packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts create mode 100644 packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts create mode 100644 packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts create mode 100644 packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts create mode 100644 packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts create mode 100644 packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts create mode 100644 packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts create mode 100644 packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts create mode 100644 packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts create mode 100644 packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyAllowSession.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts create mode 100644 packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py create mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py diff --git a/.changeset/20260923044631-regenerate-sdk-from-openapi.md b/.changeset/20260923044631-regenerate-sdk-from-openapi.md new file mode 100644 index 000000000..efd8ff00f --- /dev/null +++ b/.changeset/20260923044631-regenerate-sdk-from-openapi.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge-sdk": patch +--- + +Regenerate SDK from updated OpenAPI spec. diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 114708b55..a98af15c3 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -1163,6 +1163,37 @@ ], "type": "object" }, + "CreateTurnInboundEventRequest": { + "properties": { + "events": { + "description": "One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", + "items": { + "$ref": "#/components/schemas/TurnInboundEventItem" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "events" + ], + "type": "object" + }, + "CreateTurnInboundEventResponse": { + "properties": { + "data": { + "description": "Created inbox events with server-minted `id` and `created_at`, in request order.", + "items": { + "$ref": "#/components/schemas/CreatedTurnInboundEvent" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "CreateTurnRequest": { "properties": { "input": { @@ -1210,6 +1241,27 @@ ], "type": "object" }, + "CreatedTurnInboundEvent": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + } + ] + }, "CronExpression": { "description": "Standard 5-field cron expression, evaluated in `timezone`.", "pattern": "^[\\d*,\\-/]+(?:\\s+[\\d*,\\-/]+){4}$", @@ -4108,7 +4160,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -4148,6 +4203,15 @@ }, { "$ref": "#/components/schemas/ToolResponseRequiredEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" } ] }, @@ -4778,6 +4842,58 @@ ], "type": "object" }, + "ToolApprovalPolicyAllowSession": { + "properties": { + "expire_at": { + "description": "ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session.", + "type": "string" + }, + "type": { + "description": "Allow matching tool calls for the rest of this session.", + "enum": [ + "allow_session" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ToolApprovalPolicyItem": { + "properties": { + "action": { + "discriminator": { + "mapping": { + "allow_session": "#/components/schemas/ToolApprovalPolicyAllowSession" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/ToolApprovalPolicyAllowSession" + } + ] + }, + "server": { + "description": "MCP server name the tool belongs to.", + "minLength": 1, + "type": "string" + }, + "tool_name": { + "description": "Tool name this policy applies to.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "server", + "tool_name", + "action" + ], + "type": "object" + }, "ToolApprovalRequiredEvent": { "properties": { "created_at": { @@ -5228,6 +5344,27 @@ ], "type": "object" }, + "TurnInboundEventItem": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalMessage", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyMessage", + "user.tool_response": "#/components/schemas/UserToolResponseMessage" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalMessage" + }, + { + "$ref": "#/components/schemas/UserToolResponseMessage" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyMessage" + } + ] + }, "TurnInputItem": { "discriminator": { "mapping": { @@ -5467,7 +5604,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -5502,6 +5642,15 @@ { "$ref": "#/components/schemas/ToolResponseRequiredEvent" }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + }, { "$ref": "#/components/schemas/TurnCreatedEvent" }, @@ -5764,6 +5913,47 @@ } ] }, + "UserToolApprovalEvent": { + "properties": { + "approval": { + "$ref": "#/components/schemas/ApprovalDecision" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id being approved or denied.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.approval_required.", + "enum": [ + "user.tool_approval" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "approval", + "id", + "created_at" + ], + "type": "object" + }, "UserToolApprovalMessage": { "properties": { "approval": { @@ -5795,6 +5985,107 @@ ], "type": "object" }, + "UserToolApprovalPolicyEvent": { + "properties": { + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "policies": { + "description": "One or more (server, tool_name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies", + "id", + "created_at" + ], + "type": "object" + }, + "UserToolApprovalPolicyMessage": { + "properties": { + "policies": { + "description": "One or more (server, tool_name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies" + ], + "type": "object" + }, + "UserToolResponseEvent": { + "properties": { + "content": { + "description": "Client-side tool result content.", + "minLength": 1, + "type": "string" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id receiving the client response.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.response_required.", + "enum": [ + "user.tool_response" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "content", + "id", + "created_at" + ], + "type": "object" + }, "UserToolResponseMessage": { "properties": { "content": { @@ -9349,6 +9640,104 @@ "sessions" ], "x-fern-sdk-method-name": "list_turn_events" + }, + "post": { + "description": "Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step.", + "parameters": [ + { + "description": "Session identifier.", + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "description": "Session identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + }, + { + "description": "Turn identifier.", + "in": "path", + "name": "turn_id", + "required": true, + "schema": { + "description": "Turn identifier.", + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnInboundEventRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnInboundEventResponse" + } + } + }, + "description": "Events created in the turn inbox." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Caller is not the session creator." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Session or turn not found." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Turn is terminal, or an event id already exists." + } + }, + "summary": "Create turn inbound events", + "tags": [ + "Agent Sessions" + ], + "x-fern-sdk-group-name": [ + "sessions" + ], + "x-fern-sdk-method-name": "create_turn_inbound_event" } }, "/api/v1/sessions/{session_id}/turns/{turn_id}/subscribe": { diff --git a/docs/openapi.json b/docs/openapi.json index 114708b55..a98af15c3 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1163,6 +1163,37 @@ ], "type": "object" }, + "CreateTurnInboundEventRequest": { + "properties": { + "events": { + "description": "One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", + "items": { + "$ref": "#/components/schemas/TurnInboundEventItem" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "events" + ], + "type": "object" + }, + "CreateTurnInboundEventResponse": { + "properties": { + "data": { + "description": "Created inbox events with server-minted `id` and `created_at`, in request order.", + "items": { + "$ref": "#/components/schemas/CreatedTurnInboundEvent" + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "CreateTurnRequest": { "properties": { "input": { @@ -1210,6 +1241,27 @@ ], "type": "object" }, + "CreatedTurnInboundEvent": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + } + ] + }, "CronExpression": { "description": "Standard 5-field cron expression, evaluated in `timezone`.", "pattern": "^[\\d*,\\-/]+(?:\\s+[\\d*,\\-/]+){4}$", @@ -4108,7 +4160,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -4148,6 +4203,15 @@ }, { "$ref": "#/components/schemas/ToolResponseRequiredEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" } ] }, @@ -4778,6 +4842,58 @@ ], "type": "object" }, + "ToolApprovalPolicyAllowSession": { + "properties": { + "expire_at": { + "description": "ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session.", + "type": "string" + }, + "type": { + "description": "Allow matching tool calls for the rest of this session.", + "enum": [ + "allow_session" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ToolApprovalPolicyItem": { + "properties": { + "action": { + "discriminator": { + "mapping": { + "allow_session": "#/components/schemas/ToolApprovalPolicyAllowSession" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/ToolApprovalPolicyAllowSession" + } + ] + }, + "server": { + "description": "MCP server name the tool belongs to.", + "minLength": 1, + "type": "string" + }, + "tool_name": { + "description": "Tool name this policy applies to.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "server", + "tool_name", + "action" + ], + "type": "object" + }, "ToolApprovalRequiredEvent": { "properties": { "created_at": { @@ -5228,6 +5344,27 @@ ], "type": "object" }, + "TurnInboundEventItem": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalMessage", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyMessage", + "user.tool_response": "#/components/schemas/UserToolResponseMessage" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalMessage" + }, + { + "$ref": "#/components/schemas/UserToolResponseMessage" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyMessage" + } + ] + }, "TurnInputItem": { "discriminator": { "mapping": { @@ -5467,7 +5604,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -5502,6 +5642,15 @@ { "$ref": "#/components/schemas/ToolResponseRequiredEvent" }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + }, { "$ref": "#/components/schemas/TurnCreatedEvent" }, @@ -5764,6 +5913,47 @@ } ] }, + "UserToolApprovalEvent": { + "properties": { + "approval": { + "$ref": "#/components/schemas/ApprovalDecision" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id being approved or denied.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.approval_required.", + "enum": [ + "user.tool_approval" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "approval", + "id", + "created_at" + ], + "type": "object" + }, "UserToolApprovalMessage": { "properties": { "approval": { @@ -5795,6 +5985,107 @@ ], "type": "object" }, + "UserToolApprovalPolicyEvent": { + "properties": { + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "policies": { + "description": "One or more (server, tool_name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies", + "id", + "created_at" + ], + "type": "object" + }, + "UserToolApprovalPolicyMessage": { + "properties": { + "policies": { + "description": "One or more (server, tool_name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies" + ], + "type": "object" + }, + "UserToolResponseEvent": { + "properties": { + "content": { + "description": "Client-side tool result content.", + "minLength": 1, + "type": "string" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id receiving the client response.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.response_required.", + "enum": [ + "user.tool_response" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "content", + "id", + "created_at" + ], + "type": "object" + }, "UserToolResponseMessage": { "properties": { "content": { @@ -9349,6 +9640,104 @@ "sessions" ], "x-fern-sdk-method-name": "list_turn_events" + }, + "post": { + "description": "Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step.", + "parameters": [ + { + "description": "Session identifier.", + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "description": "Session identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + }, + { + "description": "Turn identifier.", + "in": "path", + "name": "turn_id", + "required": true, + "schema": { + "description": "Turn identifier.", + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnInboundEventRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnInboundEventResponse" + } + } + }, + "description": "Events created in the turn inbox." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Caller is not the session creator." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Session or turn not found." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Turn is terminal, or an event id already exists." + } + }, + "summary": "Create turn inbound events", + "tags": [ + "Agent Sessions" + ], + "x-fern-sdk-group-name": [ + "sessions" + ], + "x-fern-sdk-method-name": "create_turn_inbound_event" } }, "/api/v1/sessions/{session_id}/turns/{turn_id}/subscribe": { diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index d4d1e893f..7ca82eba3 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -2366,6 +2366,94 @@ const response = page.response; + + + + +
client.sessions.createTurnInboundEvent(session_id, turn_id, { ...params }) -> TrueForge.CreateTurnInboundEventResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + events: [{ + approval: { + status: "allow" + }, + threadId: "thread_id", + toolCallId: "tool_call_id", + type: "user.tool_approval" + }] +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `string` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `string` — Turn identifier. + +
+
+ +
+
+ +**request:** `TrueForge.CreateTurnInboundEventRequest` + +
+
+ +
+
+ +**requestOptions:** `SessionsClient.RequestOptions` + +
+
+
+
+ +
diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts index 422c38ef2..b4555d19d 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts @@ -1742,6 +1742,158 @@ export class SessionsClient { }); } + /** + * Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + * + * @param {string} session_id - Session identifier. + * @param {string} turn_id - Turn identifier. + * @param {TrueForge.CreateTurnInboundEventRequest} request + * @param {SessionsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.ForbiddenError} + * @throws {@link TrueForge.NotFoundError} + * @throws {@link TrueForge.ConflictError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + * events: [{ + * approval: { + * status: "allow" + * }, + * threadId: "thread_id", + * toolCallId: "tool_call_id", + * type: "user.tool_approval" + * }] + * }) + */ + public createTurnInboundEvent( + session_id: string, + turn_id: string, + request: TrueForge.CreateTurnInboundEventRequest, + requestOptions?: SessionsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise( + this.__createTurnInboundEvent(session_id, turn_id, request, requestOptions), + ); + } + + private async __createTurnInboundEvent( + session_id: string, + turn_id: string, + request: TrueForge.CreateTurnInboundEventRequest, + requestOptions?: SessionsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `api/v1/sessions/${core.url.encodePathParam(session_id)}/turns/${core.url.encodePathParam(turn_id)}/events`, + ), + method: "POST", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters( + serializers.CreateTurnInboundEventRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + omitUndefined: true, + }), + requestOptions?.additionalBodyParameters, + ), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.CreateTurnInboundEventResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 403: + throw new TrueForge.ForbiddenError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 404: + throw new TrueForge.NotFoundError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 409: + throw new TrueForge.ConflictError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/api/v1/sessions/{session_id}/turns/{turn_id}/events", + ); + } + /** * Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). */ diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts new file mode 100644 index 000000000..89bcebb2e --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../index.js"; + +/** + * @example + * { + * events: [{ + * approval: { + * status: "allow" + * }, + * threadId: "thread_id", + * toolCallId: "tool_call_id", + * type: "user.tool_approval" + * }] + * } + */ +export interface CreateTurnInboundEventRequest { + /** One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). */ + events: TrueForge.TurnInboundEventItem[]; +} diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts index 5299c014d..a113751e0 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts @@ -1,5 +1,6 @@ export type { CancelSessionRequest } from "./CancelSessionRequest.js"; export type { CreateSessionRequest } from "./CreateSessionRequest.js"; +export type { CreateTurnInboundEventRequest } from "./CreateTurnInboundEventRequest.js"; export type { CreateTurnSessionsRequest } from "./CreateTurnSessionsRequest.js"; export type { CreateTurnSessionsStreamRequest } from "./CreateTurnSessionsStreamRequest.js"; export type { DownloadSandboxFileSessionsRequest } from "./DownloadSandboxFileSessionsRequest.js"; diff --git a/packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts b/packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts new file mode 100644 index 000000000..af4d86548 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface CreateTurnInboundEventResponse { + /** Created inbox events with server-minted `id` and `created_at`, in request order. */ + data: TrueForge.CreatedTurnInboundEvent[]; +} diff --git a/packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts b/packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts new file mode 100644 index 000000000..7a065c229 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export type CreatedTurnInboundEvent = + | TrueForge.UserToolApprovalEvent + | TrueForge.UserToolApprovalPolicyEvent + | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/SessionEvent.ts b/packages/trueforge-sdk/src/api/types/SessionEvent.ts index 8d305b279..d47a36b5b 100644 --- a/packages/trueforge-sdk/src/api/types/SessionEvent.ts +++ b/packages/trueforge-sdk/src/api/types/SessionEvent.ts @@ -14,4 +14,7 @@ export type SessionEvent = | TrueForge.ToolResponseRequiredEvent | TrueForge.TurnCreatedEvent | TrueForge.TurnDoneEvent - | TrueForge.TurnUpdateEvent; + | TrueForge.TurnUpdateEvent + | TrueForge.UserToolApprovalEvent + | TrueForge.UserToolApprovalPolicyEvent + | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts new file mode 100644 index 000000000..a01e8383b --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface ToolApprovalPolicyAllowSession { + /** ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session. */ + expireAt?: string; + type: "allow_session"; +} diff --git a/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts new file mode 100644 index 000000000..ee585d838 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts @@ -0,0 +1,11 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface ToolApprovalPolicyItem { + action: TrueForge.ToolApprovalPolicyAllowSession; + /** MCP server name the tool belongs to. */ + server: string; + /** Tool name this policy applies to. */ + toolName: string; +} diff --git a/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts b/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts new file mode 100644 index 000000000..e23d0668c --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export type TurnInboundEventItem = + | TrueForge.UserToolApprovalMessage + | TrueForge.UserToolApprovalPolicyMessage + | TrueForge.UserToolResponseMessage; diff --git a/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts b/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts index 61f6515ac..35de83b0b 100644 --- a/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts +++ b/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts @@ -15,4 +15,7 @@ export type TurnStreamingEvent = | TrueForge.ToolResponseRequiredEvent | TrueForge.TurnCreatedEvent | TrueForge.TurnDoneEvent - | TrueForge.TurnUpdateEvent; + | TrueForge.TurnUpdateEvent + | TrueForge.UserToolApprovalEvent + | TrueForge.UserToolApprovalPolicyEvent + | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts new file mode 100644 index 000000000..d026b2b79 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface UserToolApprovalEvent { + approval: TrueForge.ApprovalDecision; + /** ISO 8601 event timestamp. */ + createdAt: string; + /** Unique identifier for the event (monotonic ULID). */ + id: string; + /** Thread that owns the pending tool call. */ + threadId: string; + /** Tool call id being approved or denied. */ + toolCallId: string; + type: "user.tool_approval"; +} diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts new file mode 100644 index 000000000..346e5b739 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface UserToolApprovalPolicyEvent { + /** ISO 8601 event timestamp. */ + createdAt: string; + /** Unique identifier for the event (monotonic ULID). */ + id: string; + /** One or more (server, tool_name) policy entries. */ + policies: TrueForge.ToolApprovalPolicyItem[]; + type: "user.tool_approval_policy"; +} diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts new file mode 100644 index 000000000..41a7ac8ea --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface UserToolApprovalPolicyMessage { + /** One or more (server, tool_name) policy entries. */ + policies: TrueForge.ToolApprovalPolicyItem[]; + type: "user.tool_approval_policy"; +} diff --git a/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts new file mode 100644 index 000000000..aec481da0 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface UserToolResponseEvent { + /** Client-side tool result content. */ + content: string; + /** ISO 8601 event timestamp. */ + createdAt: string; + /** Unique identifier for the event (monotonic ULID). */ + id: string; + /** Thread that owns the pending tool call. */ + threadId: string; + /** Tool call id receiving the client response. */ + toolCallId: string; + type: "user.tool_response"; +} diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index b51454d87..1c08669c3 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -47,8 +47,10 @@ export * from "./ConfiguredSkill.js"; export * from "./ConfiguredWebSearchProvider.js"; export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; +export * from "./CreatedTurnInboundEvent.js"; export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; +export * from "./CreateTurnInboundEventResponse.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; export * from "./DaytonaSandboxProviderAuth.js"; @@ -203,6 +205,8 @@ export * from "./ThreadStateError.js"; export * from "./Timezone.js"; export * from "./TogetherAiModelProvider.js"; export * from "./TokenPagination.js"; +export * from "./ToolApprovalPolicyAllowSession.js"; +export * from "./ToolApprovalPolicyItem.js"; export * from "./ToolApprovalRequiredEvent.js"; export * from "./ToolCall.js"; export * from "./ToolCallRef.js"; @@ -217,6 +221,7 @@ export * from "./Turn.js"; export * from "./TurnCreatedEvent.js"; export * from "./TurnDoneEvent.js"; export * from "./TurnDoneEventState.js"; +export * from "./TurnInboundEventItem.js"; export * from "./TurnInputItem.js"; export * from "./TurnMetrics.js"; export * from "./TurnState.js"; @@ -235,7 +240,11 @@ export * from "./TurnUpdateStateRunning.js"; export * from "./UserMessage.js"; export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; +export * from "./UserToolApprovalEvent.js"; export * from "./UserToolApprovalMessage.js"; +export * from "./UserToolApprovalPolicyEvent.js"; +export * from "./UserToolApprovalPolicyMessage.js"; +export * from "./UserToolResponseEvent.js"; export * from "./UserToolResponseMessage.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts new file mode 100644 index 000000000..69464825a --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; +import { TurnInboundEventItem } from "../../../../types/TurnInboundEventItem.js"; + +export const CreateTurnInboundEventRequest: core.serialization.Schema< + serializers.CreateTurnInboundEventRequest.Raw, + TrueForge.CreateTurnInboundEventRequest +> = core.serialization.object({ + events: core.serialization.list(TurnInboundEventItem), +}); + +export declare namespace CreateTurnInboundEventRequest { + export interface Raw { + events: TurnInboundEventItem.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts index 834355641..d5b8969f1 100644 --- a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts +++ b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts @@ -1,5 +1,6 @@ export { CancelSessionRequest } from "./CancelSessionRequest.js"; export { CreateSessionRequest } from "./CreateSessionRequest.js"; +export { CreateTurnInboundEventRequest } from "./CreateTurnInboundEventRequest.js"; export { CreateTurnSessionsRequest } from "./CreateTurnSessionsRequest.js"; export { CreateTurnSessionsStreamRequest } from "./CreateTurnSessionsStreamRequest.js"; export { UpdateSessionRequest } from "./UpdateSessionRequest.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts b/packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts new file mode 100644 index 000000000..af0be6de0 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { CreatedTurnInboundEvent } from "./CreatedTurnInboundEvent.js"; + +export const CreateTurnInboundEventResponse: core.serialization.ObjectSchema< + serializers.CreateTurnInboundEventResponse.Raw, + TrueForge.CreateTurnInboundEventResponse +> = core.serialization.object({ + data: core.serialization.list(CreatedTurnInboundEvent), +}); + +export declare namespace CreateTurnInboundEventResponse { + export interface Raw { + data: CreatedTurnInboundEvent.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts b/packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts new file mode 100644 index 000000000..ae2db93d7 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; +import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; +import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; + +export const CreatedTurnInboundEvent: core.serialization.Schema< + serializers.CreatedTurnInboundEvent.Raw, + TrueForge.CreatedTurnInboundEvent +> = core.serialization.undiscriminatedUnion([ + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, +]); + +export declare namespace CreatedTurnInboundEvent { + export type Raw = UserToolApprovalEvent.Raw | UserToolApprovalPolicyEvent.Raw | UserToolResponseEvent.Raw; +} diff --git a/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts b/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts index bd14830f2..68d0482fc 100644 --- a/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts @@ -15,6 +15,9 @@ import { ToolResponseRequiredEvent } from "./ToolResponseRequiredEvent.js"; import { TurnCreatedEvent } from "./TurnCreatedEvent.js"; import { TurnDoneEvent } from "./TurnDoneEvent.js"; import { TurnUpdateEvent } from "./TurnUpdateEvent.js"; +import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; +import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; +import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; export const SessionEvent: core.serialization.Schema = core.serialization.undiscriminatedUnion([ @@ -30,6 +33,9 @@ export const SessionEvent: core.serialization.Schema = core.serialization.object({ + expireAt: core.serialization.property("expire_at", core.serialization.string().optional()), + type: core.serialization.stringLiteral("allow_session"), +}); + +export declare namespace ToolApprovalPolicyAllowSession { + export interface Raw { + expire_at?: string | null; + type: "allow_session"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts b/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts new file mode 100644 index 000000000..0390bc75c --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts @@ -0,0 +1,23 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ToolApprovalPolicyAllowSession } from "./ToolApprovalPolicyAllowSession.js"; + +export const ToolApprovalPolicyItem: core.serialization.ObjectSchema< + serializers.ToolApprovalPolicyItem.Raw, + TrueForge.ToolApprovalPolicyItem +> = core.serialization.object({ + action: ToolApprovalPolicyAllowSession, + server: core.serialization.string(), + toolName: core.serialization.property("tool_name", core.serialization.string()), +}); + +export declare namespace ToolApprovalPolicyItem { + export interface Raw { + action: ToolApprovalPolicyAllowSession.Raw; + server: string; + tool_name: string; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts b/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts new file mode 100644 index 000000000..e8770bb75 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { UserToolApprovalMessage } from "./UserToolApprovalMessage.js"; +import { UserToolApprovalPolicyMessage } from "./UserToolApprovalPolicyMessage.js"; +import { UserToolResponseMessage } from "./UserToolResponseMessage.js"; + +export const TurnInboundEventItem: core.serialization.Schema< + serializers.TurnInboundEventItem.Raw, + TrueForge.TurnInboundEventItem +> = core.serialization.undiscriminatedUnion([ + UserToolApprovalMessage, + UserToolApprovalPolicyMessage, + UserToolResponseMessage, +]); + +export declare namespace TurnInboundEventItem { + export type Raw = UserToolApprovalMessage.Raw | UserToolApprovalPolicyMessage.Raw | UserToolResponseMessage.Raw; +} diff --git a/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts b/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts index 29756a573..d160ee721 100644 --- a/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts @@ -16,6 +16,9 @@ import { ToolResponseRequiredEvent } from "./ToolResponseRequiredEvent.js"; import { TurnCreatedEvent } from "./TurnCreatedEvent.js"; import { TurnDoneEvent } from "./TurnDoneEvent.js"; import { TurnUpdateEvent } from "./TurnUpdateEvent.js"; +import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; +import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; +import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; export const TurnStreamingEvent: core.serialization.Schema< serializers.TurnStreamingEvent.Raw, @@ -34,6 +37,9 @@ export const TurnStreamingEvent: core.serialization.Schema< TurnCreatedEvent, TurnDoneEvent, TurnUpdateEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, ]); export declare namespace TurnStreamingEvent { @@ -50,5 +56,8 @@ export declare namespace TurnStreamingEvent { | ToolResponseRequiredEvent.Raw | TurnCreatedEvent.Raw | TurnDoneEvent.Raw - | TurnUpdateEvent.Raw; + | TurnUpdateEvent.Raw + | UserToolApprovalEvent.Raw + | UserToolApprovalPolicyEvent.Raw + | UserToolResponseEvent.Raw; } diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts new file mode 100644 index 000000000..edce2158c --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts @@ -0,0 +1,29 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ApprovalDecision } from "./ApprovalDecision.js"; + +export const UserToolApprovalEvent: core.serialization.ObjectSchema< + serializers.UserToolApprovalEvent.Raw, + TrueForge.UserToolApprovalEvent +> = core.serialization.object({ + approval: ApprovalDecision, + createdAt: core.serialization.property("created_at", core.serialization.string()), + id: core.serialization.string(), + threadId: core.serialization.property("thread_id", core.serialization.string()), + toolCallId: core.serialization.property("tool_call_id", core.serialization.string()), + type: core.serialization.stringLiteral("user.tool_approval"), +}); + +export declare namespace UserToolApprovalEvent { + export interface Raw { + approval: ApprovalDecision.Raw; + created_at: string; + id: string; + thread_id: string; + tool_call_id: string; + type: "user.tool_approval"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts new file mode 100644 index 000000000..80d201d3c --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts @@ -0,0 +1,25 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ToolApprovalPolicyItem } from "./ToolApprovalPolicyItem.js"; + +export const UserToolApprovalPolicyEvent: core.serialization.ObjectSchema< + serializers.UserToolApprovalPolicyEvent.Raw, + TrueForge.UserToolApprovalPolicyEvent +> = core.serialization.object({ + createdAt: core.serialization.property("created_at", core.serialization.string()), + id: core.serialization.string(), + policies: core.serialization.list(ToolApprovalPolicyItem), + type: core.serialization.stringLiteral("user.tool_approval_policy"), +}); + +export declare namespace UserToolApprovalPolicyEvent { + export interface Raw { + created_at: string; + id: string; + policies: ToolApprovalPolicyItem.Raw[]; + type: "user.tool_approval_policy"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts new file mode 100644 index 000000000..a8ca2cea7 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ToolApprovalPolicyItem } from "./ToolApprovalPolicyItem.js"; + +export const UserToolApprovalPolicyMessage: core.serialization.ObjectSchema< + serializers.UserToolApprovalPolicyMessage.Raw, + TrueForge.UserToolApprovalPolicyMessage +> = core.serialization.object({ + policies: core.serialization.list(ToolApprovalPolicyItem), + type: core.serialization.stringLiteral("user.tool_approval_policy"), +}); + +export declare namespace UserToolApprovalPolicyMessage { + export interface Raw { + policies: ToolApprovalPolicyItem.Raw[]; + type: "user.tool_approval_policy"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts new file mode 100644 index 000000000..bcce18b61 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts @@ -0,0 +1,28 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const UserToolResponseEvent: core.serialization.ObjectSchema< + serializers.UserToolResponseEvent.Raw, + TrueForge.UserToolResponseEvent +> = core.serialization.object({ + content: core.serialization.string(), + createdAt: core.serialization.property("created_at", core.serialization.string()), + id: core.serialization.string(), + threadId: core.serialization.property("thread_id", core.serialization.string()), + toolCallId: core.serialization.property("tool_call_id", core.serialization.string()), + type: core.serialization.stringLiteral("user.tool_response"), +}); + +export declare namespace UserToolResponseEvent { + export interface Raw { + content: string; + created_at: string; + id: string; + thread_id: string; + tool_call_id: string; + type: "user.tool_response"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index b51454d87..1c08669c3 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -47,8 +47,10 @@ export * from "./ConfiguredSkill.js"; export * from "./ConfiguredWebSearchProvider.js"; export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; +export * from "./CreatedTurnInboundEvent.js"; export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; +export * from "./CreateTurnInboundEventResponse.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; export * from "./DaytonaSandboxProviderAuth.js"; @@ -203,6 +205,8 @@ export * from "./ThreadStateError.js"; export * from "./Timezone.js"; export * from "./TogetherAiModelProvider.js"; export * from "./TokenPagination.js"; +export * from "./ToolApprovalPolicyAllowSession.js"; +export * from "./ToolApprovalPolicyItem.js"; export * from "./ToolApprovalRequiredEvent.js"; export * from "./ToolCall.js"; export * from "./ToolCallRef.js"; @@ -217,6 +221,7 @@ export * from "./Turn.js"; export * from "./TurnCreatedEvent.js"; export * from "./TurnDoneEvent.js"; export * from "./TurnDoneEventState.js"; +export * from "./TurnInboundEventItem.js"; export * from "./TurnInputItem.js"; export * from "./TurnMetrics.js"; export * from "./TurnState.js"; @@ -235,7 +240,11 @@ export * from "./TurnUpdateStateRunning.js"; export * from "./UserMessage.js"; export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; +export * from "./UserToolApprovalEvent.js"; export * from "./UserToolApprovalMessage.js"; +export * from "./UserToolApprovalPolicyEvent.js"; +export * from "./UserToolApprovalPolicyMessage.js"; +export * from "./UserToolResponseEvent.js"; export * from "./UserToolResponseMessage.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; diff --git a/packages/trueforge-sdk/tests/wire/sessions.test.ts b/packages/trueforge-sdk/tests/wire/sessions.test.ts index 0eb6c2dc9..9686a1422 100644 --- a/packages/trueforge-sdk/tests/wire/sessions.test.ts +++ b/packages/trueforge-sdk/tests/wire/sessions.test.ts @@ -1398,6 +1398,245 @@ describe("SessionsClient", () => { }).rejects.toThrow(TrueForgeTypes.NotFoundError); }); + test("create_turn_inbound_event (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { + approval: { status: "allow" }, + thread_id: "thread_id", + tool_call_id: "tool_call_id", + type: "user.tool_approval", + }, + ], + }; + const rawResponseBody = { + data: [ + { + approval: { status: "allow" }, + created_at: "created_at", + id: "id", + thread_id: "thread_id", + tool_call_id: "tool_call_id", + type: "user.tool_approval", + }, + ], + }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "thread_id", + toolCallId: "tool_call_id", + type: "user.tool_approval", + }, + ], + }); + expect(response).toEqual({ + data: [ + { + approval: { + status: "allow", + }, + createdAt: "created_at", + id: "id", + threadId: "thread_id", + toolCallId: "tool_call_id", + type: "user.tool_approval", + }, + ], + }); + }); + + test("create_turn_inbound_event (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("create_turn_inbound_event (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(403) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.ForbiddenError); + }); + + test("create_turn_inbound_event (4)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(404) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.NotFoundError); + }); + + test("create_turn_inbound_event (5)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(409) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.ConflictError); + }); + test("subscribe_to_turn (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index e6f916a46..1a8f9cb15 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -2827,6 +2827,105 @@ client.sessions.list_turn_events( + + + + +
client.sessions.create_turn_inbound_event(...) -> CreateTurnInboundEventResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, UserToolApprovalMessage, ApprovalAllow + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create_turn_inbound_event( + session_id="session_id", + turn_id="turn_id", + events=[ + UserToolApprovalMessage( + approval=ApprovalAllow( + status="allow", + ), + thread_id="thread_id", + tool_call_id="tool_call_id", + type="user.tool_approval", + ) + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `str` — Turn identifier. + +
+
+ +
+
+ +**events:** `typing.List[TurnInboundEventItem]` — One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index 700b078bc..f218965e9 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -57,7 +57,9 @@ ContextManagementConfig, CreateScheduleRunResponse, CreateSessionAgent, + CreateTurnInboundEventResponse, CreatedBySubject, + CreatedTurnInboundEvent, CronExpression, CustomModelProvider, DaytonaSandboxProviderAuth, @@ -212,6 +214,8 @@ Timezone, TogetherAiModelProvider, TokenPagination, + ToolApprovalPolicyAllowSession, + ToolApprovalPolicyItem, ToolApprovalRequiredEvent, ToolCall, ToolCallRef, @@ -226,6 +230,7 @@ TurnCreatedEvent, TurnDoneEvent, TurnDoneEventState, + TurnInboundEventItem, TurnInputItem, TurnMetrics, TurnState, @@ -244,7 +249,11 @@ UserMessage, UserMessageContent, UserMessageContentItem, + UserToolApprovalEvent, UserToolApprovalMessage, + UserToolApprovalPolicyEvent, + UserToolApprovalPolicyMessage, + UserToolResponseEvent, UserToolResponseMessage, WebSearchCapability, WebSearchConfig, @@ -325,7 +334,9 @@ "ContextManagementConfig": ".types", "CreateScheduleRunResponse": ".types", "CreateSessionAgent": ".types", + "CreateTurnInboundEventResponse": ".types", "CreatedBySubject": ".types", + "CreatedTurnInboundEvent": ".types", "CronExpression": ".types", "CustomModelProvider": ".types", "DaytonaSandboxProviderAuth": ".types", @@ -488,6 +499,8 @@ "Timezone": ".types", "TogetherAiModelProvider": ".types", "TokenPagination": ".types", + "ToolApprovalPolicyAllowSession": ".types", + "ToolApprovalPolicyItem": ".types", "ToolApprovalRequiredEvent": ".types", "ToolCall": ".types", "ToolCallRef": ".types", @@ -503,6 +516,7 @@ "TurnCreatedEvent": ".types", "TurnDoneEvent": ".types", "TurnDoneEventState": ".types", + "TurnInboundEventItem": ".types", "TurnInputItem": ".types", "TurnMetrics": ".types", "TurnState": ".types", @@ -523,7 +537,11 @@ "UserMessage": ".types", "UserMessageContent": ".types", "UserMessageContentItem": ".types", + "UserToolApprovalEvent": ".types", "UserToolApprovalMessage": ".types", + "UserToolApprovalPolicyEvent": ".types", + "UserToolApprovalPolicyMessage": ".types", + "UserToolResponseEvent": ".types", "UserToolResponseMessage": ".types", "WebSearchCapability": ".types", "WebSearchConfig": ".types", @@ -621,7 +639,9 @@ def __dir__(): "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", + "CreateTurnInboundEventResponse", "CreatedBySubject", + "CreatedTurnInboundEvent", "CronExpression", "CustomModelProvider", "DaytonaSandboxProviderAuth", @@ -784,6 +804,8 @@ def __dir__(): "Timezone", "TogetherAiModelProvider", "TokenPagination", + "ToolApprovalPolicyAllowSession", + "ToolApprovalPolicyItem", "ToolApprovalRequiredEvent", "ToolCall", "ToolCallRef", @@ -799,6 +821,7 @@ def __dir__(): "TurnCreatedEvent", "TurnDoneEvent", "TurnDoneEventState", + "TurnInboundEventItem", "TurnInputItem", "TurnMetrics", "TurnState", @@ -819,7 +842,11 @@ def __dir__(): "UserMessage", "UserMessageContent", "UserMessageContentItem", + "UserToolApprovalEvent", "UserToolApprovalMessage", + "UserToolApprovalPolicyEvent", + "UserToolApprovalPolicyMessage", + "UserToolResponseEvent", "UserToolResponseMessage", "WebSearchCapability", "WebSearchConfig", diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py index ce744a779..d1ad04565 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py @@ -9,6 +9,7 @@ from ..core.stream import AsyncStream, Stream, StreamEvent from ..types.cancel_session_response import CancelSessionResponse from ..types.create_session_agent import CreateSessionAgent +from ..types.create_turn_inbound_event_response import CreateTurnInboundEventResponse from ..types.get_session_response import GetSessionResponse from ..types.get_turn_response import GetTurnResponse from ..types.list_session_events_response import ListSessionEventsResponse @@ -25,6 +26,7 @@ from ..types.session_metadata import SessionMetadata from ..types.session_source_type import SessionSourceType from ..types.turn import Turn +from ..types.turn_inbound_event_item import TurnInboundEventItem from ..types.turn_input_item import TurnInputItem from ..types.turn_streaming_event import TurnStreamingEvent from .raw_client import AsyncRawSessionsClient, RawSessionsClient @@ -685,6 +687,61 @@ def list_turn_events( request_options=request_options, ) + def create_turn_inbound_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateTurnInboundEventResponse: + """ + Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateTurnInboundEventResponse + Events created in the turn inbox. + + Examples + -------- + from trueforge_sdk import ApprovalAllow, TrueForge, UserToolApprovalMessage + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.create_turn_inbound_event( + session_id="session_id", + turn_id="turn_id", + events=[ + UserToolApprovalMessage( + approval=ApprovalAllow(), + thread_id="thread_id", + tool_call_id="tool_call_id", + ) + ], + ) + """ + _response = self._raw_client.create_turn_inbound_event( + session_id=session_id, turn_id=turn_id, events=events, request_options=request_options + ) + return _response.data + def subscribe_to_turn( self, *, @@ -1509,6 +1566,69 @@ async def main() -> None: request_options=request_options, ) + async def create_turn_inbound_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateTurnInboundEventResponse: + """ + Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateTurnInboundEventResponse + Events created in the turn inbox. + + Examples + -------- + import asyncio + + from trueforge_sdk import ApprovalAllow, AsyncTrueForge, UserToolApprovalMessage + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.create_turn_inbound_event( + session_id="session_id", + turn_id="turn_id", + events=[ + UserToolApprovalMessage( + approval=ApprovalAllow(), + thread_id="thread_id", + tool_call_id="tool_call_id", + ) + ], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_turn_inbound_event( + session_id=session_id, turn_id=turn_id, events=events, request_options=request_options + ) + return _response.data + def subscribe_to_turn( self, *, diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py index 1a8065d01..616b3c0fd 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py @@ -20,6 +20,7 @@ from ..core.stream import AsyncStream, Stream, StreamEvent from ..core.unchecked_base_model import construct_type from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError from ..errors.content_too_large_error import ContentTooLargeError from ..errors.failed_dependency_error import FailedDependencyError from ..errors.forbidden_error import ForbiddenError @@ -30,6 +31,7 @@ from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.cancel_session_response import CancelSessionResponse from ..types.create_session_agent import CreateSessionAgent +from ..types.create_turn_inbound_event_response import CreateTurnInboundEventResponse from ..types.get_session_response import GetSessionResponse from ..types.get_turn_response import GetTurnResponse from ..types.list_session_events_response import ListSessionEventsResponse @@ -47,6 +49,7 @@ from ..types.session_metadata import SessionMetadata from ..types.session_source_type import SessionSourceType from ..types.turn import Turn +from ..types.turn_inbound_event_item import TurnInboundEventItem from ..types.turn_input_item import TurnInputItem from ..types.turn_streaming_event import TurnStreamingEvent from pydantic import ValidationError @@ -1436,6 +1439,113 @@ def list_turn_events( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + def create_turn_inbound_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[CreateTurnInboundEventResponse]: + """ + Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreateTurnInboundEventResponse] + Events created in the turn inbox. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", + method="POST", + json={ + "events": convert_and_respect_annotation_metadata( + object_=events, annotation=typing.Sequence[TurnInboundEventItem], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateTurnInboundEventResponse, + construct_type( + type_=CreateTurnInboundEventResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + @contextlib.contextmanager def subscribe_to_turn( self, @@ -2971,6 +3081,113 @@ async def _get_next(): ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def create_turn_inbound_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[CreateTurnInboundEventResponse]: + """ + Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreateTurnInboundEventResponse] + Events created in the turn inbox. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", + method="POST", + json={ + "events": convert_and_respect_annotation_metadata( + object_=events, annotation=typing.Sequence[TurnInboundEventItem], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateTurnInboundEventResponse, + construct_type( + type_=CreateTurnInboundEventResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + @contextlib.asynccontextmanager async def subscribe_to_turn( self, diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index f1fb9e3ef..6569a7a2c 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -56,7 +56,9 @@ from .context_management_config import ContextManagementConfig from .create_schedule_run_response import CreateScheduleRunResponse from .create_session_agent import CreateSessionAgent + from .create_turn_inbound_event_response import CreateTurnInboundEventResponse from .created_by_subject import CreatedBySubject + from .created_turn_inbound_event import CreatedTurnInboundEvent from .cron_expression import CronExpression from .custom_model_provider import CustomModelProvider from .daytona_sandbox_provider_auth import DaytonaSandboxProviderAuth @@ -211,6 +213,8 @@ from .timezone import Timezone from .together_ai_model_provider import TogetherAiModelProvider from .token_pagination import TokenPagination + from .tool_approval_policy_allow_session import ToolApprovalPolicyAllowSession + from .tool_approval_policy_item import ToolApprovalPolicyItem from .tool_approval_required_event import ToolApprovalRequiredEvent from .tool_call import ToolCall from .tool_call_ref import ToolCallRef @@ -225,6 +229,7 @@ from .turn_created_event import TurnCreatedEvent from .turn_done_event import TurnDoneEvent from .turn_done_event_state import TurnDoneEventState + from .turn_inbound_event_item import TurnInboundEventItem from .turn_input_item import TurnInputItem from .turn_metrics import TurnMetrics from .turn_state import TurnState @@ -243,7 +248,11 @@ from .user_message import UserMessage from .user_message_content import UserMessageContent from .user_message_content_item import UserMessageContentItem + from .user_tool_approval_event import UserToolApprovalEvent from .user_tool_approval_message import UserToolApprovalMessage + from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent + from .user_tool_approval_policy_message import UserToolApprovalPolicyMessage + from .user_tool_response_event import UserToolResponseEvent from .user_tool_response_message import UserToolResponseMessage from .web_search_capability import WebSearchCapability from .web_search_config import WebSearchConfig @@ -300,7 +309,9 @@ "ContextManagementConfig": ".context_management_config", "CreateScheduleRunResponse": ".create_schedule_run_response", "CreateSessionAgent": ".create_session_agent", + "CreateTurnInboundEventResponse": ".create_turn_inbound_event_response", "CreatedBySubject": ".created_by_subject", + "CreatedTurnInboundEvent": ".created_turn_inbound_event", "CronExpression": ".cron_expression", "CustomModelProvider": ".custom_model_provider", "DaytonaSandboxProviderAuth": ".daytona_sandbox_provider_auth", @@ -455,6 +466,8 @@ "Timezone": ".timezone", "TogetherAiModelProvider": ".together_ai_model_provider", "TokenPagination": ".token_pagination", + "ToolApprovalPolicyAllowSession": ".tool_approval_policy_allow_session", + "ToolApprovalPolicyItem": ".tool_approval_policy_item", "ToolApprovalRequiredEvent": ".tool_approval_required_event", "ToolCall": ".tool_call", "ToolCallRef": ".tool_call_ref", @@ -469,6 +482,7 @@ "TurnCreatedEvent": ".turn_created_event", "TurnDoneEvent": ".turn_done_event", "TurnDoneEventState": ".turn_done_event_state", + "TurnInboundEventItem": ".turn_inbound_event_item", "TurnInputItem": ".turn_input_item", "TurnMetrics": ".turn_metrics", "TurnState": ".turn_state", @@ -487,7 +501,11 @@ "UserMessage": ".user_message", "UserMessageContent": ".user_message_content", "UserMessageContentItem": ".user_message_content_item", + "UserToolApprovalEvent": ".user_tool_approval_event", "UserToolApprovalMessage": ".user_tool_approval_message", + "UserToolApprovalPolicyEvent": ".user_tool_approval_policy_event", + "UserToolApprovalPolicyMessage": ".user_tool_approval_policy_message", + "UserToolResponseEvent": ".user_tool_response_event", "UserToolResponseMessage": ".user_tool_response_message", "WebSearchCapability": ".web_search_capability", "WebSearchConfig": ".web_search_config", @@ -568,7 +586,9 @@ def __dir__(): "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", + "CreateTurnInboundEventResponse", "CreatedBySubject", + "CreatedTurnInboundEvent", "CronExpression", "CustomModelProvider", "DaytonaSandboxProviderAuth", @@ -723,6 +743,8 @@ def __dir__(): "Timezone", "TogetherAiModelProvider", "TokenPagination", + "ToolApprovalPolicyAllowSession", + "ToolApprovalPolicyItem", "ToolApprovalRequiredEvent", "ToolCall", "ToolCallRef", @@ -737,6 +759,7 @@ def __dir__(): "TurnCreatedEvent", "TurnDoneEvent", "TurnDoneEventState", + "TurnInboundEventItem", "TurnInputItem", "TurnMetrics", "TurnState", @@ -755,7 +778,11 @@ def __dir__(): "UserMessage", "UserMessageContent", "UserMessageContentItem", + "UserToolApprovalEvent", "UserToolApprovalMessage", + "UserToolApprovalPolicyEvent", + "UserToolApprovalPolicyMessage", + "UserToolResponseEvent", "UserToolResponseMessage", "WebSearchCapability", "WebSearchConfig", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py new file mode 100644 index 000000000..ff5de2a43 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .created_turn_inbound_event import CreatedTurnInboundEvent + + +class CreateTurnInboundEventResponse(UncheckedBaseModel): + data: typing.List[CreatedTurnInboundEvent] = pydantic.Field() + """ + Created inbox events with server-minted `id` and `created_at`, in request order. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py new file mode 100644 index 000000000..81ec54077 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent +from .user_tool_response_event import UserToolResponseEvent + +CreatedTurnInboundEvent = typing.Union[UserToolApprovalEvent, UserToolApprovalPolicyEvent, UserToolResponseEvent] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py index 6119fc25c..cbedb13d2 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py @@ -14,6 +14,9 @@ from .turn_created_event import TurnCreatedEvent from .turn_done_event import TurnDoneEvent from .turn_update_event import TurnUpdateEvent +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent +from .user_tool_response_event import UserToolResponseEvent SessionEvent = typing.Union[ McpAuthRequiredEvent, @@ -28,4 +31,7 @@ TurnCreatedEvent, TurnDoneEvent, TurnUpdateEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, ] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py new file mode 100644 index 000000000..93c9ebf9f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ToolApprovalPolicyAllowSession(UncheckedBaseModel): + expire_at: typing.Optional[str] = pydantic.Field(default=None) + """ + ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session. + """ + + type: typing.Literal["allow_session"] = "allow_session" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py new file mode 100644 index 000000000..54ba130d5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_approval_policy_allow_session import ToolApprovalPolicyAllowSession + + +class ToolApprovalPolicyItem(UncheckedBaseModel): + action: ToolApprovalPolicyAllowSession + server: str = pydantic.Field() + """ + MCP server name the tool belongs to. + """ + + tool_name: str = pydantic.Field() + """ + Tool name this policy applies to. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py new file mode 100644 index 000000000..6cb951a28 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .user_tool_approval_message import UserToolApprovalMessage +from .user_tool_approval_policy_message import UserToolApprovalPolicyMessage +from .user_tool_response_message import UserToolResponseMessage + +TurnInboundEventItem = typing.Union[UserToolApprovalMessage, UserToolApprovalPolicyMessage, UserToolResponseMessage] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py index 3570d5ed6..1a9d40b9a 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py @@ -15,6 +15,9 @@ from .turn_created_event import TurnCreatedEvent from .turn_done_event import TurnDoneEvent from .turn_update_event import TurnUpdateEvent +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent +from .user_tool_response_event import UserToolResponseEvent TurnStreamingEvent = typing.Union[ McpAuthRequiredEvent, @@ -30,4 +33,7 @@ TurnCreatedEvent, TurnDoneEvent, TurnUpdateEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, ] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py new file mode 100644 index 000000000..a57a02864 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .approval_decision import ApprovalDecision + + +class UserToolApprovalEvent(UncheckedBaseModel): + approval: ApprovalDecision + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call id being approved or denied. + """ + + type: typing.Literal["user.tool_approval"] = "user.tool_approval" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py new file mode 100644 index 000000000..13ae72c36 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_approval_policy_item import ToolApprovalPolicyItem + + +class UserToolApprovalPolicyEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + policies: typing.List[ToolApprovalPolicyItem] = pydantic.Field() + """ + One or more (server, tool_name) policy entries. + """ + + type: typing.Literal["user.tool_approval_policy"] = "user.tool_approval_policy" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py new file mode 100644 index 000000000..1b5bcefcf --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_approval_policy_item import ToolApprovalPolicyItem + + +class UserToolApprovalPolicyMessage(UncheckedBaseModel): + policies: typing.List[ToolApprovalPolicyItem] = pydantic.Field() + """ + One or more (server, tool_name) policy entries. + """ + + type: typing.Literal["user.tool_approval_policy"] = "user.tool_approval_policy" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py new file mode 100644 index 000000000..f902e885e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UserToolResponseEvent(UncheckedBaseModel): + content: str = pydantic.Field() + """ + Client-side tool result content. + """ + + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call id receiving the client response. + """ + + type: typing.Literal["user.tool_response"] = "user.tool_response" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow From a6e4cea49013fe9e785b3f557e025fdd75744567 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Thu, 24 Sep 2026 12:34:06 +0530 Subject: [PATCH 19/23] rename stuff --- .changeset/turn-inbound-events-api.md | 2 +- .github/fern/openapi/openapi.json | 141 +++++------------- docs/openapi.json | 141 +++++------------- .../trueforge-core/src/core/events/schema.ts | 1 - packages/trueforge-sdk/reference.md | 8 +- .../api/resources/sessions/client/Client.ts | 24 +-- ...ntRequest.ts => CreateTurnEventRequest.ts} | 4 +- .../sessions/client/requests/index.ts | 2 +- .../src/api/types/CreateTurnEventResponse.ts | 8 + ....ts => CreateTurnEventResponseDataItem.ts} | 2 +- .../types/CreateTurnInboundEventResponse.ts | 8 - .../src/api/types/ToolApprovalPolicyItem.ts | 6 +- .../src/api/types/TurnInboundEventItem.ts | 4 +- .../src/api/types/UserToolApprovalMessage.ts | 12 -- .../api/types/UserToolApprovalPolicyEvent.ts | 2 +- .../types/UserToolApprovalPolicyMessage.ts | 2 +- .../src/api/types/UserToolResponseMessage.ts | 11 -- packages/trueforge-sdk/src/api/types/index.ts | 6 +- ...ntRequest.ts => CreateTurnEventRequest.ts} | 8 +- .../sessions/client/requests/index.ts | 2 +- .../types/CreateTurnEventResponse.ts | 19 +++ ....ts => CreateTurnEventResponseDataItem.ts} | 8 +- .../types/CreateTurnInboundEventResponse.ts | 19 --- .../types/ToolApprovalPolicyItem.ts | 4 +- .../types/TurnInboundEventItem.ts | 13 +- .../types/UserToolApprovalMessage.ts | 25 ---- .../types/UserToolResponseMessage.ts | 24 --- .../src/serialization/types/index.ts | 6 +- .../trueforge-sdk/tests/wire/sessions.test.ts | 20 +-- packages/trueforge/src/apis/turns.ts | 8 +- packages/trueforge/src/routes/turnRoutes.ts | 23 ++- packages/trueforge/src/schemas/events.ts | 30 ++-- .../tests/unit/apis/sessionHttp.test.ts | 2 +- python/trueforge_sdk/reference.md | 12 +- .../src/trueforge_sdk/__init__.py | 18 +-- .../src/trueforge_sdk/sessions/client.py | 46 +++--- .../src/trueforge_sdk/sessions/raw_client.py | 34 ++--- .../src/trueforge_sdk/types/__init__.py | 18 +-- ...ponse.py => create_turn_event_response.py} | 8 +- ...> create_turn_event_response_data_item.py} | 4 +- .../types/tool_approval_policy_item.py | 8 +- .../types/turn_inbound_event_item.py | 8 +- .../types/user_tool_approval_message.py | 31 ---- .../types/user_tool_approval_policy_event.py | 2 +- .../user_tool_approval_policy_message.py | 2 +- .../types/user_tool_response_message.py | 34 ----- 46 files changed, 267 insertions(+), 553 deletions(-) rename packages/trueforge-sdk/src/api/resources/sessions/client/requests/{CreateTurnInboundEventRequest.ts => CreateTurnEventRequest.ts} (74%) create mode 100644 packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts rename packages/trueforge-sdk/src/api/types/{CreatedTurnInboundEvent.ts => CreateTurnEventResponseDataItem.ts} (83%) delete mode 100644 packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts delete mode 100644 packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts delete mode 100644 packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts rename packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/{CreateTurnInboundEventRequest.ts => CreateTurnEventRequest.ts} (69%) create mode 100644 packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts rename packages/trueforge-sdk/src/serialization/types/{CreatedTurnInboundEvent.ts => CreateTurnEventResponseDataItem.ts} (74%) delete mode 100644 packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts delete mode 100644 packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts delete mode 100644 packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts rename python/trueforge_sdk/src/trueforge_sdk/types/{create_turn_inbound_event_response.py => create_turn_event_response.py} (63%) rename python/trueforge_sdk/src/trueforge_sdk/types/{created_turn_inbound_event.py => create_turn_event_response_data_item.py} (68%) delete mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py delete mode 100644 python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py diff --git a/.changeset/turn-inbound-events-api.md b/.changeset/turn-inbound-events-api.md index 909824e72..af33dd314 100644 --- a/.changeset/turn-inbound-events-api.md +++ b/.changeset/turn-inbound-events-api.md @@ -3,4 +3,4 @@ "@truefoundry/trueforge-core": patch --- -Add `POST /sessions/{session_id}/turns/{turn_id}/events` to persist inbound tip HITL / policy events. Response items use the same `UserToolApprovalEvent` / `UserToolResponseEvent` / `UserToolApprovalPolicyEvent` shapes as `SessionEvent` and the turn SSE stream. +Add `POST /sessions/{session_id}/turns/{turn_id}/events` for tip HITL / policy events. Response items use the same `UserToolApprovalEvent` / `UserToolResponseEvent` / `UserToolApprovalPolicyEvent` shapes as `SessionEvent` and the turn SSE stream. diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 0d28f760b..2af2a5d16 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -1163,10 +1163,10 @@ ], "type": "object" }, - "CreateTurnInboundEventRequest": { + "CreateTurnEventRequest": { "properties": { "events": { - "description": "One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", + "description": "One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", "items": { "$ref": "#/components/schemas/TurnInboundEventItem" }, @@ -1179,12 +1179,30 @@ ], "type": "object" }, - "CreateTurnInboundEventResponse": { + "CreateTurnEventResponse": { "properties": { "data": { - "description": "Created inbox events with server-minted `id` and `created_at`, in request order.", + "description": "Events with server-minted `id` and `created_at`, in request order.", "items": { - "$ref": "#/components/schemas/CreatedTurnInboundEvent" + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + } + ] }, "type": "array" } @@ -1241,27 +1259,6 @@ ], "type": "object" }, - "CreatedTurnInboundEvent": { - "discriminator": { - "mapping": { - "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", - "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", - "user.tool_response": "#/components/schemas/UserToolResponseEvent" - }, - "propertyName": "type" - }, - "oneOf": [ - { - "$ref": "#/components/schemas/UserToolApprovalEvent" - }, - { - "$ref": "#/components/schemas/UserToolResponseEvent" - }, - { - "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" - } - ] - }, "CronExpression": { "description": "Standard 5-field cron expression, evaluated in `timezone`.", "pattern": "^[\\d*,\\-/]+(?:\\s+[\\d*,\\-/]+){4}$", @@ -4876,13 +4873,13 @@ } ] }, - "server_name": { - "description": "Configured MCP server name (same as tool_info.server_name on model.message).", + "name": { + "description": "Tool name on that server.", "minLength": 1, "type": "string" }, - "name": { - "description": "Tool name on that server (same as tool_info.name on model.message).", + "server_name": { + "description": "MCP server name.", "minLength": 1, "type": "string" } @@ -5347,18 +5344,18 @@ "TurnInboundEventItem": { "discriminator": { "mapping": { - "user.tool_approval": "#/components/schemas/UserToolApprovalMessage", + "user.tool_approval": "#/components/schemas/UserToolApprovalInputEvent", "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyMessage", - "user.tool_response": "#/components/schemas/UserToolResponseMessage" + "user.tool_response": "#/components/schemas/UserToolResponseInputEvent" }, "propertyName": "type" }, "oneOf": [ { - "$ref": "#/components/schemas/UserToolApprovalMessage" + "$ref": "#/components/schemas/UserToolApprovalInputEvent" }, { - "$ref": "#/components/schemas/UserToolResponseMessage" + "$ref": "#/components/schemas/UserToolResponseInputEvent" }, { "$ref": "#/components/schemas/UserToolApprovalPolicyMessage" @@ -5985,37 +5982,6 @@ ], "type": "object" }, - "UserToolApprovalMessage": { - "properties": { - "approval": { - "$ref": "#/components/schemas/ApprovalDecision" - }, - "thread_id": { - "description": "Thread that owns the pending tool call.", - "minLength": 1, - "type": "string" - }, - "tool_call_id": { - "description": "Tool call id being approved or denied.", - "minLength": 1, - "type": "string" - }, - "type": { - "description": "Client resume after tool.approval_required.", - "enum": [ - "user.tool_approval" - ], - "type": "string" - } - }, - "required": [ - "type", - "thread_id", - "tool_call_id", - "approval" - ], - "type": "object" - }, "UserToolApprovalPolicyEvent": { "properties": { "created_at": { @@ -6150,39 +6116,6 @@ ], "type": "object" }, - "UserToolResponseMessage": { - "properties": { - "content": { - "description": "Client-side tool result content.", - "minLength": 1, - "type": "string" - }, - "thread_id": { - "description": "Thread that owns the pending tool call.", - "minLength": 1, - "type": "string" - }, - "tool_call_id": { - "description": "Tool call id receiving the client response.", - "minLength": 1, - "type": "string" - }, - "type": { - "description": "Client resume after tool.response_required.", - "enum": [ - "user.tool_response" - ], - "type": "string" - } - }, - "required": [ - "type", - "thread_id", - "tool_call_id", - "content" - ], - "type": "object" - }, "WebSearchCapability": { "properties": { "enabled": { @@ -9706,7 +9639,7 @@ "x-fern-sdk-method-name": "list_turn_events" }, "post": { - "description": "Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step.", + "description": "Create events for a turn. Only the session creator may create them.", "parameters": [ { "description": "Session identifier.", @@ -9736,7 +9669,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTurnInboundEventRequest" + "$ref": "#/components/schemas/CreateTurnEventRequest" } } }, @@ -9747,11 +9680,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTurnInboundEventResponse" + "$ref": "#/components/schemas/CreateTurnEventResponse" } } }, - "description": "Events created in the turn inbox." + "description": "Events created." }, "400": { "content": { @@ -9794,14 +9727,14 @@ "description": "Turn is terminal, or an event id already exists." } }, - "summary": "Create turn inbound events", + "summary": "Create turn events", "tags": [ "Agent Sessions" ], "x-fern-sdk-group-name": [ "sessions" ], - "x-fern-sdk-method-name": "create_turn_inbound_event" + "x-fern-sdk-method-name": "create_turn_event" } }, "/api/v1/sessions/{session_id}/turns/{turn_id}/subscribe": { diff --git a/docs/openapi.json b/docs/openapi.json index 0d28f760b..2af2a5d16 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1163,10 +1163,10 @@ ], "type": "object" }, - "CreateTurnInboundEventRequest": { + "CreateTurnEventRequest": { "properties": { "events": { - "description": "One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", + "description": "One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", "items": { "$ref": "#/components/schemas/TurnInboundEventItem" }, @@ -1179,12 +1179,30 @@ ], "type": "object" }, - "CreateTurnInboundEventResponse": { + "CreateTurnEventResponse": { "properties": { "data": { - "description": "Created inbox events with server-minted `id` and `created_at`, in request order.", + "description": "Events with server-minted `id` and `created_at`, in request order.", "items": { - "$ref": "#/components/schemas/CreatedTurnInboundEvent" + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + } + ] }, "type": "array" } @@ -1241,27 +1259,6 @@ ], "type": "object" }, - "CreatedTurnInboundEvent": { - "discriminator": { - "mapping": { - "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", - "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", - "user.tool_response": "#/components/schemas/UserToolResponseEvent" - }, - "propertyName": "type" - }, - "oneOf": [ - { - "$ref": "#/components/schemas/UserToolApprovalEvent" - }, - { - "$ref": "#/components/schemas/UserToolResponseEvent" - }, - { - "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" - } - ] - }, "CronExpression": { "description": "Standard 5-field cron expression, evaluated in `timezone`.", "pattern": "^[\\d*,\\-/]+(?:\\s+[\\d*,\\-/]+){4}$", @@ -4876,13 +4873,13 @@ } ] }, - "server_name": { - "description": "Configured MCP server name (same as tool_info.server_name on model.message).", + "name": { + "description": "Tool name on that server.", "minLength": 1, "type": "string" }, - "name": { - "description": "Tool name on that server (same as tool_info.name on model.message).", + "server_name": { + "description": "MCP server name.", "minLength": 1, "type": "string" } @@ -5347,18 +5344,18 @@ "TurnInboundEventItem": { "discriminator": { "mapping": { - "user.tool_approval": "#/components/schemas/UserToolApprovalMessage", + "user.tool_approval": "#/components/schemas/UserToolApprovalInputEvent", "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyMessage", - "user.tool_response": "#/components/schemas/UserToolResponseMessage" + "user.tool_response": "#/components/schemas/UserToolResponseInputEvent" }, "propertyName": "type" }, "oneOf": [ { - "$ref": "#/components/schemas/UserToolApprovalMessage" + "$ref": "#/components/schemas/UserToolApprovalInputEvent" }, { - "$ref": "#/components/schemas/UserToolResponseMessage" + "$ref": "#/components/schemas/UserToolResponseInputEvent" }, { "$ref": "#/components/schemas/UserToolApprovalPolicyMessage" @@ -5985,37 +5982,6 @@ ], "type": "object" }, - "UserToolApprovalMessage": { - "properties": { - "approval": { - "$ref": "#/components/schemas/ApprovalDecision" - }, - "thread_id": { - "description": "Thread that owns the pending tool call.", - "minLength": 1, - "type": "string" - }, - "tool_call_id": { - "description": "Tool call id being approved or denied.", - "minLength": 1, - "type": "string" - }, - "type": { - "description": "Client resume after tool.approval_required.", - "enum": [ - "user.tool_approval" - ], - "type": "string" - } - }, - "required": [ - "type", - "thread_id", - "tool_call_id", - "approval" - ], - "type": "object" - }, "UserToolApprovalPolicyEvent": { "properties": { "created_at": { @@ -6150,39 +6116,6 @@ ], "type": "object" }, - "UserToolResponseMessage": { - "properties": { - "content": { - "description": "Client-side tool result content.", - "minLength": 1, - "type": "string" - }, - "thread_id": { - "description": "Thread that owns the pending tool call.", - "minLength": 1, - "type": "string" - }, - "tool_call_id": { - "description": "Tool call id receiving the client response.", - "minLength": 1, - "type": "string" - }, - "type": { - "description": "Client resume after tool.response_required.", - "enum": [ - "user.tool_response" - ], - "type": "string" - } - }, - "required": [ - "type", - "thread_id", - "tool_call_id", - "content" - ], - "type": "object" - }, "WebSearchCapability": { "properties": { "enabled": { @@ -9706,7 +9639,7 @@ "x-fern-sdk-method-name": "list_turn_events" }, "post": { - "description": "Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step.", + "description": "Create events for a turn. Only the session creator may create them.", "parameters": [ { "description": "Session identifier.", @@ -9736,7 +9669,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTurnInboundEventRequest" + "$ref": "#/components/schemas/CreateTurnEventRequest" } } }, @@ -9747,11 +9680,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTurnInboundEventResponse" + "$ref": "#/components/schemas/CreateTurnEventResponse" } } }, - "description": "Events created in the turn inbox." + "description": "Events created." }, "400": { "content": { @@ -9794,14 +9727,14 @@ "description": "Turn is terminal, or an event id already exists." } }, - "summary": "Create turn inbound events", + "summary": "Create turn events", "tags": [ "Agent Sessions" ], "x-fern-sdk-group-name": [ "sessions" ], - "x-fern-sdk-method-name": "create_turn_inbound_event" + "x-fern-sdk-method-name": "create_turn_event" } }, "/api/v1/sessions/{session_id}/turns/{turn_id}/subscribe": { diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index 32dc46dbd..a5e9ee9a8 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -79,7 +79,6 @@ export const ApprovalDecisionSchema = z .discriminatedUnion('status', [AgentApprovalDecisionAllowSchema, AgentApprovalDecisionDenySchema]) .openapi('ApprovalDecision'); -// TODO: Remove this after we have migrated to the new schema. export const UserToolApprovalMessageSchema = z .object({ type: z.literal(EventType.USER_TOOL_APPROVAL).describe('Client resume after tool.approval_required.'), diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index 7ca82eba3..18f7dec6d 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -2370,7 +2370,7 @@ const response = page.response; -
client.sessions.createTurnInboundEvent(session_id, turn_id, { ...params }) -> TrueForge.CreateTurnInboundEventResponse +
client.sessions.createTurnEvent(session_id, turn_id, { ...params }) -> TrueForge.CreateTurnEventResponse
@@ -2382,7 +2382,7 @@ const response = page.response;
-Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. +Create events for a turn. Only the session creator may create them.
@@ -2397,7 +2397,7 @@ Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_ap
```typescript -await client.sessions.createTurnInboundEvent("session_id", "turn_id", { +await client.sessions.createTurnEvent("session_id", "turn_id", { events: [{ approval: { status: "allow" @@ -2438,7 +2438,7 @@ await client.sessions.createTurnInboundEvent("session_id", "turn_id", {
-**request:** `TrueForge.CreateTurnInboundEventRequest` +**request:** `TrueForge.CreateTurnEventRequest`
diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts index b4555d19d..e7fd1c4d2 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts @@ -1743,11 +1743,11 @@ export class SessionsClient { } /** - * Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + * Create events for a turn. Only the session creator may create them. * * @param {string} session_id - Session identifier. * @param {string} turn_id - Turn identifier. - * @param {TrueForge.CreateTurnInboundEventRequest} request + * @param {TrueForge.CreateTurnEventRequest} request * @param {SessionsClient.RequestOptions} requestOptions - Request-specific configuration. * * @throws {@link TrueForge.BadRequestError} @@ -1758,7 +1758,7 @@ export class SessionsClient { * @throws {@link errors.TrueForgeTimeoutError} * * @example - * await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + * await client.sessions.createTurnEvent("session_id", "turn_id", { * events: [{ * approval: { * status: "allow" @@ -1769,23 +1769,23 @@ export class SessionsClient { * }] * }) */ - public createTurnInboundEvent( + public createTurnEvent( session_id: string, turn_id: string, - request: TrueForge.CreateTurnInboundEventRequest, + request: TrueForge.CreateTurnEventRequest, requestOptions?: SessionsClient.RequestOptions, - ): core.HttpResponsePromise { + ): core.HttpResponsePromise { return core.HttpResponsePromise.fromPromise( - this.__createTurnInboundEvent(session_id, turn_id, request, requestOptions), + this.__createTurnEvent(session_id, turn_id, request, requestOptions), ); } - private async __createTurnInboundEvent( + private async __createTurnEvent( session_id: string, turn_id: string, - request: TrueForge.CreateTurnInboundEventRequest, + request: TrueForge.CreateTurnEventRequest, requestOptions?: SessionsClient.RequestOptions, - ): Promise> { + ): Promise> { const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); const _headers: core.Fetcher.Args["headers"] = mergeHeaders( _authRequest.headers, @@ -1804,7 +1804,7 @@ export class SessionsClient { queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), requestType: "json", body: mergeAdditionalBodyParameters( - serializers.CreateTurnInboundEventRequest.jsonOrThrow(request, { + serializers.CreateTurnEventRequest.jsonOrThrow(request, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, @@ -1820,7 +1820,7 @@ export class SessionsClient { }); if (_response.ok) { return { - data: serializers.CreateTurnInboundEventResponse.parseOrThrow(_response.body, { + data: serializers.CreateTurnEventResponse.parseOrThrow(_response.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnEventRequest.ts similarity index 74% rename from packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts rename to packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnEventRequest.ts index 89bcebb2e..3424eae7f 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnEventRequest.ts @@ -15,7 +15,7 @@ import type * as TrueForge from "../../../../index.js"; * }] * } */ -export interface CreateTurnInboundEventRequest { - /** One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). */ +export interface CreateTurnEventRequest { + /** One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). */ events: TrueForge.TurnInboundEventItem[]; } diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts index a113751e0..1d3fd9bcb 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts @@ -1,6 +1,6 @@ export type { CancelSessionRequest } from "./CancelSessionRequest.js"; export type { CreateSessionRequest } from "./CreateSessionRequest.js"; -export type { CreateTurnInboundEventRequest } from "./CreateTurnInboundEventRequest.js"; +export type { CreateTurnEventRequest } from "./CreateTurnEventRequest.js"; export type { CreateTurnSessionsRequest } from "./CreateTurnSessionsRequest.js"; export type { CreateTurnSessionsStreamRequest } from "./CreateTurnSessionsStreamRequest.js"; export type { DownloadSandboxFileSessionsRequest } from "./DownloadSandboxFileSessionsRequest.js"; diff --git a/packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts new file mode 100644 index 000000000..54c94cc2f --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface CreateTurnEventResponse { + /** Events with server-minted `id` and `created_at`, in request order. */ + data: TrueForge.CreateTurnEventResponseDataItem[]; +} diff --git a/packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponseDataItem.ts similarity index 83% rename from packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts rename to packages/trueforge-sdk/src/api/types/CreateTurnEventResponseDataItem.ts index 7a065c229..c7632854a 100644 --- a/packages/trueforge-sdk/src/api/types/CreatedTurnInboundEvent.ts +++ b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponseDataItem.ts @@ -2,7 +2,7 @@ import type * as TrueForge from "../index.js"; -export type CreatedTurnInboundEvent = +export type CreateTurnEventResponseDataItem = | TrueForge.UserToolApprovalEvent | TrueForge.UserToolApprovalPolicyEvent | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts b/packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts deleted file mode 100644 index af4d86548..000000000 --- a/packages/trueforge-sdk/src/api/types/CreateTurnInboundEventResponse.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../index.js"; - -export interface CreateTurnInboundEventResponse { - /** Created inbox events with server-minted `id` and `created_at`, in request order. */ - data: TrueForge.CreatedTurnInboundEvent[]; -} diff --git a/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts index c57681d84..bcbe037cd 100644 --- a/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts +++ b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts @@ -4,8 +4,8 @@ import type * as TrueForge from "../index.js"; export interface ToolApprovalPolicyItem { action: TrueForge.ToolApprovalPolicyAllowSession; - /** Configured MCP server name (same as tool_info.server_name on model.message). */ - serverName: string; - /** Tool name on that server (same as tool_info.name on model.message). */ + /** Tool name on that server. */ name: string; + /** MCP server name. */ + serverName: string; } diff --git a/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts b/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts index e23d0668c..70bc1cae6 100644 --- a/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts +++ b/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts @@ -3,6 +3,6 @@ import type * as TrueForge from "../index.js"; export type TurnInboundEventItem = - | TrueForge.UserToolApprovalMessage + | TrueForge.UserToolApprovalInputEvent | TrueForge.UserToolApprovalPolicyMessage - | TrueForge.UserToolResponseMessage; + | TrueForge.UserToolResponseInputEvent; diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts deleted file mode 100644 index a9c9f3226..000000000 --- a/packages/trueforge-sdk/src/api/types/UserToolApprovalMessage.ts +++ /dev/null @@ -1,12 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../index.js"; - -export interface UserToolApprovalMessage { - approval: TrueForge.ApprovalDecision; - /** Thread that owns the pending tool call. */ - threadId: string; - /** Tool call id being approved or denied. */ - toolCallId: string; - type: "user.tool_approval"; -} diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts index 346e5b739..0b9008ac0 100644 --- a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts @@ -7,7 +7,7 @@ export interface UserToolApprovalPolicyEvent { createdAt: string; /** Unique identifier for the event (monotonic ULID). */ id: string; - /** One or more (server, tool_name) policy entries. */ + /** One or more (server_name, name) policy entries. */ policies: TrueForge.ToolApprovalPolicyItem[]; type: "user.tool_approval_policy"; } diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts index 41a7ac8ea..703d45998 100644 --- a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts @@ -3,7 +3,7 @@ import type * as TrueForge from "../index.js"; export interface UserToolApprovalPolicyMessage { - /** One or more (server, tool_name) policy entries. */ + /** One or more (server_name, name) policy entries. */ policies: TrueForge.ToolApprovalPolicyItem[]; type: "user.tool_approval_policy"; } diff --git a/packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts b/packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts deleted file mode 100644 index b1bc4d80a..000000000 --- a/packages/trueforge-sdk/src/api/types/UserToolResponseMessage.ts +++ /dev/null @@ -1,11 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -export interface UserToolResponseMessage { - /** Client-side tool result content. */ - content: string; - /** Thread that owns the pending tool call. */ - threadId: string; - /** Tool call id receiving the client response. */ - toolCallId: string; - type: "user.tool_response"; -} diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index c04a15bce..77eafba9e 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -47,10 +47,10 @@ export * from "./ConfiguredSkill.js"; export * from "./ConfiguredWebSearchProvider.js"; export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; -export * from "./CreatedTurnInboundEvent.js"; export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; -export * from "./CreateTurnInboundEventResponse.js"; +export * from "./CreateTurnEventResponse.js"; +export * from "./CreateTurnEventResponseDataItem.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; export * from "./DaytonaSandboxProviderAuth.js"; @@ -242,12 +242,10 @@ export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; export * from "./UserToolApprovalEvent.js"; export * from "./UserToolApprovalInputEvent.js"; -export * from "./UserToolApprovalMessage.js"; export * from "./UserToolApprovalPolicyEvent.js"; export * from "./UserToolApprovalPolicyMessage.js"; export * from "./UserToolResponseEvent.js"; export * from "./UserToolResponseInputEvent.js"; -export * from "./UserToolResponseMessage.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; export * from "./WebSearchProviderManifest.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnEventRequest.ts similarity index 69% rename from packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts rename to packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnEventRequest.ts index 69464825a..41d8a1a4a 100644 --- a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnInboundEventRequest.ts +++ b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnEventRequest.ts @@ -5,14 +5,14 @@ import * as core from "../../../../../core/index.js"; import type * as serializers from "../../../../index.js"; import { TurnInboundEventItem } from "../../../../types/TurnInboundEventItem.js"; -export const CreateTurnInboundEventRequest: core.serialization.Schema< - serializers.CreateTurnInboundEventRequest.Raw, - TrueForge.CreateTurnInboundEventRequest +export const CreateTurnEventRequest: core.serialization.Schema< + serializers.CreateTurnEventRequest.Raw, + TrueForge.CreateTurnEventRequest > = core.serialization.object({ events: core.serialization.list(TurnInboundEventItem), }); -export declare namespace CreateTurnInboundEventRequest { +export declare namespace CreateTurnEventRequest { export interface Raw { events: TurnInboundEventItem.Raw[]; } diff --git a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts index d5b8969f1..5a5183300 100644 --- a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts +++ b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts @@ -1,6 +1,6 @@ export { CancelSessionRequest } from "./CancelSessionRequest.js"; export { CreateSessionRequest } from "./CreateSessionRequest.js"; -export { CreateTurnInboundEventRequest } from "./CreateTurnInboundEventRequest.js"; +export { CreateTurnEventRequest } from "./CreateTurnEventRequest.js"; export { CreateTurnSessionsRequest } from "./CreateTurnSessionsRequest.js"; export { CreateTurnSessionsStreamRequest } from "./CreateTurnSessionsStreamRequest.js"; export { UpdateSessionRequest } from "./UpdateSessionRequest.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts new file mode 100644 index 000000000..48c22ebe6 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { CreateTurnEventResponseDataItem } from "./CreateTurnEventResponseDataItem.js"; + +export const CreateTurnEventResponse: core.serialization.ObjectSchema< + serializers.CreateTurnEventResponse.Raw, + TrueForge.CreateTurnEventResponse +> = core.serialization.object({ + data: core.serialization.list(CreateTurnEventResponseDataItem), +}); + +export declare namespace CreateTurnEventResponse { + export interface Raw { + data: CreateTurnEventResponseDataItem.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponseDataItem.ts similarity index 74% rename from packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts rename to packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponseDataItem.ts index ae2db93d7..5f8ccdb9b 100644 --- a/packages/trueforge-sdk/src/serialization/types/CreatedTurnInboundEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponseDataItem.ts @@ -7,15 +7,15 @@ import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; -export const CreatedTurnInboundEvent: core.serialization.Schema< - serializers.CreatedTurnInboundEvent.Raw, - TrueForge.CreatedTurnInboundEvent +export const CreateTurnEventResponseDataItem: core.serialization.Schema< + serializers.CreateTurnEventResponseDataItem.Raw, + TrueForge.CreateTurnEventResponseDataItem > = core.serialization.undiscriminatedUnion([ UserToolApprovalEvent, UserToolApprovalPolicyEvent, UserToolResponseEvent, ]); -export declare namespace CreatedTurnInboundEvent { +export declare namespace CreateTurnEventResponseDataItem { export type Raw = UserToolApprovalEvent.Raw | UserToolApprovalPolicyEvent.Raw | UserToolResponseEvent.Raw; } diff --git a/packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts b/packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts deleted file mode 100644 index af0be6de0..000000000 --- a/packages/trueforge-sdk/src/serialization/types/CreateTurnInboundEventResponse.ts +++ /dev/null @@ -1,19 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../api/index.js"; -import * as core from "../../core/index.js"; -import type * as serializers from "../index.js"; -import { CreatedTurnInboundEvent } from "./CreatedTurnInboundEvent.js"; - -export const CreateTurnInboundEventResponse: core.serialization.ObjectSchema< - serializers.CreateTurnInboundEventResponse.Raw, - TrueForge.CreateTurnInboundEventResponse -> = core.serialization.object({ - data: core.serialization.list(CreatedTurnInboundEvent), -}); - -export declare namespace CreateTurnInboundEventResponse { - export interface Raw { - data: CreatedTurnInboundEvent.Raw[]; - } -} diff --git a/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts b/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts index f19252e49..ef64c3415 100644 --- a/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts +++ b/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts @@ -10,14 +10,14 @@ export const ToolApprovalPolicyItem: core.serialization.ObjectSchema< TrueForge.ToolApprovalPolicyItem > = core.serialization.object({ action: ToolApprovalPolicyAllowSession, - serverName: core.serialization.property("server_name", core.serialization.string()), name: core.serialization.string(), + serverName: core.serialization.property("server_name", core.serialization.string()), }); export declare namespace ToolApprovalPolicyItem { export interface Raw { action: ToolApprovalPolicyAllowSession.Raw; - server_name: string; name: string; + server_name: string; } } diff --git a/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts b/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts index e8770bb75..4865759c9 100644 --- a/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts +++ b/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts @@ -3,19 +3,22 @@ import type * as TrueForge from "../../api/index.js"; import * as core from "../../core/index.js"; import type * as serializers from "../index.js"; -import { UserToolApprovalMessage } from "./UserToolApprovalMessage.js"; +import { UserToolApprovalInputEvent } from "./UserToolApprovalInputEvent.js"; import { UserToolApprovalPolicyMessage } from "./UserToolApprovalPolicyMessage.js"; -import { UserToolResponseMessage } from "./UserToolResponseMessage.js"; +import { UserToolResponseInputEvent } from "./UserToolResponseInputEvent.js"; export const TurnInboundEventItem: core.serialization.Schema< serializers.TurnInboundEventItem.Raw, TrueForge.TurnInboundEventItem > = core.serialization.undiscriminatedUnion([ - UserToolApprovalMessage, + UserToolApprovalInputEvent, UserToolApprovalPolicyMessage, - UserToolResponseMessage, + UserToolResponseInputEvent, ]); export declare namespace TurnInboundEventItem { - export type Raw = UserToolApprovalMessage.Raw | UserToolApprovalPolicyMessage.Raw | UserToolResponseMessage.Raw; + export type Raw = + | UserToolApprovalInputEvent.Raw + | UserToolApprovalPolicyMessage.Raw + | UserToolResponseInputEvent.Raw; } diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts deleted file mode 100644 index 5aa1ac58b..000000000 --- a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalMessage.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../api/index.js"; -import * as core from "../../core/index.js"; -import type * as serializers from "../index.js"; -import { ApprovalDecision } from "./ApprovalDecision.js"; - -export const UserToolApprovalMessage: core.serialization.ObjectSchema< - serializers.UserToolApprovalMessage.Raw, - TrueForge.UserToolApprovalMessage -> = core.serialization.object({ - approval: ApprovalDecision, - threadId: core.serialization.property("thread_id", core.serialization.string()), - toolCallId: core.serialization.property("tool_call_id", core.serialization.string()), - type: core.serialization.stringLiteral("user.tool_approval"), -}); - -export declare namespace UserToolApprovalMessage { - export interface Raw { - approval: ApprovalDecision.Raw; - thread_id: string; - tool_call_id: string; - type: "user.tool_approval"; - } -} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts b/packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts deleted file mode 100644 index 4849207ef..000000000 --- a/packages/trueforge-sdk/src/serialization/types/UserToolResponseMessage.ts +++ /dev/null @@ -1,24 +0,0 @@ -// This file was auto-generated by Fern from our API Definition. - -import type * as TrueForge from "../../api/index.js"; -import * as core from "../../core/index.js"; -import type * as serializers from "../index.js"; - -export const UserToolResponseMessage: core.serialization.ObjectSchema< - serializers.UserToolResponseMessage.Raw, - TrueForge.UserToolResponseMessage -> = core.serialization.object({ - content: core.serialization.string(), - threadId: core.serialization.property("thread_id", core.serialization.string()), - toolCallId: core.serialization.property("tool_call_id", core.serialization.string()), - type: core.serialization.stringLiteral("user.tool_response"), -}); - -export declare namespace UserToolResponseMessage { - export interface Raw { - content: string; - thread_id: string; - tool_call_id: string; - type: "user.tool_response"; - } -} diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index c04a15bce..77eafba9e 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -47,10 +47,10 @@ export * from "./ConfiguredSkill.js"; export * from "./ConfiguredWebSearchProvider.js"; export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; -export * from "./CreatedTurnInboundEvent.js"; export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; -export * from "./CreateTurnInboundEventResponse.js"; +export * from "./CreateTurnEventResponse.js"; +export * from "./CreateTurnEventResponseDataItem.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; export * from "./DaytonaSandboxProviderAuth.js"; @@ -242,12 +242,10 @@ export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; export * from "./UserToolApprovalEvent.js"; export * from "./UserToolApprovalInputEvent.js"; -export * from "./UserToolApprovalMessage.js"; export * from "./UserToolApprovalPolicyEvent.js"; export * from "./UserToolApprovalPolicyMessage.js"; export * from "./UserToolResponseEvent.js"; export * from "./UserToolResponseInputEvent.js"; -export * from "./UserToolResponseMessage.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; export * from "./WebSearchProviderManifest.js"; diff --git a/packages/trueforge-sdk/tests/wire/sessions.test.ts b/packages/trueforge-sdk/tests/wire/sessions.test.ts index 9686a1422..1c585abaa 100644 --- a/packages/trueforge-sdk/tests/wire/sessions.test.ts +++ b/packages/trueforge-sdk/tests/wire/sessions.test.ts @@ -1398,7 +1398,7 @@ describe("SessionsClient", () => { }).rejects.toThrow(TrueForgeTypes.NotFoundError); }); - test("create_turn_inbound_event (1)", async () => { + test("create_turn_event (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); const rawRequestBody = { @@ -1433,7 +1433,7 @@ describe("SessionsClient", () => { .jsonBody(rawResponseBody) .build(); - const response = await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + const response = await client.sessions.createTurnEvent("session_id", "turn_id", { events: [ { approval: { @@ -1461,7 +1461,7 @@ describe("SessionsClient", () => { }); }); - test("create_turn_inbound_event (2)", async () => { + test("create_turn_event (2)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); const rawRequestBody = { @@ -1482,7 +1482,7 @@ describe("SessionsClient", () => { .build(); await expect(async () => { - return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + return await client.sessions.createTurnEvent("session_id", "turn_id", { events: [ { approval: { @@ -1505,7 +1505,7 @@ describe("SessionsClient", () => { }).rejects.toThrow(TrueForgeTypes.BadRequestError); }); - test("create_turn_inbound_event (3)", async () => { + test("create_turn_event (3)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); const rawRequestBody = { @@ -1526,7 +1526,7 @@ describe("SessionsClient", () => { .build(); await expect(async () => { - return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + return await client.sessions.createTurnEvent("session_id", "turn_id", { events: [ { approval: { @@ -1549,7 +1549,7 @@ describe("SessionsClient", () => { }).rejects.toThrow(TrueForgeTypes.ForbiddenError); }); - test("create_turn_inbound_event (4)", async () => { + test("create_turn_event (4)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); const rawRequestBody = { @@ -1570,7 +1570,7 @@ describe("SessionsClient", () => { .build(); await expect(async () => { - return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + return await client.sessions.createTurnEvent("session_id", "turn_id", { events: [ { approval: { @@ -1593,7 +1593,7 @@ describe("SessionsClient", () => { }).rejects.toThrow(TrueForgeTypes.NotFoundError); }); - test("create_turn_inbound_event (5)", async () => { + test("create_turn_event (5)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); const rawRequestBody = { @@ -1614,7 +1614,7 @@ describe("SessionsClient", () => { .build(); await expect(async () => { - return await client.sessions.createTurnInboundEvent("session_id", "turn_id", { + return await client.sessions.createTurnEvent("session_id", "turn_id", { events: [ { approval: { diff --git a/packages/trueforge/src/apis/turns.ts b/packages/trueforge/src/apis/turns.ts index 52a7a4639..b86799e5e 100644 --- a/packages/trueforge/src/apis/turns.ts +++ b/packages/trueforge/src/apis/turns.ts @@ -43,7 +43,7 @@ import type { ISkillStore } from '../db/skillStore'; import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { createAndExecuteTurnRoute, - createTurnInboundEventRoute, + createTurnEventRoute, downloadSandboxFileRoute, getTurnRoute, listTurnEventsRoute, @@ -925,8 +925,8 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { }); }; - /** Accept inbound send-events. Persist / apply / wake land later. */ - const createTurnInboundEventHandler: RouteHandler = async c => { + /** Accept send-events. Persist / apply / wake land later. */ + const createTurnEventHandler: RouteHandler = async c => { const { session_id: sessionId, turn_id: turnId } = c.req.valid('param'); const body = c.req.valid('json'); const requestContext = deps.resolveRequestContext(c); @@ -972,7 +972,7 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { router.openapi(getTurnRoute, getTurnHandler); router.openapi(downloadSandboxFileRoute, downloadSandboxFileHandler); router.openapi(listTurnEventsRoute, listTurnEventsHandler); - router.openapi(createTurnInboundEventRoute, createTurnInboundEventHandler); + router.openapi(createTurnEventRoute, createTurnEventHandler); router.openapi(subscribeTurnRoute, subscribeTurnHandler); return router; } diff --git a/packages/trueforge/src/routes/turnRoutes.ts b/packages/trueforge/src/routes/turnRoutes.ts index 641d2b2ca..2a270a2af 100644 --- a/packages/trueforge/src/routes/turnRoutes.ts +++ b/packages/trueforge/src/routes/turnRoutes.ts @@ -6,8 +6,8 @@ import { createRoute, z } from '@hono/zod-openapi'; import { RequestErrorResponseSchema } from '../schemas/errors'; import { - CreateTurnInboundEventRequestSchema, - CreateTurnInboundEventResponseSchema, + CreateTurnEventRequestSchema, + CreateTurnEventResponseSchema, TurnStreamingEventSchema, } from '../schemas/events'; import { @@ -172,30 +172,25 @@ export const listTurnEventsRoute = createRoute({ }, }); -/** - * Create inbound tip HITL / policy events in `turn_inbound_events`. - * Apply / wake / auto-continue land in follow-up work — this route only inserts. - */ -export const createTurnInboundEventRoute = createRoute({ +export const createTurnEventRoute = createRoute({ method: 'post', path: '/{session_id}/turns/{turn_id}/events', tags: [OpenApiTag.AGENT_SESSIONS], - summary: 'Create turn inbound events', - description: - 'Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step.', + summary: 'Create turn events', + description: 'Create events for a turn. Only the session creator may create them.', 'x-fern-sdk-group-name': ['sessions'], - 'x-fern-sdk-method-name': 'create_turn_inbound_event', + 'x-fern-sdk-method-name': 'create_turn_event', request: { params: TurnIdParamsSchema, body: { - content: { 'application/json': { schema: CreateTurnInboundEventRequestSchema } }, + content: { 'application/json': { schema: CreateTurnEventRequestSchema } }, required: true, }, }, responses: { 201: { - content: { 'application/json': { schema: CreateTurnInboundEventResponseSchema } }, - description: 'Events created in the turn inbox.', + content: { 'application/json': { schema: CreateTurnEventResponseSchema } }, + description: 'Events created.', }, 400: { content: { 'application/json': { schema: RequestErrorResponseSchema } }, diff --git a/packages/trueforge/src/schemas/events.ts b/packages/trueforge/src/schemas/events.ts index 2717cecd1..d14a4ffb9 100644 --- a/packages/trueforge/src/schemas/events.ts +++ b/packages/trueforge/src/schemas/events.ts @@ -32,32 +32,28 @@ import { EVENTS_PAGE_LIMIT } from './common'; export type { TurnCreatedEvent } from '@truefoundry/trueforge-core/agent-session'; export { EventType }; -/** Client → harness inbound events (tip HITL / sticky policy). Persisted to the turn inbox. */ -export const CreateTurnInboundEventRequestSchema = z +export const CreateTurnEventRequestSchema = z .object({ events: z .array(TurnInboundEventItemSchema) .min(1) - .describe('One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).'), + .describe('One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).'), }) - .openapi('CreateTurnInboundEventRequest'); + .openapi('CreateTurnEventRequest'); -/** Same shapes as the durable stream / {@link SessionEvent} members for these types. */ -export const CreatedTurnInboundEventSchema = z - .discriminatedUnion('type', [ - UserToolApprovalEventSchema, - UserToolResponseEventSchema, - UserToolApprovalPolicyEventSchema, - ]) - .openapi('CreatedTurnInboundEvent'); - -export const CreateTurnInboundEventResponseSchema = z +export const CreateTurnEventResponseSchema = z .object({ data: z - .array(CreatedTurnInboundEventSchema) - .describe('Created inbox events with server-minted `id` and `created_at`, in request order.'), + .array( + z.discriminatedUnion('type', [ + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, + ]), + ) + .describe('Events with server-minted `id` and `created_at`, in request order.'), }) - .openapi('CreateTurnInboundEventResponse'); + .openapi('CreateTurnEventResponse'); /** Live SSE stream for session turns — content events, deltas and lifecycle. */ export const TurnStreamingEventSchema = z diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 29b3c028e..b9102a1df 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -472,7 +472,7 @@ describe('sessions HTTP agent binding', () => { expect(allowed.status).toBe(200); }); - it('POST /sessions/{id}/turns/{turn_id}/events mints ids for inbound send-events', async () => { + it('POST /sessions/{id}/turns/{turn_id}/events mints ids for events', async () => { const created = await app.request('/', jsonInit('POST', { agent: { spec: inlineSpec } })); expect(created.status).toBe(201); const { data: session } = (await created.json()) as { data: { id: string } }; diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index 1a8f9cb15..245c0aad8 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -2831,7 +2831,7 @@ client.sessions.list_turn_events(
-
client.sessions.create_turn_inbound_event(...) -> CreateTurnInboundEventResponse +
client.sessions.create_turn_event(...) -> CreateTurnEventResponse
@@ -2843,7 +2843,7 @@ client.sessions.list_turn_events(
-Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. +Create events for a turn. Only the session creator may create them.
@@ -2858,18 +2858,18 @@ Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_ap
```python -from trueforge_sdk import TrueForge, UserToolApprovalMessage, ApprovalAllow +from trueforge_sdk import TrueForge, UserToolApprovalInputEvent, ApprovalAllow client = TrueForge( token="", base_url="https://yourhost.com/path/to/api", ) -client.sessions.create_turn_inbound_event( +client.sessions.create_turn_event( session_id="session_id", turn_id="turn_id", events=[ - UserToolApprovalMessage( + UserToolApprovalInputEvent( approval=ApprovalAllow( status="allow", ), @@ -2910,7 +2910,7 @@ client.sessions.create_turn_inbound_event(
-**events:** `typing.List[TurnInboundEventItem]` — One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). +**events:** `typing.List[TurnInboundEventItem]` — One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).
diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index cbdf7ce18..6e256920d 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -57,9 +57,9 @@ ContextManagementConfig, CreateScheduleRunResponse, CreateSessionAgent, - CreateTurnInboundEventResponse, + CreateTurnEventResponse, + CreateTurnEventResponseDataItem, CreatedBySubject, - CreatedTurnInboundEvent, CronExpression, CustomModelProvider, DaytonaSandboxProviderAuth, @@ -251,12 +251,10 @@ UserMessageContentItem, UserToolApprovalEvent, UserToolApprovalInputEvent, - UserToolApprovalMessage, UserToolApprovalPolicyEvent, UserToolApprovalPolicyMessage, UserToolResponseEvent, UserToolResponseInputEvent, - UserToolResponseMessage, WebSearchCapability, WebSearchConfig, WebSearchProviderManifest, @@ -336,9 +334,9 @@ "ContextManagementConfig": ".types", "CreateScheduleRunResponse": ".types", "CreateSessionAgent": ".types", - "CreateTurnInboundEventResponse": ".types", + "CreateTurnEventResponse": ".types", + "CreateTurnEventResponseDataItem": ".types", "CreatedBySubject": ".types", - "CreatedTurnInboundEvent": ".types", "CronExpression": ".types", "CustomModelProvider": ".types", "DaytonaSandboxProviderAuth": ".types", @@ -541,12 +539,10 @@ "UserMessageContentItem": ".types", "UserToolApprovalEvent": ".types", "UserToolApprovalInputEvent": ".types", - "UserToolApprovalMessage": ".types", "UserToolApprovalPolicyEvent": ".types", "UserToolApprovalPolicyMessage": ".types", "UserToolResponseEvent": ".types", "UserToolResponseInputEvent": ".types", - "UserToolResponseMessage": ".types", "WebSearchCapability": ".types", "WebSearchConfig": ".types", "WebSearchProviderManifest": ".types", @@ -643,9 +639,9 @@ def __dir__(): "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", - "CreateTurnInboundEventResponse", + "CreateTurnEventResponse", + "CreateTurnEventResponseDataItem", "CreatedBySubject", - "CreatedTurnInboundEvent", "CronExpression", "CustomModelProvider", "DaytonaSandboxProviderAuth", @@ -848,12 +844,10 @@ def __dir__(): "UserMessageContentItem", "UserToolApprovalEvent", "UserToolApprovalInputEvent", - "UserToolApprovalMessage", "UserToolApprovalPolicyEvent", "UserToolApprovalPolicyMessage", "UserToolResponseEvent", "UserToolResponseInputEvent", - "UserToolResponseMessage", "WebSearchCapability", "WebSearchConfig", "WebSearchProviderManifest", diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py index d1ad04565..96a18fab8 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py @@ -9,7 +9,7 @@ from ..core.stream import AsyncStream, Stream, StreamEvent from ..types.cancel_session_response import CancelSessionResponse from ..types.create_session_agent import CreateSessionAgent -from ..types.create_turn_inbound_event_response import CreateTurnInboundEventResponse +from ..types.create_turn_event_response import CreateTurnEventResponse from ..types.get_session_response import GetSessionResponse from ..types.get_turn_response import GetTurnResponse from ..types.list_session_events_response import ListSessionEventsResponse @@ -687,16 +687,16 @@ def list_turn_events( request_options=request_options, ) - def create_turn_inbound_event( + def create_turn_event( self, *, session_id: str, turn_id: str, events: typing.Sequence[TurnInboundEventItem], request_options: typing.Optional[RequestOptions] = None, - ) -> CreateTurnInboundEventResponse: + ) -> CreateTurnEventResponse: """ - Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + Create events for a turn. Only the session creator may create them. Parameters ---------- @@ -707,29 +707,29 @@ def create_turn_inbound_event( Turn identifier. events : typing.Sequence[TurnInboundEventItem] - One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - CreateTurnInboundEventResponse - Events created in the turn inbox. + CreateTurnEventResponse + Events created. Examples -------- - from trueforge_sdk import ApprovalAllow, TrueForge, UserToolApprovalMessage + from trueforge_sdk import ApprovalAllow, TrueForge, UserToolApprovalInputEvent client = TrueForge( token="YOUR_TOKEN", base_url="https://yourhost.com/path/to/api", ) - client.sessions.create_turn_inbound_event( + client.sessions.create_turn_event( session_id="session_id", turn_id="turn_id", events=[ - UserToolApprovalMessage( + UserToolApprovalInputEvent( approval=ApprovalAllow(), thread_id="thread_id", tool_call_id="tool_call_id", @@ -737,7 +737,7 @@ def create_turn_inbound_event( ], ) """ - _response = self._raw_client.create_turn_inbound_event( + _response = self._raw_client.create_turn_event( session_id=session_id, turn_id=turn_id, events=events, request_options=request_options ) return _response.data @@ -1566,16 +1566,16 @@ async def main() -> None: request_options=request_options, ) - async def create_turn_inbound_event( + async def create_turn_event( self, *, session_id: str, turn_id: str, events: typing.Sequence[TurnInboundEventItem], request_options: typing.Optional[RequestOptions] = None, - ) -> CreateTurnInboundEventResponse: + ) -> CreateTurnEventResponse: """ - Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + Create events for a turn. Only the session creator may create them. Parameters ---------- @@ -1586,21 +1586,25 @@ async def create_turn_inbound_event( Turn identifier. events : typing.Sequence[TurnInboundEventItem] - One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - CreateTurnInboundEventResponse - Events created in the turn inbox. + CreateTurnEventResponse + Events created. Examples -------- import asyncio - from trueforge_sdk import ApprovalAllow, AsyncTrueForge, UserToolApprovalMessage + from trueforge_sdk import ( + ApprovalAllow, + AsyncTrueForge, + UserToolApprovalInputEvent, + ) client = AsyncTrueForge( token="YOUR_TOKEN", @@ -1609,11 +1613,11 @@ async def create_turn_inbound_event( async def main() -> None: - await client.sessions.create_turn_inbound_event( + await client.sessions.create_turn_event( session_id="session_id", turn_id="turn_id", events=[ - UserToolApprovalMessage( + UserToolApprovalInputEvent( approval=ApprovalAllow(), thread_id="thread_id", tool_call_id="tool_call_id", @@ -1624,7 +1628,7 @@ async def main() -> None: asyncio.run(main()) """ - _response = await self._raw_client.create_turn_inbound_event( + _response = await self._raw_client.create_turn_event( session_id=session_id, turn_id=turn_id, events=events, request_options=request_options ) return _response.data diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py index 616b3c0fd..992928c8e 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py @@ -31,7 +31,7 @@ from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.cancel_session_response import CancelSessionResponse from ..types.create_session_agent import CreateSessionAgent -from ..types.create_turn_inbound_event_response import CreateTurnInboundEventResponse +from ..types.create_turn_event_response import CreateTurnEventResponse from ..types.get_session_response import GetSessionResponse from ..types.get_turn_response import GetTurnResponse from ..types.list_session_events_response import ListSessionEventsResponse @@ -1439,16 +1439,16 @@ def list_turn_events( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - def create_turn_inbound_event( + def create_turn_event( self, *, session_id: str, turn_id: str, events: typing.Sequence[TurnInboundEventItem], request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[CreateTurnInboundEventResponse]: + ) -> HttpResponse[CreateTurnEventResponse]: """ - Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + Create events for a turn. Only the session creator may create them. Parameters ---------- @@ -1459,15 +1459,15 @@ def create_turn_inbound_event( Turn identifier. events : typing.Sequence[TurnInboundEventItem] - One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - HttpResponse[CreateTurnInboundEventResponse] - Events created in the turn inbox. + HttpResponse[CreateTurnEventResponse] + Events created. """ _response = self._client_wrapper.httpx_client.request( f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", @@ -1486,9 +1486,9 @@ def create_turn_inbound_event( try: if 200 <= _response.status_code < 300: _data = typing.cast( - CreateTurnInboundEventResponse, + CreateTurnEventResponse, construct_type( - type_=CreateTurnInboundEventResponse, # type: ignore + type_=CreateTurnEventResponse, # type: ignore object_=_response.json(), ), ) @@ -3081,16 +3081,16 @@ async def _get_next(): ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - async def create_turn_inbound_event( + async def create_turn_event( self, *, session_id: str, turn_id: str, events: typing.Sequence[TurnInboundEventItem], request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[CreateTurnInboundEventResponse]: + ) -> AsyncHttpResponse[CreateTurnEventResponse]: """ - Create inbound events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`) in the durable turn inbox. Only the session creator may create. Events are stored unconsumed; applying them to the turn is a separate step. + Create events for a turn. Only the session creator may create them. Parameters ---------- @@ -3101,15 +3101,15 @@ async def create_turn_inbound_event( Turn identifier. events : typing.Sequence[TurnInboundEventItem] - One or more inbound items (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). request_options : typing.Optional[RequestOptions] Request-specific configuration. Returns ------- - AsyncHttpResponse[CreateTurnInboundEventResponse] - Events created in the turn inbox. + AsyncHttpResponse[CreateTurnEventResponse] + Events created. """ _response = await self._client_wrapper.httpx_client.request( f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", @@ -3128,9 +3128,9 @@ async def create_turn_inbound_event( try: if 200 <= _response.status_code < 300: _data = typing.cast( - CreateTurnInboundEventResponse, + CreateTurnEventResponse, construct_type( - type_=CreateTurnInboundEventResponse, # type: ignore + type_=CreateTurnEventResponse, # type: ignore object_=_response.json(), ), ) diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index c62727ee0..126971be8 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -56,9 +56,9 @@ from .context_management_config import ContextManagementConfig from .create_schedule_run_response import CreateScheduleRunResponse from .create_session_agent import CreateSessionAgent - from .create_turn_inbound_event_response import CreateTurnInboundEventResponse + from .create_turn_event_response import CreateTurnEventResponse + from .create_turn_event_response_data_item import CreateTurnEventResponseDataItem from .created_by_subject import CreatedBySubject - from .created_turn_inbound_event import CreatedTurnInboundEvent from .cron_expression import CronExpression from .custom_model_provider import CustomModelProvider from .daytona_sandbox_provider_auth import DaytonaSandboxProviderAuth @@ -250,12 +250,10 @@ from .user_message_content_item import UserMessageContentItem from .user_tool_approval_event import UserToolApprovalEvent from .user_tool_approval_input_event import UserToolApprovalInputEvent - from .user_tool_approval_message import UserToolApprovalMessage from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent from .user_tool_approval_policy_message import UserToolApprovalPolicyMessage from .user_tool_response_event import UserToolResponseEvent from .user_tool_response_input_event import UserToolResponseInputEvent - from .user_tool_response_message import UserToolResponseMessage from .web_search_capability import WebSearchCapability from .web_search_config import WebSearchConfig from .web_search_provider_manifest import WebSearchProviderManifest @@ -311,9 +309,9 @@ "ContextManagementConfig": ".context_management_config", "CreateScheduleRunResponse": ".create_schedule_run_response", "CreateSessionAgent": ".create_session_agent", - "CreateTurnInboundEventResponse": ".create_turn_inbound_event_response", + "CreateTurnEventResponse": ".create_turn_event_response", + "CreateTurnEventResponseDataItem": ".create_turn_event_response_data_item", "CreatedBySubject": ".created_by_subject", - "CreatedTurnInboundEvent": ".created_turn_inbound_event", "CronExpression": ".cron_expression", "CustomModelProvider": ".custom_model_provider", "DaytonaSandboxProviderAuth": ".daytona_sandbox_provider_auth", @@ -505,12 +503,10 @@ "UserMessageContentItem": ".user_message_content_item", "UserToolApprovalEvent": ".user_tool_approval_event", "UserToolApprovalInputEvent": ".user_tool_approval_input_event", - "UserToolApprovalMessage": ".user_tool_approval_message", "UserToolApprovalPolicyEvent": ".user_tool_approval_policy_event", "UserToolApprovalPolicyMessage": ".user_tool_approval_policy_message", "UserToolResponseEvent": ".user_tool_response_event", "UserToolResponseInputEvent": ".user_tool_response_input_event", - "UserToolResponseMessage": ".user_tool_response_message", "WebSearchCapability": ".web_search_capability", "WebSearchConfig": ".web_search_config", "WebSearchProviderManifest": ".web_search_provider_manifest", @@ -590,9 +586,9 @@ def __dir__(): "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", - "CreateTurnInboundEventResponse", + "CreateTurnEventResponse", + "CreateTurnEventResponseDataItem", "CreatedBySubject", - "CreatedTurnInboundEvent", "CronExpression", "CustomModelProvider", "DaytonaSandboxProviderAuth", @@ -784,12 +780,10 @@ def __dir__(): "UserMessageContentItem", "UserToolApprovalEvent", "UserToolApprovalInputEvent", - "UserToolApprovalMessage", "UserToolApprovalPolicyEvent", "UserToolApprovalPolicyMessage", "UserToolResponseEvent", "UserToolResponseInputEvent", - "UserToolResponseMessage", "WebSearchCapability", "WebSearchConfig", "WebSearchProviderManifest", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response.py similarity index 63% rename from python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py rename to python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response.py index ff5de2a43..707e507f5 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_inbound_event_response.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response.py @@ -5,13 +5,13 @@ import pydantic from ..core.pydantic_utilities import IS_PYDANTIC_V2 from ..core.unchecked_base_model import UncheckedBaseModel -from .created_turn_inbound_event import CreatedTurnInboundEvent +from .create_turn_event_response_data_item import CreateTurnEventResponseDataItem -class CreateTurnInboundEventResponse(UncheckedBaseModel): - data: typing.List[CreatedTurnInboundEvent] = pydantic.Field() +class CreateTurnEventResponse(UncheckedBaseModel): + data: typing.List[CreateTurnEventResponseDataItem] = pydantic.Field() """ - Created inbox events with server-minted `id` and `created_at`, in request order. + Events with server-minted `id` and `created_at`, in request order. """ if IS_PYDANTIC_V2: diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response_data_item.py similarity index 68% rename from python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py rename to python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response_data_item.py index 81ec54077..688e6a401 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/created_turn_inbound_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response_data_item.py @@ -6,4 +6,6 @@ from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent from .user_tool_response_event import UserToolResponseEvent -CreatedTurnInboundEvent = typing.Union[UserToolApprovalEvent, UserToolApprovalPolicyEvent, UserToolResponseEvent] +CreateTurnEventResponseDataItem = typing.Union[ + UserToolApprovalEvent, UserToolApprovalPolicyEvent, UserToolResponseEvent +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py index 5fbf08c68..1c1ce0e89 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py @@ -10,14 +10,14 @@ class ToolApprovalPolicyItem(UncheckedBaseModel): action: ToolApprovalPolicyAllowSession - server_name: str = pydantic.Field() + name: str = pydantic.Field() """ - Configured MCP server name (same as tool_info.server_name on model.message). + Tool name on that server. """ - name: str = pydantic.Field() + server_name: str = pydantic.Field() """ - Tool name on that server (same as tool_info.name on model.message). + MCP server name. """ if IS_PYDANTIC_V2: diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py index 6cb951a28..47a5cfe5a 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py @@ -2,8 +2,10 @@ import typing -from .user_tool_approval_message import UserToolApprovalMessage +from .user_tool_approval_input_event import UserToolApprovalInputEvent from .user_tool_approval_policy_message import UserToolApprovalPolicyMessage -from .user_tool_response_message import UserToolResponseMessage +from .user_tool_response_input_event import UserToolResponseInputEvent -TurnInboundEventItem = typing.Union[UserToolApprovalMessage, UserToolApprovalPolicyMessage, UserToolResponseMessage] +TurnInboundEventItem = typing.Union[ + UserToolApprovalInputEvent, UserToolApprovalPolicyMessage, UserToolResponseInputEvent +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py deleted file mode 100644 index 07a39d669..000000000 --- a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_message.py +++ /dev/null @@ -1,31 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2 -from ..core.unchecked_base_model import UncheckedBaseModel -from .approval_decision import ApprovalDecision - - -class UserToolApprovalMessage(UncheckedBaseModel): - approval: ApprovalDecision - thread_id: str = pydantic.Field() - """ - Thread that owns the pending tool call. - """ - - tool_call_id: str = pydantic.Field() - """ - Tool call id being approved or denied. - """ - - type: typing.Literal["user.tool_approval"] = "user.tool_approval" - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 - else: - - class Config: - smart_union = True - extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py index 13ae72c36..155b5651c 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py @@ -21,7 +21,7 @@ class UserToolApprovalPolicyEvent(UncheckedBaseModel): policies: typing.List[ToolApprovalPolicyItem] = pydantic.Field() """ - One or more (server, tool_name) policy entries. + One or more (server_name, name) policy entries. """ type: typing.Literal["user.tool_approval_policy"] = "user.tool_approval_policy" diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py index 1b5bcefcf..6ab2ee34f 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py @@ -11,7 +11,7 @@ class UserToolApprovalPolicyMessage(UncheckedBaseModel): policies: typing.List[ToolApprovalPolicyItem] = pydantic.Field() """ - One or more (server, tool_name) policy entries. + One or more (server_name, name) policy entries. """ type: typing.Literal["user.tool_approval_policy"] = "user.tool_approval_policy" diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py deleted file mode 100644 index a7e919420..000000000 --- a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_message.py +++ /dev/null @@ -1,34 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2 -from ..core.unchecked_base_model import UncheckedBaseModel - - -class UserToolResponseMessage(UncheckedBaseModel): - content: str = pydantic.Field() - """ - Client-side tool result content. - """ - - thread_id: str = pydantic.Field() - """ - Thread that owns the pending tool call. - """ - - tool_call_id: str = pydantic.Field() - """ - Tool call id receiving the client response. - """ - - type: typing.Literal["user.tool_response"] = "user.tool_response" - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 - else: - - class Config: - smart_union = True - extra = pydantic.Extra.allow From 0a72eb065a2cc6e1467bd3a2a3ffe820440f89f4 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Thu, 24 Sep 2026 12:35:51 +0530 Subject: [PATCH 20/23] fix changeset --- .changeset/turn-inbound-events-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/turn-inbound-events-api.md b/.changeset/turn-inbound-events-api.md index af33dd314..b5e2c656a 100644 --- a/.changeset/turn-inbound-events-api.md +++ b/.changeset/turn-inbound-events-api.md @@ -3,4 +3,5 @@ "@truefoundry/trueforge-core": patch --- -Add `POST /sessions/{session_id}/turns/{turn_id}/events` for tip HITL / policy events. Response items use the same `UserToolApprovalEvent` / `UserToolResponseEvent` / `UserToolApprovalPolicyEvent` shapes as `SessionEvent` and the turn SSE stream. +Add `POST /sessions/{session_id}/turns/{turn_id}/events` for tip HITL / policy events. + From 96fd0f098e28a1a6f4cd03a6c6800a1c8df041e6 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Thu, 24 Sep 2026 12:37:56 +0530 Subject: [PATCH 21/23] delete mig --- .../20260918_000002_turn_inbound_events.ts | 37 ------------------- .../20260918_000002_turn_inbound_events.ts | 35 ------------------ 2 files changed, 72 deletions(-) delete mode 100644 packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts delete mode 100644 packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts diff --git a/packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts b/packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts deleted file mode 100644 index 54af1de25..000000000 --- a/packages/trueforge/src/db/postgres/migrations/20260918_000002_turn_inbound_events.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { sql, type Kysely } from 'kysely'; - -/** - * Turn-scoped inbound send-event inbox (tip HITL / policies). - * `turn_id` is required — every row belongs to a tip. - */ -export async function up(db: Kysely): Promise { - await sql`SET LOCAL lock_timeout = '5s'`.execute(db); - - await db.schema - .createTable('turn_inbound_events') - .addColumn('session_id', 'text', col => col.notNull()) - .addColumn('turn_id', 'text', col => col.notNull()) - .addColumn('event_id', 'text', col => col.notNull()) - .addColumn('payload', 'jsonb', col => col.notNull()) - .addColumn('consumed', 'boolean', col => col.notNull().defaultTo(false)) - .addColumn('created_at', 'timestamptz', col => col.notNull()) - .addPrimaryKeyConstraint('turn_inbound_events_pkey', ['session_id', 'turn_id', 'event_id']) - .execute(); - - await db.schema - .alterTable('turn_inbound_events') - .addForeignKeyConstraint('turn_inbound_events_session_fkey', ['session_id'], 'session', ['session_id']) - .onDelete('cascade') - .execute(); - - await sql` - CREATE INDEX turn_inbound_events_unconsumed_idx - ON turn_inbound_events (session_id, turn_id, event_id) - WHERE consumed = false - `.execute(db); -} - -export async function down(db: Kysely): Promise { - await sql`SET LOCAL lock_timeout = '5s'`.execute(db); - await db.schema.dropTable('turn_inbound_events').ifExists().cascade().execute(); -} diff --git a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts b/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts deleted file mode 100644 index 4ccd16009..000000000 --- a/packages/trueforge/src/db/sqlite/migrations/20260918_000002_turn_inbound_events.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type Kysely, sql } from 'kysely'; - -/** - * Turn-scoped inbound send-event inbox (tip HITL / policies). - * `turn_id` is required — every row belongs to a tip. - * Kysely does not wrap SQLite migrations — keep CREATE TABLE + INDEX atomic. - */ -export async function up(db: Kysely): Promise { - await db.transaction().execute(async trx => { - await sql` - CREATE TABLE turn_inbound_events ( - session_id TEXT NOT NULL REFERENCES session(session_id) ON DELETE CASCADE, - turn_id TEXT NOT NULL, - event_id TEXT NOT NULL, - payload BLOB NOT NULL, - consumed INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL, - PRIMARY KEY (session_id, turn_id, event_id) - ) STRICT - `.execute(trx); - - await sql` - CREATE INDEX turn_inbound_events_unconsumed_idx - ON turn_inbound_events (session_id, turn_id, event_id) - WHERE consumed = 0 - `.execute(trx); - }); -} - -export async function down(db: Kysely): Promise { - await db.transaction().execute(async trx => { - await sql`DROP INDEX IF EXISTS turn_inbound_events_unconsumed_idx`.execute(trx); - await sql`DROP TABLE IF EXISTS turn_inbound_events`.execute(trx); - }); -} From 823929f6190fa75d51ae21a5ed99ddeac96fa71e Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Thu, 24 Sep 2026 12:42:09 +0530 Subject: [PATCH 22/23] fix for UI --- .../toUiTurnState.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts index afc837de0..56ac6edcf 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts @@ -35,16 +35,30 @@ function toUiTerminalTurnState(state: TrueForgeApi.TurnDoneEventState): Exclude< } } +/** SDK stream types the assistant-ui-runtime union does not include yet. */ +// TODO: Remove this after we have migrated to the new schema. +function isUnmappedSdkEvent(event: TrueForgeApi.SessionEvent | TrueForgeApi.TurnStreamingEvent): event is Extract< + TrueForgeApi.SessionEvent | TrueForgeApi.TurnStreamingEvent, + { + type: 'turn.update' | 'user.tool_approval' | 'user.tool_response' | 'user.tool_approval_policy'; + } +> { + return ( + event.type === 'turn.update' || + event.type === 'user.tool_approval' || + event.type === 'user.tool_response' || + event.type === 'user.tool_approval_policy' + ); +} + export function toUiSessionEvent(event: TrueForgeApi.SessionEvent): SessionEventItem['event'] | undefined { - // TODO: map turn.update when the runtime event union includes it - if (event.type === 'turn.update') return undefined; + if (isUnmappedSdkEvent(event)) return undefined; if (event.type !== 'turn.done') return { ...event }; return { ...event, state: toUiTerminalTurnState(event.state) }; } export function toUiStreamingEvent(event: TrueForgeApi.TurnStreamingEvent): TurnStreamingEvent | undefined { - // TODO: map turn.update when the runtime event union includes it - if (event.type === 'turn.update') return undefined; + if (isUnmappedSdkEvent(event)) return undefined; if (event.type !== 'turn.done') return { ...event }; return { ...event, state: toUiTerminalTurnState(event.state) }; } From ff0937df2d7146c5b0acf9d76bb565f2b1c56a98 Mon Sep 17 00:00:00 2001 From: Heer Ambavi Date: Thu, 24 Sep 2026 13:10:10 +0530 Subject: [PATCH 23/23] lint --- packages/trueforge/src/apis/turns.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/trueforge/src/apis/turns.ts b/packages/trueforge/src/apis/turns.ts index b86799e5e..c74b9ebcb 100644 --- a/packages/trueforge/src/apis/turns.ts +++ b/packages/trueforge/src/apis/turns.ts @@ -925,9 +925,8 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { }); }; - /** Accept send-events. Persist / apply / wake land later. */ const createTurnEventHandler: RouteHandler = async c => { - const { session_id: sessionId, turn_id: turnId } = c.req.valid('param'); + const { session_id: sessionId } = c.req.valid('param'); const body = c.req.valid('json'); const requestContext = deps.resolveRequestContext(c); const session = await deps.sessions.get({