@lumenflow/packs-agent-runtime 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.
Files changed (61) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +147 -0
  3. package/config.schema.json +87 -0
  4. package/dist/capability-factory.d.ts +3 -0
  5. package/dist/capability-factory.d.ts.map +1 -0
  6. package/dist/capability-factory.js +76 -0
  7. package/dist/capability-factory.js.map +1 -0
  8. package/dist/constants.d.ts +19 -0
  9. package/dist/constants.d.ts.map +1 -0
  10. package/dist/constants.js +21 -0
  11. package/dist/constants.js.map +1 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +11 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/manifest.d.ts +46 -0
  17. package/dist/manifest.d.ts.map +1 -0
  18. package/dist/manifest.js +173 -0
  19. package/dist/manifest.js.map +1 -0
  20. package/dist/orchestration.d.ts +243 -0
  21. package/dist/orchestration.d.ts.map +1 -0
  22. package/dist/orchestration.js +1173 -0
  23. package/dist/orchestration.js.map +1 -0
  24. package/dist/pack-registration.d.ts +14 -0
  25. package/dist/pack-registration.d.ts.map +1 -0
  26. package/dist/pack-registration.js +79 -0
  27. package/dist/pack-registration.js.map +1 -0
  28. package/dist/policy-factory.d.ts +5 -0
  29. package/dist/policy-factory.d.ts.map +1 -0
  30. package/dist/policy-factory.js +109 -0
  31. package/dist/policy-factory.js.map +1 -0
  32. package/dist/tool-impl/agent-turn-tools.d.ts +3 -0
  33. package/dist/tool-impl/agent-turn-tools.d.ts.map +1 -0
  34. package/dist/tool-impl/agent-turn-tools.js +582 -0
  35. package/dist/tool-impl/agent-turn-tools.js.map +1 -0
  36. package/dist/tool-impl/index.d.ts +3 -0
  37. package/dist/tool-impl/index.d.ts.map +1 -0
  38. package/dist/tool-impl/index.js +5 -0
  39. package/dist/tool-impl/index.js.map +1 -0
  40. package/dist/tool-impl/provider-adapters.d.ts +65 -0
  41. package/dist/tool-impl/provider-adapters.d.ts.map +1 -0
  42. package/dist/tool-impl/provider-adapters.js +928 -0
  43. package/dist/tool-impl/provider-adapters.js.map +1 -0
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.d.ts.map +1 -0
  46. package/dist/tools/index.js +4 -0
  47. package/dist/tools/index.js.map +1 -0
  48. package/dist/tools/types.d.ts +26 -0
  49. package/dist/tools/types.d.ts.map +1 -0
  50. package/dist/tools/types.js +17 -0
  51. package/dist/tools/types.js.map +1 -0
  52. package/dist/types.d.ts +96 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +21 -0
  55. package/dist/types.js.map +1 -0
  56. package/dist/vitest.config.d.ts +3 -0
  57. package/dist/vitest.config.d.ts.map +1 -0
  58. package/dist/vitest.config.js +11 -0
  59. package/dist/vitest.config.js.map +1 -0
  60. package/manifest.yaml +193 -0
  61. package/package.json +57 -0
