@lumenflow/cli 5.5.0 → 5.7.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +42 -40
  2. package/dist/db-journal-recover.js +400 -0
  3. package/dist/db-journal-recover.js.map +1 -0
  4. package/dist/docs-sync.js +8 -3
  5. package/dist/docs-sync.js.map +1 -1
  6. package/dist/gate-defaults.js +37 -0
  7. package/dist/gate-defaults.js.map +1 -1
  8. package/dist/gates/monolithic-file-contention-guard.js +167 -0
  9. package/dist/gates/monolithic-file-contention-guard.js.map +1 -0
  10. package/dist/gates/prod-migration-drift.js +207 -0
  11. package/dist/gates/prod-migration-drift.js.map +1 -0
  12. package/dist/gates/test-over-deletion-guard.js +255 -0
  13. package/dist/gates/test-over-deletion-guard.js.map +1 -0
  14. package/dist/gates-runners.js +44 -3
  15. package/dist/gates-runners.js.map +1 -1
  16. package/dist/gates.js +3 -2
  17. package/dist/gates.js.map +1 -1
  18. package/dist/lumenflow-setup.js +144 -0
  19. package/dist/lumenflow-setup.js.map +1 -0
  20. package/dist/lumenflow-upgrade.js +2 -1
  21. package/dist/lumenflow-upgrade.js.map +1 -1
  22. package/dist/mem-create.js +10 -1
  23. package/dist/mem-create.js.map +1 -1
  24. package/dist/mem-signal.js +21 -4
  25. package/dist/mem-signal.js.map +1 -1
  26. package/dist/orchestrate-initiative.js +28 -3
  27. package/dist/orchestrate-initiative.js.map +1 -1
  28. package/dist/public-manifest.js +17 -0
  29. package/dist/public-manifest.js.map +1 -1
  30. package/dist/release.js +53 -18
  31. package/dist/release.js.map +1 -1
  32. package/dist/wu-done-gates.js +13 -9
  33. package/dist/wu-done-gates.js.map +1 -1
  34. package/dist/wu-edit-operations.js +74 -0
  35. package/dist/wu-edit-operations.js.map +1 -1
  36. package/dist/wu-edit-validators.js +58 -0
  37. package/dist/wu-edit-validators.js.map +1 -1
  38. package/dist/wu-edit.js +106 -4
  39. package/dist/wu-edit.js.map +1 -1
  40. package/dist/wu-prep.js +41 -7
  41. package/dist/wu-prep.js.map +1 -1
  42. package/dist/wu-recover.js +6 -0
  43. package/dist/wu-recover.js.map +1 -1
  44. package/dist/wu-release.js +120 -2
  45. package/dist/wu-release.js.map +1 -1
  46. package/dist/wu-sizing-validation.js +47 -17
  47. package/dist/wu-sizing-validation.js.map +1 -1
  48. package/dist/wu-status.js +33 -0
  49. package/dist/wu-status.js.map +1 -1
  50. package/package.json +13 -11
  51. package/packs/agent-runtime/package.json +1 -1
  52. package/packs/sidekick/package.json +1 -1
  53. package/packs/software-delivery/package.json +1 -1
  54. package/templates/core/AGENTS.md.template +67 -3
  55. package/templates/core/LUMENFLOW.md.template +197 -47
  56. package/packs/agent-runtime/agent-heartbeat.ts +0 -163
  57. package/packs/agent-runtime/auto-session-integration.ts +0 -888
  58. package/packs/agent-runtime/capability-factory.ts +0 -104
  59. package/packs/agent-runtime/constants.ts +0 -21
  60. package/packs/agent-runtime/delegation-registry-schema.ts +0 -220
  61. package/packs/agent-runtime/delegation-registry-store.ts +0 -269
  62. package/packs/agent-runtime/delegation-tree.ts +0 -328
  63. package/packs/agent-runtime/index.ts +0 -20
  64. package/packs/agent-runtime/manifest.ts +0 -348
  65. package/packs/agent-runtime/memory-coordination-contract.ts +0 -86
  66. package/packs/agent-runtime/orchestration.ts +0 -2027
  67. package/packs/agent-runtime/pack-registration.ts +0 -110
  68. package/packs/agent-runtime/policy-factory.ts +0 -165
  69. package/packs/agent-runtime/remote-controls/index.ts +0 -7
  70. package/packs/agent-runtime/remote-controls/operations.ts +0 -405
  71. package/packs/agent-runtime/remote-controls/port.ts +0 -48
  72. package/packs/agent-runtime/remote-controls/state-store.ts +0 -258
  73. package/packs/agent-runtime/remote-controls/types.ts +0 -105
  74. package/packs/agent-runtime/session-schema.ts +0 -467
  75. package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +0 -793
  76. package/packs/agent-runtime/tool-impl/index.ts +0 -6
  77. package/packs/agent-runtime/tool-impl/provider-adapters.ts +0 -1245
  78. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +0 -256
  79. package/packs/agent-runtime/tool-impl/remote-controls.ts +0 -273
  80. package/packs/agent-runtime/tools/index.ts +0 -4
  81. package/packs/agent-runtime/tools/types.ts +0 -47
  82. package/packs/agent-runtime/turn-lifecycle-events.ts +0 -590
  83. package/packs/agent-runtime/types.ts +0 -128
  84. package/packs/agent-runtime/vitest.config.ts +0 -11
  85. package/packs/sidekick/channel-ingress.ts +0 -137
  86. package/packs/sidekick/constants.ts +0 -10
  87. package/packs/sidekick/index.ts +0 -8
  88. package/packs/sidekick/manifest-schema.ts +0 -49
  89. package/packs/sidekick/manifest.ts +0 -512
  90. package/packs/sidekick/pack-registration.ts +0 -110
  91. package/packs/sidekick/policy-factory.ts +0 -38
  92. package/packs/sidekick/sidekick-events.ts +0 -694
  93. package/packs/sidekick/src/adapters/cloud-queue.ts +0 -101
  94. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +0 -386
  95. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +0 -228
  96. package/packs/sidekick/src/domain/channel.types.ts +0 -64
  97. package/packs/sidekick/src/ports/channel-bridge.port.ts +0 -92
  98. package/packs/sidekick/src/routines/commit.ts +0 -74
  99. package/packs/sidekick/tool-impl/channel-tools.ts +0 -577
  100. package/packs/sidekick/tool-impl/channel-transports.ts +0 -75
  101. package/packs/sidekick/tool-impl/index.ts +0 -29
  102. package/packs/sidekick/tool-impl/memory-tools.ts +0 -290
  103. package/packs/sidekick/tool-impl/routine-commit.ts +0 -102
  104. package/packs/sidekick/tool-impl/routine-tools.ts +0 -440
  105. package/packs/sidekick/tool-impl/runtime-context.ts +0 -28
  106. package/packs/sidekick/tool-impl/shared.ts +0 -125
  107. package/packs/sidekick/tool-impl/storage.ts +0 -325
  108. package/packs/sidekick/tool-impl/system-tools.ts +0 -160
  109. package/packs/sidekick/tool-impl/task-tools.ts +0 -506
  110. package/packs/sidekick/tools/channel-tools.ts +0 -53
  111. package/packs/sidekick/tools/index.ts +0 -9
  112. package/packs/sidekick/tools/memory-tools.ts +0 -53
  113. package/packs/sidekick/tools/routine-tools.ts +0 -53
  114. package/packs/sidekick/tools/system-tools.ts +0 -47
  115. package/packs/sidekick/tools/task-tools.ts +0 -61
  116. package/packs/sidekick/tools/types.ts +0 -57
  117. package/packs/sidekick/vitest.config.ts +0 -11
  118. package/packs/software-delivery/constants.ts +0 -10
  119. package/packs/software-delivery/extensions.ts +0 -140
  120. package/packs/software-delivery/gate-policies.ts +0 -134
  121. package/packs/software-delivery/index.ts +0 -8
  122. package/packs/software-delivery/manifest-schema.ts +0 -268
  123. package/packs/software-delivery/manifest.ts +0 -657
  124. package/packs/software-delivery/pack-registration.ts +0 -113
  125. package/packs/software-delivery/src/commands/index.ts +0 -5
  126. package/packs/software-delivery/src/config/delivery-review-contract.ts +0 -256
  127. package/packs/software-delivery/src/config/env-accessors.ts +0 -66
  128. package/packs/software-delivery/src/config/index.ts +0 -8
  129. package/packs/software-delivery/src/config/normalize-config-keys.ts +0 -9
  130. package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +0 -460
  131. package/packs/software-delivery/src/config/workspace-reader.ts +0 -375
  132. package/packs/software-delivery/src/constants/backlog-patterns.ts +0 -31
  133. package/packs/software-delivery/src/constants/client-ids.ts +0 -19
  134. package/packs/software-delivery/src/constants/config-contract.ts +0 -7
  135. package/packs/software-delivery/src/constants/docs-layout-presets.ts +0 -50
  136. package/packs/software-delivery/src/constants/duration-constants.ts +0 -20
  137. package/packs/software-delivery/src/constants/gate-constants.ts +0 -32
  138. package/packs/software-delivery/src/constants/index.ts +0 -29
  139. package/packs/software-delivery/src/constants/lock-constants.ts +0 -35
  140. package/packs/software-delivery/src/constants/object-guards.ts +0 -12
  141. package/packs/software-delivery/src/constants/section-headings.ts +0 -107
  142. package/packs/software-delivery/src/constants/wu-cli-constants.ts +0 -500
  143. package/packs/software-delivery/src/constants/wu-domain-constants.ts +0 -466
  144. package/packs/software-delivery/src/constants/wu-git-constants.ts +0 -7
  145. package/packs/software-delivery/src/constants/wu-id-format.ts +0 -327
  146. package/packs/software-delivery/src/constants/wu-paths-constants.ts +0 -384
  147. package/packs/software-delivery/src/constants/wu-statuses.ts +0 -287
  148. package/packs/software-delivery/src/constants/wu-type-helpers.ts +0 -67
  149. package/packs/software-delivery/src/constants/wu-ui-constants.ts +0 -267
  150. package/packs/software-delivery/src/constants/wu-validation-constants.ts +0 -73
  151. package/packs/software-delivery/src/domain/index.ts +0 -5
  152. package/packs/software-delivery/src/domain/orchestration.constants.ts +0 -166
  153. package/packs/software-delivery/src/domain/orchestration.schemas.ts +0 -238
  154. package/packs/software-delivery/src/domain/orchestration.types.ts +0 -176
  155. package/packs/software-delivery/src/methodology/incremental-test.ts +0 -122
  156. package/packs/software-delivery/src/methodology/index.ts +0 -6
  157. package/packs/software-delivery/src/methodology/manual-test-validator.ts +0 -292
  158. package/packs/software-delivery/src/policy/coverage-gate.ts +0 -270
  159. package/packs/software-delivery/src/policy/gates-agent-mode.ts +0 -223
  160. package/packs/software-delivery/src/policy/gates-config-internal.ts +0 -121
  161. package/packs/software-delivery/src/policy/gates-config.ts +0 -300
  162. package/packs/software-delivery/src/policy/gates-coverage.ts +0 -356
  163. package/packs/software-delivery/src/policy/gates-presets.ts +0 -134
  164. package/packs/software-delivery/src/policy/gates-schemas.ts +0 -173
  165. package/packs/software-delivery/src/policy/index.ts +0 -22
  166. package/packs/software-delivery/src/policy/package-manager-resolver.ts +0 -319
  167. package/packs/software-delivery/src/policy/resolve-policy.ts +0 -601
  168. package/packs/software-delivery/src/ports/config.ports.ts +0 -90
  169. package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +0 -125
  170. package/packs/software-delivery/src/ports/index.ts +0 -10
  171. package/packs/software-delivery/src/ports/sync-validator.ports.ts +0 -59
  172. package/packs/software-delivery/src/ports/wu-helpers.ports.ts +0 -168
  173. package/packs/software-delivery/src/ports/wu-state.ports.ts +0 -241
  174. package/packs/software-delivery/src/primitives/index.ts +0 -5
  175. package/packs/software-delivery/src/runtime/index.ts +0 -6
  176. package/packs/software-delivery/src/runtime/work-classifier.ts +0 -561
  177. package/packs/software-delivery/src/sandbox/index.ts +0 -10
  178. package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +0 -118
  179. package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +0 -88
  180. package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +0 -154
  181. package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +0 -47
  182. package/packs/software-delivery/src/sandbox/sandbox-profile.ts +0 -153
  183. package/packs/software-delivery/src/schemas/index.ts +0 -5
  184. package/packs/software-delivery/src/state/date-utils.ts +0 -158
  185. package/packs/software-delivery/src/state/index.ts +0 -15
  186. package/packs/software-delivery/src/state/state-machine.ts +0 -119
  187. package/packs/software-delivery/src/state/wu-doc-types.ts +0 -51
  188. package/packs/software-delivery/src/state/wu-paths.ts +0 -381
  189. package/packs/software-delivery/src/state/wu-schema.ts +0 -1139
  190. package/packs/software-delivery/src/state/wu-state-schema.ts +0 -255
  191. package/packs/software-delivery/src/state/wu-yaml.ts +0 -338
  192. package/packs/software-delivery/tool-impl/agent-tools.ts +0 -263
  193. package/packs/software-delivery/tool-impl/delegation-tools.ts +0 -66
  194. package/packs/software-delivery/tool-impl/flow-metrics-tools.ts +0 -219
  195. package/packs/software-delivery/tool-impl/git-runner.ts +0 -113
  196. package/packs/software-delivery/tool-impl/git-tools.ts +0 -316
  197. package/packs/software-delivery/tool-impl/index.ts +0 -15
  198. package/packs/software-delivery/tool-impl/initiative-orchestration-tools.ts +0 -720
  199. package/packs/software-delivery/tool-impl/lane-lock.ts +0 -246
  200. package/packs/software-delivery/tool-impl/memory-tools.ts +0 -470
  201. package/packs/software-delivery/tool-impl/pending-runtime-tools.ts +0 -21
  202. package/packs/software-delivery/tool-impl/runtime-cli-adapter.ts +0 -329
  203. package/packs/software-delivery/tool-impl/runtime-native-tools.ts +0 -687
  204. package/packs/software-delivery/tool-impl/worker-loader.ts +0 -52
  205. package/packs/software-delivery/tool-impl/worktree-tools.ts +0 -46
  206. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +0 -807
  207. package/packs/software-delivery/tools/delegation-tools.ts +0 -23
  208. package/packs/software-delivery/tools/git-tools.ts +0 -55
  209. package/packs/software-delivery/tools/index.ts +0 -8
  210. package/packs/software-delivery/tools/lane-lock-tool.ts +0 -37
  211. package/packs/software-delivery/tools/types.ts +0 -71
  212. package/packs/software-delivery/tools/worktree-tools.ts +0 -49
  213. package/packs/software-delivery/vitest.config.ts +0 -11
