stratus-sdk 0.7.6 → 0.9.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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/dist/azure/chat-completions-model.d.ts.map +1 -1
  3. package/dist/azure/chat-completions-model.js +10 -0
  4. package/dist/azure/chat-completions-model.js.map +1 -1
  5. package/dist/azure/responses-model.d.ts.map +1 -1
  6. package/dist/azure/responses-model.js +72 -7
  7. package/dist/azure/responses-model.js.map +1 -1
  8. package/dist/core/builtin-tools.d.ts +11 -0
  9. package/dist/core/builtin-tools.d.ts.map +1 -1
  10. package/dist/core/builtin-tools.js +26 -0
  11. package/dist/core/builtin-tools.js.map +1 -1
  12. package/dist/core/errors.d.ts +5 -0
  13. package/dist/core/errors.d.ts.map +1 -1
  14. package/dist/core/errors.js +10 -0
  15. package/dist/core/errors.js.map +1 -1
  16. package/dist/core/guardrails.d.ts +26 -2
  17. package/dist/core/guardrails.d.ts.map +1 -1
  18. package/dist/core/guardrails.js +22 -6
  19. package/dist/core/guardrails.js.map +1 -1
  20. package/dist/core/handoff.d.ts +18 -1
  21. package/dist/core/handoff.d.ts.map +1 -1
  22. package/dist/core/handoff.js +8 -1
  23. package/dist/core/handoff.js.map +1 -1
  24. package/dist/core/hooks.d.ts +65 -1
  25. package/dist/core/hooks.d.ts.map +1 -1
  26. package/dist/core/index.d.ts +9 -9
  27. package/dist/core/index.d.ts.map +1 -1
  28. package/dist/core/index.js +3 -3
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/model.d.ts +4 -0
  31. package/dist/core/model.d.ts.map +1 -1
  32. package/dist/core/result.d.ts +7 -0
  33. package/dist/core/result.d.ts.map +1 -1
  34. package/dist/core/result.js +8 -0
  35. package/dist/core/result.js.map +1 -1
  36. package/dist/core/run.d.ts +35 -5
  37. package/dist/core/run.d.ts.map +1 -1
  38. package/dist/core/run.js +252 -32
  39. package/dist/core/run.js.map +1 -1
  40. package/dist/core/session.d.ts +15 -2
  41. package/dist/core/session.d.ts.map +1 -1
  42. package/dist/core/session.js +22 -5
  43. package/dist/core/session.js.map +1 -1
  44. package/dist/core/todo.d.ts +13 -39
  45. package/dist/core/todo.d.ts.map +1 -1
  46. package/dist/core/tool.d.ts +6 -0
  47. package/dist/core/tool.d.ts.map +1 -1
  48. package/dist/core/tool.js +2 -0
  49. package/dist/core/tool.js.map +1 -1
  50. package/dist/core/types.d.ts +13 -1
  51. package/dist/core/types.d.ts.map +1 -1
  52. package/dist/core/utils/zod.d.ts +2 -2
  53. package/dist/core/utils/zod.d.ts.map +1 -1
  54. package/dist/core/utils/zod.js +4 -70
  55. package/dist/core/utils/zod.js.map +1 -1
  56. package/package.json +3 -3
package/dist/core/run.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { RunContext } from "./context";
2
- import { MaxBudgetExceededError, MaxTurnsExceededError, OutputParseError, RunAbortedError, StratusError, } from "./errors";
3
- import { runInputGuardrails, runOutputGuardrails } from "./guardrails";
2
+ import { MaxBudgetExceededError, MaxTurnsExceededError, OutputParseError, RunAbortedError, StratusError, ToolTimeoutError, } from "./errors";
3
+ import { runInputGuardrails, runOutputGuardrails, runToolInputGuardrails, runToolOutputGuardrails, } from "./guardrails";
4
4
  import { handoffToDefinition } from "./handoff";
5
5
  import { isHostedTool, isFunctionTool } from "./hosted-tool";
6
6
  import { subagentToDefinition, subagentToTool } from "./subagent";
