diff --git a/.changeset/workflow-enter-origin.md b/.changeset/workflow-enter-origin.md new file mode 100644 index 00000000..4a2f1393 --- /dev/null +++ b/.changeset/workflow-enter-origin.md @@ -0,0 +1,5 @@ +--- +"@pythoughts/pythinker-code": patch +--- + +Record the origin of the prompt that entered Dynamic Workflow mode, so a fan-out started by a scheduled job or hook is attributable in the session records. diff --git a/packages/agent-core/src/agent/dynamic-workflow/index.ts b/packages/agent-core/src/agent/dynamic-workflow/index.ts index 4d8e4127..cd9b0e8e 100644 --- a/packages/agent-core/src/agent/dynamic-workflow/index.ts +++ b/packages/agent-core/src/agent/dynamic-workflow/index.ts @@ -18,7 +18,14 @@ export class DynamicWorkflowMode { enter(trigger: DynamicWorkflowModeTrigger): void { if (this.active !== null) return; - this.agent.records.logRecord({ type: 'dynamic_workflow_mode.enter', trigger }); + this.agent.records.logRecord({ + type: 'dynamic_workflow_mode.enter', + trigger, + // The 'tool' trigger always fires inside a turn, so the origin names + // what drove the model to fan out (user, cron, hook, ...). The RPC + // triggers run between turns and record no origin. + origin: this.agent.turn.activeTurnOrigin, + }); this.active = trigger; if (trigger !== 'tool') { const sizeNote = workflowSizeGuidelineNote( diff --git a/packages/agent-core/src/agent/records/types.ts b/packages/agent-core/src/agent/records/types.ts index b6fc85ba..2c914ace 100644 --- a/packages/agent-core/src/agent/records/types.ts +++ b/packages/agent-core/src/agent/records/types.ts @@ -54,6 +54,12 @@ export interface AgentRecordEvents { 'dynamic_workflow_mode.enter': { trigger: DynamicWorkflowModeTrigger; + /** + * Origin of the prompt whose turn entered the mode, so a cron- or + * hook-originated fan-out is attributable after the fact. Absent when the + * mode was entered outside a turn (e.g. the explicit RPC toggle). + */ + origin?: PromptOrigin; }; 'dynamic_workflow_mode.exit': {}; diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts index 47cda036..a839f563 100644 --- a/packages/agent-core/src/agent/turn/index.ts +++ b/packages/agent-core/src/agent/turn/index.ts @@ -62,6 +62,8 @@ interface ActiveTurn { readonly controller: AbortController; readonly promise: Promise; readonly firstRequest: ControlledPromise; + /** Origin of the prompt that launched this turn, for consumers that need to know who is driving (e.g. the dynamic_workflow_mode.enter record). */ + readonly origin: PromptOrigin; } interface BufferedSteer { @@ -229,6 +231,7 @@ export class TurnFlow { controller, promise, firstRequest, + origin, }; void firstRequest.catch(() => undefined); @@ -297,6 +300,13 @@ export class TurnFlow { return this.activeTurn !== null && this.activeTurn !== 'resuming'; } + /** Origin of the prompt that launched the active turn; undefined between turns and while resuming. */ + get activeTurnOrigin(): PromptOrigin | undefined { + return this.activeTurn === null || this.activeTurn === 'resuming' + ? undefined + : this.activeTurn.origin; + } + private ensureActiveTurn(): ActiveTurn { if (this.activeTurn === null || this.activeTurn === 'resuming') { throw new Error('No active turn'); diff --git a/packages/agent-core/test/agent/turn.test.ts b/packages/agent-core/test/agent/turn.test.ts index d9463083..aae31f93 100644 --- a/packages/agent-core/test/agent/turn.test.ts +++ b/packages/agent-core/test/agent/turn.test.ts @@ -533,7 +533,9 @@ describe('Agent turn flow', () => { .filter((origin) => origin?.kind === 'injection'); expect(runQueued).toHaveBeenCalledTimes(1); - expect(enterEvent?.args).toMatchObject({ trigger: 'tool' }); + // The enter record names who drove the fan-out: a cron- or hook-originated + // turn calling DynamicWorkflow is attributable after the fact. + expect(enterEvent?.args).toMatchObject({ trigger: 'tool', origin: { kind: 'user' } }); expect(ctx.agent.dynamicWorkflowMode.isActive).toBe(false); expect(eventIndex(ctx, '[wire]', 'dynamic_workflow_mode.exit')).toBeGreaterThan( eventIndex(ctx, '[rpc]', 'turn.ended'),