diff --git a/.changeset/transcript-phase-on-attach.md b/.changeset/transcript-phase-on-attach.md new file mode 100644 index 000000000..43e4cd4ab --- /dev/null +++ b/.changeset/transcript-phase-on-attach.md @@ -0,0 +1,5 @@ +--- +"@pymodel/pythinker-code": patch +--- + +Clients that open a session while a tool approval is pending now show the agent's status instead of none. diff --git a/packages/agent-gateway/src/services/transcript/coreBinding.ts b/packages/agent-gateway/src/services/transcript/coreBinding.ts index 6c35fbf11..e40a8e114 100644 --- a/packages/agent-gateway/src/services/transcript/coreBinding.ts +++ b/packages/agent-gateway/src/services/transcript/coreBinding.ts @@ -155,6 +155,7 @@ export function bindSessionTranscript( if (loopStatus?.state === 'running' && loopStatus.activeTurnId !== undefined) { const promptId = loopStatus.activePromptId; projector.seedActiveTurn({ turnId: loopStatus.activeTurnId, promptId }); + applyOps(handle.id, projector.seedPhase()); } const list = agentDisposables.get(handle.id) ?? []; list.push(busD); diff --git a/packages/agent-gateway/src/services/transcript/coreEventMap.ts b/packages/agent-gateway/src/services/transcript/coreEventMap.ts index e37aefe04..5952a0932 100644 --- a/packages/agent-gateway/src/services/transcript/coreEventMap.ts +++ b/packages/agent-gateway/src/services/transcript/coreEventMap.ts @@ -357,7 +357,12 @@ export class AgentTranscriptProjector { return [...ops, { op: 'meta.merge', meta: { agent: { phase } } }]; } - private phaseFor(event: ProjectorBusEvent): ReturnType { + seedPhase(): TranscriptOperation[] { + const phase = this.tracker()?.recompute(); + return phase === undefined ? [] : [{ op: 'meta.merge', meta: { agent: { phase } } }]; + } + + private tracker(): LegacyActivityTracker | undefined { if (this.lookups?.activitySnapshot === undefined || this.lookups.pendingApprovals === undefined) { return undefined; } @@ -365,7 +370,12 @@ export class AgentTranscriptProjector { this.lookups.activitySnapshot, this.lookups.pendingApprovals, ); - return phaseFromDomainEvent(this.activityTracker, event); + return this.activityTracker; + } + + private phaseFor(event: ProjectorBusEvent): ReturnType { + const tracker = this.tracker(); + return tracker === undefined ? undefined : phaseFromDomainEvent(tracker, event); } private mapEvent(event: ProjectorBusEvent): TranscriptOperation[] { diff --git a/packages/agent-gateway/test/services/transcript.test.ts b/packages/agent-gateway/test/services/transcript.test.ts index 90f2845a2..7d87aabb7 100644 --- a/packages/agent-gateway/test/services/transcript.test.ts +++ b/packages/agent-gateway/test/services/transcript.test.ts @@ -1597,6 +1597,24 @@ describe('AgentTranscriptProjector', () => { expect(tx.getMeta().agent).toMatchObject({ model: 'k3', thinkingEffort: 'high' }); }); + it('seeds meta.agent.phase when attaching to a turn blocked on an approval', () => { + const projector = new AgentTranscriptProjector('main', TEST_SESSION_ID, { + activitySnapshot: () => ({ + turn: { turnId: 1, phase: 'running', step: 1, ending: false, activeToolCalls: [], since: 1000 }, + }), + pendingApprovals: () => [{ approvalId: 'ap1', toolCallId: 'c1', since: 1500 }], + }); + const tx = new AgentTranscript('main'); + + tx.apply(projector.seedPhase()); + + expect(tx.getMeta().agent?.phase).toMatchObject({ + kind: 'awaiting_approval', + turnId: 1, + approval: { approvalId: 'ap1', toolCallId: 'c1' }, + }); + }); + it('maps domain events into meta.agent.phase', () => { let snapshot: AgentActivitySnapshot = {}; let approvals: readonly LegacyActivityApproval[] = [];