@@ -70,6 +70,33 @@ async function resolveAfterToolCallHook(hook, params) {
70
70
  }
71
71
  }
72
72
  }
73
+ /** Check if a tool/handoff isEnabled field resolves to true */
74
+ async function checkEnabled(isEnabled, context) {
75
+ if (isEnabled === undefined)
76
+ return true;
77
+ if (typeof isEnabled === "boolean")
78
+ return isEnabled;
79
+ return isEnabled(context);
80
+ }
81
+ /** Execute a tool with optional timeout */
82
+ async function executeWithTimeout(fn, timeout, toolName) {
83
+ if (!timeout)
84
+ return fn();
85
+ return new Promise((resolve, reject) => {
86
+ const timer = setTimeout(() => {
87
+ reject(new ToolTimeoutError(toolName, timeout));
88
+ }, timeout);
89
+ Promise.resolve(fn())
90
+ .then((result) => {
91
+ clearTimeout(timer);
92
+ resolve(result);
93
+ })
94
+ .catch((error) => {
95
+ clearTimeout(timer);
96
+ reject(error);
97
+ });
98
+ });
99
+ }
73
100
  function checkAborted(signal) {
74
101
  if (signal?.aborted) {
75
102
  throw new RunAbortedError();
@@ -90,6 +117,11 @@ function checkBudget(ctx, maxBudgetUsd) {
90
117
  throw new MaxBudgetExceededError(maxBudgetUsd, ctx.totalCostUsd);
91
118
  }
92
119
  }
120
+ function formatToolError(toolName, error, formatter) {
121
+ if (formatter)
122
+ return formatter(toolName, error);
123
+ return `Error executing tool "${toolName}": ${getErrorMessage(error)}`;
124
+ }
93
125
  export async function run(agent, input, options) {
94
126
  validateBudgetOptions(options);
95
127
  const model = options?.model ?? agent.model;
@@ -103,24 +135,30 @@ export async function run(agent, input, options) {
103
135
  const maxBudgetUsd = options?.maxBudgetUsd;
104
136
  const ctx = new RunContext(options?.context);
105
137
  const trace = getCurrentTrace();
138
+ const runHooks = options?.runHooks;
139
+ const toolErrorFmt = options?.toolErrorFormatter;
140
+ const callModelInputFilter = options?.callModelInputFilter;
141
+ const toolInputGuardrails = options?.toolInputGuardrails ?? [];
142
+ const toolOutputGuardrails = options?.toolOutputGuardrails ?? [];
106
143
  // Fire beforeRun hook on the entry agent
107
144
  const inputText = typeof input === "string" ? input : extractUserText(input);
108
145
  if (agent.hooks.beforeRun) {
109
146
  await agent.hooks.beforeRun({ agent, input: inputText, context: ctx.context });
110
147
  }
111
148
  // Run input guardrails on the starting agent
149
+ let inputGuardrailResults = [];
112
150
  if (agent.inputGuardrails.length > 0) {
113
151
  if (trace) {
114
152
  const span = trace.startSpan("input_guardrails", "guardrail");
115
153
  try {
116
- await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
154
+ inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
117
155
  }
118
156
  finally {
119
157
  trace.endSpan(span);
120
158
  }
121
159
  }
122
160
  else {
123
- await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
161
+ inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
124
162
  }
125
163
  }
126
164
  const messages = [];
@@ -137,16 +175,33 @@ export async function run(agent, input, options) {
137
175
  }
138
176
  let lastFinishReason;
139
177
  let lastResponseId;
178
+ // Fire run-level onAgentStart
179
+ if (runHooks?.onAgentStart) {
180
+ await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
181
+ }
140
182
  for (let turn = 0; turn < maxTurns; turn++) {
141
183
  checkAborted(signal);
142
- const toolDefs = buildToolDefs(currentAgent);
143
- const request = {
184
+ const toolDefs = await buildToolDefs(currentAgent, ctx.context);
185
+ let request = {
144
186
  messages,
145
187
  tools: toolDefs.length > 0 ? toolDefs : undefined,
146
- modelSettings: currentAgent.modelSettings,
188
+ modelSettings: currentAgent.modelSettings
189
+ ? applyResetToolChoice(currentAgent.modelSettings, turn, options?.resetToolChoice)
190
+ : undefined,
147
191
  responseFormat: currentAgent.getResponseFormat(),
148
192
  previousResponseId: lastResponseId,
149
193
  };
194
+ // Apply callModelInputFilter
195
+ if (callModelInputFilter) {
196
+ request = callModelInputFilter({ agent: currentAgent, request, context: ctx.context });
197
+ }
198
+ // Fire onLlmStart hooks
199
+ if (currentAgent.hooks.onLlmStart) {
200
+ await currentAgent.hooks.onLlmStart({ agent: currentAgent, messages, context: ctx.context });
201
+ }
202
+ if (runHooks?.onLlmStart) {
203
+ await runHooks.onLlmStart({ agent: currentAgent, request, context: ctx.context });
204
+ }
150
205
  let response;
151
206
  if (trace) {
152
207
  const span = trace.startSpan(`model_call:${currentAgent.name}`, "model_call", {
@@ -168,6 +223,14 @@ export async function run(agent, input, options) {
168
223
  else {
169
224
  response = await model.getResponse(request, { signal });
170
225
  }
226
+ // Fire onLlmEnd hooks
227
+ const llmEndInfo = { content: response.content, toolCallCount: response.toolCalls.length };
228
+ if (currentAgent.hooks.onLlmEnd) {
229
+ await currentAgent.hooks.onLlmEnd({ agent: currentAgent, response: llmEndInfo, context: ctx.context });
230
+ }
231
+ if (runHooks?.onLlmEnd) {
232
+ await runHooks.onLlmEnd({ agent: currentAgent, response: llmEndInfo, context: ctx.context });
233
+ }
171
234
  checkAborted(signal);
172
235
  lastFinishReason = response.finishReason;
173
236
  if (response.responseId)
@@ -196,12 +259,16 @@ export async function run(agent, input, options) {
196
259
  };
197
260
  messages.push(assistantMsg);
198
261
  if (response.toolCalls.length === 0) {
199
- return buildFinalResult(agent, currentAgent, messages, ctx, trace, lastFinishReason, lastResponseId);
262
+ // Fire run-level onAgentEnd
263
+ if (runHooks?.onAgentEnd) {
264
+ await runHooks.onAgentEnd({ agent: currentAgent, output: response.content ?? "", context: ctx.context });
265
+ }
266
+ return buildFinalResult(agent, currentAgent, messages, ctx, trace, lastFinishReason, lastResponseId, inputGuardrailResults);
200
267
  }
201
- const { toolMessages, handoffAgent } = await executeToolCallsWithHandoffs(currentAgent, ctx, response.toolCalls, trace, signal);
268
+ const { toolMessages, handoffAgent } = await executeToolCallsWithHandoffs(currentAgent, ctx, response.toolCalls, trace, signal, toolErrorFmt, runHooks, toolInputGuardrails, toolOutputGuardrails);
202
269
  messages.push(...toolMessages);
203
270
  // Check toolUseBehavior — should we stop instead of calling the LLM again?
204
- if (shouldStopAfterToolCalls(currentAgent, response.toolCalls)) {
271
+ if (await shouldStopAfterToolCalls(currentAgent, response.toolCalls, toolMessages)) {
205
272
  const toolOutput = toolMessages.map((m) => m.content).join("\n");
206
273
  return new RunResult({
207
274
  output: toolOutput,
@@ -212,6 +279,7 @@ export async function run(agent, input, options) {
212
279
  numTurns: ctx.numTurns,
213
280
  totalCostUsd: ctx.totalCostUsd,
214
281
  responseId: lastResponseId,
282
+ inputGuardrailResults,
215
283
  });
216
284
  }
217
285
  if (handoffAgent) {
@@ -238,10 +306,25 @@ export async function run(agent, input, options) {
238
306
  }
239
307
  }
240
308
  if (allowHandoff) {
309
+ // Fire run-level onAgentEnd for current agent
310
+ if (runHooks?.onAgentEnd) {
311
+ await runHooks.onAgentEnd({ agent: currentAgent, output: response.content ?? "", context: ctx.context });
312
+ }
313
+ // Fire run-level onHandoff
314
+ if (runHooks?.onHandoff) {
315
+ await runHooks.onHandoff({ fromAgent: currentAgent, toAgent: handoffAgent, context: ctx.context });
316
+ }
241
317
  if (trace) {
242
318
  const span = trace.startSpan(`handoff:${currentAgent.name}->${handoffAgent.name}`, "handoff", { fromAgent: currentAgent.name, toAgent: handoffAgent.name });
243
319
  trace.endSpan(span);
244
320
  }
321
+ // Apply handoff inputFilter if present
322
+ const matchedHandoff = currentAgent.handoffs.find((h) => h.agent === handoffAgent || h.agent.name === handoffAgent.name);
323
+ if (matchedHandoff?.inputFilter) {
324
+ const filtered = matchedHandoff.inputFilter({ history: [...messages] });
325
+ messages.length = 0;
326
+ messages.push(...filtered);
327
+ }
245
328
  currentAgent = handoffAgent;
246
329
  // Replace system message with new agent's prompt
247
330
  const newSystemPrompt = await currentAgent.getSystemPrompt(ctx.context);
@@ -257,6 +340,10 @@ export async function run(agent, input, options) {
257
340
  else if (systemIdx >= 0) {
258
341
  messages.splice(systemIdx, 1);
259
342
  }
343
+ // Fire run-level onAgentStart for new agent
344
+ if (runHooks?.onAgentStart) {
345
+ await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
346
+ }
260
347
  }
261
348
  }
262
349
  }
@@ -268,6 +355,15 @@ export async function run(agent, input, options) {
268
355
  reason: "max_turns",
269
356
  });
270
357
  }
358
+ // Check for error handler
359
+ if (options?.errorHandlers?.maxTurns) {
360
+ return options.errorHandlers.maxTurns({
361
+ agent: currentAgent,
362
+ messages,
363
+ context: ctx.context,
364
+ maxTurns,
365
+ });
366
+ }
271
367
  throw new MaxTurnsExceededError(maxTurns);
272
368
  }
273
369
  export function stream(agent, input, options) {
@@ -294,24 +390,30 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
294
390
  const maxBudgetUsd = options?.maxBudgetUsd;
295
391
  const ctx = new RunContext(options?.context);
296
392
  const trace = getCurrentTrace();
393
+ const runHooks = options?.runHooks;
394
+ const toolErrorFmt = options?.toolErrorFormatter;
395
+ const callModelInputFilter = options?.callModelInputFilter;
396
+ const toolInputGuardrails = options?.toolInputGuardrails ?? [];
397
+ const toolOutputGuardrails = options?.toolOutputGuardrails ?? [];
297
398
  // Fire beforeRun hook on the entry agent
298
399
  const inputText = typeof input === "string" ? input : extractUserText(input);
299
400
  if (agent.hooks.beforeRun) {
300
401
  await agent.hooks.beforeRun({ agent, input: inputText, context: ctx.context });
301
402
  }
302
403
  // Run input guardrails on the starting agent
404
+ let inputGuardrailResults = [];
303
405
  if (agent.inputGuardrails.length > 0) {
304
406
  if (trace) {
305
407
  const span = trace.startSpan("input_guardrails", "guardrail");
306
408
  try {
307
- await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
409
+ inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
308
410
  }
309
411
  finally {
310
412
  trace.endSpan(span);
311
413
  }
312
414
  }
313
415
  else {
314
- await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
416
+ inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
315
417
  }
316
418
  }
317
419
  const messages = [];
@@ -328,16 +430,33 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
328
430
  }
329
431
  let lastFinishReason;
330
432
  let lastResponseId;
433
+ // Fire run-level onAgentStart
434
+ if (runHooks?.onAgentStart) {
435
+ await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
436
+ }
331
437
  for (let turn = 0; turn < maxTurns; turn++) {
332
438
  checkAborted(signal);
333
- const toolDefs = buildToolDefs(currentAgent);
334
- const request = {
439
+ const toolDefs = await buildToolDefs(currentAgent, ctx.context);
440
+ let request = {
335
441
  messages,
336
442
  tools: toolDefs.length > 0 ? toolDefs : undefined,
337
- modelSettings: currentAgent.modelSettings,
443
+ modelSettings: currentAgent.modelSettings
444
+ ? applyResetToolChoice(currentAgent.modelSettings, turn, options?.resetToolChoice)
445
+ : undefined,
338
446
  responseFormat: currentAgent.getResponseFormat(),
339
447
  previousResponseId: lastResponseId,
340
448
  };
449
+ // Apply callModelInputFilter
450
+ if (callModelInputFilter) {
451
+ request = callModelInputFilter({ agent: currentAgent, request, context: ctx.context });
452
+ }
453
+ // Fire onLlmStart hooks
454
+ if (currentAgent.hooks.onLlmStart) {
455
+ await currentAgent.hooks.onLlmStart({ agent: currentAgent, messages, context: ctx.context });
456
+ }
457
+ if (runHooks?.onLlmStart) {
458
+ await runHooks.onLlmStart({ agent: currentAgent, request, context: ctx.context });
459
+ }
341
460
  let finalResponse;
342
461
  let gotDone = false;
343
462
  for await (const event of model.getStreamedResponse(request, { signal })) {
@@ -350,6 +469,14 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
350
469
  if (!gotDone) {
351
470
  throw new StratusError("Stream ended without a done event");
352
471
  }
472
+ // Fire onLlmEnd hooks
473
+ const llmEndInfo = { content: finalResponse.content, toolCallCount: finalResponse.toolCalls.length };
474
+ if (currentAgent.hooks.onLlmEnd) {
475
+ await currentAgent.hooks.onLlmEnd({ agent: currentAgent, response: llmEndInfo, context: ctx.context });
476
+ }
477
+ if (runHooks?.onLlmEnd) {
478
+ await runHooks.onLlmEnd({ agent: currentAgent, response: llmEndInfo, context: ctx.context });
479
+ }
353
480
  checkAborted(signal);
354
481
  lastFinishReason = finalResponse.finishReason;
355
482
  if (finalResponse.responseId)
@@ -380,14 +507,17 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
380
507
  };
381
508
  messages.push(assistantMsg);
382
509
  if (finalResponse.toolCalls.length === 0) {
383
- const result = await buildFinalResult(agent, currentAgent, messages, ctx, trace, lastFinishReason, lastResponseId);
510
+ if (runHooks?.onAgentEnd) {
511
+ await runHooks.onAgentEnd({ agent: currentAgent, output: finalResponse.content ?? "", context: ctx.context });
512
+ }
513
+ const result = await buildFinalResult(agent, currentAgent, messages, ctx, trace, lastFinishReason, lastResponseId, inputGuardrailResults);
384
514
  resolveResult(result);
385
515
  return;
386
516
  }
387
- const { toolMessages, handoffAgent } = await executeToolCallsWithHandoffs(currentAgent, ctx, finalResponse.toolCalls, trace, signal);
517
+ const { toolMessages, handoffAgent } = await executeToolCallsWithHandoffs(currentAgent, ctx, finalResponse.toolCalls, trace, signal, toolErrorFmt, runHooks, toolInputGuardrails, toolOutputGuardrails);
388
518
  messages.push(...toolMessages);
389
519
  // Check toolUseBehavior
390
- if (shouldStopAfterToolCalls(currentAgent, finalResponse.toolCalls)) {
520
+ if (await shouldStopAfterToolCalls(currentAgent, finalResponse.toolCalls, toolMessages)) {
391
521
  const toolOutput = toolMessages.map((m) => m.content).join("\n");
392
522
  resolveResult(new RunResult({
393
523
  output: toolOutput,
@@ -398,6 +528,7 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
398
528
  numTurns: ctx.numTurns,
399
529
  totalCostUsd: ctx.totalCostUsd,
400
530
  responseId: lastResponseId,
531
+ inputGuardrailResults,
401
532
  }));
402
533
  return;
403
534
  }
@@ -423,6 +554,19 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
423
554
  }
424
555
  }
425
556
  if (allowHandoff) {
557
+ if (runHooks?.onAgentEnd) {
558
+ await runHooks.onAgentEnd({ agent: currentAgent, output: finalResponse.content ?? "", context: ctx.context });
559
+ }
560
+ if (runHooks?.onHandoff) {
561
+ await runHooks.onHandoff({ fromAgent: currentAgent, toAgent: handoffAgent, context: ctx.context });
562
+ }
563
+ // Apply handoff inputFilter if present
564
+ const matchedHandoff = currentAgent.handoffs.find((h) => h.agent === handoffAgent || h.agent.name === handoffAgent.name);
565
+ if (matchedHandoff?.inputFilter) {
566
+ const filtered = matchedHandoff.inputFilter({ history: [...messages] });
567
+ messages.length = 0;
568
+ messages.push(...filtered);
569
+ }
426
570
  currentAgent = handoffAgent;
427
571
  const newSystemPrompt = await currentAgent.getSystemPrompt(ctx.context);
428
572
  const systemIdx = messages.findIndex((m) => m.role === "system");
@@ -437,6 +581,9 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
437
581
  else if (systemIdx >= 0) {
438
582
  messages.splice(systemIdx, 1);
439
583
  }
584
+ if (runHooks?.onAgentStart) {
585
+ await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
586
+ }
440
587
  }
441
588
  }
442
589
  }
@@ -448,6 +595,16 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
448
595
  reason: "max_turns",
449
596
  });
450
597
  }
598
+ // Check for error handler
599
+ if (options?.errorHandlers?.maxTurns) {
600
+ resolveResult(await options.errorHandlers.maxTurns({
601
+ agent: currentAgent,
602
+ messages,
603
+ context: ctx.context,
604
+ maxTurns,
605
+ }));
606
+ return;
607
+ }
451
608
  throw new MaxTurnsExceededError(maxTurns);
452
609
  }
453
610
  catch (error) {
@@ -455,22 +612,23 @@ async function* streamInternal(agent, input, options, resolveResult, rejectResul
455
612
  throw error;
456
613
  }
457
614
  }
458
- async function buildFinalResult(entryAgent, currentAgent, messages, ctx, trace, finishReason, responseId) {
615
+ async function buildFinalResult(entryAgent, currentAgent, messages, ctx, trace, finishReason, responseId, inputGuardrailResults) {
459
616
  const lastMessage = messages[messages.length - 1];
460
617
  const rawOutput = lastMessage && lastMessage.role === "assistant" ? (lastMessage.content ?? "") : "";
461
618
  // Run output guardrails on the current (possibly handed-off) agent
619
+ let outputGuardrailResults = [];
462
620
  if (currentAgent.outputGuardrails.length > 0) {
463
621
  if (trace) {
464
622
  const span = trace.startSpan("output_guardrails", "guardrail");
465
623
  try {
466
- await runOutputGuardrails(currentAgent.outputGuardrails, rawOutput, ctx.context);
624
+ outputGuardrailResults = await runOutputGuardrails(currentAgent.outputGuardrails, rawOutput, ctx.context);
467
625
  }
468
626
  finally {
469
627
  trace.endSpan(span);
470
628
  }
471
629
  }
472
630
  else {
473
- await runOutputGuardrails(currentAgent.outputGuardrails, rawOutput, ctx.context);
631
+ outputGuardrailResults = await runOutputGuardrails(currentAgent.outputGuardrails, rawOutput, ctx.context);
474
632
  }
475
633
  }
476
634
  // Parse structured output if outputType is set
@@ -494,6 +652,8 @@ async function buildFinalResult(entryAgent, currentAgent, messages, ctx, trace,
494
652
  numTurns: ctx.numTurns,
495
653
  totalCostUsd: ctx.totalCostUsd,
496
654
  responseId,
655
+ inputGuardrailResults,
656
+ outputGuardrailResults,
497
657
  });
498
658
  // Fire afterRun hook on the entry agent
499
659
  if (entryAgent.hooks.afterRun) {
@@ -501,13 +661,16 @@ async function buildFinalResult(entryAgent, currentAgent, messages, ctx, trace,
501
661
  }
502
662
  return result;
503
663
  }
504
- function buildToolDefs(agent) {
664
+ async function buildToolDefs(agent, context) {
505
665
  const defs = [];
506
666
  for (const t of agent.tools) {
507
667
  if (isHostedTool(t)) {
508
668
  defs.push(t.definition);
509
669
  }
510
670
  else {
671
+ // Check isEnabled for function tools
672
+ if (!(await checkEnabled(t.isEnabled, context)))
673
+ continue;
511
674
  defs.push(toolToDefinition(t));
512
675
  }
513
676
  }
@@ -515,6 +678,9 @@ function buildToolDefs(agent) {
515
678
  defs.push(subagentToDefinition(sa));
516
679
  }
517
680
  for (const h of agent.handoffs) {
681
+ // Check isEnabled for handoffs
682
+ if (!(await checkEnabled(h.isEnabled, context)))
683
+ continue;
518
684
  defs.push(handoffToDefinition(h));
519
685
  }
520
686
  return defs;
@@ -537,18 +703,36 @@ function extractUserText(messages) {
537
703
  }
538
704
  return texts.join("\n");
539
705
  }
540
- function shouldStopAfterToolCalls(agent, toolCalls) {
541
- if (agent.toolUseBehavior === "run_llm_again")
706
+ async function shouldStopAfterToolCalls(agent, toolCalls, toolMessages) {
707
+ const behavior = agent.toolUseBehavior;
708
+ if (behavior === "run_llm_again")
542
709
  return false;
543
- if (agent.toolUseBehavior === "stop_on_first_tool")
710
+ if (behavior === "stop_on_first_tool")
544
711
  return true;
545
- if ("stopAtToolNames" in agent.toolUseBehavior) {
546
- const stopNames = new Set(agent.toolUseBehavior.stopAtToolNames);
712
+ if (typeof behavior === "function") {
713
+ // Custom function variant
714
+ const results = toolCalls.map((tc, i) => ({
715
+ toolName: tc.function.name,
716
+ result: toolMessages[i]?.content ?? "",
717
+ }));
718
+ return behavior(results);
719
+ }
720
+ if (typeof behavior === "object" && "stopAtToolNames" in behavior) {
721
+ const stopNames = new Set(behavior.stopAtToolNames);
547
722
  return toolCalls.some((tc) => stopNames.has(tc.function.name));
548
723
  }
549
724
  return false;
550
725
  }
551
- async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal) {
726
+ function applyResetToolChoice(settings, turn, resetToolChoice) {
727
+ if (!resetToolChoice || turn === 0)
728
+ return settings;
729
+ // After the first turn, reset tool_choice to "auto" to prevent infinite loops
730
+ if (settings.toolChoice && settings.toolChoice !== "auto") {
731
+ return { ...settings, toolChoice: "auto" };
732
+ }
733
+ return settings;
734
+ }
735
+ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal, toolErrorFmt, runHooks, toolInputGuardrails, toolOutputGuardrails) {
552
736
  let handoffAgent;
553
737
  // Build O(1) lookup maps
554
738
  const handoffsByName = new Map(agent.handoffs.map((h) => [h.toolName, h]));
@@ -597,6 +781,10 @@ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal
597
781
  context: ctx.context,
598
782
  });
599
783
  }
784
+ // Fire run-level onToolStart
785
+ if (runHooks?.onToolStart) {
786
+ await runHooks.onToolStart({ agent, toolName: tcName, context: ctx.context });
787
+ }
600
788
  let result;
601
789
  if (trace) {
602
790
  const span = trace.startSpan(`subagent:${matchedSubagent.agent.name}`, "subagent", { toolName: tcName });
@@ -619,6 +807,10 @@ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal
619
807
  context: ctx.context,
620
808
  });
621
809
  }
810
+ // Fire run-level onToolEnd
811
+ if (runHooks?.onToolEnd) {
812
+ await runHooks.onToolEnd({ agent, toolName: tcName, result, context: ctx.context });
813
+ }
622
814
  await resolveAfterToolCallHook(agent.hooks.afterToolCall, {
623
815
  agent,
624
816
  toolCall: fullToolCall,
@@ -635,7 +827,7 @@ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal
635
827
  return {
636
828
  role: "tool",
637
829
  tool_call_id: tc.id,
638
- content: `Error executing sub-agent "${matchedSubagent.agent.name}": ${getErrorMessage(error)}`,
830
+ content: formatToolError(matchedSubagent.agent.name, error, toolErrorFmt),
639
831
  };
640
832
  }
641
833
  }
@@ -667,6 +859,22 @@ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal
667
859
  if (decision?.decision === "modify") {
668
860
  params = decision.modifiedParams;
669
861
  }
862
+ // Run tool input guardrails
863
+ if (toolInputGuardrails && toolInputGuardrails.length > 0) {
864
+ const guardrailResults = await runToolInputGuardrails(toolInputGuardrails, tcName, params, ctx.context);
865
+ const tripped = guardrailResults.find((r) => r.result.tripwireTriggered);
866
+ if (tripped) {
867
+ return {
868
+ role: "tool",
869
+ tool_call_id: tc.id,
870
+ content: `Tool input guardrail "${tripped.guardrailName}" blocked execution of "${tcName}"`,
871
+ };
872
+ }
873
+ }
874
+ // Fire run-level onToolStart
875
+ if (runHooks?.onToolStart) {
876
+ await runHooks.onToolStart({ agent, toolName: tcName, context: ctx.context });
877
+ }
670
878
  checkAborted(signal);
671
879
  let result;
672
880
  if (trace) {
@@ -674,14 +882,26 @@ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal
674
882
  toolName: tcName,
675
883
  });
