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