@@ -1,2027 +0,0 @@
1
- // Copyright (c) 2026 Hellmai Ltd
2
- // SPDX-License-Identifier: LicenseRef-LumenFlow-Proprietary
3
-
4
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
5
- import path from 'node:path';
6
- import {
7
- TOOL_ERROR_CODES,
8
- type ExecutionContext,
9
- type KernelRuntime,
10
- type ToolHost,
11
- type ToolOutput,
12
- } from '@lumenflow/kernel';
13
- import {
14
- AGENT_RUNTIME_AGENT_INTENT_METADATA_KEY,
15
- AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY,
16
- AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY,
17
- AGENT_RUNTIME_AGENT_WORKFLOW_NODE_ID_METADATA_KEY,
18
- } from './constants.js';
19
- import {
20
- AGENT_RUNTIME_BUDGET_LEVELS,
21
- AGENT_RUNTIME_BUDGET_SCOPES,
22
- AGENT_RUNTIME_TURN_COMPLETED_STATUSES,
23
- buildApprovalRequiredEvent,
24
- buildBudgetThresholdEvent,
25
- buildScheduledWakeupSetEvent,
26
- buildToolCalledEvent,
27
- buildTurnAbortedEvent,
28
- buildTurnCompletedEvent,
29
- buildTurnStartedEvent,
30
- buildWorkflowNodeCompletedEvent,
31
- buildWorkflowNodeFailedEvent,
32
- buildWorkflowNodeStartedEvent,
33
- buildWorkflowPausedEvent,
34
- createZeroTurnCostBreakdown,
35
- emitAgentRuntimeEvent,
36
- type AgentRuntimeEventSink,
37
- } from './turn-lifecycle-events.js';
38
- import {
39
- AGENT_RUNTIME_TOOL_NAMES,
40
- AGENT_RUNTIME_TURN_STATUSES,
41
- type AgentRuntimeExecuteTurnInput,
42
- type AgentRuntimeExecuteTurnOutput,
43
- type AgentRuntimeIntentCatalogEntry,
44
- type AgentRuntimeMessage,
45
- type AgentRuntimeRequestedTool,
46
- type AgentRuntimeToolCatalogEntry,
47
- } from './types.js';
48
-
49
- const DEFAULT_MAX_ORCHESTRATION_TURNS = 12;
50
- const TOOL_CALL_ID_PREFIX = 'agent-runtime-tool-call';
51
- const APPROVAL_STATUS_TOOL_NAME = 'kernel:approval';
52
- const LOOP_LIMIT_EXCEEDED_CODE = 'AGENT_RUNTIME_LOOP_LIMIT_EXCEEDED';
53
- const DEFAULT_GOVERNED_TOOL_CATALOG_EXCLUSIONS = [AGENT_RUNTIME_TOOL_NAMES.EXECUTE_TURN] as const;
54
- const AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION = 1 as const;
55
- const AGENT_RUNTIME_WORKFLOW_DIRECTORY = path.join('.agent-runtime', 'workflow');
56
- const WORKFLOW_SUSPEND_REASON = 'Invocation turn budget reached.';
57
- const WORKFLOW_WAITING_REASON = 'Waiting for approval-driven continuation.';
58
- const WORKFLOW_COMPLETED_REASON = 'Session reached a terminal turn.';
59
- const WORKFLOW_SCHEDULED_REASON = 'Waiting for the scheduled wake time.';
60
- const WORKFLOW_JOIN_READY_REASON = 'Join dependencies completed.';
61
- const WORKFLOW_WAKEUP_REASON = 'Scheduled wake time reached.';
62
-
63
- type LoopRuntime = Pick<KernelRuntime, 'executeTool'>;
64
- type GovernedToolCatalogHost = Pick<ToolHost, 'listGovernedTools'>;
65
-
66
- export const AGENT_RUNTIME_WORKFLOW_STATUSES = {
67
- ACTIVE: 'active',
68
- SUSPENDED: 'suspended',
69
- WAITING_APPROVAL: 'waiting_approval',
70
- SCHEDULED: 'scheduled',
71
- COMPLETED: 'completed',
72
- ERROR: 'error',
73
- } as const;
74
-
75
- export const AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS = {
76
- CREATED: 'created',
77
- RESUMED: 'resumed',
78
- SUSPENDED: 'suspended',
79
- APPROVAL_REQUIRED: 'approval_required',
80
- SCHEDULED: 'scheduled',
81
- WAKEUP: 'wakeup',
82
- BRANCH_COMPLETED: 'branch_completed',
83
- JOIN_READY: 'join_ready',
84
- COMPLETED: 'completed',
85
- ERROR: 'error',
86
- } as const;
87
-
88
- export const AGENT_RUNTIME_WORKFLOW_NODE_STATUSES = {
89
- PENDING: 'pending',
90
- READY: 'ready',
91
- SCHEDULED: 'scheduled',
92
- WAITING_APPROVAL: 'waiting_approval',
93
- SUSPENDED: 'suspended',
94
- COMPLETED: 'completed',
95
- ERROR: 'error',
96
- } as const;
97
-
98
- export interface AgentRuntimeLoopHistoryEntry {
99
- turn_index: number;
100
- turn_output: AgentRuntimeExecuteTurnOutput;
101
- tool_call_id?: string;
102
- tool_output?: ToolOutput;
103
- }
104
-
105
- export interface AgentRuntimeHostContextInput {
106
- task_summary?: string;
107
- memory_summary?: string;
108
- additional_context?: readonly string[];
109
- }
110
-
111
- export interface BuildGovernedToolCatalogInput {
112
- toolHost: GovernedToolCatalogHost;
113
- context: ExecutionContext;
114
- excludeToolNames?: readonly string[];
115
- }
116
-
117
- export interface RunGovernedAgentLoopInput {
118
- runtime: LoopRuntime;
119
- executeTurnInput: AgentRuntimeExecuteTurnInput;
120
- createContext: (metadata: Record<string, unknown>) => ExecutionContext;
121
- maxTurns?: number;
122
- eventSink?: AgentRuntimeEventSink;
123
- }
124
-
125
- export type AgentRuntimeWorkflowStatus =
126
- (typeof AGENT_RUNTIME_WORKFLOW_STATUSES)[keyof typeof AGENT_RUNTIME_WORKFLOW_STATUSES];
127
-
128
- export type AgentRuntimeWorkflowContinuationKind =
129
- (typeof AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS)[keyof typeof AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS];
130
-
131
- export interface AgentRuntimeWorkflowContinuation {
132
- sequence: number;
133
- kind: AgentRuntimeWorkflowContinuationKind;
134
- timestamp: string;
135
- reason?: string;
136
- request_id?: string;
137
- node_id?: string;
138
- }
139
-
140
- export type AgentRuntimeWorkflowNodeStatus =
141
- (typeof AGENT_RUNTIME_WORKFLOW_NODE_STATUSES)[keyof typeof AGENT_RUNTIME_WORKFLOW_NODE_STATUSES];
142
-
143
- export interface AgentRuntimeWorkflowNodeDefinition {
144
- id: string;
145
- execute_turn_input: AgentRuntimeExecuteTurnInput;
146
- depends_on?: string[];
147
- wake_at?: string;
148
- }
149
-
150
- export interface AgentRuntimeWorkflowNodeState {
151
- node_id: string;
152
- status: AgentRuntimeWorkflowNodeStatus;
153
- execute_turn_input: AgentRuntimeExecuteTurnInput;
154
- depends_on: string[];
155
- wake_at?: string;
156
- messages: AgentRuntimeMessage[];
157
- history: AgentRuntimeLoopHistoryEntry[];
158
- turn_count: number;
159
- tool_call_count: number;
160
- pending_request_id?: string;
161
- requested_tool?: AgentRuntimeRequestedTool;
162
- last_turn?: AgentRuntimeExecuteTurnOutput;
163
- }
164
-
165
- export interface AgentRuntimeWorkflowGraphState {
166
- nodes: AgentRuntimeWorkflowNodeState[];
167
- }
168
-
169
- export interface AgentRuntimeWorkflowDefinition {
170
- session_id: string;
171
- nodes: AgentRuntimeWorkflowNodeDefinition[];
172
- }
173
-
174
- export interface AgentRuntimeWorkflowState {
175
- schema_version: typeof AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION;
176
- session_id: string;
177
- task_id?: string;
178
- run_id?: string;
179
- status: AgentRuntimeWorkflowStatus;
180
- created_at: string;
181
- updated_at: string;
182
- execute_turn_input: AgentRuntimeExecuteTurnInput;
183
- messages: AgentRuntimeMessage[];
184
- history: AgentRuntimeLoopHistoryEntry[];
185
- turn_count: number;
186
- tool_call_count: number;
187
- continuations: AgentRuntimeWorkflowContinuation[];
188
- pending_request_id?: string;
189
- requested_tool?: AgentRuntimeRequestedTool;
190
- last_turn?: AgentRuntimeExecuteTurnOutput;
191
- workflow?: AgentRuntimeWorkflowGraphState;
192
- next_wake_at?: string;
193
- }
194
-
195
- export interface AgentRuntimeWorkflowStateStore {
196
- load(sessionId: string): Promise<AgentRuntimeWorkflowState | null>;
197
- save(state: AgentRuntimeWorkflowState): Promise<void>;
198
- }
199
-
200
- export interface CreateAgentRuntimeWorkflowStateStoreInput {
201
- workspaceRoot: string;
202
- now?: () => string;
203
- }
204
-
205
- export interface StartGovernedAgentSessionInput extends RunGovernedAgentLoopInput {
206
- storageRoot: string;
207
- maxTurnsPerInvocation?: number;
208
- now?: () => string;
209
- }
210
-
211
- export interface ResumeGovernedAgentSessionInput {
212
- runtime: LoopRuntime;
213
- storageRoot: string;
214
- sessionId: string;
215
- createContext: (metadata: Record<string, unknown>) => ExecutionContext;
216
- maxTurnsPerInvocation?: number;
217
- continuationMessages?: readonly AgentRuntimeMessage[];
218
- now?: () => string;
219
- eventSink?: AgentRuntimeEventSink;
220
- }
221
-
222
- export interface StartGovernedAgentWorkflowInput {
223
- runtime: LoopRuntime;
224
- storageRoot: string;
225
- workflow: AgentRuntimeWorkflowDefinition;
226
- createContext: (metadata: Record<string, unknown>) => ExecutionContext;
227
- maxTurnsPerInvocation?: number;
228
- now?: () => string;
229
- eventSink?: AgentRuntimeEventSink;
230
- }
231
-
232
- export interface ResumeGovernedAgentWorkflowInput {
233
- runtime: LoopRuntime;
234
- storageRoot: string;
235
- sessionId: string;
236
- createContext: (metadata: Record<string, unknown>) => ExecutionContext;
237
- maxTurnsPerInvocation?: number;
238
- continuationMessagesByNodeId?: Record<string, readonly AgentRuntimeMessage[]>;
239
- now?: () => string;
240
- eventSink?: AgentRuntimeEventSink;
241
- }
242
-
243
- export interface AgentRuntimeLoopCompletedResult {
244
- kind: 'completed';
245
- final_turn: AgentRuntimeExecuteTurnOutput;
246
- messages: AgentRuntimeMessage[];
247
- turn_count: number;
248
- tool_call_count: number;
249
- history: AgentRuntimeLoopHistoryEntry[];
250
- }
251
-
252
- export interface AgentRuntimeLoopApprovalRequiredResult {
253
- kind: 'approval_required';
254
- pending_request_id: string;
255
- requested_tool: AgentRuntimeRequestedTool;
256
- last_turn: AgentRuntimeExecuteTurnOutput;
257
- messages: AgentRuntimeMessage[];
258
- turn_count: number;
259
- tool_call_count: number;
260
- history: AgentRuntimeLoopHistoryEntry[];
261
- }
262
-
263
- export interface AgentRuntimeLoopSuspendedResult {
264
- kind: 'suspended';
265
- messages: AgentRuntimeMessage[];
266
- turn_count: number;
267
- tool_call_count: number;
268
- history: AgentRuntimeLoopHistoryEntry[];
269
- }
270
-
271
- export interface AgentRuntimeWorkflowCompletedResult {
272
- kind: 'completed';
273
- completed_node_ids: string[];
274
- }
275
-
276
- export interface AgentRuntimeWorkflowScheduledResult {
277
- kind: 'scheduled';
278
- next_wake_at: string;
279
- scheduled_node_ids: string[];
280
- completed_node_ids: string[];
281
- }
282
-
283
- export interface AgentRuntimeWorkflowApprovalRequiredResult {
284
- kind: 'approval_required';
285
- node_id: string;
286
- pending_request_id: string;
287
- requested_tool: AgentRuntimeRequestedTool;
288
- }
289
-
290
- export interface AgentRuntimeWorkflowSuspendedResult {
291
- kind: 'suspended';
292
- node_id: string;
293
- }
294
-
295
- export interface AgentRuntimeWorkflowErrorResult {
296
- kind: 'error';
297
- node_id: string;
298
- error: {
299
- code: string;
300
- message: string;
301
- };
302
- }
303
-
304
- export type AgentRuntimeWorkflowAdvanceResult =
305
- | AgentRuntimeWorkflowCompletedResult
306
- | AgentRuntimeWorkflowScheduledResult
307
- | AgentRuntimeWorkflowApprovalRequiredResult
308
- | AgentRuntimeWorkflowSuspendedResult
309
- | AgentRuntimeWorkflowErrorResult;
310
-
311
- export interface AgentRuntimeLoopErrorResult {
312
- kind: 'error';
313
- stage: 'execute_turn' | 'loop_limit';
314
- error: {
315
- code: string;
316
- message: string;
317
- };
318
- messages: AgentRuntimeMessage[];
319
- turn_count: number;
320
- tool_call_count: number;
321
- history: AgentRuntimeLoopHistoryEntry[];
322
- }
323
-
324
- export type AgentRuntimeLoopResult =
325
- | AgentRuntimeLoopCompletedResult
326
- | AgentRuntimeLoopApprovalRequiredResult
327
- | AgentRuntimeLoopErrorResult;
328
-
329
- export type AgentRuntimePersistedSessionResult =
330
- | AgentRuntimeLoopResult
331
- | AgentRuntimeLoopSuspendedResult;
332
-
333
- interface GovernedLoopCursor {
334
- messages: AgentRuntimeMessage[];
335
- history: AgentRuntimeLoopHistoryEntry[];
336
- turnCount: number;
337
- toolCallCount: number;
338
- }
339
-
340
- interface RunGovernedAgentLoopInternalInput extends RunGovernedAgentLoopInput {
341
- turnBudgetBehavior: 'error' | 'suspend';
342
- initialCursor?: GovernedLoopCursor;
343
- }
344
-
345
- export async function buildGovernedToolCatalog(
346
- input: BuildGovernedToolCatalogInput,
347
- ): Promise<AgentRuntimeToolCatalogEntry[]> {
348
- const excludedToolNames = new Set(
349
- input.excludeToolNames ?? DEFAULT_GOVERNED_TOOL_CATALOG_EXCLUSIONS,
350
- );
351
- const governedTools = await input.toolHost.listGovernedTools(input.context);
352
-
353
- return governedTools
354
- .filter((entry) => !excludedToolNames.has(entry.capability.name))
355
- .map((entry) => ({
356
- name: entry.capability.name,
357
- description: entry.capability.description,
358
- }));
359
- }
360
-
361
- export async function runGovernedAgentLoop(
362
- input: RunGovernedAgentLoopInput,
363
- ): Promise<AgentRuntimeLoopResult> {
364
- const result = await runGovernedAgentLoopInternal({
365
- ...input,
366
- turnBudgetBehavior: 'error',
367
- });
368
-
369
- if (result.kind === 'suspended') {
370
- return {
371
- kind: 'error',
372
- stage: 'loop_limit',
373
- error: {
374
- code: LOOP_LIMIT_EXCEEDED_CODE,
375
- message: `Host loop reached maxTurns=${input.maxTurns ?? DEFAULT_MAX_ORCHESTRATION_TURNS} before the agent reached a terminal reply.`,
376
- },
377
- messages: result.messages,
378
- turn_count: result.turn_count,
379
- tool_call_count: result.tool_call_count,
380
- history: result.history,
381
- };
382
- }
383
-
384
- return result;
385
- }
386
-
387
- export function createAgentRuntimeWorkflowStateStore(
388
- input: CreateAgentRuntimeWorkflowStateStoreInput,
389
- ): AgentRuntimeWorkflowStateStore {
390
- const workflowRoot = path.join(input.workspaceRoot, AGENT_RUNTIME_WORKFLOW_DIRECTORY);
391
- return {
392
- async load(sessionId: string): Promise<AgentRuntimeWorkflowState | null> {
393
- const filePath = path.join(workflowRoot, `${sessionId}.json`);
394
- try {
395
- const raw = await readFile(filePath, 'utf8');
396
- return parseWorkflowState(JSON.parse(raw), filePath);
397
- } catch (error) {
398
- const nodeError = error as NodeJS.ErrnoException;
399
- if (nodeError.code === 'ENOENT') {
400
- return null;
401
- }
402
- throw error;
403
- }
404
- },
405
-
406
- async save(state: AgentRuntimeWorkflowState): Promise<void> {
407
- const filePath = path.join(workflowRoot, `${state.session_id}.json`);
408
- await mkdir(workflowRoot, { recursive: true });
409
- await writeFile(filePath, JSON.stringify(state), 'utf8');
410
- },
411
- };
412
- }
413
-
414
- export async function startGovernedAgentSession(
415
- input: StartGovernedAgentSessionInput,
416
- ): Promise<AgentRuntimePersistedSessionResult> {
417
- const store = createAgentRuntimeWorkflowStateStore({
418
- workspaceRoot: input.storageRoot,
419
- });
420
- const now = resolveCurrentTimestamp(input.now);
421
- const baseContext = input.createContext({});
422
- const initialState: AgentRuntimeWorkflowState = {
423
- schema_version: AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION,
424
- session_id: input.executeTurnInput.session_id,
425
- task_id: baseContext.task_id,
426
- run_id: baseContext.run_id,
427
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
428
- created_at: now,
429
- updated_at: now,
430
- execute_turn_input: cloneExecuteTurnInput(input.executeTurnInput),
431
- messages: [...input.executeTurnInput.messages],
432
- history: [],
433
- turn_count: 0,
434
- tool_call_count: 0,
435
- continuations: [
436
- {
437
- sequence: 0,
438
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.CREATED,
439
- timestamp: now,
440
- },
441
- ],
442
- };
443
-
444
- const result = await runGovernedAgentLoopInternal({
445
- ...input,
446
- maxTurns: input.maxTurnsPerInvocation,
447
- turnBudgetBehavior: 'suspend',
448
- });
449
-
450
- await store.save(materializeWorkflowState(initialState, result, now));
451
- return result;
452
- }
453
-
454
- export async function resumeGovernedAgentSession(
455
- input: ResumeGovernedAgentSessionInput,
456
- ): Promise<AgentRuntimePersistedSessionResult> {
457
- const store = createAgentRuntimeWorkflowStateStore({
458
- workspaceRoot: input.storageRoot,
459
- });
460
- const existing = await store.load(input.sessionId);
461
- if (!existing) {
462
- throw new Error(
463
- `No persisted agent workflow state found for session "${input.sessionId}". Create or restore the session before calling resume.`,
464
- );
465
- }
466
-
467
- if (existing.status === AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED) {
468
- throw new Error(
469
- `Agent workflow session "${input.sessionId}" is already completed and cannot be resumed.`,
470
- );
471
- }
472
-
473
- const resumedAt = resolveCurrentTimestamp(input.now);
474
- const resumedState: AgentRuntimeWorkflowState = {
475
- ...existing,
476
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
477
- updated_at: resumedAt,
478
- messages: [...existing.messages, ...(input.continuationMessages ?? [])],
479
- pending_request_id: undefined,
480
- continuations: appendWorkflowContinuation(existing.continuations, {
481
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.RESUMED,
482
- timestamp: resumedAt,
483
- }),
484
- };
485
-
486
- const result = await runGovernedAgentLoopInternal({
487
- runtime: input.runtime,
488
- executeTurnInput: {
489
- ...cloneExecuteTurnInput(existing.execute_turn_input),
490
- messages: [...resumedState.messages],
491
- },
492
- createContext: input.createContext,
493
- maxTurns: input.maxTurnsPerInvocation,
494
- turnBudgetBehavior: 'suspend',
495
- initialCursor: {
496
- messages: resumedState.messages,
497
- history: resumedState.history,
498
- turnCount: resumedState.turn_count,
499
- toolCallCount: resumedState.tool_call_count,
500
- },
501
- });
502
-
503
- await store.save(materializeWorkflowState(resumedState, result, resumedAt));
504
- return result;
505
- }
506
-
507
- export async function startGovernedAgentWorkflow(
508
- input: StartGovernedAgentWorkflowInput,
509
- ): Promise<AgentRuntimeWorkflowAdvanceResult> {
510
- const timestamp = resolveCurrentTimestamp(input.now);
511
- const baseContext = input.createContext({});
512
- const workflowState: AgentRuntimeWorkflowState = {
513
- schema_version: AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION,
514
- session_id: input.workflow.session_id,
515
- task_id: baseContext.task_id,
516
- run_id: baseContext.run_id,
517
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
518
- created_at: timestamp,
519
- updated_at: timestamp,
520
- execute_turn_input: cloneExecuteTurnInput(
521
- input.workflow.nodes[0]?.execute_turn_input ?? {
522
- session_id: input.workflow.session_id,
523
- model_profile: 'default',
524
- url: 'https://model-provider.invalid/',
525
- messages: [],
526
- },
527
- ),
528
- messages: [],
529
- history: [],
530
- turn_count: 0,
531
- tool_call_count: 0,
532
- continuations: [
533
- {
534
- sequence: 0,
535
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.CREATED,
536
- timestamp,
537
- },
538
- ],
539
- workflow: {
540
- nodes: input.workflow.nodes.map((node) => ({
541
- node_id: node.id,
542
- status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.PENDING,
543
- execute_turn_input: cloneExecuteTurnInput(node.execute_turn_input),
544
- depends_on: [...(node.depends_on ?? [])],
545
- ...(node.wake_at ? { wake_at: node.wake_at } : {}),
546
- messages: [...node.execute_turn_input.messages],
547
- history: [],
548
- turn_count: 0,
549
- tool_call_count: 0,
550
- })),
551
- },
552
- };
553
-
554
- const workflowNodes = workflowState.workflow?.nodes;
555
- if (!workflowNodes) {
556
- throw new Error(
557
- `Workflow session "${input.workflow.session_id}" could not be initialized because no workflow nodes were materialized.`,
558
- );
559
- }
560
- assertWorkflowDefinitions(workflowNodes);
561
-
562
- const result = await advanceGovernedAgentWorkflowState({
563
- runtime: input.runtime,
564
- state: workflowState,
565
- createContext: input.createContext,
566
- maxTurnsPerInvocation: input.maxTurnsPerInvocation,
567
- timestamp,
568
- eventSink: input.eventSink,
569
- });
570
-
571
- const store = createAgentRuntimeWorkflowStateStore({
572
- workspaceRoot: input.storageRoot,
573
- });
574
- await store.save(result.state);
575
- return result.result;
576
- }
577
-
578
- export async function resumeGovernedAgentWorkflow(
579
- input: ResumeGovernedAgentWorkflowInput,
580
- ): Promise<AgentRuntimeWorkflowAdvanceResult> {
581
- const store = createAgentRuntimeWorkflowStateStore({
582
- workspaceRoot: input.storageRoot,
583
- });
584
- const existing = await store.load(input.sessionId);
585
- if (!existing?.workflow) {
586
- throw new Error(
587
- `No persisted workflow graph found for session "${input.sessionId}". Start the workflow before attempting to resume it.`,
588
- );
589
- }
590
-
591
- const timestamp = resolveCurrentTimestamp(input.now);
592
- const resumedNodes = existing.workflow.nodes.map((node) => {
593
- const continuationMessages = input.continuationMessagesByNodeId?.[node.node_id] ?? [];
594
- if (continuationMessages.length === 0) {
595
- return node;
596
- }
597
-
598
- return {
599
- ...node,
600
- messages: [...node.messages, ...continuationMessages],
601
- pending_request_id: undefined,
602
- requested_tool: undefined,
603
- };
604
- });
605
-
606
- const resumedState: AgentRuntimeWorkflowState = {
607
- ...existing,
608
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
609
- updated_at: timestamp,
610
- workflow: {
611
- nodes: resumedNodes,
612
- },
613
- continuations: appendWorkflowContinuation(existing.continuations, {
614
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.RESUMED,
615
- timestamp,
616
- }),
617
- next_wake_at: undefined,
618
- };
619
-
620
- const result = await advanceGovernedAgentWorkflowState({
621
- runtime: input.runtime,
622
- state: resumedState,
623
- createContext: input.createContext,
624
- maxTurnsPerInvocation: input.maxTurnsPerInvocation,
625
- timestamp,
626
- eventSink: input.eventSink,
627
- });
628
-
629
- await store.save(result.state);
630
- return result.result;
631
- }
632
-
633
- async function advanceGovernedAgentWorkflowState(input: {
634
- runtime: LoopRuntime;
635
- state: AgentRuntimeWorkflowState;
636
- createContext: (metadata: Record<string, unknown>) => ExecutionContext;
637
- maxTurnsPerInvocation?: number;
638
- timestamp: string;
639
- eventSink?: AgentRuntimeEventSink;
640
- }): Promise<{ state: AgentRuntimeWorkflowState; result: AgentRuntimeWorkflowAdvanceResult }> {
641
- const workflow = input.state.workflow;
642
- if (!workflow) {
643
- throw new Error(
644
- `Workflow state for session "${input.state.session_id}" is missing the workflow graph payload.`,
645
- );
646
- }
647
-
648
- let state: AgentRuntimeWorkflowState = {
649
- ...input.state,
650
- workflow: {
651
- nodes: workflow.nodes.map((node) => ({
652
- ...node,
653
- messages: [...node.messages],
654
- history: [...node.history],
655
- })),
656
- },
657
- next_wake_at: undefined,
658
- };
659
-
660
- while (true) {
661
- const currentWorkflow = state.workflow;
662
- if (!currentWorkflow) {
663
- throw new Error(
664
- `Workflow state for session "${state.session_id}" is missing workflow nodes during advancement.`,
665
- );
666
- }
667
-
668
- const readyNodes = getReadyWorkflowNodes(state, input.timestamp);
669
- if (readyNodes.length === 0) {
670
- const scheduledNodes = getScheduledWorkflowNodes(state, input.timestamp);
671
- if (scheduledNodes.length > 0) {
672
- const nextWakeAt = scheduledNodes
673
- .map((node) => node.wake_at)
674
- .filter((value): value is string => typeof value === 'string')
675
- .sort()[0];
676
-
677
- const scheduledNodeIds = new Set(scheduledNodes.map((node) => node.node_id));
678
-
679
- const scheduledState: AgentRuntimeWorkflowState = {
680
- ...state,
681
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.SCHEDULED,
682
- updated_at: input.timestamp,
683
- next_wake_at: nextWakeAt,
684
- workflow: {
685
- nodes: currentWorkflow.nodes.map((node) =>
686
- scheduledNodeIds.has(node.node_id)
687
- ? { ...node, status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.SCHEDULED }
688
- : node,
689
- ),
690
- },
691
- continuations: scheduledNodes.reduce(
692
- (continuations, node) =>
693
- hasContinuationForNode(
694
- continuations,
695
- AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SCHEDULED,
696
- node.node_id,
697
- )
698
- ? continuations
699
- : appendWorkflowContinuation(continuations, {
700
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SCHEDULED,
701
- timestamp: input.timestamp,
702
- reason: WORKFLOW_SCHEDULED_REASON,
703
- node_id: node.node_id,
704
- }),
705
- state.continuations,
706
- ),
707
- };
708
-
709
- for (const node of scheduledNodes) {
710
- if (typeof node.wake_at === 'string') {
711
- emitAgentRuntimeEvent(
712
- input.eventSink,
713
- buildScheduledWakeupSetEvent({
714
- session_id: state.session_id,
715
- node_id: node.node_id,
716
- wake_at: node.wake_at,
717
- }),
718
- );
719
- }
720
- }
721
-
722
- return {
723
- state: scheduledState,
724
- result: {
725
- kind: 'scheduled',
726
- next_wake_at: nextWakeAt ?? input.timestamp,
727
- scheduled_node_ids: scheduledNodes.map((node) => node.node_id),
728
- completed_node_ids: getCompletedNodeIds(scheduledState),
729
- },
730
- };
731
- }
732
-
733
- if (allWorkflowNodesCompleted(state)) {
734
- const completedState: AgentRuntimeWorkflowState = {
735
- ...state,
736
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED,
737
- updated_at: input.timestamp,
738
- continuations: appendWorkflowContinuation(state.continuations, {
739
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.COMPLETED,
740
- timestamp: input.timestamp,
741
- reason: WORKFLOW_COMPLETED_REASON,
742
- }),
743
- };
744
-
745
- return {
746
- state: completedState,
747
- result: {
748
- kind: 'completed',
749
- completed_node_ids: getCompletedNodeIds(completedState),
750
- },
751
- };
752
- }
753
-
754
- emitAgentRuntimeEvent(
755
- input.eventSink,
756
- buildWorkflowNodeFailedEvent({
757
- session_id: state.session_id,
758
- node_id: 'workflow',
759
- error_code: 'AGENT_RUNTIME_WORKFLOW_STALLED',
760
- error_message: 'Workflow cannot make progress because no nodes are ready or scheduled.',
761
- }),
762
- );
763
-
764
- return {
765
- state: {
766
- ...state,
767
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.ERROR,
768
- updated_at: input.timestamp,
769
- continuations: appendWorkflowContinuation(state.continuations, {
770
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.ERROR,
771
- timestamp: input.timestamp,
772
- reason: 'Workflow has incomplete nodes but no ready or scheduled work.',
773
- }),
774
- },
775
- result: {
776
- kind: 'error',
777
- node_id: 'workflow',
778
- error: {
779
- code: 'AGENT_RUNTIME_WORKFLOW_STALLED',
780
- message: 'Workflow cannot make progress because no nodes are ready or scheduled.',
781
- },
782
- },
783
- };
784
- }
785
-
786
- const node = readyNodes[0];
787
- if (!node) {
788
- continue;
789
- }
790
-
791
- emitAgentRuntimeEvent(
792
- input.eventSink,
793
- buildWorkflowNodeStartedEvent({
794
- session_id: state.session_id,
795
- node_id: node.node_id,
796
- }),
797
- );
798
-
799
- const nodeResult = await runGovernedAgentLoopInternal({
800
- runtime: input.runtime,
801
- executeTurnInput: {
802
- ...cloneExecuteTurnInput(node.execute_turn_input),
803
- messages: [...node.messages],
804
- },
805
- createContext: (metadata) =>
806
- input.createContext({
807
- ...metadata,
808
- [AGENT_RUNTIME_AGENT_WORKFLOW_NODE_ID_METADATA_KEY]: node.node_id,
809
- }),
810
- maxTurns: input.maxTurnsPerInvocation,
811
- turnBudgetBehavior: 'suspend',
812
- eventSink: input.eventSink,
813
- initialCursor: {
814
- messages: node.messages,
815
- history: node.history,
816
- turnCount: node.turn_count,
817
- toolCallCount: node.tool_call_count,
818
- },
819
- });
820
-
821
- state = updateWorkflowStateForNodeResult(state, node.node_id, nodeResult, input.timestamp);
822
-
823
- if (nodeResult.kind === 'completed') {
824
- emitAgentRuntimeEvent(
825
- input.eventSink,
826
- buildWorkflowNodeCompletedEvent({
827
- session_id: state.session_id,
828
- node_id: node.node_id,
829
- }),
830
- );
831
- } else if (nodeResult.kind === 'approval_required') {
832
- emitAgentRuntimeEvent(
833
- input.eventSink,
834
- buildWorkflowPausedEvent({
835
- session_id: state.session_id,
836
- node_id: node.node_id,
837
- reason: WORKFLOW_WAITING_REASON,
838
- }),
839
- );
840
- } else if (nodeResult.kind === 'suspended') {
841
- emitAgentRuntimeEvent(
842
- input.eventSink,
843
- buildWorkflowPausedEvent({
844
- session_id: state.session_id,
845
- node_id: node.node_id,
846
- reason: WORKFLOW_SUSPEND_REASON,
847
- }),
848
- );
849
- } else {
850
- emitAgentRuntimeEvent(
851
- input.eventSink,
852
- buildWorkflowNodeFailedEvent({
853
- session_id: state.session_id,
854
- node_id: node.node_id,
855
- error_code: nodeResult.error.code,
856
- error_message: nodeResult.error.message,
857
- }),
858
- );
859
- }
860
-
861
- if (nodeResult.kind === 'approval_required') {
862
- return {
863
- state,
864
- result: {
865
- kind: 'approval_required',
866
- node_id: node.node_id,
867
- pending_request_id: nodeResult.pending_request_id,
868
- requested_tool: nodeResult.requested_tool,
869
- },
870
- };
871
- }
872
-
873
- if (nodeResult.kind === 'suspended') {
874
- return {
875
- state,
876
- result: {
877
- kind: 'suspended',
878
- node_id: node.node_id,
879
- },
880
- };
881
- }
882
-
883
- if (nodeResult.kind === 'error') {
884
- return {
885
- state,
886
- result: {
887
- kind: 'error',
888
- node_id: node.node_id,
889
- error: nodeResult.error,
890
- },
891
- };
892
- }
893
- }
894
- }
895
-
896
- async function runGovernedAgentLoopInternal(
897
- input: RunGovernedAgentLoopInternalInput,
898
- ): Promise<AgentRuntimePersistedSessionResult> {
899
- const maxTurns = input.maxTurns ?? DEFAULT_MAX_ORCHESTRATION_TURNS;
900
- const sessionId = input.executeTurnInput.session_id;
901
- const messages = input.initialCursor
902
- ? [...input.initialCursor.messages]
903
- : [...input.executeTurnInput.messages];
904
- const history = input.initialCursor ? [...input.initialCursor.history] : [];
905
- let turnCount = input.initialCursor?.turnCount ?? 0;
906
- let toolCallCount = input.initialCursor?.toolCallCount ?? 0;
907
- let invocationTurnCount = 0;
908
-
909
- while (invocationTurnCount < maxTurns) {
910
- emitAgentRuntimeEvent(
911
- input.eventSink,
912
- buildTurnStartedEvent({
913
- session_id: sessionId,
914
- turn_index: turnCount,
915
- model_profile: input.executeTurnInput.model_profile,
916
- }),
917
- );
918
-
919
- let executeTurnOutput: ToolOutput;
920
- try {
921
- executeTurnOutput = await input.runtime.executeTool(
922
- AGENT_RUNTIME_TOOL_NAMES.EXECUTE_TURN,
923
- {
924
- ...input.executeTurnInput,
925
- messages: [...messages],
926
- },
927
- input.createContext({
928
- [AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY]: turnCount,
929
- [AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY]: toolCallCount,
930
- }),
931
- );
932
- } catch (error) {
933
- const message = error instanceof Error ? error.message : String(error);
934
- emitAgentRuntimeEvent(
935
- input.eventSink,
936
- buildTurnAbortedEvent({
937
- session_id: sessionId,
938
- turn_index: turnCount,
939
- cleanup_status: 'clean',
940
- reason: message,
941
- }),
942
- );
943
- return {
944
- kind: 'error',
945
- stage: 'execute_turn',
946
- error: {
947
- code: TOOL_ERROR_CODES.TOOL_EXECUTION_FAILED,
948
- message,
949
- },
950
- messages,
951
- turn_count: turnCount,
952
- tool_call_count: toolCallCount,
953
- history,
954
- };
955
- }
956
-
957
- if (!executeTurnOutput.success) {
958
- const error = normalizeToolError(
959
- executeTurnOutput.error,
960
- 'agent:execute-turn failed in the host loop.',
961
- );
962
- emitAgentRuntimeEvent(
963
- input.eventSink,
964
- buildTurnAbortedEvent({
965
- session_id: sessionId,
966
- turn_index: turnCount,
967
- cleanup_status: 'clean',
968
- reason: error.message,
969
- }),
970
- );
971
- return {
972
- kind: 'error',
973
- stage: 'execute_turn',
974
- error,
975
- messages,
976
- turn_count: turnCount,
977
- tool_call_count: toolCallCount,
978
- history,
979
- };
980
- }
981
-
982
- const normalizedTurn = normalizeTurnOutput(executeTurnOutput.data);
983
- if (!normalizedTurn) {
984
- emitAgentRuntimeEvent(
985
- input.eventSink,
986
- buildTurnAbortedEvent({
987
- session_id: sessionId,
988
- turn_index: turnCount,
989
- cleanup_status: 'clean',
990
- reason:
991
- 'agent:execute-turn returned a payload that does not match the governed turn contract.',
992
- }),
993
- );
994
- return {
995
- kind: 'error',
996
- stage: 'execute_turn',
997
- error: {
998
- code: TOOL_ERROR_CODES.INVALID_OUTPUT,
999
- message:
1000
- 'agent:execute-turn returned a payload that does not match the governed turn contract.',
1001
- },
1002
- messages,
1003
- turn_count: turnCount,
1004
- tool_call_count: toolCallCount,
1005
- history,
1006
- };
1007
- }
1008
-
1009
- const currentTurnIndex = turnCount;
1010
- turnCount += 1;
1011
- invocationTurnCount += 1;
1012
- const historyEntry: AgentRuntimeLoopHistoryEntry = {
1013
- turn_index: currentTurnIndex,
1014
- turn_output: normalizedTurn,
1015
- };
1016
- history.push(historyEntry);
1017
-
1018
- emitAgentRuntimeEvent(
1019
- input.eventSink,
1020
- buildTurnCompletedEvent({
1021
- session_id: sessionId,
1022
- turn_index: currentTurnIndex,
1023
- // WU-2829: TurnCompletedEvent is only emitted after execute-turn
1024
- // returns a normalized, successful turn output — every reachable
1025
- // emission here represents a SUCCESS lifecycle outcome. The
1026
- // execute-turn-failure, normalization-failure, and tool-failure
1027
- // branches emit TurnAbortedEvent instead (see earlier branches).
1028
- status: AGENT_RUNTIME_TURN_COMPLETED_STATUSES.SUCCESS,
1029
- cost_breakdown: createZeroTurnCostBreakdown(),
1030
- }),
1031
- );
1032
-
1033
- if (
1034
- normalizedTurn.status !== AGENT_RUNTIME_TURN_STATUSES.TOOL_REQUEST ||
1035
- !normalizedTurn.requested_tool
1036
- ) {
1037
- return {
1038
- kind: 'completed',
1039
- final_turn: normalizedTurn,
1040
- messages,
1041
- turn_count: turnCount,
1042
- tool_call_count: toolCallCount,
1043
- history,
1044
- };
1045
- }
1046
-
1047
- const toolCallId = `${TOOL_CALL_ID_PREFIX}-${toolCallCount + 1}`;
1048
- let toolOutput: ToolOutput;
1049
- try {
1050
- toolOutput = await input.runtime.executeTool(
1051
- normalizedTurn.requested_tool.name,
1052
- normalizedTurn.requested_tool.input,
1053
- input.createContext({
1054
- [AGENT_RUNTIME_AGENT_INTENT_METADATA_KEY]: normalizedTurn.intent,
1055
- [AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY]: currentTurnIndex,
1056
- [AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY]: toolCallCount,
1057
- }),
1058
- );
1059
- } catch (error) {
1060
- const message = error instanceof Error ? error.message : String(error);
1061
- emitAgentRuntimeEvent(
1062
- input.eventSink,
1063
- buildTurnAbortedEvent({
1064
- session_id: sessionId,
1065
- turn_index: currentTurnIndex,
1066
- cleanup_status: 'partial',
1067
- reason: message,
1068
- }),
1069
- );
1070
- return {
1071
- kind: 'error',
1072
- stage: 'execute_turn',
1073
- error: {
1074
- code: TOOL_ERROR_CODES.TOOL_EXECUTION_FAILED,
1075
- message,
1076
- },
1077
- messages,
1078
- turn_count: turnCount,
1079
- tool_call_count: toolCallCount,
1080
- history,
1081
- };
1082
- }
1083
-
1084
- toolCallCount += 1;
1085
- historyEntry.tool_call_id = toolCallId;
1086
- historyEntry.tool_output = toolOutput;
1087
-
1088
- // WU-2829: approved=false when the tool-host blocked pending
1089
- // approval (a paired APPROVAL_REQUIRED event follows). Every other
1090
- // reachable branch means the call proceeded, so approved=true.
1091
- const toolCallApproved = !(
1092
- !toolOutput.success && toolOutput.error?.code === TOOL_ERROR_CODES.APPROVAL_REQUIRED
1093
- );
1094
- emitAgentRuntimeEvent(
1095
- input.eventSink,
1096
- buildToolCalledEvent({
1097
- session_id: sessionId,
1098
- turn_index: currentTurnIndex,
1099
- tool_name: normalizedTurn.requested_tool.name,
1100
- tool_call_id: toolCallId,
1101
- approved: toolCallApproved,
1102
- }),
1103
- );
1104
-
1105
- if (!toolOutput.success && toolOutput.error?.code === TOOL_ERROR_CODES.APPROVAL_REQUIRED) {
1106
- const requestId = extractApprovalRequestId(toolOutput);
1107
- emitAgentRuntimeEvent(
1108
- input.eventSink,
1109
- buildApprovalRequiredEvent({
1110
- session_id: sessionId,
1111
- turn_index: currentTurnIndex,
1112
- tool_name: normalizedTurn.requested_tool.name,
1113
- request_id: requestId,
1114
- }),
1115
- );
1116
- return {
1117
- kind: 'approval_required',
1118
- pending_request_id: requestId,
1119
- requested_tool: normalizedTurn.requested_tool,
1120
- last_turn: normalizedTurn,
1121
- messages,
1122
- turn_count: turnCount,
1123
- tool_call_count: toolCallCount,
1124
- history,
1125
- };
1126
- }
1127
-
1128
- messages.push(
1129
- createToolResultMessage({
1130
- toolName: normalizedTurn.requested_tool.name,
1131
- toolCallId,
1132
- output: toolOutput,
1133
- }),
1134
- );
1135
- }
1136
-
1137
- emitAgentRuntimeEvent(
1138
- input.eventSink,
1139
- buildBudgetThresholdEvent({
1140
- session_id: sessionId,
1141
- budget_name: 'max_turns_per_invocation',
1142
- threshold: maxTurns,
1143
- observed_value: invocationTurnCount,
1144
- // WU-2829: we reach this emission only after the invocation turn
1145
- // budget is saturated — level is EXCEEDED, remaining is 0, and
1146
- // the scope is the session that ran out of turn budget. This
1147
- // budget is in turns, not USD; remaining_usd mirrors "0 headroom
1148
- // in the counted unit" and cloud renders the context via
1149
- // budget_name.
1150
- level: AGENT_RUNTIME_BUDGET_LEVELS.EXCEEDED,
1151
- remaining_usd: 0,
1152
- scope: AGENT_RUNTIME_BUDGET_SCOPES.SESSION,
1153
- scope_id: sessionId,
1154
- }),
1155
- );
1156
-
1157
- if (input.turnBudgetBehavior === 'suspend') {
1158
- return {
1159
- kind: 'suspended',
1160
- messages,
1161
- turn_count: turnCount,
1162
- tool_call_count: toolCallCount,
1163
- history,
1164
- };
1165
- }
1166
-
1167
- return {
1168
- kind: 'error',
1169
- stage: 'loop_limit',
1170
- error: {
1171
- code: LOOP_LIMIT_EXCEEDED_CODE,
1172
- message: `Host loop reached maxTurns=${maxTurns} before the agent reached a terminal reply.`,
1173
- },
1174
- messages,
1175
- turn_count: turnCount,
1176
- tool_call_count: toolCallCount,
1177
- history,
1178
- };
1179
- }
1180
-
1181
- export function createHostContextMessages(
1182
- input: AgentRuntimeHostContextInput,
1183
- ): AgentRuntimeMessage[] {
1184
- const messages: AgentRuntimeMessage[] = [];
1185
- const taskSummary = normalizeOptionalText(input.task_summary);
1186
- if (taskSummary) {
1187
- messages.push({
1188
- role: 'system',
1189
- content: `Task context:\n${taskSummary}`,
1190
- });
1191
- }
1192
-
1193
- const memorySummary = normalizeOptionalText(input.memory_summary);
1194
- if (memorySummary) {
1195
- messages.push({
1196
- role: 'system',
1197
- content: `Memory context:\n${memorySummary}`,
1198
- });
1199
- }
1200
-
1201
- for (const note of input.additional_context ?? []) {
1202
- const normalizedNote = normalizeOptionalText(note);
1203
- if (!normalizedNote) {
1204
- continue;
1205
- }
1206
- messages.push({
1207
- role: 'system',
1208
- content: `Additional context:\n${normalizedNote}`,
1209
- });
1210
- }
1211
-
1212
- return messages;
1213
- }
1214
-
1215
- export function createToolResultMessage(input: {
1216
- toolName: string;
1217
- toolCallId: string;
1218
- output: ToolOutput;
1219
- }): AgentRuntimeMessage {
1220
- return {
1221
- role: 'tool',
1222
- tool_name: input.toolName,
1223
- tool_call_id: input.toolCallId,
1224
- content: JSON.stringify({
1225
- success: input.output.success,
1226
- ...(input.output.success ? { data: input.output.data ?? null } : {}),
1227
- ...(!input.output.success ? { error: input.output.error ?? null } : {}),
1228
- }),
1229
- };
1230
- }
1231
-
1232
- export function createApprovalResolutionMessage(input: {
1233
- requestId: string;
1234
- approved: boolean;
1235
- approvedBy: string;
1236
- toolName?: string;
1237
- reason?: string;
1238
- }): AgentRuntimeMessage {
1239
- return {
1240
- role: 'tool',
1241
- tool_name: input.toolName ?? APPROVAL_STATUS_TOOL_NAME,
1242
- tool_call_id: input.requestId,
1243
- content: JSON.stringify({
1244
- approval: {
1245
- request_id: input.requestId,
1246
- approved: input.approved,
1247
- approved_by: input.approvedBy,
1248
- ...(input.reason ? { reason: input.reason } : {}),
1249
- },
1250
- }),
1251
- };
1252
- }
1253
-
1254
- function normalizeToolError(
1255
- error: ToolOutput['error'],
1256
- fallbackMessage: string,
1257
- ): { code: string; message: string } {
1258
- return {
1259
- code: error?.code ?? TOOL_ERROR_CODES.TOOL_EXECUTION_FAILED,
1260
- message: error?.message ?? fallbackMessage,
1261
- };
1262
- }
1263
-
1264
- function normalizeTurnOutput(value: unknown): AgentRuntimeExecuteTurnOutput | null {
1265
- if (!isRecord(value)) {
1266
- return null;
1267
- }
1268
-
1269
- if (
1270
- typeof value.status !== 'string' ||
1271
- typeof value.intent !== 'string' ||
1272
- typeof value.assistant_message !== 'string' ||
1273
- typeof value.finish_reason !== 'string'
1274
- ) {
1275
- return null;
1276
- }
1277
-
1278
- const provider = isRecord(value.provider) ? value.provider : null;
1279
- if (!provider || typeof provider.kind !== 'string' || typeof provider.model !== 'string') {
1280
- return null;
1281
- }
1282
-
1283
- const requestedTool = isRecord(value.requested_tool) ? value.requested_tool : undefined;
1284
-
1285
- return {
1286
- status: value.status,
1287
- intent: value.intent,
1288
- assistant_message: value.assistant_message,
1289
- ...(requestedTool && typeof requestedTool.name === 'string' && isRecord(requestedTool.input)
1290
- ? {
1291
- requested_tool: {
1292
- name: requestedTool.name,
1293
- input: requestedTool.input,
1294
- },
1295
- }
1296
- : {}),
1297
- provider: {
1298
- kind: provider.kind,
1299
- model: provider.model,
1300
- },
1301
- ...(isRecord(value.usage) ? { usage: value.usage } : {}),
1302
- finish_reason: value.finish_reason,
1303
- } as AgentRuntimeExecuteTurnOutput;
1304
- }
1305
-
1306
- function extractApprovalRequestId(output: ToolOutput): string {
1307
- const details = isRecord(output.error?.details) ? output.error?.details : null;
1308
- const requestId = details?.request_id;
1309
- return typeof requestId === 'string' && requestId.trim().length > 0
1310
- ? requestId
1311
- : 'approval-request-missing';
1312
- }
1313
-
1314
- function normalizeOptionalText(value: string | undefined): string | null {
1315
- if (typeof value !== 'string') {
1316
- return null;
1317
- }
1318
-
1319
- const trimmed = value.trim();
1320
- return trimmed.length > 0 ? trimmed : null;
1321
- }
1322
-
1323
- function isRecord(value: unknown): value is Record<string, unknown> {
1324
- return typeof value === 'object' && value !== null && !Array.isArray(value);
1325
- }
1326
-
1327
- function materializeWorkflowState(
1328
- baseState: AgentRuntimeWorkflowState,
1329
- result: AgentRuntimePersistedSessionResult,
1330
- timestamp: string,
1331
- ): AgentRuntimeWorkflowState {
1332
- if (result.kind === 'completed') {
1333
- return {
1334
- ...baseState,
1335
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED,
1336
- updated_at: timestamp,
1337
- messages: result.messages,
1338
- history: result.history,
1339
- turn_count: result.turn_count,
1340
- tool_call_count: result.tool_call_count,
1341
- pending_request_id: undefined,
1342
- requested_tool: undefined,
1343
- last_turn: result.final_turn,
1344
- continuations: appendWorkflowContinuation(baseState.continuations, {
1345
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.COMPLETED,
1346
- timestamp,
1347
- reason: WORKFLOW_COMPLETED_REASON,
1348
- }),
1349
- };
1350
- }
1351
-
1352
- if (result.kind === 'approval_required') {
1353
- return {
1354
- ...baseState,
1355
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.WAITING_APPROVAL,
1356
- updated_at: timestamp,
1357
- messages: result.messages,
1358
- history: result.history,
1359
- turn_count: result.turn_count,
1360
- tool_call_count: result.tool_call_count,
1361
- pending_request_id: result.pending_request_id,
1362
- requested_tool: result.requested_tool,
1363
- last_turn: result.last_turn,
1364
- continuations: appendWorkflowContinuation(baseState.continuations, {
1365
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.APPROVAL_REQUIRED,
1366
- timestamp,
1367
- reason: WORKFLOW_WAITING_REASON,
1368
- request_id: result.pending_request_id,
1369
- }),
1370
- };
1371
- }
1372
-
1373
- if (result.kind === 'suspended') {
1374
- return {
1375
- ...baseState,
1376
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.SUSPENDED,
1377
- updated_at: timestamp,
1378
- messages: result.messages,
1379
- history: result.history,
1380
- turn_count: result.turn_count,
1381
- tool_call_count: result.tool_call_count,
1382
- pending_request_id: undefined,
1383
- requested_tool: undefined,
1384
- continuations: appendWorkflowContinuation(baseState.continuations, {
1385
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SUSPENDED,
1386
- timestamp,
1387
- reason: WORKFLOW_SUSPEND_REASON,
1388
- }),
1389
- };
1390
- }
1391
-
1392
- return {
1393
- ...baseState,
1394
- status: AGENT_RUNTIME_WORKFLOW_STATUSES.ERROR,
1395
- updated_at: timestamp,
1396
- messages: result.messages,
1397
- history: result.history,
1398
- turn_count: result.turn_count,
1399
- tool_call_count: result.tool_call_count,
1400
- pending_request_id: undefined,
1401
- requested_tool: undefined,
1402
- continuations: appendWorkflowContinuation(baseState.continuations, {
1403
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.ERROR,
1404
- timestamp,
1405
- reason: result.error.message,
1406
- }),
1407
- };
1408
- }
1409
-
1410
- function appendWorkflowContinuation(
1411
- continuations: readonly AgentRuntimeWorkflowContinuation[],
1412
- input: Omit<AgentRuntimeWorkflowContinuation, 'sequence'>,
1413
- ): AgentRuntimeWorkflowContinuation[] {
1414
- return [
1415
- ...continuations,
1416
- {
1417
- sequence: continuations.length,
1418
- ...input,
1419
- },
1420
- ];
1421
- }
1422
-
1423
- function cloneExecuteTurnInput(input: AgentRuntimeExecuteTurnInput): AgentRuntimeExecuteTurnInput {
1424
- return {
1425
- ...input,
1426
- messages: [...input.messages],
1427
- ...(input.tool_catalog ? { tool_catalog: [...input.tool_catalog] } : {}),
1428
- ...(input.intent_catalog ? { intent_catalog: [...input.intent_catalog] } : {}),
1429
- ...(input.limits ? { limits: { ...input.limits } } : {}),
1430
- };
1431
- }
1432
-
1433
- function parseWorkflowState(value: unknown, filePath: string): AgentRuntimeWorkflowState {
1434
- if (!isRecord(value)) {
1435
- throw new Error(`Failed to parse workflow state at ${filePath}: expected an object payload.`);
1436
- }
1437
-
1438
- if (
1439
- value.schema_version !== AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION ||
1440
- typeof value.session_id !== 'string' ||
1441
- typeof value.status !== 'string'
1442
- ) {
1443
- throw new Error(
1444
- `Failed to parse workflow state at ${filePath}: missing schema_version, session_id, or status.`,
1445
- );
1446
- }
1447
-
1448
- const requestedTool = parseWorkflowRequestedTool(value.requested_tool);
1449
- const lastTurn = normalizeTurnOutput(value.last_turn);
1450
-
1451
- return {
1452
- schema_version: AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION,
1453
- session_id: value.session_id,
1454
- ...(typeof value.task_id === 'string' ? { task_id: value.task_id } : {}),
1455
- ...(typeof value.run_id === 'string' ? { run_id: value.run_id } : {}),
1456
- status: value.status as AgentRuntimeWorkflowStatus,
1457
- created_at: readWorkflowTimestamp(value.created_at, `${filePath}.created_at`),
1458
- updated_at: readWorkflowTimestamp(value.updated_at, `${filePath}.updated_at`),
1459
- execute_turn_input: parseWorkflowExecuteTurnInput(
1460
- value.execute_turn_input,
1461
- `${filePath}.execute_turn_input`,
1462
- ),
1463
- messages: parseWorkflowMessages(value.messages, `${filePath}.messages`),
1464
- history: parseWorkflowHistory(value.history, `${filePath}.history`),
1465
- turn_count: readWorkflowCount(value.turn_count, `${filePath}.turn_count`),
1466
- tool_call_count: readWorkflowCount(value.tool_call_count, `${filePath}.tool_call_count`),
1467
- continuations: parseWorkflowContinuations(value.continuations, `${filePath}.continuations`),
1468
- ...(typeof value.pending_request_id === 'string'
1469
- ? { pending_request_id: value.pending_request_id }
1470
- : {}),
1471
- ...(requestedTool ? { requested_tool: requestedTool } : {}),
1472
- ...(lastTurn ? { last_turn: lastTurn } : {}),
1473
- ...(isRecord(value.workflow)
1474
- ? { workflow: parseWorkflowGraphState(value.workflow, `${filePath}.workflow`) }
1475
- : {}),
1476
- ...(typeof value.next_wake_at === 'string' ? { next_wake_at: value.next_wake_at } : {}),
1477
- };
1478
- }
1479
-
1480
- function parseWorkflowExecuteTurnInput(
1481
- value: unknown,
1482
- filePath: string,
1483
- ): AgentRuntimeExecuteTurnInput {
1484
- if (!isRecord(value)) {
1485
- throw new Error(
1486
- `Failed to parse workflow state at ${filePath}: execute_turn_input is invalid.`,
1487
- );
1488
- }
1489
-
1490
- const sessionId = readRequiredString(value.session_id, `${filePath}.session_id`);
1491
- const modelProfile = readRequiredString(value.model_profile, `${filePath}.model_profile`);
1492
- const url = readRequiredString(value.url, `${filePath}.url`);
1493
- const messages = parseWorkflowMessages(value.messages, `${filePath}.messages`);
1494
- const toolCatalog = Array.isArray(value.tool_catalog)
1495
- ? value.tool_catalog.map((entry, index) =>
1496
- parseWorkflowToolCatalogEntry(entry, `${filePath}.tool_catalog[${index}]`),
1497
- )
1498
- : undefined;
1499
- const intentCatalog = Array.isArray(value.intent_catalog)
1500
- ? value.intent_catalog.map((entry, index) =>
1501
- parseWorkflowIntentCatalogEntry(entry, `${filePath}.intent_catalog[${index}]`),
1502
- )
1503
- : undefined;
1504
-
1505
- return {
1506
- session_id: sessionId,
1507
- model_profile: modelProfile,
1508
- url,
1509
- ...(typeof value.stream === 'boolean' ? { stream: value.stream } : {}),
1510
- messages,
1511
- ...(toolCatalog ? { tool_catalog: toolCatalog } : {}),
1512
- ...(intentCatalog ? { intent_catalog: intentCatalog } : {}),
1513
- ...(isRecord(value.limits) ? { limits: { ...value.limits } } : {}),
1514
- };
1515
- }
1516
-
1517
- function parseWorkflowMessages(value: unknown, filePath: string): AgentRuntimeMessage[] {
1518
- if (!Array.isArray(value)) {
1519
- throw new Error(
1520
- `Failed to parse workflow state at ${filePath}: expected an array of messages.`,
1521
- );
1522
- }
1523
-
1524
- return value.map((entry, index) => parseWorkflowMessage(entry, `${filePath}[${index}]`));
1525
- }
1526
-
1527
- function parseWorkflowMessage(value: unknown, filePath: string): AgentRuntimeMessage {
1528
- if (!isRecord(value)) {
1529
- throw new Error(`Failed to parse workflow state at ${filePath}: message entry is invalid.`);
1530
- }
1531
-
1532
- const role = readRequiredString(value.role, `${filePath}.role`);
1533
- const content = readRequiredString(value.content, `${filePath}.content`);
1534
- if (!isAgentRuntimeMessageRole(role)) {
1535
- throw new Error(
1536
- `Failed to parse workflow state at ${filePath}: message role must be system, user, assistant, or tool.`,
1537
- );
1538
- }
1539
-
1540
- return {
1541
- role,
1542
- content,
1543
- ...(typeof value.tool_name === 'string' ? { tool_name: value.tool_name } : {}),
1544
- ...(typeof value.tool_call_id === 'string' ? { tool_call_id: value.tool_call_id } : {}),
1545
- };
1546
- }
1547
-
1548
- function isAgentRuntimeMessageRole(value: string): value is AgentRuntimeMessage['role'] {
1549
- return value === 'system' || value === 'user' || value === 'assistant' || value === 'tool';
1550
- }
1551
-
1552
- function parseWorkflowHistory(value: unknown, filePath: string): AgentRuntimeLoopHistoryEntry[] {
1553
- if (!Array.isArray(value)) {
1554
- throw new Error(
1555
- `Failed to parse workflow state at ${filePath}: expected an array of history entries.`,
1556
- );
1557
- }
1558
-
1559
- return value.map((entry, index) => parseWorkflowHistoryEntry(entry, `${filePath}[${index}]`));
1560
- }
1561
-
1562
- function parseWorkflowHistoryEntry(value: unknown, filePath: string): AgentRuntimeLoopHistoryEntry {
1563
- if (!isRecord(value)) {
1564
- throw new Error(`Failed to parse workflow state at ${filePath}: history entry is invalid.`);
1565
- }
1566
-
1567
- const turnIndex = readWorkflowCount(value.turn_index, `${filePath}.turn_index`);
1568
- const turnOutput = normalizeTurnOutput(value.turn_output);
1569
- if (!turnOutput) {
1570
- throw new Error(`Failed to parse workflow state at ${filePath}: turn_output is invalid.`);
1571
- }
1572
-
1573
- const toolOutput = parseWorkflowToolOutput(value.tool_output, `${filePath}.tool_output`);
1574
-
1575
- return {
1576
- turn_index: turnIndex,
1577
- turn_output: turnOutput,
1578
- ...(typeof value.tool_call_id === 'string' ? { tool_call_id: value.tool_call_id } : {}),
1579
- ...(toolOutput ? { tool_output: toolOutput } : {}),
1580
- };
1581
- }
1582
-
1583
- function parseWorkflowToolOutput(value: unknown, filePath: string): ToolOutput | undefined {
1584
- if (value === undefined) {
1585
- return undefined;
1586
- }
1587
- if (!isRecord(value) || typeof value.success !== 'boolean') {
1588
- throw new Error(`Failed to parse workflow state at ${filePath}: tool_output is invalid.`);
1589
- }
1590
-
1591
- return {
1592
- success: value.success,
1593
- ...(value.success ? { data: value.data } : {}),
1594
- ...(!value.success && isRecord(value.error)
1595
- ? {
1596
- error: {
1597
- code: readRequiredString(value.error.code, `${filePath}.error.code`),
1598
- message: readRequiredString(value.error.message, `${filePath}.error.message`),
1599
- ...(isRecord(value.error.details) ? { details: value.error.details } : {}),
1600
- },
1601
- }
1602
- : {}),
1603
- };
1604
- }
1605
-
1606
- function parseWorkflowContinuations(
1607
- value: unknown,
1608
- filePath: string,
1609
- ): AgentRuntimeWorkflowContinuation[] {
1610
- if (!Array.isArray(value)) {
1611
- throw new Error(
1612
- `Failed to parse workflow state at ${filePath}: expected an array of continuations.`,
1613
- );
1614
- }
1615
-
1616
- return value.map((entry, index) => {
1617
- if (!isRecord(entry)) {
1618
- throw new Error(
1619
- `Failed to parse workflow state at ${filePath}[${index}]: continuation entry is invalid.`,
1620
- );
1621
- }
1622
-
1623
- return {
1624
- sequence: readWorkflowCount(entry.sequence, `${filePath}[${index}].sequence`),
1625
- kind: readRequiredString(
1626
- entry.kind,
1627
- `${filePath}[${index}].kind`,
1628
- ) as AgentRuntimeWorkflowContinuationKind,
1629
- timestamp: readWorkflowTimestamp(entry.timestamp, `${filePath}[${index}].timestamp`),
1630
- ...(typeof entry.reason === 'string' ? { reason: entry.reason } : {}),
1631
- ...(typeof entry.request_id === 'string' ? { request_id: entry.request_id } : {}),
1632
- ...(typeof entry.node_id === 'string' ? { node_id: entry.node_id } : {}),
1633
- };
1634
- });
1635
- }
1636
-
1637
- function parseWorkflowGraphState(
1638
- value: Record<string, unknown>,
1639
- filePath: string,
1640
- ): AgentRuntimeWorkflowGraphState {
1641
- if (!Array.isArray(value.nodes)) {
1642
- throw new Error(
1643
- `Failed to parse workflow state at ${filePath}.nodes: expected an array of workflow nodes.`,
1644
- );
1645
- }
1646
-
1647
- return {
1648
- nodes: value.nodes.map((entry, index) =>
1649
- parseWorkflowNodeState(entry, `${filePath}.nodes[${index}]`),
1650
- ),
1651
- };
1652
- }
1653
-
1654
- function parseWorkflowNodeState(value: unknown, filePath: string): AgentRuntimeWorkflowNodeState {
1655
- if (!isRecord(value)) {
1656
- throw new Error(`Failed to parse workflow state at ${filePath}: workflow node is invalid.`);
1657
- }
1658
-
1659
- const requestedTool = parseWorkflowRequestedTool(value.requested_tool);
1660
- const lastTurn = normalizeTurnOutput(value.last_turn);
1661
-
1662
- return {
1663
- node_id: readRequiredString(value.node_id, `${filePath}.node_id`),
1664
- status: readRequiredString(
1665
- value.status,
1666
- `${filePath}.status`,
1667
- ) as AgentRuntimeWorkflowNodeStatus,
1668
- execute_turn_input: parseWorkflowExecuteTurnInput(
1669
- value.execute_turn_input,
1670
- `${filePath}.execute_turn_input`,
1671
- ),
1672
- depends_on: Array.isArray(value.depends_on)
1673
- ? value.depends_on.map((entry, index) =>
1674
- readRequiredString(entry, `${filePath}.depends_on[${index}]`),
1675
- )
1676
- : [],
1677
- ...(typeof value.wake_at === 'string' ? { wake_at: value.wake_at } : {}),
1678
- messages: parseWorkflowMessages(value.messages, `${filePath}.messages`),
1679
- history: parseWorkflowHistory(value.history, `${filePath}.history`),
1680
- turn_count: readWorkflowCount(value.turn_count, `${filePath}.turn_count`),
1681
- tool_call_count: readWorkflowCount(value.tool_call_count, `${filePath}.tool_call_count`),
1682
- ...(typeof value.pending_request_id === 'string'
1683
- ? { pending_request_id: value.pending_request_id }
1684
- : {}),
1685
- ...(requestedTool ? { requested_tool: requestedTool } : {}),
1686
- ...(lastTurn ? { last_turn: lastTurn } : {}),
1687
- };
1688
- }
1689
-
1690
- function parseWorkflowRequestedTool(value: unknown): AgentRuntimeRequestedTool | undefined {
1691
- if (!isRecord(value) || typeof value.name !== 'string' || !isRecord(value.input)) {
1692
- return undefined;
1693
- }
1694
-
1695
- return {
1696
- name: value.name,
1697
- input: value.input,
1698
- };
1699
- }
1700
-
1701
- function parseWorkflowToolCatalogEntry(
1702
- value: unknown,
1703
- filePath: string,
1704
- ): AgentRuntimeToolCatalogEntry {
1705
- if (!isRecord(value)) {
1706
- throw new Error(
1707
- `Failed to parse workflow state at ${filePath}: tool catalog entry is invalid.`,
1708
- );
1709
- }
1710
-
1711
- return {
1712
- name: readRequiredString(value.name, `${filePath}.name`),
1713
- description: readRequiredString(value.description, `${filePath}.description`),
1714
- ...(isRecord(value.input_schema) ? { input_schema: value.input_schema } : {}),
1715
- };
1716
- }
1717
-
1718
- function parseWorkflowIntentCatalogEntry(
1719
- value: unknown,
1720
- filePath: string,
1721
- ): AgentRuntimeIntentCatalogEntry {
1722
- if (!isRecord(value)) {
1723
- throw new Error(
1724
- `Failed to parse workflow state at ${filePath}: intent catalog entry is invalid.`,
1725
- );
1726
- }
1727
-
1728
- return {
1729
- id: readRequiredString(value.id, `${filePath}.id`),
1730
- description: readRequiredString(value.description, `${filePath}.description`),
1731
- };
1732
- }
1733
-
1734
- function readRequiredString(value: unknown, filePath: string): string {
1735
- if (typeof value !== 'string' || value.trim().length === 0) {
1736
- throw new Error(`Failed to parse workflow state at ${filePath}: expected a non-empty string.`);
1737
- }
1738
-
1739
- return value;
1740
- }
1741
-
1742
- function readWorkflowTimestamp(value: unknown, filePath: string): string {
1743
- return readRequiredString(value, filePath);
1744
- }
1745
-
1746
- function readWorkflowCount(value: unknown, filePath: string): number {
1747
- if (!Number.isInteger(value) || Number(value) < 0) {
1748
- throw new Error(
1749
- `Failed to parse workflow state at ${filePath}: expected a non-negative integer.`,
1750
- );
1751
- }
1752
-
1753
- return Number(value);
1754
- }
1755
-
1756
- function resolveCurrentTimestamp(now?: () => string): string {
1757
- return now ? now() : new Date().toISOString();
1758
- }
1759
-
1760
- function assertWorkflowDefinitions(nodes: readonly AgentRuntimeWorkflowNodeState[]): void {
1761
- const ids = new Set(nodes.map((node) => node.node_id));
1762
- if (ids.size !== nodes.length) {
1763
- throw new Error(
1764
- 'Workflow definition contains duplicate node IDs. Each workflow node must declare a unique id.',
1765
- );
1766
- }
1767
-
1768
- for (const node of nodes) {
1769
- for (const dependencyId of node.depends_on) {
1770
- if (!ids.has(dependencyId)) {
1771
- throw new Error(
1772
- `Workflow node "${node.node_id}" depends on "${dependencyId}", but that node is not defined.`,
1773
- );
1774
- }
1775
- }
1776
- }
1777
- }
1778
-
1779
- function getReadyWorkflowNodes(
1780
- state: AgentRuntimeWorkflowState,
1781
- timestamp: string,
1782
- ): AgentRuntimeWorkflowNodeState[] {
1783
- const workflow = state.workflow;
1784
- if (!workflow) {
1785
- return [];
1786
- }
1787
-
1788
- return workflow.nodes.filter((node) => isWorkflowNodeReady(node, workflow.nodes, timestamp));
1789
- }
1790
-
1791
- function getScheduledWorkflowNodes(
1792
- state: AgentRuntimeWorkflowState,
1793
- timestamp: string,
1794
- ): AgentRuntimeWorkflowNodeState[] {
1795
- const workflow = state.workflow;
1796
- if (!workflow) {
1797
- return [];
1798
- }
1799
-
1800
- return workflow.nodes.filter(
1801
- (node) =>
1802
- node.status !== AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED &&
1803
- node.status !== AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR &&
1804
- node.status !== AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL &&
1805
- typeof node.wake_at === 'string' &&
1806
- node.wake_at > timestamp &&
1807
- areWorkflowDependenciesCompleted(node, workflow.nodes),
1808
- );
1809
- }
1810
-
1811
- function isWorkflowNodeReady(
1812
- node: AgentRuntimeWorkflowNodeState,
1813
- allNodes: readonly AgentRuntimeWorkflowNodeState[],
1814
- timestamp: string,
1815
- ): boolean {
1816
- if (
1817
- node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED ||
1818
- node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR ||
1819
- node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL
1820
- ) {
1821
- return false;
1822
- }
1823
-
1824
- if (!areWorkflowDependenciesCompleted(node, allNodes)) {
1825
- return false;
1826
- }
1827
-
1828
- if (typeof node.wake_at === 'string' && node.wake_at > timestamp) {
1829
- return false;
1830
- }
1831
-
1832
- return true;
1833
- }
1834
-
1835
- function areWorkflowDependenciesCompleted(
1836
- node: AgentRuntimeWorkflowNodeState,
1837
- allNodes: readonly AgentRuntimeWorkflowNodeState[],
1838
- ): boolean {
1839
- return node.depends_on.every((dependencyId) =>
1840
- allNodes.some(
1841
- (candidate) =>
1842
- candidate.node_id === dependencyId &&
1843
- candidate.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED,
1844
- ),
1845
- );
1846
- }
1847
-
1848
- function updateWorkflowStateForNodeResult(
1849
- state: AgentRuntimeWorkflowState,
1850
- nodeId: string,
1851
- result: AgentRuntimePersistedSessionResult,
1852
- timestamp: string,
1853
- ): AgentRuntimeWorkflowState {
1854
- const workflow = state.workflow;
1855
- if (!workflow) {
1856
- return state;
1857
- }
1858
-
1859
- const updatedNodes = workflow.nodes.map((node) => {
1860
- if (node.node_id !== nodeId) {
1861
- return node;
1862
- }
1863
-
1864
- const baseNode = {
1865
- ...node,
1866
- messages: result.messages,
1867
- history: result.history,
1868
- turn_count: result.turn_count,
1869
- tool_call_count: result.tool_call_count,
1870
- pending_request_id: undefined,
1871
- requested_tool: undefined,
1872
- };
1873
-
1874
- if (result.kind === 'completed') {
1875
- return {
1876
- ...baseNode,
1877
- status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED,
1878
- last_turn: result.final_turn,
1879
- };
1880
- }
1881
-
1882
- if (result.kind === 'approval_required') {
1883
- return {
1884
- ...baseNode,
1885
- status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL,
1886
- pending_request_id: result.pending_request_id,
1887
- requested_tool: result.requested_tool,
1888
- last_turn: result.last_turn,
1889
- };
1890
- }
1891
-
1892
- if (result.kind === 'suspended') {
1893
- return {
1894
- ...baseNode,
1895
- status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.SUSPENDED,
1896
- };
1897
- }
1898
-
1899
- return {
1900
- ...baseNode,
1901
- status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR,
1902
- };
1903
- });
1904
-
1905
- let continuations = state.continuations;
1906
- if (result.kind === 'completed') {
1907
- continuations = appendWorkflowContinuation(continuations, {
1908
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.BRANCH_COMPLETED,
1909
- timestamp,
1910
- node_id: nodeId,
1911
- });
1912
- } else if (result.kind === 'approval_required') {
1913
- continuations = appendWorkflowContinuation(continuations, {
1914
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.APPROVAL_REQUIRED,
1915
- timestamp,
1916
- reason: WORKFLOW_WAITING_REASON,
1917
- request_id: result.pending_request_id,
1918
- node_id: nodeId,
1919
- });
1920
- } else if (result.kind === 'suspended') {
1921
- continuations = appendWorkflowContinuation(continuations, {
1922
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SUSPENDED,
1923
- timestamp,
1924
- reason: WORKFLOW_SUSPEND_REASON,
1925
- node_id: nodeId,
1926
- });
1927
- } else {
1928
- continuations = appendWorkflowContinuation(continuations, {
1929
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.ERROR,
1930
- timestamp,
1931
- reason: result.error.message,
1932
- node_id: nodeId,
1933
- });
1934
- }
1935
-
1936
- for (const node of updatedNodes) {
1937
- if (
1938
- node.depends_on.length > 1 &&
1939
- isWorkflowNodeReady(node, updatedNodes, timestamp) &&
1940
- !hasContinuationForNode(
1941
- continuations,
1942
- AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.JOIN_READY,
1943
- node.node_id,
1944
- )
1945
- ) {
1946
- continuations = appendWorkflowContinuation(continuations, {
1947
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.JOIN_READY,
1948
- timestamp,
1949
- reason: WORKFLOW_JOIN_READY_REASON,
1950
- node_id: node.node_id,
1951
- });
1952
- }
1953
-
1954
- if (
1955
- typeof node.wake_at === 'string' &&
1956
- node.wake_at <= timestamp &&
1957
- !hasContinuationForNode(
1958
- continuations,
1959
- AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.WAKEUP,
1960
- node.node_id,
1961
- ) &&
1962
- areWorkflowDependenciesCompleted(node, updatedNodes)
1963
- ) {
1964
- continuations = appendWorkflowContinuation(continuations, {
1965
- kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.WAKEUP,
1966
- timestamp,
1967
- reason: WORKFLOW_WAKEUP_REASON,
1968
- node_id: node.node_id,
1969
- });
1970
- }
1971
- }
1972
-
1973
- return {
1974
- ...state,
1975
- status: deriveWorkflowStatus(updatedNodes),
1976
- updated_at: timestamp,
1977
- workflow: {
1978
- nodes: updatedNodes,
1979
- },
1980
- continuations,
1981
- next_wake_at: undefined,
1982
- };
1983
- }
1984
-
1985
- function deriveWorkflowStatus(
1986
- nodes: readonly AgentRuntimeWorkflowNodeState[],
1987
- ): AgentRuntimeWorkflowStatus {
1988
- if (nodes.every((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED)) {
1989
- return AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED;
1990
- }
1991
- if (nodes.some((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL)) {
1992
- return AGENT_RUNTIME_WORKFLOW_STATUSES.WAITING_APPROVAL;
1993
- }
1994
- if (nodes.some((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR)) {
1995
- return AGENT_RUNTIME_WORKFLOW_STATUSES.ERROR;
1996
- }
1997
- if (nodes.some((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.SUSPENDED)) {
1998
- return AGENT_RUNTIME_WORKFLOW_STATUSES.SUSPENDED;
1999
- }
2000
- return AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE;
2001
- }
2002
-
2003
- function allWorkflowNodesCompleted(state: AgentRuntimeWorkflowState): boolean {
2004
- return (
2005
- state.workflow?.nodes.every(
2006
- (node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED,
2007
- ) ?? false
2008
- );
2009
- }
2010
-
2011
- function getCompletedNodeIds(state: AgentRuntimeWorkflowState): string[] {
2012
- return (
2013
- state.workflow?.nodes
2014
- .filter((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED)
2015
- .map((node) => node.node_id) ?? []
2016
- );
2017
- }
2018
-
2019
- function hasContinuationForNode(
2020
- continuations: readonly AgentRuntimeWorkflowContinuation[],
2021
- kind: AgentRuntimeWorkflowContinuationKind,
2022
- nodeId: string,
2023
- ): boolean {
2024
- return continuations.some(
2025
- (continuation) => continuation.kind === kind && continuation.node_id === nodeId,
2026
- );
2027
- }