@adhd/agent-engine-orchestrator 2.1.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 (173) hide show
  1. package/package.json +64 -0
  2. package/src/clients/http-client.d.ts +24 -0
  3. package/src/clients/http-client.d.ts.map +1 -0
  4. package/src/clients/http-client.js +96 -0
  5. package/src/clients/http-client.js.map +1 -0
  6. package/src/clients/in-process.d.ts +19 -0
  7. package/src/clients/in-process.d.ts.map +1 -0
  8. package/src/clients/in-process.js +25 -0
  9. package/src/clients/in-process.js.map +1 -0
  10. package/src/clients/index.d.ts +9 -0
  11. package/src/clients/index.d.ts.map +1 -0
  12. package/src/clients/index.js +6 -0
  13. package/src/clients/index.js.map +1 -0
  14. package/src/clients/registry.d.ts +30 -0
  15. package/src/clients/registry.d.ts.map +1 -0
  16. package/src/clients/registry.js +173 -0
  17. package/src/clients/registry.js.map +1 -0
  18. package/src/clients/stdio-client.d.ts +19 -0
  19. package/src/clients/stdio-client.d.ts.map +1 -0
  20. package/src/clients/stdio-client.js +91 -0
  21. package/src/clients/stdio-client.js.map +1 -0
  22. package/src/clients/tool-naming.d.ts +8 -0
  23. package/src/clients/tool-naming.d.ts.map +1 -0
  24. package/src/clients/tool-naming.js +26 -0
  25. package/src/clients/tool-naming.js.map +1 -0
  26. package/src/clients/types.d.ts +7 -0
  27. package/src/clients/types.d.ts.map +1 -0
  28. package/src/clients/types.js +2 -0
  29. package/src/clients/types.js.map +1 -0
  30. package/src/engine/context-window.d.ts +74 -0
  31. package/src/engine/context-window.d.ts.map +1 -0
  32. package/src/engine/context-window.js +156 -0
  33. package/src/engine/context-window.js.map +1 -0
  34. package/src/engine/dag-engine.d.ts +69 -0
  35. package/src/engine/dag-engine.d.ts.map +1 -0
  36. package/src/engine/dag-engine.js +103 -0
  37. package/src/engine/dag-engine.js.map +1 -0
  38. package/src/engine/hooks.d.ts +10 -0
  39. package/src/engine/hooks.d.ts.map +1 -0
  40. package/src/engine/hooks.js +39 -0
  41. package/src/engine/hooks.js.map +1 -0
  42. package/src/engine/orchestrator.d.ts +59 -0
  43. package/src/engine/orchestrator.d.ts.map +1 -0
  44. package/src/engine/orchestrator.js +665 -0
  45. package/src/engine/orchestrator.js.map +1 -0
  46. package/src/engine/policy.d.ts +22 -0
  47. package/src/engine/policy.d.ts.map +1 -0
  48. package/src/engine/policy.js +73 -0
  49. package/src/engine/policy.js.map +1 -0
  50. package/src/engine/prompt-resolver.d.ts +24 -0
  51. package/src/engine/prompt-resolver.d.ts.map +1 -0
  52. package/src/engine/prompt-resolver.js +55 -0
  53. package/src/engine/prompt-resolver.js.map +1 -0
  54. package/src/engine/queue.d.ts +12 -0
  55. package/src/engine/queue.d.ts.map +1 -0
  56. package/src/engine/queue.js +31 -0
  57. package/src/engine/queue.js.map +1 -0
  58. package/src/engine/tool-advertisement.d.ts +5 -0
  59. package/src/engine/tool-advertisement.d.ts.map +1 -0
  60. package/src/engine/tool-advertisement.js +70 -0
  61. package/src/engine/tool-advertisement.js.map +1 -0
  62. package/src/index.d.ts +50 -0
  63. package/src/index.d.ts.map +1 -0
  64. package/src/index.js +39 -0
  65. package/src/index.js.map +1 -0
  66. package/src/interfaces.d.ts +70 -0
  67. package/src/interfaces.d.ts.map +1 -0
  68. package/src/interfaces.js +9 -0
  69. package/src/interfaces.js.map +1 -0
  70. package/src/plugins/index.d.ts +4 -0
  71. package/src/plugins/index.d.ts.map +1 -0
  72. package/src/plugins/index.js +3 -0
  73. package/src/plugins/index.js.map +1 -0
  74. package/src/plugins/loader.d.ts +47 -0
  75. package/src/plugins/loader.d.ts.map +1 -0
  76. package/src/plugins/loader.js +174 -0
  77. package/src/plugins/loader.js.map +1 -0
  78. package/src/plugins/usage-plugin.d.ts +15 -0
  79. package/src/plugins/usage-plugin.d.ts.map +1 -0
  80. package/src/plugins/usage-plugin.js +140 -0
  81. package/src/plugins/usage-plugin.js.map +1 -0
  82. package/src/providers/anthropic.d.ts +35 -0
  83. package/src/providers/anthropic.d.ts.map +1 -0
  84. package/src/providers/anthropic.js +244 -0
  85. package/src/providers/anthropic.js.map +1 -0
  86. package/src/providers/claudecli.d.ts +36 -0
  87. package/src/providers/claudecli.d.ts.map +1 -0
  88. package/src/providers/claudecli.js +324 -0
  89. package/src/providers/claudecli.js.map +1 -0
  90. package/src/providers/factory.d.ts +5 -0
  91. package/src/providers/factory.d.ts.map +1 -0
  92. package/src/providers/factory.js +18 -0
  93. package/src/providers/factory.js.map +1 -0
  94. package/src/providers/index.d.ts +6 -0
  95. package/src/providers/index.d.ts.map +1 -0
  96. package/src/providers/index.js +5 -0
  97. package/src/providers/index.js.map +1 -0
  98. package/src/providers/openai.d.ts +30 -0
  99. package/src/providers/openai.d.ts.map +1 -0
  100. package/src/providers/openai.js +177 -0
  101. package/src/providers/openai.js.map +1 -0
  102. package/src/providers/types.d.ts +22 -0
  103. package/src/providers/types.d.ts.map +1 -0
  104. package/src/providers/types.js +2 -0
  105. package/src/providers/types.js.map +1 -0
  106. package/src/tools/agent-crud.d.ts +29 -0
  107. package/src/tools/agent-crud.d.ts.map +1 -0
  108. package/src/tools/agent-crud.js +28 -0
  109. package/src/tools/agent-crud.js.map +1 -0
  110. package/src/tools/session.d.ts +35 -0
  111. package/src/tools/session.d.ts.map +1 -0
  112. package/src/tools/session.js +52 -0
  113. package/src/tools/session.js.map +1 -0
  114. package/src/tools/task.d.ts +94 -0
  115. package/src/tools/task.d.ts.map +1 -0
  116. package/src/tools/task.js +336 -0
  117. package/src/tools/task.js.map +1 -0
  118. package/src/tools/usage.d.ts +19 -0
  119. package/src/tools/usage.d.ts.map +1 -0
  120. package/src/tools/usage.js +152 -0
  121. package/src/tools/usage.js.map +1 -0
  122. package/src/utils/ids.d.ts +2 -0
  123. package/src/utils/ids.d.ts.map +1 -0
  124. package/src/utils/ids.js +5 -0
  125. package/src/utils/ids.js.map +1 -0
  126. package/src/utils/timestamps.d.ts +2 -0
  127. package/src/utils/timestamps.d.ts.map +1 -0
  128. package/src/utils/timestamps.js +4 -0
  129. package/src/utils/timestamps.js.map +1 -0
  130. package/src/validation/agent.d.ts +556 -0
  131. package/src/validation/agent.d.ts.map +1 -0
  132. package/src/validation/agent.js +175 -0
  133. package/src/validation/agent.js.map +1 -0
  134. package/src/validation/agents.d.ts +2 -0
  135. package/src/validation/agents.d.ts.map +1 -0
  136. package/src/validation/agents.js +3 -0
  137. package/src/validation/agents.js.map +1 -0
  138. package/src/validation/errors.d.ts +32 -0
  139. package/src/validation/errors.d.ts.map +1 -0
  140. package/src/validation/errors.js +34 -0
  141. package/src/validation/errors.js.map +1 -0
  142. package/src/validation/execution.d.ts +101 -0
  143. package/src/validation/execution.d.ts.map +1 -0
  144. package/src/validation/execution.js +14 -0
  145. package/src/validation/execution.js.map +1 -0
  146. package/src/validation/index.d.ts +9 -0
  147. package/src/validation/index.d.ts.map +1 -0
  148. package/src/validation/index.js +9 -0
  149. package/src/validation/index.js.map +1 -0
  150. package/src/validation/mcp.d.ts +54 -0
  151. package/src/validation/mcp.d.ts.map +1 -0
  152. package/src/validation/mcp.js +33 -0
  153. package/src/validation/mcp.js.map +1 -0
  154. package/src/validation/message.d.ts +43 -0
  155. package/src/validation/message.d.ts.map +1 -0
  156. package/src/validation/message.js +28 -0
  157. package/src/validation/message.js.map +1 -0
  158. package/src/validation/session.d.ts +49 -0
  159. package/src/validation/session.d.ts.map +1 -0
  160. package/src/validation/session.js +32 -0
  161. package/src/validation/session.js.map +1 -0
  162. package/src/validation/task.d.ts +156 -0
  163. package/src/validation/task.d.ts.map +1 -0
  164. package/src/validation/task.js +84 -0
  165. package/src/validation/task.js.map +1 -0
  166. package/src/validation/tasks.d.ts +2 -0
  167. package/src/validation/tasks.d.ts.map +1 -0
  168. package/src/validation/tasks.js +3 -0
  169. package/src/validation/tasks.js.map +1 -0
  170. package/src/validation/usage.d.ts +75 -0
  171. package/src/validation/usage.d.ts.map +1 -0
  172. package/src/validation/usage.js +51 -0
  173. package/src/validation/usage.js.map +1 -0
