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