@lumenflow/cli 5.5.0 → 5.7.14

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 (229) hide show
  1. package/README.md +41 -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/doctor.js +11 -0
  7. package/dist/doctor.js.map +1 -1
  8. package/dist/gate-defaults.js +37 -0
  9. package/dist/gate-defaults.js.map +1 -1
  10. package/dist/gates/monolithic-file-contention-guard.js +167 -0
  11. package/dist/gates/monolithic-file-contention-guard.js.map +1 -0
  12. package/dist/gates/prod-migration-drift.js +207 -0
  13. package/dist/gates/prod-migration-drift.js.map +1 -0
  14. package/dist/gates/test-over-deletion-guard.js +280 -0
  15. package/dist/gates/test-over-deletion-guard.js.map +1 -0
  16. package/dist/gates-runners.js +44 -3
  17. package/dist/gates-runners.js.map +1 -1
  18. package/dist/gates.js +3 -2
  19. package/dist/gates.js.map +1 -1
  20. package/dist/hooks/config-resolver.js +16 -1
  21. package/dist/hooks/config-resolver.js.map +1 -1
  22. package/dist/hooks/dirty-guard.js +43 -2
  23. package/dist/hooks/dirty-guard.js.map +1 -1
  24. package/dist/hooks/git-status-parser.js +22 -8
  25. package/dist/hooks/git-status-parser.js.map +1 -1
  26. package/dist/init-templates.js +241 -0
  27. package/dist/init-templates.js.map +1 -1
  28. package/dist/init.js +122 -16
  29. package/dist/init.js.map +1 -1
  30. package/dist/lumenflow-setup.js +144 -0
  31. package/dist/lumenflow-setup.js.map +1 -0
  32. package/dist/lumenflow-upgrade.js +43 -1
  33. package/dist/lumenflow-upgrade.js.map +1 -1
  34. package/dist/mem-create.js +10 -1
  35. package/dist/mem-create.js.map +1 -1
  36. package/dist/mem-signal.js +21 -4
  37. package/dist/mem-signal.js.map +1 -1
  38. package/dist/orchestrate-initiative.js +28 -3
  39. package/dist/orchestrate-initiative.js.map +1 -1
  40. package/dist/public-manifest.js +17 -7
  41. package/dist/public-manifest.js.map +1 -1
  42. package/dist/release.js +53 -18
  43. package/dist/release.js.map +1 -1
  44. package/dist/wu-done-gates.js +13 -9
  45. package/dist/wu-done-gates.js.map +1 -1
  46. package/dist/wu-done.js +14 -2
  47. package/dist/wu-done.js.map +1 -1
  48. package/dist/wu-edit-operations.js +74 -0
  49. package/dist/wu-edit-operations.js.map +1 -1
  50. package/dist/wu-edit-validators.js +58 -0
  51. package/dist/wu-edit-validators.js.map +1 -1
  52. package/dist/wu-edit.js +106 -4
  53. package/dist/wu-edit.js.map +1 -1
  54. package/dist/wu-prep.js +57 -9
  55. package/dist/wu-prep.js.map +1 -1
  56. package/dist/wu-recover.js +6 -0
  57. package/dist/wu-recover.js.map +1 -1
  58. package/dist/wu-release.js +120 -2
  59. package/dist/wu-release.js.map +1 -1
  60. package/dist/wu-sizing-validation.js +47 -17
  61. package/dist/wu-sizing-validation.js.map +1 -1
  62. package/dist/wu-status.js +33 -0
  63. package/dist/wu-status.js.map +1 -1
  64. package/package.json +13 -12
  65. package/packs/agent-runtime/package.json +1 -1
  66. package/packs/sidekick/package.json +1 -1
  67. package/packs/software-delivery/package.json +1 -1
  68. package/templates/core/AGENTS.md.template +67 -3
  69. package/templates/core/LUMENFLOW.md.template +196 -47
  70. package/dist/distribution-preflight.js +0 -230
  71. package/dist/distribution-preflight.js.map +0 -1
  72. package/packs/agent-runtime/agent-heartbeat.ts +0 -163
  73. package/packs/agent-runtime/auto-session-integration.ts +0 -888
  74. package/packs/agent-runtime/capability-factory.ts +0 -104
  75. package/packs/agent-runtime/constants.ts +0 -21
  76. package/packs/agent-runtime/delegation-registry-schema.ts +0 -220
  77. package/packs/agent-runtime/delegation-registry-store.ts +0 -269
  78. package/packs/agent-runtime/delegation-tree.ts +0 -328
  79. package/packs/agent-runtime/index.ts +0 -20
  80. package/packs/agent-runtime/manifest.ts +0 -348
  81. package/packs/agent-runtime/memory-coordination-contract.ts +0 -86
  82. package/packs/agent-runtime/orchestration.ts +0 -2027
  83. package/packs/agent-runtime/pack-registration.ts +0 -110
  84. package/packs/agent-runtime/policy-factory.ts +0 -165
  85. package/packs/agent-runtime/remote-controls/index.ts +0 -7
  86. package/packs/agent-runtime/remote-controls/operations.ts +0 -405
  87. package/packs/agent-runtime/remote-controls/port.ts +0 -48
  88. package/packs/agent-runtime/remote-controls/state-store.ts +0 -258
  89. package/packs/agent-runtime/remote-controls/types.ts +0 -105
  90. package/packs/agent-runtime/session-schema.ts +0 -467
  91. package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +0 -793
  92. package/packs/agent-runtime/tool-impl/index.ts +0 -6
  93. package/packs/agent-runtime/tool-impl/provider-adapters.ts +0 -1245
  94. package/packs/agent-runtime/tool-impl/remote-controls.mock.ts +0 -256
  95. package/packs/agent-runtime/tool-impl/remote-controls.ts +0 -273
  96. package/packs/agent-runtime/tools/index.ts +0 -4
  97. package/packs/agent-runtime/tools/types.ts +0 -47
  98. package/packs/agent-runtime/turn-lifecycle-events.ts +0 -590
  99. package/packs/agent-runtime/types.ts +0 -128
  100. package/packs/agent-runtime/vitest.config.ts +0 -11
  101. package/packs/sidekick/channel-ingress.ts +0 -137
  102. package/packs/sidekick/constants.ts +0 -10
  103. package/packs/sidekick/index.ts +0 -8
  104. package/packs/sidekick/manifest-schema.ts +0 -49
  105. package/packs/sidekick/manifest.ts +0 -512
  106. package/packs/sidekick/pack-registration.ts +0 -110
  107. package/packs/sidekick/policy-factory.ts +0 -38
  108. package/packs/sidekick/sidekick-events.ts +0 -694
  109. package/packs/sidekick/src/adapters/cloud-queue.ts +0 -101
  110. package/packs/sidekick/src/adapters/control-plane-bridge.adapter.ts +0 -386
  111. package/packs/sidekick/src/adapters/filesystem-bridge.adapter.ts +0 -228
  112. package/packs/sidekick/src/domain/channel.types.ts +0 -64
  113. package/packs/sidekick/src/ports/channel-bridge.port.ts +0 -92
  114. package/packs/sidekick/src/routines/commit.ts +0 -74
  115. package/packs/sidekick/tool-impl/channel-tools.ts +0 -577
  116. package/packs/sidekick/tool-impl/channel-transports.ts +0 -75
  117. package/packs/sidekick/tool-impl/index.ts +0 -29
  118. package/packs/sidekick/tool-impl/memory-tools.ts +0 -290
  119. package/packs/sidekick/tool-impl/routine-commit.ts +0 -102
  120. package/packs/sidekick/tool-impl/routine-tools.ts +0 -440
  121. package/packs/sidekick/tool-impl/runtime-context.ts +0 -28
  122. package/packs/sidekick/tool-impl/shared.ts +0 -125
  123. package/packs/sidekick/tool-impl/storage.ts +0 -325
  124. package/packs/sidekick/tool-impl/system-tools.ts +0 -160
  125. package/packs/sidekick/tool-impl/task-tools.ts +0 -506
  126. package/packs/sidekick/tools/channel-tools.ts +0 -53
  127. package/packs/sidekick/tools/index.ts +0 -9
  128. package/packs/sidekick/tools/memory-tools.ts +0 -53
  129. package/packs/sidekick/tools/routine-tools.ts +0 -53
  130. package/packs/sidekick/tools/system-tools.ts +0 -47
  131. package/packs/sidekick/tools/task-tools.ts +0 -61
  132. package/packs/sidekick/tools/types.ts +0 -57
  133. package/packs/sidekick/vitest.config.ts +0 -11
  134. package/packs/software-delivery/constants.ts +0 -10
  135. package/packs/software-delivery/extensions.ts +0 -140
  136. package/packs/software-delivery/gate-policies.ts +0 -134
  137. package/packs/software-delivery/index.ts +0 -8
  138. package/packs/software-delivery/manifest-schema.ts +0 -268
  139. package/packs/software-delivery/manifest.ts +0 -657
  140. package/packs/software-delivery/pack-registration.ts +0 -113
  141. package/packs/software-delivery/src/commands/index.ts +0 -5
  142. package/packs/software-delivery/src/config/delivery-review-contract.ts +0 -256
  143. package/packs/software-delivery/src/config/env-accessors.ts +0 -66
  144. package/packs/software-delivery/src/config/index.ts +0 -8
  145. package/packs/software-delivery/src/config/normalize-config-keys.ts +0 -9
  146. package/packs/software-delivery/src/config/schemas/lumenflow-config-schema-types.ts +0 -460
  147. package/packs/software-delivery/src/config/workspace-reader.ts +0 -375
  148. package/packs/software-delivery/src/constants/backlog-patterns.ts +0 -31
  149. package/packs/software-delivery/src/constants/client-ids.ts +0 -19
  150. package/packs/software-delivery/src/constants/config-contract.ts +0 -7
  151. package/packs/software-delivery/src/constants/docs-layout-presets.ts +0 -50
  152. package/packs/software-delivery/src/constants/duration-constants.ts +0 -20
  153. package/packs/software-delivery/src/constants/gate-constants.ts +0 -32
  154. package/packs/software-delivery/src/constants/index.ts +0 -29
  155. package/packs/software-delivery/src/constants/lock-constants.ts +0 -35
  156. package/packs/software-delivery/src/constants/object-guards.ts +0 -12
  157. package/packs/software-delivery/src/constants/section-headings.ts +0 -107
  158. package/packs/software-delivery/src/constants/wu-cli-constants.ts +0 -500
  159. package/packs/software-delivery/src/constants/wu-domain-constants.ts +0 -466
  160. package/packs/software-delivery/src/constants/wu-git-constants.ts +0 -7
  161. package/packs/software-delivery/src/constants/wu-id-format.ts +0 -327
  162. package/packs/software-delivery/src/constants/wu-paths-constants.ts +0 -384
  163. package/packs/software-delivery/src/constants/wu-statuses.ts +0 -287
  164. package/packs/software-delivery/src/constants/wu-type-helpers.ts +0 -67
  165. package/packs/software-delivery/src/constants/wu-ui-constants.ts +0 -267
  166. package/packs/software-delivery/src/constants/wu-validation-constants.ts +0 -73
  167. package/packs/software-delivery/src/domain/index.ts +0 -5
  168. package/packs/software-delivery/src/domain/orchestration.constants.ts +0 -166
  169. package/packs/software-delivery/src/domain/orchestration.schemas.ts +0 -238
  170. package/packs/software-delivery/src/domain/orchestration.types.ts +0 -176
  171. package/packs/software-delivery/src/methodology/incremental-test.ts +0 -122
  172. package/packs/software-delivery/src/methodology/index.ts +0 -6
  173. package/packs/software-delivery/src/methodology/manual-test-validator.ts +0 -292
  174. package/packs/software-delivery/src/policy/coverage-gate.ts +0 -270
  175. package/packs/software-delivery/src/policy/gates-agent-mode.ts +0 -223
  176. package/packs/software-delivery/src/policy/gates-config-internal.ts +0 -121
  177. package/packs/software-delivery/src/policy/gates-config.ts +0 -300
  178. package/packs/software-delivery/src/policy/gates-coverage.ts +0 -356
  179. package/packs/software-delivery/src/policy/gates-presets.ts +0 -134
  180. package/packs/software-delivery/src/policy/gates-schemas.ts +0 -173
  181. package/packs/software-delivery/src/policy/index.ts +0 -22
  182. package/packs/software-delivery/src/policy/package-manager-resolver.ts +0 -319
  183. package/packs/software-delivery/src/policy/resolve-policy.ts +0 -601
  184. package/packs/software-delivery/src/ports/config.ports.ts +0 -90
  185. package/packs/software-delivery/src/ports/dashboard-renderer.port.ts +0 -125
  186. package/packs/software-delivery/src/ports/index.ts +0 -10
  187. package/packs/software-delivery/src/ports/sync-validator.ports.ts +0 -59
  188. package/packs/software-delivery/src/ports/wu-helpers.ports.ts +0 -168
  189. package/packs/software-delivery/src/ports/wu-state.ports.ts +0 -241
  190. package/packs/software-delivery/src/primitives/index.ts +0 -5
  191. package/packs/software-delivery/src/runtime/index.ts +0 -6
  192. package/packs/software-delivery/src/runtime/work-classifier.ts +0 -561
  193. package/packs/software-delivery/src/sandbox/index.ts +0 -10
  194. package/packs/software-delivery/src/sandbox/sandbox-allowlist.ts +0 -118
  195. package/packs/software-delivery/src/sandbox/sandbox-backend-linux.ts +0 -88
  196. package/packs/software-delivery/src/sandbox/sandbox-backend-macos.ts +0 -154
  197. package/packs/software-delivery/src/sandbox/sandbox-backend-windows.ts +0 -47
  198. package/packs/software-delivery/src/sandbox/sandbox-profile.ts +0 -153
  199. package/packs/software-delivery/src/schemas/index.ts +0 -5
  200. package/packs/software-delivery/src/state/date-utils.ts +0 -158
  201. package/packs/software-delivery/src/state/index.ts +0 -15
  202. package/packs/software-delivery/src/state/state-machine.ts +0 -119
  203. package/packs/software-delivery/src/state/wu-doc-types.ts +0 -51
  204. package/packs/software-delivery/src/state/wu-paths.ts +0 -381
  205. package/packs/software-delivery/src/state/wu-schema.ts +0 -1139
  206. package/packs/software-delivery/src/state/wu-state-schema.ts +0 -255
  207. package/packs/software-delivery/src/state/wu-yaml.ts +0 -338
  208. package/packs/software-delivery/tool-impl/agent-tools.ts +0 -263
  209. package/packs/software-delivery/tool-impl/delegation-tools.ts +0 -66
  210. package/packs/software-delivery/tool-impl/flow-metrics-tools.ts +0 -219
  211. package/packs/software-delivery/tool-impl/git-runner.ts +0 -113
  212. package/packs/software-delivery/tool-impl/git-tools.ts +0 -316
  213. package/packs/software-delivery/tool-impl/index.ts +0 -15
  214. package/packs/software-delivery/tool-impl/initiative-orchestration-tools.ts +0 -720
  215. package/packs/software-delivery/tool-impl/lane-lock.ts +0 -246
  216. package/packs/software-delivery/tool-impl/memory-tools.ts +0 -470
  217. package/packs/software-delivery/tool-impl/pending-runtime-tools.ts +0 -21
  218. package/packs/software-delivery/tool-impl/runtime-cli-adapter.ts +0 -329
  219. package/packs/software-delivery/tool-impl/runtime-native-tools.ts +0 -687
  220. package/packs/software-delivery/tool-impl/worker-loader.ts +0 -52
  221. package/packs/software-delivery/tool-impl/worktree-tools.ts +0 -46
  222. package/packs/software-delivery/tool-impl/wu-lifecycle-tools.ts +0 -807
  223. package/packs/software-delivery/tools/delegation-tools.ts +0 -23
  224. package/packs/software-delivery/tools/git-tools.ts +0 -55
  225. package/packs/software-delivery/tools/index.ts +0 -8
  226. package/packs/software-delivery/tools/lane-lock-tool.ts +0 -37
  227. package/packs/software-delivery/tools/types.ts +0 -71
  228. package/packs/software-delivery/tools/worktree-tools.ts +0 -49
  229. 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
- }