@@ -0,0 +1,1173 @@
1
+ // Copyright (c) 2026 Hellmai Ltd
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { TOOL_ERROR_CODES, } from '@lumenflow/kernel';
6
+ import { AGENT_RUNTIME_AGENT_INTENT_METADATA_KEY, AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY, AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY, AGENT_RUNTIME_AGENT_WORKFLOW_NODE_ID_METADATA_KEY, } from './constants.js';
7
+ import { AGENT_RUNTIME_TOOL_NAMES, AGENT_RUNTIME_TURN_STATUSES, } from './types.js';
8
+ const DEFAULT_MAX_ORCHESTRATION_TURNS = 12;
9
+ const TOOL_CALL_ID_PREFIX = 'agent-runtime-tool-call';
10
+ const APPROVAL_STATUS_TOOL_NAME = 'kernel:approval';
11
+ const LOOP_LIMIT_EXCEEDED_CODE = 'AGENT_RUNTIME_LOOP_LIMIT_EXCEEDED';
12
+ const DEFAULT_GOVERNED_TOOL_CATALOG_EXCLUSIONS = [AGENT_RUNTIME_TOOL_NAMES.EXECUTE_TURN];
13
+ const AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION = 1;
14
+ const AGENT_RUNTIME_WORKFLOW_DIRECTORY = path.join('.agent-runtime', 'workflow');
15
+ const WORKFLOW_SUSPEND_REASON = 'Invocation turn budget reached.';
16
+ const WORKFLOW_WAITING_REASON = 'Waiting for approval-driven continuation.';
17
+ const WORKFLOW_COMPLETED_REASON = 'Session reached a terminal turn.';
18
+ const WORKFLOW_SCHEDULED_REASON = 'Waiting for the scheduled wake time.';
19
+ const WORKFLOW_JOIN_READY_REASON = 'Join dependencies completed.';
20
+ const WORKFLOW_WAKEUP_REASON = 'Scheduled wake time reached.';
21
+ export const AGENT_RUNTIME_WORKFLOW_STATUSES = {
22
+ ACTIVE: 'active',
23
+ SUSPENDED: 'suspended',
24
+ WAITING_APPROVAL: 'waiting_approval',
25
+ SCHEDULED: 'scheduled',
26
+ COMPLETED: 'completed',
27
+ ERROR: 'error',
28
+ };
29
+ export const AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS = {
30
+ CREATED: 'created',
31
+ RESUMED: 'resumed',
32
+ SUSPENDED: 'suspended',
33
+ APPROVAL_REQUIRED: 'approval_required',
34
+ SCHEDULED: 'scheduled',
35
+ WAKEUP: 'wakeup',
36
+ BRANCH_COMPLETED: 'branch_completed',
37
+ JOIN_READY: 'join_ready',
38
+ COMPLETED: 'completed',
39
+ ERROR: 'error',
40
+ };
41
+ export const AGENT_RUNTIME_WORKFLOW_NODE_STATUSES = {
42
+ PENDING: 'pending',
43
+ READY: 'ready',
44
+ SCHEDULED: 'scheduled',
45
+ WAITING_APPROVAL: 'waiting_approval',
46
+ SUSPENDED: 'suspended',
47
+ COMPLETED: 'completed',
48
+ ERROR: 'error',
49
+ };
50
+ export async function buildGovernedToolCatalog(input) {
51
+ const excludedToolNames = new Set(input.excludeToolNames ?? DEFAULT_GOVERNED_TOOL_CATALOG_EXCLUSIONS);
52
+ const governedTools = await input.toolHost.listGovernedTools(input.context);
53
+ return governedTools
54
+ .filter((entry) => !excludedToolNames.has(entry.capability.name))
55
+ .map((entry) => ({
56
+ name: entry.capability.name,
57
+ description: entry.capability.description,
58
+ }));
59
+ }
60
+ export async function runGovernedAgentLoop(input) {
61
+ const result = await runGovernedAgentLoopInternal({
62
+ ...input,
63
+ turnBudgetBehavior: 'error',
64
+ });
65
+ if (result.kind === 'suspended') {
66
+ return {
67
+ kind: 'error',
68
+ stage: 'loop_limit',
69
+ error: {
70
+ code: LOOP_LIMIT_EXCEEDED_CODE,
71
+ message: `Host loop reached maxTurns=${input.maxTurns ?? DEFAULT_MAX_ORCHESTRATION_TURNS} before the agent reached a terminal reply.`,
72
+ },
73
+ messages: result.messages,
74
+ turn_count: result.turn_count,
75
+ tool_call_count: result.tool_call_count,
76
+ history: result.history,
77
+ };
78
+ }
79
+ return result;
80
+ }
81
+ export function createAgentRuntimeWorkflowStateStore(input) {
82
+ const workflowRoot = path.join(input.workspaceRoot, AGENT_RUNTIME_WORKFLOW_DIRECTORY);
83
+ return {
84
+ async load(sessionId) {
85
+ const filePath = path.join(workflowRoot, `${sessionId}.json`);
86
+ try {
87
+ const raw = await readFile(filePath, 'utf8');
88
+ return parseWorkflowState(JSON.parse(raw), filePath);
89
+ }
90
+ catch (error) {
91
+ const nodeError = error;
92
+ if (nodeError.code === 'ENOENT') {
93
+ return null;
94
+ }
95
+ throw error;
96
+ }
97
+ },
98
+ async save(state) {
99
+ const filePath = path.join(workflowRoot, `${state.session_id}.json`);
100
+ await mkdir(workflowRoot, { recursive: true });
101
+ await writeFile(filePath, JSON.stringify(state), 'utf8');
102
+ },
103
+ };
104
+ }
105
+ export async function startGovernedAgentSession(input) {
106
+ const store = createAgentRuntimeWorkflowStateStore({
107
+ workspaceRoot: input.storageRoot,
108
+ });
109
+ const now = resolveCurrentTimestamp(input.now);
110
+ const baseContext = input.createContext({});
111
+ const initialState = {
112
+ schema_version: AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION,
113
+ session_id: input.executeTurnInput.session_id,
114
+ task_id: baseContext.task_id,
115
+ run_id: baseContext.run_id,
116
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
117
+ created_at: now,
118
+ updated_at: now,
119
+ execute_turn_input: cloneExecuteTurnInput(input.executeTurnInput),
120
+ messages: [...input.executeTurnInput.messages],
121
+ history: [],
122
+ turn_count: 0,
123
+ tool_call_count: 0,
124
+ continuations: [
125
+ {
126
+ sequence: 0,
127
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.CREATED,
128
+ timestamp: now,
129
+ },
130
+ ],
131
+ };
132
+ const result = await runGovernedAgentLoopInternal({
133
+ ...input,
134
+ maxTurns: input.maxTurnsPerInvocation,
135
+ turnBudgetBehavior: 'suspend',
136
+ });
137
+ await store.save(materializeWorkflowState(initialState, result, now));
138
+ return result;
139
+ }
140
+ export async function resumeGovernedAgentSession(input) {
141
+ const store = createAgentRuntimeWorkflowStateStore({
142
+ workspaceRoot: input.storageRoot,
143
+ });
144
+ const existing = await store.load(input.sessionId);
145
+ if (!existing) {
146
+ throw new Error(`No persisted agent workflow state found for session "${input.sessionId}". Create or restore the session before calling resume.`);
147
+ }
148
+ if (existing.status === AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED) {
149
+ throw new Error(`Agent workflow session "${input.sessionId}" is already completed and cannot be resumed.`);
150
+ }
151
+ const resumedAt = resolveCurrentTimestamp(input.now);
152
+ const resumedState = {
153
+ ...existing,
154
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
155
+ updated_at: resumedAt,
156
+ messages: [...existing.messages, ...(input.continuationMessages ?? [])],
157
+ pending_request_id: undefined,
158
+ continuations: appendWorkflowContinuation(existing.continuations, {
159
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.RESUMED,
160
+ timestamp: resumedAt,
161
+ }),
162
+ };
163
+ const result = await runGovernedAgentLoopInternal({
164
+ runtime: input.runtime,
165
+ executeTurnInput: {
166
+ ...cloneExecuteTurnInput(existing.execute_turn_input),
167
+ messages: [...resumedState.messages],
168
+ },
169
+ createContext: input.createContext,
170
+ maxTurns: input.maxTurnsPerInvocation,
171
+ turnBudgetBehavior: 'suspend',
172
+ initialCursor: {
173
+ messages: resumedState.messages,
174
+ history: resumedState.history,
175
+ turnCount: resumedState.turn_count,
176
+ toolCallCount: resumedState.tool_call_count,
177
+ },
178
+ });
179
+ await store.save(materializeWorkflowState(resumedState, result, resumedAt));
180
+ return result;
181
+ }
182
+ export async function startGovernedAgentWorkflow(input) {
183
+ const timestamp = resolveCurrentTimestamp(input.now);
184
+ const baseContext = input.createContext({});
185
+ const workflowState = {
186
+ schema_version: AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION,
187
+ session_id: input.workflow.session_id,
188
+ task_id: baseContext.task_id,
189
+ run_id: baseContext.run_id,
190
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
191
+ created_at: timestamp,
192
+ updated_at: timestamp,
193
+ execute_turn_input: cloneExecuteTurnInput(input.workflow.nodes[0]?.execute_turn_input ?? {
194
+ session_id: input.workflow.session_id,
195
+ model_profile: 'default',
196
+ url: 'https://model-provider.invalid/',
197
+ messages: [],
198
+ }),
199
+ messages: [],
200
+ history: [],
201
+ turn_count: 0,
202
+ tool_call_count: 0,
203
+ continuations: [
204
+ {
205
+ sequence: 0,
206
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.CREATED,
207
+ timestamp,
208
+ },
209
+ ],
210
+ workflow: {
211
+ nodes: input.workflow.nodes.map((node) => ({
212
+ node_id: node.id,
213
+ status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.PENDING,
214
+ execute_turn_input: cloneExecuteTurnInput(node.execute_turn_input),
215
+ depends_on: [...(node.depends_on ?? [])],
216
+ ...(node.wake_at ? { wake_at: node.wake_at } : {}),
217
+ messages: [...node.execute_turn_input.messages],
218
+ history: [],
219
+ turn_count: 0,
220
+ tool_call_count: 0,
221
+ })),
222
+ },
223
+ };
224
+ const workflowNodes = workflowState.workflow?.nodes;
225
+ if (!workflowNodes) {
226
+ throw new Error(`Workflow session "${input.workflow.session_id}" could not be initialized because no workflow nodes were materialized.`);
227
+ }
228
+ assertWorkflowDefinitions(workflowNodes);
229
+ const result = await advanceGovernedAgentWorkflowState({
230
+ runtime: input.runtime,
231
+ state: workflowState,
232
+ createContext: input.createContext,
233
+ maxTurnsPerInvocation: input.maxTurnsPerInvocation,
234
+ timestamp,
235
+ });
236
+ const store = createAgentRuntimeWorkflowStateStore({
237
+ workspaceRoot: input.storageRoot,
238
+ });
239
+ await store.save(result.state);
240
+ return result.result;
241
+ }
242
+ export async function resumeGovernedAgentWorkflow(input) {
243
+ const store = createAgentRuntimeWorkflowStateStore({
244
+ workspaceRoot: input.storageRoot,
245
+ });
246
+ const existing = await store.load(input.sessionId);
247
+ if (!existing?.workflow) {
248
+ throw new Error(`No persisted workflow graph found for session "${input.sessionId}". Start the workflow before attempting to resume it.`);
249
+ }
250
+ const timestamp = resolveCurrentTimestamp(input.now);
251
+ const resumedNodes = existing.workflow.nodes.map((node) => {
252
+ const continuationMessages = input.continuationMessagesByNodeId?.[node.node_id] ?? [];
253
+ if (continuationMessages.length === 0) {
254
+ return node;
255
+ }
256
+ return {
257
+ ...node,
258
+ messages: [...node.messages, ...continuationMessages],
259
+ pending_request_id: undefined,
260
+ requested_tool: undefined,
261
+ };
262
+ });
263
+ const resumedState = {
264
+ ...existing,
265
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE,
266
+ updated_at: timestamp,
267
+ workflow: {
268
+ nodes: resumedNodes,
269
+ },
270
+ continuations: appendWorkflowContinuation(existing.continuations, {
271
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.RESUMED,
272
+ timestamp,
273
+ }),
274
+ next_wake_at: undefined,
275
+ };
276
+ const result = await advanceGovernedAgentWorkflowState({
277
+ runtime: input.runtime,
278
+ state: resumedState,
279
+ createContext: input.createContext,
280
+ maxTurnsPerInvocation: input.maxTurnsPerInvocation,
281
+ timestamp,
282
+ });
283
+ await store.save(result.state);
284
+ return result.result;
285
+ }
286
+ async function advanceGovernedAgentWorkflowState(input) {
287
+ const workflow = input.state.workflow;
288
+ if (!workflow) {
289
+ throw new Error(`Workflow state for session "${input.state.session_id}" is missing the workflow graph payload.`);
290
+ }
291
+ let state = {
292
+ ...input.state,
293
+ workflow: {
294
+ nodes: workflow.nodes.map((node) => ({
295
+ ...node,
296
+ messages: [...node.messages],
297
+ history: [...node.history],
298
+ })),
299
+ },
300
+ next_wake_at: undefined,
301
+ };
302
+ while (true) {
303
+ const currentWorkflow = state.workflow;
304
+ if (!currentWorkflow) {
305
+ throw new Error(`Workflow state for session "${state.session_id}" is missing workflow nodes during advancement.`);
306
+ }
307
+ const readyNodes = getReadyWorkflowNodes(state, input.timestamp);
308
+ if (readyNodes.length === 0) {
309
+ const scheduledNodes = getScheduledWorkflowNodes(state, input.timestamp);
310
+ if (scheduledNodes.length > 0) {
311
+ const nextWakeAt = scheduledNodes
312
+ .map((node) => node.wake_at)
313
+ .filter((value) => typeof value === 'string')
314
+ .sort()[0];
315
+ const scheduledNodeIds = new Set(scheduledNodes.map((node) => node.node_id));
316
+ const scheduledState = {
317
+ ...state,
318
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.SCHEDULED,
319
+ updated_at: input.timestamp,
320
+ next_wake_at: nextWakeAt,
321
+ workflow: {
322
+ nodes: currentWorkflow.nodes.map((node) => scheduledNodeIds.has(node.node_id)
323
+ ? { ...node, status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.SCHEDULED }
324
+ : node),
325
+ },
326
+ continuations: scheduledNodes.reduce((continuations, node) => hasContinuationForNode(continuations, AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SCHEDULED, node.node_id)
327
+ ? continuations
328
+ : appendWorkflowContinuation(continuations, {
329
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SCHEDULED,
330
+ timestamp: input.timestamp,
331
+ reason: WORKFLOW_SCHEDULED_REASON,
332
+ node_id: node.node_id,
333
+ }), state.continuations),
334
+ };
335
+ return {
336
+ state: scheduledState,
337
+ result: {
338
+ kind: 'scheduled',
339
+ next_wake_at: nextWakeAt ?? input.timestamp,
340
+ scheduled_node_ids: scheduledNodes.map((node) => node.node_id),
341
+ completed_node_ids: getCompletedNodeIds(scheduledState),
342
+ },
343
+ };
344
+ }
345
+ if (allWorkflowNodesCompleted(state)) {
346
+ const completedState = {
347
+ ...state,
348
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED,
349
+ updated_at: input.timestamp,
350
+ continuations: appendWorkflowContinuation(state.continuations, {
351
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.COMPLETED,
352
+ timestamp: input.timestamp,
353
+ reason: WORKFLOW_COMPLETED_REASON,
354
+ }),
355
+ };
356
+ return {
357
+ state: completedState,
358
+ result: {
359
+ kind: 'completed',
360
+ completed_node_ids: getCompletedNodeIds(completedState),
361
+ },
362
+ };
363
+ }
364
+ return {
365
+ state: {
366
+ ...state,
367
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.ERROR,
368
+ updated_at: input.timestamp,
369
+ continuations: appendWorkflowContinuation(state.continuations, {
370
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.ERROR,
371
+ timestamp: input.timestamp,
372
+ reason: 'Workflow has incomplete nodes but no ready or scheduled work.',
373
+ }),
374
+ },
375
+ result: {
376
+ kind: 'error',
377
+ node_id: 'workflow',
378
+ error: {
379
+ code: 'AGENT_RUNTIME_WORKFLOW_STALLED',
380
+ message: 'Workflow cannot make progress because no nodes are ready or scheduled.',
381
+ },
382
+ },
383
+ };
384
+ }
385
+ const node = readyNodes[0];
386
+ if (!node) {
387
+ continue;
388
+ }
389
+ const nodeResult = await runGovernedAgentLoopInternal({
390
+ runtime: input.runtime,
391
+ executeTurnInput: {
392
+ ...cloneExecuteTurnInput(node.execute_turn_input),
393
+ messages: [...node.messages],
394
+ },
395
+ createContext: (metadata) => input.createContext({
396
+ ...metadata,
397
+ [AGENT_RUNTIME_AGENT_WORKFLOW_NODE_ID_METADATA_KEY]: node.node_id,
398
+ }),
399
+ maxTurns: input.maxTurnsPerInvocation,
400
+ turnBudgetBehavior: 'suspend',
401
+ initialCursor: {
402
+ messages: node.messages,
403
+ history: node.history,
404
+ turnCount: node.turn_count,
405
+ toolCallCount: node.tool_call_count,
406
+ },
407
+ });
408
+ state = updateWorkflowStateForNodeResult(state, node.node_id, nodeResult, input.timestamp);
409
+ if (nodeResult.kind === 'approval_required') {
410
+ return {
411
+ state,
412
+ result: {
413
+ kind: 'approval_required',
414
+ node_id: node.node_id,
415
+ pending_request_id: nodeResult.pending_request_id,
416
+ requested_tool: nodeResult.requested_tool,
417
+ },
418
+ };
419
+ }
420
+ if (nodeResult.kind === 'suspended') {
421
+ return {
422
+ state,
423
+ result: {
424
+ kind: 'suspended',
425
+ node_id: node.node_id,
426
+ },
427
+ };
428
+ }
429
+ if (nodeResult.kind === 'error') {
430
+ return {
431
+ state,
432
+ result: {
433
+ kind: 'error',
434
+ node_id: node.node_id,
435
+ error: nodeResult.error,
436
+ },
437
+ };
438
+ }
439
+ }
440
+ }
441
+ async function runGovernedAgentLoopInternal(input) {
442
+ const maxTurns = input.maxTurns ?? DEFAULT_MAX_ORCHESTRATION_TURNS;
443
+ const messages = input.initialCursor
444
+ ? [...input.initialCursor.messages]
445
+ : [...input.executeTurnInput.messages];
446
+ const history = input.initialCursor ? [...input.initialCursor.history] : [];
447
+ let turnCount = input.initialCursor?.turnCount ?? 0;
448
+ let toolCallCount = input.initialCursor?.toolCallCount ?? 0;
449
+ let invocationTurnCount = 0;
450
+ while (invocationTurnCount < maxTurns) {
451
+ const executeTurnOutput = await input.runtime.executeTool(AGENT_RUNTIME_TOOL_NAMES.EXECUTE_TURN, {
452
+ ...input.executeTurnInput,
453
+ messages: [...messages],
454
+ }, input.createContext({
455
+ [AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY]: turnCount,
456
+ [AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY]: toolCallCount,
457
+ }));
458
+ if (!executeTurnOutput.success) {
459
+ return {
460
+ kind: 'error',
461
+ stage: 'execute_turn',
462
+ error: normalizeToolError(executeTurnOutput.error, 'agent:execute-turn failed in the host loop.'),
463
+ messages,
464
+ turn_count: turnCount,
465
+ tool_call_count: toolCallCount,
466
+ history,
467
+ };
468
+ }
469
+ const normalizedTurn = normalizeTurnOutput(executeTurnOutput.data);
470
+ if (!normalizedTurn) {
471
+ return {
472
+ kind: 'error',
473
+ stage: 'execute_turn',
474
+ error: {
475
+ code: TOOL_ERROR_CODES.INVALID_OUTPUT,
476
+ message: 'agent:execute-turn returned a payload that does not match the governed turn contract.',
477
+ },
478
+ messages,
479
+ turn_count: turnCount,
480
+ tool_call_count: toolCallCount,
481
+ history,
482
+ };
483
+ }
484
+ const currentTurnIndex = turnCount;
485
+ turnCount += 1;
486
+ invocationTurnCount += 1;
487
+ const historyEntry = {
488
+ turn_index: currentTurnIndex,
489
+ turn_output: normalizedTurn,
490
+ };
491
+ history.push(historyEntry);
492
+ if (normalizedTurn.status !== AGENT_RUNTIME_TURN_STATUSES.TOOL_REQUEST ||
493
+ !normalizedTurn.requested_tool) {
494
+ return {
495
+ kind: 'completed',
496
+ final_turn: normalizedTurn,
497
+ messages,
498
+ turn_count: turnCount,
499
+ tool_call_count: toolCallCount,
500
+ history,
501
+ };
502
+ }
503
+ const toolCallId = `${TOOL_CALL_ID_PREFIX}-${toolCallCount + 1}`;
504
+ const toolOutput = await input.runtime.executeTool(normalizedTurn.requested_tool.name, normalizedTurn.requested_tool.input, input.createContext({
505
+ [AGENT_RUNTIME_AGENT_INTENT_METADATA_KEY]: normalizedTurn.intent,
506
+ [AGENT_RUNTIME_AGENT_TURN_INDEX_METADATA_KEY]: currentTurnIndex,
507
+ [AGENT_RUNTIME_AGENT_TOOL_CALL_COUNT_METADATA_KEY]: toolCallCount,
508
+ }));
509
+ toolCallCount += 1;
510
+ historyEntry.tool_call_id = toolCallId;
511
+ historyEntry.tool_output = toolOutput;
512
+ if (!toolOutput.success && toolOutput.error?.code === TOOL_ERROR_CODES.APPROVAL_REQUIRED) {
513
+ return {
514
+ kind: 'approval_required',
515
+ pending_request_id: extractApprovalRequestId(toolOutput),
516
+ requested_tool: normalizedTurn.requested_tool,
517
+ last_turn: normalizedTurn,
518
+ messages,
519
+ turn_count: turnCount,
520
+ tool_call_count: toolCallCount,
521
+ history,
522
+ };
523
+ }
524
+ messages.push(createToolResultMessage({
525
+ toolName: normalizedTurn.requested_tool.name,
526
+ toolCallId,
527
+ output: toolOutput,
528
+ }));
529
+ }
530
+ if (input.turnBudgetBehavior === 'suspend') {
531
+ return {
532
+ kind: 'suspended',
533
+ messages,
534
+ turn_count: turnCount,
535
+ tool_call_count: toolCallCount,
536
+ history,
537
+ };
538
+ }
539
+ return {
540
+ kind: 'error',
541
+ stage: 'loop_limit',
542
+ error: {
543
+ code: LOOP_LIMIT_EXCEEDED_CODE,
544
+ message: `Host loop reached maxTurns=${maxTurns} before the agent reached a terminal reply.`,
545
+ },
546
+ messages,
547
+ turn_count: turnCount,
548
+ tool_call_count: toolCallCount,
549
+ history,
550
+ };
551
+ }
552
+ export function createHostContextMessages(input) {
553
+ const messages = [];
554
+ const taskSummary = normalizeOptionalText(input.task_summary);
555
+ if (taskSummary) {
556
+ messages.push({
557
+ role: 'system',
558
+ content: `Task context:\n${taskSummary}`,
559
+ });
560
+ }
561
+ const memorySummary = normalizeOptionalText(input.memory_summary);
562
+ if (memorySummary) {
563
+ messages.push({
564
+ role: 'system',
565
+ content: `Memory context:\n${memorySummary}`,
566
+ });
567
+ }
568
+ for (const note of input.additional_context ?? []) {
569
+ const normalizedNote = normalizeOptionalText(note);
570
+ if (!normalizedNote) {
571
+ continue;
572
+ }
573
+ messages.push({
574
+ role: 'system',
575
+ content: `Additional context:\n${normalizedNote}`,
576
+ });
577
+ }
578
+ return messages;
579
+ }
580
+ export function createToolResultMessage(input) {
581
+ return {
582
+ role: 'tool',
583
+ tool_name: input.toolName,
584
+ tool_call_id: input.toolCallId,
585
+ content: JSON.stringify({
586
+ success: input.output.success,
587
+ ...(input.output.success ? { data: input.output.data ?? null } : {}),
588
+ ...(!input.output.success ? { error: input.output.error ?? null } : {}),
589
+ }),
590
+ };
591
+ }
592
+ export function createApprovalResolutionMessage(input) {
593
+ return {
594
+ role: 'tool',
595
+ tool_name: input.toolName ?? APPROVAL_STATUS_TOOL_NAME,
596
+ tool_call_id: input.requestId,
597
+ content: JSON.stringify({
598
+ approval: {
599
+ request_id: input.requestId,
600
+ approved: input.approved,
601
+ approved_by: input.approvedBy,
602
+ ...(input.reason ? { reason: input.reason } : {}),
603
+ },
604
+ }),
605
+ };
606
+ }
607
+ function normalizeToolError(error, fallbackMessage) {
608
+ return {
609
+ code: error?.code ?? TOOL_ERROR_CODES.TOOL_EXECUTION_FAILED,
610
+ message: error?.message ?? fallbackMessage,
611
+ };
612
+ }
613
+ function normalizeTurnOutput(value) {
614
+ if (!isRecord(value)) {
615
+ return null;
616
+ }
617
+ if (typeof value.status !== 'string' ||
618
+ typeof value.intent !== 'string' ||
619
+ typeof value.assistant_message !== 'string' ||
620
+ typeof value.finish_reason !== 'string') {
621
+ return null;
622
+ }
623
+ const provider = isRecord(value.provider) ? value.provider : null;
624
+ if (!provider || typeof provider.kind !== 'string' || typeof provider.model !== 'string') {
625
+ return null;
626
+ }
627
+ const requestedTool = isRecord(value.requested_tool) ? value.requested_tool : undefined;
628
+ return {
629
+ status: value.status,
630
+ intent: value.intent,
631
+ assistant_message: value.assistant_message,
632
+ ...(requestedTool && typeof requestedTool.name === 'string' && isRecord(requestedTool.input)
633
+ ? {
634
+ requested_tool: {
635
+ name: requestedTool.name,
636
+ input: requestedTool.input,
637
+ },
638
+ }
639
+ : {}),
640
+ provider: {
641
+ kind: provider.kind,
642
+ model: provider.model,
643
+ },
644
+ ...(isRecord(value.usage) ? { usage: value.usage } : {}),
645
+ finish_reason: value.finish_reason,
646
+ };
647
+ }
648
+ function extractApprovalRequestId(output) {
649
+ const details = isRecord(output.error?.details) ? output.error?.details : null;
650
+ const requestId = details?.request_id;
651
+ return typeof requestId === 'string' && requestId.trim().length > 0
652
+ ? requestId
653
+ : 'approval-request-missing';
654
+ }
655
+ function normalizeOptionalText(value) {
656
+ if (typeof value !== 'string') {
657
+ return null;
658
+ }
659
+ const trimmed = value.trim();
660
+ return trimmed.length > 0 ? trimmed : null;
661
+ }
662
+ function isRecord(value) {
663
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
664
+ }
665
+ function materializeWorkflowState(baseState, result, timestamp) {
666
+ if (result.kind === 'completed') {
667
+ return {
668
+ ...baseState,
669
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED,
670
+ updated_at: timestamp,
671
+ messages: result.messages,
672
+ history: result.history,
673
+ turn_count: result.turn_count,
674
+ tool_call_count: result.tool_call_count,
675
+ pending_request_id: undefined,
676
+ requested_tool: undefined,
677
+ last_turn: result.final_turn,
678
+ continuations: appendWorkflowContinuation(baseState.continuations, {
679
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.COMPLETED,
680
+ timestamp,
681
+ reason: WORKFLOW_COMPLETED_REASON,
682
+ }),
683
+ };
684
+ }
685
+ if (result.kind === 'approval_required') {
686
+ return {
687
+ ...baseState,
688
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.WAITING_APPROVAL,
689
+ updated_at: timestamp,
690
+ messages: result.messages,
691
+ history: result.history,
692
+ turn_count: result.turn_count,
693
+ tool_call_count: result.tool_call_count,
694
+ pending_request_id: result.pending_request_id,
695
+ requested_tool: result.requested_tool,
696
+ last_turn: result.last_turn,
697
+ continuations: appendWorkflowContinuation(baseState.continuations, {
698
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.APPROVAL_REQUIRED,
699
+ timestamp,
700
+ reason: WORKFLOW_WAITING_REASON,
701
+ request_id: result.pending_request_id,
702
+ }),
703
+ };
704
+ }
705
+ if (result.kind === 'suspended') {
706
+ return {
707
+ ...baseState,
708
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.SUSPENDED,
709
+ updated_at: timestamp,
710
+ messages: result.messages,
711
+ history: result.history,
712
+ turn_count: result.turn_count,
713
+ tool_call_count: result.tool_call_count,
714
+ pending_request_id: undefined,
715
+ requested_tool: undefined,
716
+ continuations: appendWorkflowContinuation(baseState.continuations, {
717
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SUSPENDED,
718
+ timestamp,
719
+ reason: WORKFLOW_SUSPEND_REASON,
720
+ }),
721
+ };
722
+ }
723
+ return {
724
+ ...baseState,
725
+ status: AGENT_RUNTIME_WORKFLOW_STATUSES.ERROR,
726
+ updated_at: timestamp,
727
+ messages: result.messages,
728
+ history: result.history,
729
+ turn_count: result.turn_count,
730
+ tool_call_count: result.tool_call_count,
731
+ pending_request_id: undefined,
732
+ requested_tool: undefined,
733
+ continuations: appendWorkflowContinuation(baseState.continuations, {
734
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.ERROR,
735
+ timestamp,
736
+ reason: result.error.message,
737
+ }),
738
+ };
739
+ }
740
+ function appendWorkflowContinuation(continuations, input) {
741
+ return [
742
+ ...continuations,
743
+ {
744
+ sequence: continuations.length,
745
+ ...input,
746
+ },
747
+ ];
748
+ }
749
+ function cloneExecuteTurnInput(input) {
750
+ return {
751
+ ...input,
752
+ messages: [...input.messages],
753
+ ...(input.tool_catalog ? { tool_catalog: [...input.tool_catalog] } : {}),
754
+ ...(input.intent_catalog ? { intent_catalog: [...input.intent_catalog] } : {}),
755
+ ...(input.limits ? { limits: { ...input.limits } } : {}),
756
+ };
757
+ }
758
+ function parseWorkflowState(value, filePath) {
759
+ if (!isRecord(value)) {
760
+ throw new Error(`Failed to parse workflow state at ${filePath}: expected an object payload.`);
761
+ }
762
+ if (value.schema_version !== AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION ||
763
+ typeof value.session_id !== 'string' ||
764
+ typeof value.status !== 'string') {
765
+ throw new Error(`Failed to parse workflow state at ${filePath}: missing schema_version, session_id, or status.`);
766
+ }
767
+ const requestedTool = parseWorkflowRequestedTool(value.requested_tool);
768
+ const lastTurn = normalizeTurnOutput(value.last_turn);
769
+ return {
770
+ schema_version: AGENT_RUNTIME_WORKFLOW_SCHEMA_VERSION,
771
+ session_id: value.session_id,
772
+ ...(typeof value.task_id === 'string' ? { task_id: value.task_id } : {}),
773
+ ...(typeof value.run_id === 'string' ? { run_id: value.run_id } : {}),
774
+ status: value.status,
775
+ created_at: readWorkflowTimestamp(value.created_at, `${filePath}.created_at`),
776
+ updated_at: readWorkflowTimestamp(value.updated_at, `${filePath}.updated_at`),
777
+ execute_turn_input: parseWorkflowExecuteTurnInput(value.execute_turn_input, `${filePath}.execute_turn_input`),
778
+ messages: parseWorkflowMessages(value.messages, `${filePath}.messages`),
779
+ history: parseWorkflowHistory(value.history, `${filePath}.history`),
780
+ turn_count: readWorkflowCount(value.turn_count, `${filePath}.turn_count`),
781
+ tool_call_count: readWorkflowCount(value.tool_call_count, `${filePath}.tool_call_count`),
782
+ continuations: parseWorkflowContinuations(value.continuations, `${filePath}.continuations`),
783
+ ...(typeof value.pending_request_id === 'string'
784
+ ? { pending_request_id: value.pending_request_id }
785
+ : {}),
786
+ ...(requestedTool ? { requested_tool: requestedTool } : {}),
787
+ ...(lastTurn ? { last_turn: lastTurn } : {}),
788
+ ...(isRecord(value.workflow)
789
+ ? { workflow: parseWorkflowGraphState(value.workflow, `${filePath}.workflow`) }
790
+ : {}),
791
+ ...(typeof value.next_wake_at === 'string' ? { next_wake_at: value.next_wake_at } : {}),
792
+ };
793
+ }
794
+ function parseWorkflowExecuteTurnInput(value, filePath) {
795
+ if (!isRecord(value)) {
796
+ throw new Error(`Failed to parse workflow state at ${filePath}: execute_turn_input is invalid.`);
797
+ }
798
+ const sessionId = readRequiredString(value.session_id, `${filePath}.session_id`);
799
+ const modelProfile = readRequiredString(value.model_profile, `${filePath}.model_profile`);
800
+ const url = readRequiredString(value.url, `${filePath}.url`);
801
+ const messages = parseWorkflowMessages(value.messages, `${filePath}.messages`);
802
+ const toolCatalog = Array.isArray(value.tool_catalog)
803
+ ? value.tool_catalog.map((entry, index) => parseWorkflowToolCatalogEntry(entry, `${filePath}.tool_catalog[${index}]`))
804
+ : undefined;
805
+ const intentCatalog = Array.isArray(value.intent_catalog)
806
+ ? value.intent_catalog.map((entry, index) => parseWorkflowIntentCatalogEntry(entry, `${filePath}.intent_catalog[${index}]`))
807
+ : undefined;
808
+ return {
809
+ session_id: sessionId,
810
+ model_profile: modelProfile,
811
+ url,
812
+ ...(typeof value.stream === 'boolean' ? { stream: value.stream } : {}),
813
+ messages,
814
+ ...(toolCatalog ? { tool_catalog: toolCatalog } : {}),
815
+ ...(intentCatalog ? { intent_catalog: intentCatalog } : {}),
816
+ ...(isRecord(value.limits) ? { limits: { ...value.limits } } : {}),
817
+ };
818
+ }
819
+ function parseWorkflowMessages(value, filePath) {
820
+ if (!Array.isArray(value)) {
821
+ throw new Error(`Failed to parse workflow state at ${filePath}: expected an array of messages.`);
822
+ }
823
+ return value.map((entry, index) => parseWorkflowMessage(entry, `${filePath}[${index}]`));
824
+ }
825
+ function parseWorkflowMessage(value, filePath) {
826
+ if (!isRecord(value)) {
827
+ throw new Error(`Failed to parse workflow state at ${filePath}: message entry is invalid.`);
828
+ }
829
+ const role = readRequiredString(value.role, `${filePath}.role`);
830
+ const content = readRequiredString(value.content, `${filePath}.content`);
831
+ if (!isAgentRuntimeMessageRole(role)) {
832
+ throw new Error(`Failed to parse workflow state at ${filePath}: message role must be system, user, assistant, or tool.`);
833
+ }
834
+ return {
835
+ role,
836
+ content,
837
+ ...(typeof value.tool_name === 'string' ? { tool_name: value.tool_name } : {}),
838
+ ...(typeof value.tool_call_id === 'string' ? { tool_call_id: value.tool_call_id } : {}),
839
+ };
840
+ }
841
+ function isAgentRuntimeMessageRole(value) {
842
+ return value === 'system' || value === 'user' || value === 'assistant' || value === 'tool';
843
+ }
844
+ function parseWorkflowHistory(value, filePath) {
845
+ if (!Array.isArray(value)) {
846
+ throw new Error(`Failed to parse workflow state at ${filePath}: expected an array of history entries.`);
847
+ }
848
+ return value.map((entry, index) => parseWorkflowHistoryEntry(entry, `${filePath}[${index}]`));
849
+ }
850
+ function parseWorkflowHistoryEntry(value, filePath) {
851
+ if (!isRecord(value)) {
852
+ throw new Error(`Failed to parse workflow state at ${filePath}: history entry is invalid.`);
853
+ }
854
+ const turnIndex = readWorkflowCount(value.turn_index, `${filePath}.turn_index`);
855
+ const turnOutput = normalizeTurnOutput(value.turn_output);
856
+ if (!turnOutput) {
857
+ throw new Error(`Failed to parse workflow state at ${filePath}: turn_output is invalid.`);
858
+ }
859
+ const toolOutput = parseWorkflowToolOutput(value.tool_output, `${filePath}.tool_output`);
860
+ return {
861
+ turn_index: turnIndex,
862
+ turn_output: turnOutput,
863
+ ...(typeof value.tool_call_id === 'string' ? { tool_call_id: value.tool_call_id } : {}),
864
+ ...(toolOutput ? { tool_output: toolOutput } : {}),
865
+ };
866
+ }
867
+ function parseWorkflowToolOutput(value, filePath) {
868
+ if (value === undefined) {
869
+ return undefined;
870
+ }
871
+ if (!isRecord(value) || typeof value.success !== 'boolean') {
872
+ throw new Error(`Failed to parse workflow state at ${filePath}: tool_output is invalid.`);
873
+ }
874
+ return {
875
+ success: value.success,
876
+ ...(value.success ? { data: value.data } : {}),
877
+ ...(!value.success && isRecord(value.error)
878
+ ? {
879
+ error: {
880
+ code: readRequiredString(value.error.code, `${filePath}.error.code`),
881
+ message: readRequiredString(value.error.message, `${filePath}.error.message`),
882
+ ...(isRecord(value.error.details) ? { details: value.error.details } : {}),
883
+ },
884
+ }
885
+ : {}),
886
+ };
887
+ }
888
+ function parseWorkflowContinuations(value, filePath) {
889
+ if (!Array.isArray(value)) {
890
+ throw new Error(`Failed to parse workflow state at ${filePath}: expected an array of continuations.`);
891
+ }
892
+ return value.map((entry, index) => {
893
+ if (!isRecord(entry)) {
894
+ throw new Error(`Failed to parse workflow state at ${filePath}[${index}]: continuation entry is invalid.`);
895
+ }
896
+ return {
897
+ sequence: readWorkflowCount(entry.sequence, `${filePath}[${index}].sequence`),
898
+ kind: readRequiredString(entry.kind, `${filePath}[${index}].kind`),
899
+ timestamp: readWorkflowTimestamp(entry.timestamp, `${filePath}[${index}].timestamp`),
900
+ ...(typeof entry.reason === 'string' ? { reason: entry.reason } : {}),
901
+ ...(typeof entry.request_id === 'string' ? { request_id: entry.request_id } : {}),
902
+ ...(typeof entry.node_id === 'string' ? { node_id: entry.node_id } : {}),
903
+ };
904
+ });
905
+ }
906
+ function parseWorkflowGraphState(value, filePath) {
907
+ if (!Array.isArray(value.nodes)) {
908
+ throw new Error(`Failed to parse workflow state at ${filePath}.nodes: expected an array of workflow nodes.`);
909
+ }
910
+ return {
911
+ nodes: value.nodes.map((entry, index) => parseWorkflowNodeState(entry, `${filePath}.nodes[${index}]`)),
912
+ };
913
+ }
914
+ function parseWorkflowNodeState(value, filePath) {
915
+ if (!isRecord(value)) {
916
+ throw new Error(`Failed to parse workflow state at ${filePath}: workflow node is invalid.`);
917
+ }
918
+ const requestedTool = parseWorkflowRequestedTool(value.requested_tool);
919
+ const lastTurn = normalizeTurnOutput(value.last_turn);
920
+ return {
921
+ node_id: readRequiredString(value.node_id, `${filePath}.node_id`),
922
+ status: readRequiredString(value.status, `${filePath}.status`),
923
+ execute_turn_input: parseWorkflowExecuteTurnInput(value.execute_turn_input, `${filePath}.execute_turn_input`),
924
+ depends_on: Array.isArray(value.depends_on)
925
+ ? value.depends_on.map((entry, index) => readRequiredString(entry, `${filePath}.depends_on[${index}]`))
926
+ : [],
927
+ ...(typeof value.wake_at === 'string' ? { wake_at: value.wake_at } : {}),
928
+ messages: parseWorkflowMessages(value.messages, `${filePath}.messages`),
929
+ history: parseWorkflowHistory(value.history, `${filePath}.history`),
930
+ turn_count: readWorkflowCount(value.turn_count, `${filePath}.turn_count`),
931
+ tool_call_count: readWorkflowCount(value.tool_call_count, `${filePath}.tool_call_count`),
932
+ ...(typeof value.pending_request_id === 'string'
933
+ ? { pending_request_id: value.pending_request_id }
934
+ : {}),
935
+ ...(requestedTool ? { requested_tool: requestedTool } : {}),
936
+ ...(lastTurn ? { last_turn: lastTurn } : {}),
937
+ };
938
+ }
939
+ function parseWorkflowRequestedTool(value) {
940
+ if (!isRecord(value) || typeof value.name !== 'string' || !isRecord(value.input)) {
941
+ return undefined;
942
+ }
943
+ return {
944
+ name: value.name,
945
+ input: value.input,
946
+ };
947
+ }
948
+ function parseWorkflowToolCatalogEntry(value, filePath) {
949
+ if (!isRecord(value)) {
950
+ throw new Error(`Failed to parse workflow state at ${filePath}: tool catalog entry is invalid.`);
951
+ }
952
+ return {
953
+ name: readRequiredString(value.name, `${filePath}.name`),
954
+ description: readRequiredString(value.description, `${filePath}.description`),
955
+ ...(isRecord(value.input_schema) ? { input_schema: value.input_schema } : {}),
956
+ };
957
+ }
958
+ function parseWorkflowIntentCatalogEntry(value, filePath) {
959
+ if (!isRecord(value)) {
960
+ throw new Error(`Failed to parse workflow state at ${filePath}: intent catalog entry is invalid.`);
961
+ }
962
+ return {
963
+ id: readRequiredString(value.id, `${filePath}.id`),
964
+ description: readRequiredString(value.description, `${filePath}.description`),
965
+ };
966
+ }
967
+ function readRequiredString(value, filePath) {
968
+ if (typeof value !== 'string' || value.trim().length === 0) {
969
+ throw new Error(`Failed to parse workflow state at ${filePath}: expected a non-empty string.`);
970
+ }
971
+ return value;
972
+ }
973
+ function readWorkflowTimestamp(value, filePath) {
974
+ return readRequiredString(value, filePath);
975
+ }
976
+ function readWorkflowCount(value, filePath) {
977
+ if (!Number.isInteger(value) || Number(value) < 0) {
978
+ throw new Error(`Failed to parse workflow state at ${filePath}: expected a non-negative integer.`);
979
+ }
980
+ return Number(value);
981
+ }
982
+ function resolveCurrentTimestamp(now) {
983
+ return now ? now() : new Date().toISOString();
984
+ }
985
+ function assertWorkflowDefinitions(nodes) {
986
+ const ids = new Set(nodes.map((node) => node.node_id));
987
+ if (ids.size !== nodes.length) {
988
+ throw new Error('Workflow definition contains duplicate node IDs. Each workflow node must declare a unique id.');
989
+ }
990
+ for (const node of nodes) {
991
+ for (const dependencyId of node.depends_on) {
992
+ if (!ids.has(dependencyId)) {
993
+ throw new Error(`Workflow node "${node.node_id}" depends on "${dependencyId}", but that node is not defined.`);
994
+ }
995
+ }
996
+ }
997
+ }
998
+ function getReadyWorkflowNodes(state, timestamp) {
999
+ const workflow = state.workflow;
1000
+ if (!workflow) {
1001
+ return [];
1002
+ }
1003
+ return workflow.nodes.filter((node) => isWorkflowNodeReady(node, workflow.nodes, timestamp));
1004
+ }
1005
+ function getScheduledWorkflowNodes(state, timestamp) {
1006
+ const workflow = state.workflow;
1007
+ if (!workflow) {
1008
+ return [];
1009
+ }
1010
+ return workflow.nodes.filter((node) => node.status !== AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED &&
1011
+ node.status !== AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR &&
1012
+ node.status !== AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL &&
1013
+ typeof node.wake_at === 'string' &&
1014
+ node.wake_at > timestamp &&
1015
+ areWorkflowDependenciesCompleted(node, workflow.nodes));
1016
+ }
1017
+ function isWorkflowNodeReady(node, allNodes, timestamp) {
1018
+ if (node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED ||
1019
+ node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR ||
1020
+ node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL) {
1021
+ return false;
1022
+ }
1023
+ if (!areWorkflowDependenciesCompleted(node, allNodes)) {
1024
+ return false;
1025
+ }
1026
+ if (typeof node.wake_at === 'string' && node.wake_at > timestamp) {
1027
+ return false;
1028
+ }
1029
+ return true;
1030
+ }
1031
+ function areWorkflowDependenciesCompleted(node, allNodes) {
1032
+ return node.depends_on.every((dependencyId) => allNodes.some((candidate) => candidate.node_id === dependencyId &&
1033
+ candidate.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED));
1034
+ }
1035
+ function updateWorkflowStateForNodeResult(state, nodeId, result, timestamp) {
1036
+ const workflow = state.workflow;
1037
+ if (!workflow) {
1038
+ return state;
1039
+ }
1040
+ const updatedNodes = workflow.nodes.map((node) => {
1041
+ if (node.node_id !== nodeId) {
1042
+ return node;
1043
+ }
1044
+ const baseNode = {
1045
+ ...node,
1046
+ messages: result.messages,
1047
+ history: result.history,
1048
+ turn_count: result.turn_count,
1049
+ tool_call_count: result.tool_call_count,
1050
+ pending_request_id: undefined,
1051
+ requested_tool: undefined,
1052
+ };
1053
+ if (result.kind === 'completed') {
1054
+ return {
1055
+ ...baseNode,
1056
+ status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED,
1057
+ last_turn: result.final_turn,
1058
+ };
1059
+ }
1060
+ if (result.kind === 'approval_required') {
1061
+ return {
1062
+ ...baseNode,
1063
+ status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL,
1064
+ pending_request_id: result.pending_request_id,
1065
+ requested_tool: result.requested_tool,
1066
+ last_turn: result.last_turn,
1067
+ };
1068
+ }
1069
+ if (result.kind === 'suspended') {
1070
+ return {
1071
+ ...baseNode,
1072
+ status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.SUSPENDED,
1073
+ };
1074
+ }
1075
+ return {
1076
+ ...baseNode,
1077
+ status: AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR,
1078
+ };
1079
+ });
1080
+ let continuations = state.continuations;
1081
+ if (result.kind === 'completed') {
1082
+ continuations = appendWorkflowContinuation(continuations, {
1083
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.BRANCH_COMPLETED,
1084
+ timestamp,
1085
+ node_id: nodeId,
1086
+ });
1087
+ }
1088
+ else if (result.kind === 'approval_required') {
1089
+ continuations = appendWorkflowContinuation(continuations, {
1090
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.APPROVAL_REQUIRED,
1091
+ timestamp,
1092
+ reason: WORKFLOW_WAITING_REASON,
1093
+ request_id: result.pending_request_id,
1094
+ node_id: nodeId,
1095
+ });
1096
+ }
1097
+ else if (result.kind === 'suspended') {
1098
+ continuations = appendWorkflowContinuation(continuations, {
1099
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.SUSPENDED,
1100
+ timestamp,
1101
+ reason: WORKFLOW_SUSPEND_REASON,
1102
+ node_id: nodeId,
1103
+ });
1104
+ }
1105
+ else {
1106
+ continuations = appendWorkflowContinuation(continuations, {
1107
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.ERROR,
1108
+ timestamp,
1109
+ reason: result.error.message,
1110
+ node_id: nodeId,
1111
+ });
1112
+ }
1113
+ for (const node of updatedNodes) {
1114
+ if (node.depends_on.length > 1 &&
1115
+ isWorkflowNodeReady(node, updatedNodes, timestamp) &&
1116
+ !hasContinuationForNode(continuations, AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.JOIN_READY, node.node_id)) {
1117
+ continuations = appendWorkflowContinuation(continuations, {
1118
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.JOIN_READY,
1119
+ timestamp,
1120
+ reason: WORKFLOW_JOIN_READY_REASON,
1121
+ node_id: node.node_id,
1122
+ });
1123
+ }
1124
+ if (typeof node.wake_at === 'string' &&
1125
+ node.wake_at <= timestamp &&
1126
+ !hasContinuationForNode(continuations, AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.WAKEUP, node.node_id) &&
1127
+ areWorkflowDependenciesCompleted(node, updatedNodes)) {
1128
+ continuations = appendWorkflowContinuation(continuations, {
1129
+ kind: AGENT_RUNTIME_WORKFLOW_CONTINUATION_KINDS.WAKEUP,
1130
+ timestamp,
1131
+ reason: WORKFLOW_WAKEUP_REASON,
1132
+ node_id: node.node_id,
1133
+ });
1134
+ }
1135
+ }
1136
+ return {
1137
+ ...state,
1138
+ status: deriveWorkflowStatus(updatedNodes),
1139
+ updated_at: timestamp,
1140
+ workflow: {
1141
+ nodes: updatedNodes,
1142
+ },
1143
+ continuations,
1144
+ next_wake_at: undefined,
1145
+ };
1146
+ }
1147
+ function deriveWorkflowStatus(nodes) {
1148
+ if (nodes.every((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED)) {
1149
+ return AGENT_RUNTIME_WORKFLOW_STATUSES.COMPLETED;
1150
+ }
1151
+ if (nodes.some((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.WAITING_APPROVAL)) {
1152
+ return AGENT_RUNTIME_WORKFLOW_STATUSES.WAITING_APPROVAL;
1153
+ }
1154
+ if (nodes.some((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.ERROR)) {
1155
+ return AGENT_RUNTIME_WORKFLOW_STATUSES.ERROR;
1156
+ }
1157
+ if (nodes.some((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.SUSPENDED)) {
1158
+ return AGENT_RUNTIME_WORKFLOW_STATUSES.SUSPENDED;
1159
+ }
1160
+ return AGENT_RUNTIME_WORKFLOW_STATUSES.ACTIVE;
1161
+ }
1162
+ function allWorkflowNodesCompleted(state) {
1163
+ return (state.workflow?.nodes.every((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED) ?? false);
1164
+ }
1165
+ function getCompletedNodeIds(state) {
1166
+ return (state.workflow?.nodes
1167
+ .filter((node) => node.status === AGENT_RUNTIME_WORKFLOW_NODE_STATUSES.COMPLETED)
1168
+ .map((node) => node.node_id) ?? []);
1169
+ }
1170
+ function hasContinuationForNode(continuations, kind, nodeId) {
1171
+ return continuations.some((continuation) => continuation.kind === kind && continuation.node_id === nodeId);
1172
+ }
1173
+ //# sourceMappingURL=orchestration.js.map