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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/workflow-enter-origin.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 8 additions & 1 deletion packages/agent-core/src/agent/dynamic-workflow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions packages/agent-core/src/agent/records/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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': {};

Expand Down
10 changes: 10 additions & 0 deletions packages/agent-core/src/agent/turn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ interface ActiveTurn {
readonly controller: AbortController;
readonly promise: Promise<TurnEndResult>;
readonly firstRequest: ControlledPromise<void>;
/** 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 {
Expand Down Expand Up @@ -229,6 +231,7 @@ export class TurnFlow {
controller,
promise,
firstRequest,
origin,
};

void firstRequest.catch(() => undefined);
Expand Down Expand Up @@ -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');
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-core/test/agent/turn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading