diff --git a/packages/trueforge/src/apis/turns.ts b/packages/trueforge/src/apis/turns.ts index 42480a5a5..15684ff9e 100644 --- a/packages/trueforge/src/apis/turns.ts +++ b/packages/trueforge/src/apis/turns.ts @@ -135,6 +135,18 @@ export type BeginTurnExecutionDeps = Pick; }; +/** Extra LLM/MCP headers resolved after turn id is minted. */ +type ResolveTurnHeaders = (input: { session: SessionHandle; turnId: string }) => Record; + +interface BeginTurnExecutionParams { + session: SessionHandle; + input: TurnInputItem[] | undefined; + previous_turn_id: string | undefined; + userRef: string; + turnHeaders?: ResolveTurnHeaders; + deps: BeginTurnExecutionDeps; +} + /** * Builds the per-turn resolver. Agent / MCP / sandbox / LLM lookups are wired * the same way: async factories over the corresponding stores. @@ -149,8 +161,7 @@ function createTurnResolver(deps: { signal: AbortSignal; userRef: string; session: SessionHandle; - turnId: string; - tfyMetadata: Record | undefined; + turnHeaders: Record; }): TurnResourceResolver { const { mcpServerStore, @@ -162,14 +173,10 @@ function createTurnResolver(deps: { signal, userRef, session, - turnId, - tfyMetadata, + turnHeaders, } = deps; const tenant_id = session.tenant_id; const sessionId = session.session_id; - const metadataHeaders = isTrueFoundryModeEnabled() - ? gatewayMetadataHeaders(mergeGatewayMetadata({ session, turnId, tfyMetadata })) - : {}; return new TurnResourceResolver({ llm: async name => { @@ -182,7 +189,7 @@ function createTurnResolver(deps: { modelClient: new VercelAILLM({ providerConfig: { ...resolved.providerConfig, - headers: { ...resolved.providerConfig.headers, ...metadataHeaders }, + headers: { ...resolved.providerConfig.headers, ...turnHeaders }, }, logger, signal, @@ -207,7 +214,7 @@ function createTurnResolver(deps: { url: connection.url, headers: withGatewayMetadataHeaders({ headers: connection.headers, - metadataHeaders, + metadataHeaders: turnHeaders, }), }; }, @@ -386,15 +393,10 @@ export interface TurnEventDrainInput { * Shared create-turn engine: persist the turn, start execution, and return the * drain inputs. Does not wait for events and does not write HTTP/SSE. */ -export async function beginTurnExecution(params: { - session: SessionHandle; - input: TurnInputItem[] | undefined; - previous_turn_id: string | undefined; - userRef: string; - tfyMetadata?: Record | undefined; - deps: BeginTurnExecutionDeps; -}): Promise<{ turn: TurnHandle; drainInput: TurnEventDrainInput }> { - const { session, input, previous_turn_id: previousTurnId, userRef, tfyMetadata, deps } = params; +export async function beginTurnExecution( + params: BeginTurnExecutionParams, +): Promise<{ turn: TurnHandle; drainInput: TurnEventDrainInput }> { + const { session, input, previous_turn_id: previousTurnId, userRef, turnHeaders, deps } = params; const sessionId = session.session_id; const turnId = mintPeeredTurnId(configuration.EXECUTOR_ID); @@ -410,8 +412,7 @@ export async function beginTurnExecution(params: { signal: abortController.signal, userRef, session, - turnId, - tfyMetadata, + turnHeaders: turnHeaders?.({ session, turnId }) ?? {}, }); // First turn only: derive the title from the first user message. The store @@ -461,14 +462,7 @@ export async function beginTurnExecution(params: { * Non-stream create-turn: begin execution and resolve once the first event is * dual-written so immediate subscribe cannot 412. Same as `stream: false`. */ -export async function startTurnInProcess(params: { - session: SessionHandle; - input: TurnInputItem[] | undefined; - previous_turn_id: string | undefined; - userRef: string; - tfyMetadata?: Record | undefined; - deps: BeginTurnExecutionDeps; -}): Promise { +export async function startTurnInProcess(params: BeginTurnExecutionParams): Promise { const { turn, drainInput } = await beginTurnExecution(params); // Same unawaited drain scheduling as Hono streamSSE's run(cb). @@ -775,14 +769,17 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { } const rawTfyMetadata = c.req.header(X_TFY_METADATA); - const tfyMetadata = rawTfyMetadata === undefined ? undefined : parseGatewayMetadataHeader(rawTfyMetadata); + const requestMetadata = rawTfyMetadata === undefined ? undefined : parseGatewayMetadataHeader(rawTfyMetadata); - const turnParams = { + const turnParams: BeginTurnExecutionParams = { session, input: body.input, previous_turn_id: body.previous_turn_id, userRef: requestContext.subject.id, - tfyMetadata, + turnHeaders: ({ session: turnSession, turnId }) => + isTrueFoundryModeEnabled() + ? gatewayMetadataHeaders(mergeGatewayMetadata({ session: turnSession, turnId, requestMetadata })) + : {}, deps: { ...deps, modelProviderStore: deps.resolveModelProviderStore(c, referencedAgent), diff --git a/packages/trueforge/src/runtime/sessionResources.ts b/packages/trueforge/src/runtime/sessionResources.ts index ce75c31ef..0834209f3 100644 --- a/packages/trueforge/src/runtime/sessionResources.ts +++ b/packages/trueforge/src/runtime/sessionResources.ts @@ -72,14 +72,14 @@ export function buildGatewayMetadata(input: { session: SessionHandle; turnId: st return metadata; } -/** Inbound x-tfy-metadata first; harness tfg.* always win */ +/** Caller requestMetadata first; harness tfg.* always win */ export function mergeGatewayMetadata(input: { session: SessionHandle; turnId: string; - tfyMetadata?: Record | undefined; + requestMetadata?: Record | undefined; }): Record { return { - ...input.tfyMetadata, + ...input.requestMetadata, ...buildGatewayMetadata({ session: input.session, turnId: input.turnId }), }; } diff --git a/packages/trueforge/tests/unit/runtime/sessionResources.test.ts b/packages/trueforge/tests/unit/runtime/sessionResources.test.ts index 376665160..39cad7143 100644 --- a/packages/trueforge/tests/unit/runtime/sessionResources.test.ts +++ b/packages/trueforge/tests/unit/runtime/sessionResources.test.ts @@ -80,7 +80,7 @@ describe('buildGatewayMetadata', () => { }); describe('mergeGatewayMetadata', () => { - it('keeps tfyMetadata keys and overwrites spoofed tfg.* fields so order is maintained', async () => { + it('keeps requestMetadata keys and overwrites spoofed tfg.* fields so order is maintained', async () => { const session = await createGatewayMetadataSession({ agent: { type: 'reference', id: 'agent-1', name: 'my-agent' }, }); @@ -89,7 +89,7 @@ describe('mergeGatewayMetadata', () => { mergeGatewayMetadata({ session, turnId: 'turn-1', - tfyMetadata: { + requestMetadata: { env: 'prod', [`${TFG_METADATA_PREFIX}.session_id`]: 'spoofed-session', [`${TFG_METADATA_PREFIX}.turn_id`]: 'spoofed-turn', @@ -106,7 +106,7 @@ describe('mergeGatewayMetadata', () => { }); }); - it('matches harness-only stamps when tfyMetadata is absent', async () => { + it('matches harness-only stamps when requestMetadata is absent', async () => { const session = await createGatewayMetadataSession({ agent: { type: 'reference', id: 'agent-1', name: 'my-agent' }, });