@@ -0,0 +1,665 @@
1
+ import { ToolError } from '../validation/errors.js';
2
+ import { generateId } from '../utils/ids.js';
3
+ import { nowIso } from '../utils/timestamps.js';
4
+ import { renderToolPromptDoc, toNameOnlyTools, } from './tool-advertisement.js';
5
+ import { estimateMessageTokens, groupIntoAtomicUnits, decideCompaction, compactMessages, } from './context-window.js';
6
+ // ── HITL (Human-in-the-Loop) support ─────────────────────────────────────────
7
+ const HITL_TOOL_NAME = 'request_human_input';
8
+ const HITL_BUILTIN_TOOL_DEFINITION = {
9
+ name: 'builtin__request_human_input',
10
+ description: 'Pause the task to ask the human operator a question. The task suspends ' +
11
+ 'until a human answers via task_resume. Use when you need human confirmation, ' +
12
+ 'a decision, or missing information you cannot obtain yourself.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ prompt: {
17
+ type: 'string',
18
+ description: 'The question to ask the human.',
19
+ },
20
+ },
21
+ required: ['prompt'],
22
+ },
23
+ };
24
+ const hitlResolvers = new Map();
25
+ export function resolveHitl(taskId, userInput) {
26
+ const resolve = hitlResolvers.get(taskId);
27
+ if (!resolve)
28
+ return false;
29
+ hitlResolvers.delete(taskId);
30
+ resolve(userInput);
31
+ return true;
32
+ }
33
+ /** Duck-type check */
34
+ function isEnforcementError(err) {
35
+ return (typeof err === 'object' &&
36
+ err !== null &&
37
+ err.isEnforcementError === true);
38
+ }
39
+ const noopHooks = {
40
+ register: () => undefined,
41
+ emit: async () => undefined,
42
+ registerEnforcement: () => undefined,
43
+ enforce: async () => undefined,
44
+ };
45
+ export class Orchestrator {
46
+ async run(input) {
47
+ const { executionContext, registry, provider, policy, taskStore, sessionStore, signal, taskId, hooks = noopHooks, isEphemeral = false, emitTaskEvent, logger: loggerInput, config: configInput, } = input;
48
+ const logger = loggerInput ?? { info: () => { }, warn: () => { }, error: () => { }, debug: () => { } };
49
+ const config = configInput ?? { server: { contextLimit: 0, defaultMaxTokens: 8192 } };
50
+ const emit = (event) => {
51
+ emitTaskEvent?.(event);
52
+ };
53
+ const currentMessages = [...input.messages];
54
+ const contextLimit = config.server.contextLimit;
55
+ const delegationSessions = new Set();
56
+ let taskSucceeded = false;
57
+ try {
58
+ taskStore.updateStatus(taskId, 'running');
59
+ emit({ type: 'status_change', taskId, status: 'running' });
60
+ await hooks.emit('task:start', {
61
+ executionContext,
62
+ messages: currentMessages,
63
+ rootTaskId: executionContext.rootTaskId,
64
+ });
65
+ let finalContent = '';
66
+ const tools = await registry.listAllTools();
67
+ if (executionContext.agentDefinition.allowHumanInput === true &&
68
+ !isEphemeral) {
69
+ tools.push(HITL_BUILTIN_TOOL_DEFINITION);
70
+ }
71
+ // Default to 'full' JSON function-schemas — the contract models are trained on, and
72
+ // what published 2.0.1 always sent. Full schemas are STATIC, so they sit in the cached
73
+ // prefix and bill at the cache-hit rate after the first call, making name-only's payload
74
+ // saving marginal while its reliability cost (malformed calls -> retries) is real.
75
+ // 'names' remains available as an explicit opt-in. See BUG-ORCH-013.
76
+ const advertisementMode = executionContext.agentDefinition.provider.type === 'claudecli'
77
+ ? 'full'
78
+ : (executionContext.agentDefinition.toolAdvertisement ?? 'full');
79
+ const advertisedTools = advertisementMode === 'names' ? toNameOnlyTools(tools) : tools;
80
+ const toolDocSystemMessage = (() => {
81
+ if (advertisementMode !== 'names' || tools.length === 0)
82
+ return null;
83
+ const doc = renderToolPromptDoc(tools);
84
+ const existing = currentMessages[0];
85
+ if (existing && existing.role === 'system') {
86
+ return {
87
+ ...existing,
88
+ content: `${doc}\n\n---\n\n${existing.content}`,
89
+ };
90
+ }
91
+ return {
92
+ id: generateId(),
93
+ sessionId: executionContext.sessionId,
94
+ role: 'system',
95
+ content: doc,
96
+ createdAt: nowIso(),
97
+ };
98
+ })();
99
+ // The provider's own reported prompt_tokens from the last call — the exact, free
100
+ // ground truth for the real context size. Undefined before the first call.
101
+ let lastReportedContext;
102
+ const providerModel = 'model' in executionContext.agentDefinition.provider
103
+ ? executionContext.agentDefinition.provider.model
104
+ : undefined;
105
+ let looping = true;
106
+ while (looping) {
107
+ if (signal.aborted) {
108
+ throw new ToolError('PROVIDER_ERROR', 'Task was cancelled');
109
+ }
110
+ // Cache-preserving context management (BUG-ORCH-008): don't trim from the front
111
+ // every call. Let history grow append-only, and only when the REAL context nears
112
+ // the model's true window compact the middle into one summary — rarely, so the
113
+ // cached prefix survives between compactions.
114
+ {
115
+ const decision = decideCompaction(lastReportedContext, currentMessages, providerModel);
116
+ if (decision.shouldCompact) {
117
+ const before = currentMessages.length;
118
+ const compacted = await compactMessages(currentMessages, async (toSummarise) => {
119
+ const summary = await provider.chat({
120
+ messages: [
121
+ {
122
+ id: generateId(),
123
+ sessionId: executionContext.sessionId,
124
+ role: 'system',
125
+ content: 'Summarise the following conversation excerpt concisely, preserving ' +
126
+ 'decisions, facts, file paths, and any state a continuation would need. ' +
127
+ 'Output only the summary.',
128
+ createdAt: nowIso(),
129
+ },
130
+ ...toSummarise.map((m) => ({
131
+ ...m,
132
+ // Flatten tool traffic to plain text so the summariser needs no tools.
133
+ role: m.role === 'tool' ? 'user' : m.role,
134
+ content: flattenForSummary(m),
135
+ toolCalls: undefined,
136
+ toolResults: undefined,
137
+ })),
138
+ ],
139
+ tools: undefined,
140
+ signal,
141
+ });
142
+ return summary.message.content ?? '';
143
+ });
144
+ if (compacted.length < before) {
145
+ currentMessages.length = 0;
146
+ currentMessages.push(...compacted);
147
+ lastReportedContext = undefined; // recompute against the compacted history
148
+ logger.debug({ taskId, before, after: currentMessages.length, window: decision.window }, 'context compacted');
149
+ taskStore.appendEvent({
150
+ taskId,
151
+ type: 'CONTEXT_COMPACTED',
152
+ payload: {
153
+ messagesBefore: before,
154
+ messagesAfter: currentMessages.length,
155
+ window: decision.window,
156
+ threshold: decision.threshold,
157
+ },
158
+ });
159
+ }
160
+ }
161
+ }
162
+ const composedSignal = AbortSignal.any([
163
+ signal,
164
+ AbortSignal.timeout(executionContext.agentDefinition.provider.timeoutMs ?? 60_000),
165
+ ]);
166
+ taskStore.appendEvent({
167
+ taskId,
168
+ type: 'MODEL_REQUEST',
169
+ payload: {
170
+ messageCount: currentMessages.length,
171
+ toolCount: tools.length,
172
+ toolAdvertisement: advertisementMode,
173
+ },
174
+ });
175
+ logger.debug({
176
+ taskId,
177
+ sessionId: executionContext.sessionId,
178
+ agentName: executionContext.agentName,
179
+ messageCount: currentMessages.length,
180
+ }, 'MODEL_REQUEST');
181
+ await hooks.emit('pre:model_request', {
182
+ executionContext,
183
+ messages: currentMessages,
184
+ tools,
185
+ });
186
+ try {
187
+ await hooks.enforce('pre:model_request', {
188
+ executionContext,
189
+ messages: currentMessages,
190
+ tools,
191
+ });
192
+ }
193
+ catch (err) {
194
+ if (isEnforcementError(err)) {
195
+ throw new ToolError('BUDGET_EXCEEDED', err.message);
196
+ }
197
+ throw err;
198
+ }
199
+ let providerResponse;
200
+ try {
201
+ const baseMessages = contextLimit > 0
202
+ ? windowMessages(currentMessages, contextLimit)
203
+ : currentMessages;
204
+ let messagesToSend = baseMessages;
205
+ if (toolDocSystemMessage) {
206
+ messagesToSend =
207
+ baseMessages[0]?.role === 'system'
208
+ ? [toolDocSystemMessage, ...baseMessages.slice(1)]
209
+ : [toolDocSystemMessage, ...baseMessages];
210
+ }
211
+ providerResponse = await provider.chat({
212
+ messages: messagesToSend,
213
+ tools: advertisedTools.length > 0 ? advertisedTools : undefined,
214
+ signal: composedSignal,
215
+ executeTool: async (server, tool, args) => {
216
+ registry.assertToolAllowed?.(server, tool);
217
+ const client = await registry.getClient(server);
218
+ try {
219
+ const result = await client.callTool(tool, args, composedSignal);
220
+ return { result, isError: false };
221
+ }
222
+ catch (error) {
223
+ return {
224
+ result: error instanceof Error ? error.message : String(error),
225
+ isError: true,
226
+ };
227
+ }
228
+ },
229
+ });
230
+ }
231
+ catch (error) {
232
+ if (signal.aborted) {
233
+ throw new ToolError('PROVIDER_ERROR', 'Task was cancelled');
234
+ }
235
+ if (composedSignal.aborted ||
236
+ (error instanceof Error &&
237
+ (error.name === 'AbortError' || error.name === 'TimeoutError'))) {
238
+ const ms = executionContext.agentDefinition.provider.timeoutMs ?? 60_000;
239
+ throw new ToolError('PROVIDER_TIMEOUT', `Provider call timed out after ${ms}ms. Increase timeoutMs on the agent's provider config.`);
240
+ }
241
+ if (error instanceof Error &&
242
+ (error.constructor.name === 'AuthenticationError' ||
243
+ ('status' in error &&
244
+ error.status === 401))) {
245
+ throw new ToolError('PROVIDER_AUTH_ERROR', `Provider authentication failed: ${error.message}. ` +
246
+ `Set ANTHROPIC_AUTH_TOKEN (run \`claude setup-token\` to obtain an OAuth access token) or use authTokenEnv in the provider config`);
247
+ }
248
+ if (error instanceof Error &&
249
+ (('status' in error &&
250
+ error.status === 429) ||
251
+ error.message?.includes('rate limit') ||
252
+ error.message?.includes('429'))) {
253
+ throw new ToolError('PROVIDER_RATE_LIMITED', `Provider rate limit exceeded: ${error.message}`);
254
+ }
255
+ if (error instanceof Error &&
256
+ (('code' in error &&
257
+ error.code ===
258
+ 'context_length_exceeded') ||
259
+ error.message?.includes('context_length_exceeded') ||
260
+ error.message?.includes('prompt is too long'))) {
261
+ throw new ToolError('CONTEXT_WINDOW_EXCEEDED', `Context window exceeded. Set AGENT_MCP_CONTEXT_LIMIT to enable automatic truncation.`);
262
+ }
263
+ throw new ToolError('PROVIDER_ERROR', `Provider call failed: ${error instanceof Error ? error.message : String(error)}`);
264
+ }
265
+ const assistantMessage = {
266
+ ...providerResponse.message,
267
+ sessionId: executionContext.sessionId,
268
+ };
269
+ await sessionStore.appendMessage(executionContext.sessionId, assistantMessage);
270
+ currentMessages.push(assistantMessage);
271
+ // Record the provider's own context measurement to drive compaction decisions.
272
+ if (providerResponse.usage?.inputTokens !== undefined) {
273
+ lastReportedContext = providerResponse.usage.inputTokens;
274
+ }
275
+ await hooks.emit('post:model_response', {
276
+ executionContext,
277
+ stopReason: providerResponse.stopReason,
278
+ toolCallCount: assistantMessage.toolCalls?.length ?? 0,
279
+ tokenUsage: providerResponse.usage,
280
+ });
281
+ await hooks.emit('message:appended', {
282
+ executionContext,
283
+ message: assistantMessage,
284
+ });
285
+ taskStore.appendEvent({
286
+ taskId,
287
+ type: 'MODEL_RESPONSE',
288
+ payload: {
289
+ stopReason: providerResponse.stopReason,
290
+ hasContent: !!assistantMessage.content,
291
+ toolCallCount: assistantMessage.toolCalls?.length ?? 0,
292
+ inputTokens: providerResponse.usage?.inputTokens,
293
+ outputTokens: providerResponse.usage?.outputTokens,
294
+ cacheReadTokens: providerResponse.usage?.cacheReadTokens,
295
+ cacheCreationTokens: providerResponse.usage?.cacheCreationTokens,
296
+ rawUsage: providerResponse.rawUsage,
297
+ },
298
+ });
299
+ logger.debug({
300
+ taskId,
301
+ agentName: executionContext.agentName,
302
+ stopReason: providerResponse.stopReason,
303
+ }, 'MODEL_RESPONSE');
304
+ if (providerResponse.stopReason === 'completed') {
305
+ finalContent = assistantMessage.content ?? '';
306
+ looping = false;
307
+ continue;
308
+ }
309
+ const toolCalls = assistantMessage.toolCalls ?? [];
310
+ // Phase 1 — serial pre-dispatch loop
311
+ const hitlResults = new Map();
312
+ const warningResults = new Map();
313
+ for (const tc of toolCalls) {
314
+ if (signal.aborted) {
315
+ throw new ToolError('PROVIDER_ERROR', 'Task was cancelled before tool call');
316
+ }
317
+ if (tc.tool === HITL_TOOL_NAME) {
318
+ if (isEphemeral) {
319
+ throw new ToolError('VALIDATION_ERROR', 'request_human_input is not supported for ephemeral tasks');
320
+ }
321
+ const resumeToken = crypto.randomUUID();
322
+ await taskStore.updateStatus(taskId, 'awaiting_input', {
323
+ resumeToken,
324
+ });
325
+ emit({
326
+ type: 'status_change',
327
+ taskId,
328
+ status: 'awaiting_input',
329
+ });
330
+ let abortHandler;
331
+ const userInput = await new Promise((resolve, reject) => {
332
+ hitlResolvers.set(taskId, resolve);
333
+ abortHandler = () => {
334
+ hitlResolvers.delete(taskId);
335
+ reject(new ToolError('PROVIDER_ERROR', 'Task cancelled while awaiting human input'));
336
+ };
337
+ signal.addEventListener('abort', abortHandler, { once: true });
338
+ }).finally(() => {
339
+ if (abortHandler)
340
+ signal.removeEventListener('abort', abortHandler);
341
+ });
342
+ await taskStore.updateStatus(taskId, 'running');
343
+ emit({ type: 'status_change', taskId, status: 'running' });
344
+ hitlResults.set(tc.id, userInput);
345
+ taskStore.appendEvent({
346
+ taskId,
347
+ type: 'TOOL_CALL',
348
+ payload: { tool: HITL_TOOL_NAME, callId: tc.id, resumeToken },
349
+ });
350
+ continue;
351
+ }
352
+ const resolved = registry.resolveToolName?.(`${tc.server}__${tc.tool}`) ?? { server: tc.server, tool: tc.tool };
353
+ const qualifiedToolName = `${resolved.server}__${resolved.tool}`;
354
+ await hooks.emit('pre:tool_call', {
355
+ executionContext,
356
+ toolName: qualifiedToolName,
357
+ callId: tc.id,
358
+ toolInput: tc.arguments,
359
+ });
360
+ try {
361
+ await hooks.enforce('pre:tool_call', {
362
+ executionContext,
363
+ toolName: qualifiedToolName,
364
+ callId: tc.id,
365
+ toolInput: tc.arguments,
366
+ });
367
+ }
368
+ catch (err) {
369
+ const tw = err;
370
+ if (tw?.isToolWarning === true) {
371
+ warningResults.set(tc.id, {
372
+ result: { type: 'text', text: tw.message },
373
+ isError: true,
374
+ });
375
+ continue;
376
+ }
377
+ if (isEnforcementError(err)) {
378
+ throw new ToolError('BUDGET_EXCEEDED', err.message);
379
+ }
380
+ throw err;
381
+ }
382
+ policy.check({
383
+ executionContext,
384
+ targetTool: qualifiedToolName,
385
+ targetAgentName: qualifiedToolName === 'agent-mcp__agent'
386
+ ? tc.arguments?.name
387
+ : undefined,
388
+ });
389
+ executionContext.toolCallCount++;
390
+ }
391
+ // Phase 2 — Promise.all concurrent execution
392
+ const nonHitlToolCalls = toolCalls.filter((tc) => tc.tool !== HITL_TOOL_NAME && !warningResults.has(tc.id));
393
+ const toolResults = await Promise.all(nonHitlToolCalls.map(async (toolCall) => {
394
+ const resolved = registry.resolveToolName?.(`${toolCall.server}__${toolCall.tool}`) ?? { server: toolCall.server, tool: toolCall.tool };
395
+ const qualifiedToolName = `${resolved.server}__${resolved.tool}`;
396
+ emit({
397
+ type: 'tool_call',
398
+ taskId,
399
+ toolName: qualifiedToolName,
400
+ toolCallId: toolCall.id,
401
+ input: toolCall.arguments,
402
+ });
403
+ taskStore.appendEvent({
404
+ taskId,
405
+ type: 'TOOL_CALL',
406
+ payload: {
407
+ tool: qualifiedToolName,
408
+ callId: toolCall.id,
409
+ arguments: JSON.stringify(toolCall.arguments).slice(0, 500),
410
+ },
411
+ });
412
+ logger.info({
413
+ taskId,
414
+ agentName: executionContext.agentName,
415
+ tool: qualifiedToolName,
416
+ callId: toolCall.id,
417
+ }, 'TOOL_CALL');
418
+ registry.assertToolAllowed?.(resolved.server, resolved.tool);
419
+ let toolResult;
420
+ let isError = false;
421
+ try {
422
+ const client = await registry.getClient(resolved.server);
423
+ toolResult = await client.callTool(resolved.tool, toolCall.arguments, composedSignal);
424
+ if (qualifiedToolName === 'agent-mcp__agent' &&
425
+ toolResult != null) {
426
+ const maybeId = toolResult['session_id'];
427
+ if (typeof maybeId === 'string') {
428
+ delegationSessions.add(maybeId);
429
+ }
430
+ }
431
+ }
432
+ catch (error) {
433
+ const FATAL_CODES = [
434
+ 'MAX_DEPTH_EXCEEDED',
435
+ 'MAX_TOOL_LOOPS_EXCEEDED',
436
+ 'DELEGATION_NOT_ALLOWED',
437
+ ];
438
+ if (error instanceof ToolError &&
439
+ FATAL_CODES.includes(error.code)) {
440
+ throw error;
441
+ }
442
+ isError = true;
443
+ toolResult =
444
+ error instanceof Error ? error.message : String(error);
445
+ logger.warn({ taskId, tool: qualifiedToolName, error: toolResult }, 'TOOL_RESULT error');
446
+ }
447
+ const resultSummary = typeof toolResult === 'string'
448
+ ? toolResult.slice(0, 500)
449
+ : JSON.stringify(toolResult).slice(0, 500);
450
+ taskStore.appendEvent({
451
+ taskId,
452
+ type: 'TOOL_RESULT',
453
+ payload: {
454
+ callId: toolCall.id,
455
+ tool: qualifiedToolName,
456
+ isError,
457
+ result: resultSummary,
458
+ },
459
+ });
460
+ emit({
461
+ type: 'tool_result',
462
+ taskId,
463
+ toolCallId: toolCall.id,
464
+ content: toolResult,
465
+ });
466
+ logger.debug({ taskId, tool: qualifiedToolName, isError }, 'TOOL_RESULT');
467
+ await hooks.emit('post:tool_call', {
468
+ executionContext,
469
+ toolName: qualifiedToolName,
470
+ callId: toolCall.id,
471
+ toolInput: toolCall.arguments,
472
+ result: toolResult,
473
+ isError,
474
+ });
475
+ return { toolCall, toolResult, isError };
476
+ }));
477
+ // Phase 3 — serial result append
478
+ const toolResultByCallId = new Map(toolResults.map((r) => [r.toolCall.id, r]));
479
+ for (const tc of toolCalls) {
480
+ let toolResult;
481
+ let isError = false;
482
+ if (hitlResults.has(tc.id)) {
483
+ toolResult = hitlResults.get(tc.id);
484
+ }
485
+ else if (warningResults.has(tc.id)) {
486
+ const w = warningResults.get(tc.id);
487
+ if (!w)
488
+ continue;
489
+ toolResult = w.result;
490
+ isError = w.isError;
491
+ }
492
+ else {
493
+ const r = toolResultByCallId.get(tc.id);
494
+ if (!r)
495
+ continue;
496
+ toolResult = r.toolResult;
497
+ isError = r.isError;
498
+ const resolved = registry.resolveToolName?.(`${tc.server}__${tc.tool}`) ?? { server: tc.server, tool: tc.tool };
499
+ const qualifiedToolName = `${resolved.server}__${resolved.tool}`;
500
+ const transformPayload = {
501
+ executionContext,
502
+ toolName: qualifiedToolName,
503
+ callId: tc.id,
504
+ toolInput: tc.arguments,
505
+ result: toolResult,
506
+ isError,
507
+ };
508
+ await hooks.emit('transform:tool_result', transformPayload);
509
+ toolResult = transformPayload.result;
510
+ isError = transformPayload.isError;
511
+ }
512
+ const toolResultMessage = {
513
+ id: generateId(),
514
+ sessionId: executionContext.sessionId,
515
+ role: 'tool',
516
+ toolResults: [
517
+ {
518
+ toolCallId: tc.id,
519
+ result: toolResult,
520
+ isError,
521
+ },
522
+ ],
523
+ createdAt: nowIso(),
524
+ };
525
+ await sessionStore.appendMessage(executionContext.sessionId, toolResultMessage);
526
+ currentMessages.push(toolResultMessage);
527
+ await hooks.emit('message:appended', {
528
+ executionContext,
529
+ message: toolResultMessage,
530
+ });
531
+ }
532
+ if (providerResponse.stopReason === 'tool_calls' &&
533
+ (assistantMessage.toolCalls ?? []).length === 0) {
534
+ finalContent = assistantMessage.content ?? '';
535
+ looping = false;
536
+ }
537
+ }
538
+ taskStore.updateStatus(taskId, 'completed', {
539
+ result: finalContent,
540
+ completedAt: nowIso(),
541
+ });
542
+ emit({ type: 'status_change', taskId, status: 'completed' });
543
+ taskStore.appendEvent({
544
+ taskId,
545
+ type: 'TASK_COMPLETED',
546
+ payload: { result: finalContent },
547
+ });
548
+ logger.info({ taskId, agentName: executionContext.agentName }, 'TASK_COMPLETED');
549
+ await hooks.emit('task:completed', {
550
+ executionContext,
551
+ result: finalContent,
552
+ });
553
+ emit({
554
+ type: 'done',
555
+ taskId,
556
+ result: finalContent,
557
+ error: null,
558
+ });
559
+ taskSucceeded = true;
560
+ return { result: finalContent };
561
+ }
562
+ catch (error) {
563
+ const isCancelled = signal.aborted;
564
+ if (isCancelled) {
565
+ try {
566
+ taskStore.updateStatus(taskId, 'cancelled', {
567
+ cancelledAt: nowIso(),
568
+ error: 'Task was cancelled',
569
+ });
570
+ }
571
+ catch {
572
+ // already cancelled — ignore
573
+ }
574
+ emit({ type: 'status_change', taskId, status: 'cancelled' });
575
+ taskStore.appendEvent({ taskId, type: 'TASK_CANCELLED' });
576
+ await hooks.emit('task:cancelled', { executionContext });
577
+ emit({
578
+ type: 'done',
579
+ taskId,
580
+ result: null,
581
+ error: 'Task was cancelled',
582
+ });
583
+ }
584
+ else {
585
+ const errorMessage = error instanceof Error ? error.message : String(error);
586
+ taskStore.updateStatus(taskId, 'failed', {
587
+ error: errorMessage,
588
+ });
589
+ emit({ type: 'status_change', taskId, status: 'failed' });
590
+ taskStore.appendEvent({
591
+ taskId,
592
+ type: 'TASK_FAILED',
593
+ payload: { error: errorMessage },
594
+ });
595
+ logger.error({ taskId, agentName: executionContext.agentName, error }, 'TASK_FAILED');
596
+ await hooks.emit('task:failed', {
597
+ executionContext,
598
+ error: errorMessage,
599
+ });
600
+ emit({
601
+ type: 'done',
602
+ taskId,
603
+ result: null,
604
+ error: errorMessage,
605
+ });
606
+ }
607
+ throw error;
608
+ }
609
+ finally {
610
+ if (!taskSucceeded) {
611
+ for (const sessionId of delegationSessions) {
612
+ try {
613
+ sessionStore.close(sessionId);
614
+ }
615
+ catch {
616
+ // Already closed or not found — ignore
617
+ }
618
+ }
619
+ }
620
+ await registry.closeAll();
621
+ taskStore.unregisterCancellation(taskId);
622
+ }
623
+ }
624
+ }
625
+ /**
626
+ * Estimate-then-drop window for context-limit enforcement.
627
+ *
628
+ * Always preserves the system message at index 0, then keeps the most recent
629
+ * atomic units that fit the budget, dropping contiguously from the oldest end.
630
+ * Contiguity matters: skipping an oversized unit and then keeping an older,
631
+ * smaller one would punch a hole in the middle of the history and orphan a
632
+ * `tool` message from its `assistant` call.
633
+ *
634
+ * The most recent unit is always retained even if it alone exceeds the limit,
635
+ * so the loop makes progress; an genuinely oversized turn surfaces as the
636
+ * provider's own context-window error rather than as a silently empty history.
637
+ */
638
+ /** Render a message as plain text for the summariser (which is called without tools). */
639
+ function flattenForSummary(m) {
640
+ if (m.content)
641
+ return m.content;
642
+ if (m.toolResults)
643
+ return `[tool result] ${JSON.stringify(m.toolResults.map((r) => r.result))}`;
644
+ if (m.toolCalls) {
645
+ return `[tool calls] ${JSON.stringify(m.toolCalls.map((c) => ({ tool: c.tool, args: c.arguments })))}`;
646
+ }
647
+ return '';
648
+ }
649
+ export function windowMessages(messages, limit) {
650
+ const preserved = messages[0]?.role === 'system' ? [messages[0]] : [];
651
+ const units = groupIntoAtomicUnits(messages.slice(preserved.length));
652
+ let tokens = preserved.reduce((n, m) => n + estimateMessageTokens(m), 0);
653
+ const kept = [];
654
+ for (let i = units.length - 1; i >= 0; i--) {
655
+ const unitTokens = units[i].reduce((n, m) => n + estimateMessageTokens(m), 0);
656
+ const isMostRecent = i === units.length - 1;
657
+ if (!isMostRecent && tokens + unitTokens > limit) {
658
+ break;
659
+ }
660
+ kept.unshift(units[i]);
661
+ tokens += unitTokens;
662
+ }
663
+ return [...preserved, ...kept.flat()];
664
+ }
665
+ //# sourceMappingURL=orchestrator.js.map