676
884
  try {
677
- result = await tool.execute(ctx.context, params, { signal });
885
+ result = await executeWithTimeout(() => tool.execute(ctx.context, params, { signal }), tool.timeout, tcName);
678
886
  }
679
887
  finally {
680
888
  trace.endSpan(span);
681
889
  }
682
890
  }
683
891
  else {
684
- result = await tool.execute(ctx.context, params, { signal });
892
+ result = await executeWithTimeout(() => tool.execute(ctx.context, params, { signal }), tool.timeout, tcName);
893
+ }
894
+ // Run tool output guardrails
895
+ if (toolOutputGuardrails && toolOutputGuardrails.length > 0) {
896
+ const guardrailResults = await runToolOutputGuardrails(toolOutputGuardrails, tcName, result, ctx.context);
897
+ const tripped = guardrailResults.find((r) => r.result.tripwireTriggered);
898
+ if (tripped) {
899
+ result = `Tool output guardrail "${tripped.guardrailName}" flagged the output of "${tcName}"`;
900
+ }
901
+ }
902
+ // Fire run-level onToolEnd
903
+ if (runHooks?.onToolEnd) {
904
+ await runHooks.onToolEnd({ agent, toolName: tcName, result, context: ctx.context });
685
905
  }
686
906
  // Fire afterToolCall hook
687
907
  await resolveAfterToolCallHook(agent.hooks.afterToolCall, {
@@ -700,7 +920,7 @@ async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal
700
920
  return {
701
921
  role: "tool",
702
922
  tool_call_id: tc.id,
703
- content: `Error executing tool "${tcName}": ${getErrorMessage(error)}`,
923
+ content: formatToolError(tcName, error, toolErrorFmt),
704
924
  };
705
925
  }
706
926
  }));