@usestratus/sdk 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/azure/chat-completions-model.d.ts +23 -0
- package/dist/azure/chat-completions-model.d.ts.map +1 -0
- package/dist/azure/chat-completions-model.js +293 -0
- package/dist/azure/chat-completions-model.js.map +1 -0
- package/dist/azure/endpoint.d.ts +18 -0
- package/dist/azure/endpoint.d.ts.map +1 -0
- package/dist/azure/endpoint.js +57 -0
- package/dist/azure/endpoint.js.map +1 -0
- package/dist/azure/index.d.ts +5 -0
- package/dist/azure/index.d.ts.map +1 -0
- package/dist/azure/index.js +3 -0
- package/dist/azure/index.js.map +1 -0
- package/dist/azure/responses-model.d.ts +25 -0
- package/dist/azure/responses-model.d.ts.map +1 -0
- package/dist/azure/responses-model.js +557 -0
- package/dist/azure/responses-model.js.map +1 -0
- package/dist/azure/sse-parser.d.ts +2 -0
- package/dist/azure/sse-parser.d.ts.map +1 -0
- package/dist/azure/sse-parser.js +39 -0
- package/dist/azure/sse-parser.js.map +1 -0
- package/dist/core/agent.d.ts +47 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +74 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/builtin-tools.d.ts +41 -0
- package/dist/core/builtin-tools.d.ts.map +1 -0
- package/dist/core/builtin-tools.js +80 -0
- package/dist/core/builtin-tools.js.map +1 -0
- package/dist/core/codemode/executor.d.ts +62 -0
- package/dist/core/codemode/executor.d.ts.map +1 -0
- package/dist/core/codemode/executor.js +188 -0
- package/dist/core/codemode/executor.js.map +1 -0
- package/dist/core/codemode/index.d.ts +62 -0
- package/dist/core/codemode/index.d.ts.map +1 -0
- package/dist/core/codemode/index.js +104 -0
- package/dist/core/codemode/index.js.map +1 -0
- package/dist/core/codemode/types.d.ts +24 -0
- package/dist/core/codemode/types.d.ts.map +1 -0
- package/dist/core/codemode/types.js +405 -0
- package/dist/core/codemode/types.js.map +1 -0
- package/dist/core/context.d.ts +10 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +32 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/cost.d.ts +9 -0
- package/dist/core/cost.d.ts.map +1 -0
- package/dist/core/cost.js +14 -0
- package/dist/core/cost.js.map +1 -0
- package/dist/core/errors.d.ts +48 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +85 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/guardrails.d.ts +39 -0
- package/dist/core/guardrails.d.ts.map +1 -0
- package/dist/core/guardrails.js +40 -0
- package/dist/core/guardrails.js.map +1 -0
- package/dist/core/handoff.d.ts +35 -0
- package/dist/core/handoff.d.ts.map +1 -0
- package/dist/core/handoff.js +39 -0
- package/dist/core/handoff.js.map +1 -0
- package/dist/core/hooks.d.ts +154 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +2 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/hosted-tool.d.ts +11 -0
- package/dist/core/hosted-tool.d.ts.map +1 -0
- package/dist/core/hosted-tool.js +7 -0
- package/dist/core/hosted-tool.js.map +1 -0
- package/dist/core/index.d.ts +35 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +18 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/model.d.ts +56 -0
- package/dist/core/model.d.ts.map +1 -0
- package/dist/core/model.js +2 -0
- package/dist/core/model.js.map +1 -0
- package/dist/core/result.d.ts +34 -0
- package/dist/core/result.d.ts.map +1 -0
- package/dist/core/result.js +31 -0
- package/dist/core/result.js.map +1 -0
- package/dist/core/run.d.ts +52 -0
- package/dist/core/run.d.ts.map +1 -0
- package/dist/core/run.js +972 -0
- package/dist/core/run.js.map +1 -0
- package/dist/core/session.d.ts +77 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +160 -0
- package/dist/core/session.js.map +1 -0
- package/dist/core/subagent.d.ts +30 -0
- package/dist/core/subagent.d.ts.map +1 -0
- package/dist/core/subagent.js +52 -0
- package/dist/core/subagent.js.map +1 -0
- package/dist/core/todo.d.ts +56 -0
- package/dist/core/todo.d.ts.map +1 -0
- package/dist/core/todo.js +76 -0
- package/dist/core/todo.js.map +1 -0
- package/dist/core/tool.d.ts +26 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +23 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/tracing.d.ts +31 -0
- package/dist/core/tracing.d.ts.map +1 -0
- package/dist/core/tracing.js +62 -0
- package/dist/core/tracing.js.map +1 -0
- package/dist/core/types.d.ts +106 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/utils/zod.d.ts +5 -0
- package/dist/core/utils/zod.d.ts.map +1 -0
- package/dist/core/utils/zod.js +7 -0
- package/dist/core/utils/zod.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/dist/core/run.js
ADDED
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { RunContext } from "./context";
|
|
2
|
+
import { MaxBudgetExceededError, MaxTurnsExceededError, OutputParseError, RunAbortedError, StratusError, ToolTimeoutError, } from "./errors";
|
|
3
|
+
import { runInputGuardrails, runOutputGuardrails, runToolInputGuardrails, runToolOutputGuardrails, } from "./guardrails";
|
|
4
|
+
import { handoffToDefinition } from "./handoff";
|
|
5
|
+
import { isFunctionTool, isHostedTool } from "./hosted-tool";
|
|
6
|
+
import { RunResult } from "./result";
|
|
7
|
+
import { subagentToDefinition, subagentToTool } from "./subagent";
|
|
8
|
+
import { toolToDefinition } from "./tool";
|
|
9
|
+
import { getCurrentTrace } from "./tracing";
|
|
10
|
+
const DEFAULT_MAX_TURNS = 10;
|
|
11
|
+
function getErrorMessage(error) {
|
|
12
|
+
if (error instanceof Error)
|
|
13
|
+
return error.message;
|
|
14
|
+
return String(error);
|
|
15
|
+
}
|
|
16
|
+
function extractToolCallDecision(result) {
|
|
17
|
+
if (result && typeof result === "object" && "decision" in result) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function extractHandoffDecision(result) {
|
|
23
|
+
if (result && typeof result === "object" && "decision" in result) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function matchesToolName(matcher, name) {
|
|
29
|
+
if (typeof matcher === "string")
|
|
30
|
+
return matcher === name;
|
|
31
|
+
return matcher.test(name);
|
|
32
|
+
}
|
|
33
|
+
function matchesAny(matchers, name) {
|
|
34
|
+
if (Array.isArray(matchers)) {
|
|
35
|
+
return matchers.some((m) => matchesToolName(m, name));
|
|
36
|
+
}
|
|
37
|
+
return matchesToolName(matchers, name);
|
|
38
|
+
}
|
|
39
|
+
async function resolveBeforeToolCallHook(hook, params) {
|
|
40
|
+
if (!hook)
|
|
41
|
+
return undefined;
|
|
42
|
+
// Function form (backward compat)
|
|
43
|
+
if (typeof hook === "function") {
|
|
44
|
+
return extractToolCallDecision(await hook(params));
|
|
45
|
+
}
|
|
46
|
+
// Matched array form
|
|
47
|
+
for (const entry of hook) {
|
|
48
|
+
if (matchesAny(entry.match, params.toolCall.function.name)) {
|
|
49
|
+
const decision = extractToolCallDecision(await entry.hook(params));
|
|
50
|
+
if (decision?.decision === "deny")
|
|
51
|
+
return decision;
|
|
52
|
+
if (decision?.decision === "modify")
|
|
53
|
+
return decision;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
async function resolveAfterToolCallHook(hook, params) {
|
|
59
|
+
if (!hook)
|
|
60
|
+
return;
|
|
61
|
+
// Function form (backward compat)
|
|
62
|
+
if (typeof hook === "function") {
|
|
63
|
+
await hook(params);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Matched array form
|
|
67
|
+
for (const entry of hook) {
|
|
68
|
+
if (matchesAny(entry.match, params.toolCall.function.name)) {
|
|
69
|
+
await entry.hook(params);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
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
|
+
}
|
|
100
|
+
function checkAborted(signal) {
|
|
101
|
+
if (signal?.aborted) {
|
|
102
|
+
throw new RunAbortedError();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function validateBudgetOptions(options) {
|
|
106
|
+
if (options?.maxBudgetUsd !== undefined && !options.costEstimator) {
|
|
107
|
+
throw new StratusError("maxBudgetUsd requires a costEstimator to be provided");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function applyTurnCost(ctx, usage, costEstimator) {
|
|
111
|
+
if (costEstimator && usage) {
|
|
112
|
+
ctx.totalCostUsd += costEstimator(usage);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function checkBudget(ctx, maxBudgetUsd) {
|
|
116
|
+
if (maxBudgetUsd !== undefined && ctx.totalCostUsd > maxBudgetUsd) {
|
|
117
|
+
throw new MaxBudgetExceededError(maxBudgetUsd, ctx.totalCostUsd);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function formatToolError(toolName, error, formatter) {
|
|
121
|
+
if (formatter)
|
|
122
|
+
return formatter(toolName, error);
|
|
123
|
+
return `Error executing tool "${toolName}": ${getErrorMessage(error)}`;
|
|
124
|
+
}
|
|
125
|
+
export async function run(agent, input, options) {
|
|
126
|
+
validateBudgetOptions(options);
|
|
127
|
+
const model = options?.model ?? agent.model;
|
|
128
|
+
if (!model) {
|
|
129
|
+
throw new StratusError("No model provided. Pass a model to the agent or to run().");
|
|
130
|
+
}
|
|
131
|
+
const signal = options?.signal;
|
|
132
|
+
checkAborted(signal);
|
|
133
|
+
const maxTurns = options?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
134
|
+
const costEstimator = options?.costEstimator;
|
|
135
|
+
const maxBudgetUsd = options?.maxBudgetUsd;
|
|
136
|
+
const ctx = new RunContext(options?.context);
|
|
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 ?? [];
|
|
143
|
+
// Fire beforeRun hook on the entry agent
|
|
144
|
+
const inputText = typeof input === "string" ? input : extractUserText(input);
|
|
145
|
+
if (agent.hooks.beforeRun) {
|
|
146
|
+
await agent.hooks.beforeRun({ agent, input: inputText, context: ctx.context });
|
|
147
|
+
}
|
|
148
|
+
// Run input guardrails on the starting agent
|
|
149
|
+
let inputGuardrailResults = [];
|
|
150
|
+
if (agent.inputGuardrails.length > 0) {
|
|
151
|
+
if (trace) {
|
|
152
|
+
const span = trace.startSpan("input_guardrails", "guardrail");
|
|
153
|
+
try {
|
|
154
|
+
inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
trace.endSpan(span);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const messages = [];
|
|
165
|
+
let currentAgent = agent;
|
|
166
|
+
const systemPrompt = await currentAgent.getSystemPrompt(ctx.context);
|
|
167
|
+
if (systemPrompt) {
|
|
168
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
169
|
+
}
|
|
170
|
+
if (typeof input === "string") {
|
|
171
|
+
messages.push({ role: "user", content: input });
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
messages.push(...input);
|
|
175
|
+
}
|
|
176
|
+
let lastFinishReason;
|
|
177
|
+
let lastResponseId;
|
|
178
|
+
// Fire run-level onAgentStart
|
|
179
|
+
if (runHooks?.onAgentStart) {
|
|
180
|
+
await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
|
|
181
|
+
}
|
|
182
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
183
|
+
checkAborted(signal);
|
|
184
|
+
const toolDefs = await buildToolDefs(currentAgent, ctx.context);
|
|
185
|
+
let request = {
|
|
186
|
+
messages,
|
|
187
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
188
|
+
modelSettings: currentAgent.modelSettings
|
|
189
|
+
? applyResetToolChoice(currentAgent.modelSettings, turn, options?.resetToolChoice)
|
|
190
|
+
: undefined,
|
|
191
|
+
responseFormat: currentAgent.getResponseFormat(),
|
|
192
|
+
previousResponseId: lastResponseId,
|
|
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
|
+
}
|
|
205
|
+
let response;
|
|
206
|
+
if (trace) {
|
|
207
|
+
const span = trace.startSpan(`model_call:${currentAgent.name}`, "model_call", {
|
|
208
|
+
agent: currentAgent.name,
|
|
209
|
+
turn,
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
response = await model.getResponse(request, { signal });
|
|
213
|
+
trace.endSpan(span, {
|
|
214
|
+
usage: response.usage,
|
|
215
|
+
toolCallCount: response.toolCalls.length,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
trace.endSpan(span, { error: getErrorMessage(error) });
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
response = await model.getResponse(request, { signal });
|
|
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({
|
|
230
|
+
agent: currentAgent,
|
|
231
|
+
response: llmEndInfo,
|
|
232
|
+
context: ctx.context,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (runHooks?.onLlmEnd) {
|
|
236
|
+
await runHooks.onLlmEnd({ agent: currentAgent, response: llmEndInfo, context: ctx.context });
|
|
237
|
+
}
|
|
238
|
+
checkAborted(signal);
|
|
239
|
+
lastFinishReason = response.finishReason;
|
|
240
|
+
if (response.responseId)
|
|
241
|
+
lastResponseId = response.responseId;
|
|
242
|
+
ctx.addUsage(response.usage);
|
|
243
|
+
ctx.numTurns++;
|
|
244
|
+
applyTurnCost(ctx, response.usage, costEstimator);
|
|
245
|
+
// Check budget after each model call
|
|
246
|
+
try {
|
|
247
|
+
checkBudget(ctx, maxBudgetUsd);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error instanceof MaxBudgetExceededError && currentAgent.hooks.onStop) {
|
|
251
|
+
await currentAgent.hooks.onStop({
|
|
252
|
+
agent: currentAgent,
|
|
253
|
+
context: ctx.context,
|
|
254
|
+
reason: "max_budget",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
const assistantMsg = {
|
|
260
|
+
role: "assistant",
|
|
261
|
+
content: response.content,
|
|
262
|
+
...(response.toolCalls.length > 0 ? { tool_calls: response.toolCalls } : {}),
|
|
263
|
+
};
|
|
264
|
+
messages.push(assistantMsg);
|
|
265
|
+
if (response.toolCalls.length === 0) {
|
|
266
|
+
// Fire run-level onAgentEnd
|
|
267
|
+
if (runHooks?.onAgentEnd) {
|
|
268
|
+
await runHooks.onAgentEnd({
|
|
269
|
+
agent: currentAgent,
|
|
270
|
+
output: response.content ?? "",
|
|
271
|
+
context: ctx.context,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return buildFinalResult(agent, currentAgent, messages, ctx, trace, lastFinishReason, lastResponseId, inputGuardrailResults);
|
|
275
|
+
}
|
|
276
|
+
const { toolMessages, handoffAgent } = await executeToolCallsWithHandoffs(currentAgent, ctx, response.toolCalls, trace, signal, toolErrorFmt, runHooks, toolInputGuardrails, toolOutputGuardrails);
|
|
277
|
+
messages.push(...toolMessages);
|
|
278
|
+
// Check toolUseBehavior — should we stop instead of calling the LLM again?
|
|
279
|
+
if (await shouldStopAfterToolCalls(currentAgent, response.toolCalls, toolMessages)) {
|
|
280
|
+
const toolOutput = toolMessages.map((m) => m.content).join("\n");
|
|
281
|
+
return new RunResult({
|
|
282
|
+
output: toolOutput,
|
|
283
|
+
messages,
|
|
284
|
+
usage: ctx.usage,
|
|
285
|
+
lastAgent: currentAgent,
|
|
286
|
+
finishReason: lastFinishReason,
|
|
287
|
+
numTurns: ctx.numTurns,
|
|
288
|
+
totalCostUsd: ctx.totalCostUsd,
|
|
289
|
+
responseId: lastResponseId,
|
|
290
|
+
inputGuardrailResults,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
if (handoffAgent) {
|
|
294
|
+
let allowHandoff = true;
|
|
295
|
+
// Fire beforeHandoff hook on current agent
|
|
296
|
+
if (currentAgent.hooks.beforeHandoff) {
|
|
297
|
+
const raw = await currentAgent.hooks.beforeHandoff({
|
|
298
|
+
fromAgent: currentAgent,
|
|
299
|
+
toAgent: handoffAgent,
|
|
300
|
+
context: ctx.context,
|
|
301
|
+
});
|
|
302
|
+
const decision = extractHandoffDecision(raw);
|
|
303
|
+
if (decision?.decision === "deny") {
|
|
304
|
+
allowHandoff = false;
|
|
305
|
+
// Replace the last tool message for the handoff with the denial reason
|
|
306
|
+
const lastToolMsg = messages[messages.length - 1];
|
|
307
|
+
if (lastToolMsg && lastToolMsg.role === "tool") {
|
|
308
|
+
messages[messages.length - 1] = {
|
|
309
|
+
...lastToolMsg,
|
|
310
|
+
content: decision.reason ?? `Handoff to ${handoffAgent.name} was denied`,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (allowHandoff) {
|
|
316
|
+
// Fire run-level onAgentEnd for current agent
|
|
317
|
+
if (runHooks?.onAgentEnd) {
|
|
318
|
+
await runHooks.onAgentEnd({
|
|
319
|
+
agent: currentAgent,
|
|
320
|
+
output: response.content ?? "",
|
|
321
|
+
context: ctx.context,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
// Fire run-level onHandoff
|
|
325
|
+
if (runHooks?.onHandoff) {
|
|
326
|
+
await runHooks.onHandoff({
|
|
327
|
+
fromAgent: currentAgent,
|
|
328
|
+
toAgent: handoffAgent,
|
|
329
|
+
context: ctx.context,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (trace) {
|
|
333
|
+
const span = trace.startSpan(`handoff:${currentAgent.name}->${handoffAgent.name}`, "handoff", { fromAgent: currentAgent.name, toAgent: handoffAgent.name });
|
|
334
|
+
trace.endSpan(span);
|
|
335
|
+
}
|
|
336
|
+
// Apply handoff inputFilter if present
|
|
337
|
+
const matchedHandoff = currentAgent.handoffs.find((h) => h.agent === handoffAgent || h.agent.name === handoffAgent.name);
|
|
338
|
+
if (matchedHandoff?.inputFilter) {
|
|
339
|
+
const filtered = matchedHandoff.inputFilter({ history: [...messages] });
|
|
340
|
+
messages.length = 0;
|
|
341
|
+
messages.push(...filtered);
|
|
342
|
+
}
|
|
343
|
+
currentAgent = handoffAgent;
|
|
344
|
+
// Replace system message with new agent's prompt
|
|
345
|
+
const newSystemPrompt = await currentAgent.getSystemPrompt(ctx.context);
|
|
346
|
+
const systemIdx = messages.findIndex((m) => m.role === "system");
|
|
347
|
+
if (newSystemPrompt) {
|
|
348
|
+
if (systemIdx >= 0) {
|
|
349
|
+
messages[systemIdx] = { role: "system", content: newSystemPrompt };
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
messages.unshift({ role: "system", content: newSystemPrompt });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else if (systemIdx >= 0) {
|
|
356
|
+
messages.splice(systemIdx, 1);
|
|
357
|
+
}
|
|
358
|
+
// Fire run-level onAgentStart for new agent
|
|
359
|
+
if (runHooks?.onAgentStart) {
|
|
360
|
+
await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// Fire onStop before throwing MaxTurnsExceededError
|
|
366
|
+
if (currentAgent.hooks.onStop) {
|
|
367
|
+
await currentAgent.hooks.onStop({
|
|
368
|
+
agent: currentAgent,
|
|
369
|
+
context: ctx.context,
|
|
370
|
+
reason: "max_turns",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
// Check for error handler
|
|
374
|
+
if (options?.errorHandlers?.maxTurns) {
|
|
375
|
+
return options.errorHandlers.maxTurns({
|
|
376
|
+
agent: currentAgent,
|
|
377
|
+
messages,
|
|
378
|
+
context: ctx.context,
|
|
379
|
+
maxTurns,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
throw new MaxTurnsExceededError(maxTurns);
|
|
383
|
+
}
|
|
384
|
+
export function stream(agent, input, options) {
|
|
385
|
+
let resolveResult;
|
|
386
|
+
let rejectResult;
|
|
387
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
388
|
+
resolveResult = resolve;
|
|
389
|
+
rejectResult = reject;
|
|
390
|
+
});
|
|
391
|
+
const gen = streamInternal(agent, input, options, resolveResult, rejectResult);
|
|
392
|
+
return { stream: gen, result: resultPromise };
|
|
393
|
+
}
|
|
394
|
+
async function* streamInternal(agent, input, options, resolveResult, rejectResult) {
|
|
395
|
+
try {
|
|
396
|
+
validateBudgetOptions(options);
|
|
397
|
+
const model = options?.model ?? agent.model;
|
|
398
|
+
if (!model) {
|
|
399
|
+
throw new StratusError("No model provided. Pass a model to the agent or to run().");
|
|
400
|
+
}
|
|
401
|
+
const signal = options?.signal;
|
|
402
|
+
checkAborted(signal);
|
|
403
|
+
const maxTurns = options?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
404
|
+
const costEstimator = options?.costEstimator;
|
|
405
|
+
const maxBudgetUsd = options?.maxBudgetUsd;
|
|
406
|
+
const ctx = new RunContext(options?.context);
|
|
407
|
+
const trace = getCurrentTrace();
|
|
408
|
+
const runHooks = options?.runHooks;
|
|
409
|
+
const toolErrorFmt = options?.toolErrorFormatter;
|
|
410
|
+
const callModelInputFilter = options?.callModelInputFilter;
|
|
411
|
+
const toolInputGuardrails = options?.toolInputGuardrails ?? [];
|
|
412
|
+
const toolOutputGuardrails = options?.toolOutputGuardrails ?? [];
|
|
413
|
+
// Fire beforeRun hook on the entry agent
|
|
414
|
+
const inputText = typeof input === "string" ? input : extractUserText(input);
|
|
415
|
+
if (agent.hooks.beforeRun) {
|
|
416
|
+
await agent.hooks.beforeRun({ agent, input: inputText, context: ctx.context });
|
|
417
|
+
}
|
|
418
|
+
// Run input guardrails on the starting agent
|
|
419
|
+
let inputGuardrailResults = [];
|
|
420
|
+
if (agent.inputGuardrails.length > 0) {
|
|
421
|
+
if (trace) {
|
|
422
|
+
const span = trace.startSpan("input_guardrails", "guardrail");
|
|
423
|
+
try {
|
|
424
|
+
inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
trace.endSpan(span);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
inputGuardrailResults = await runInputGuardrails(agent.inputGuardrails, inputText, ctx.context);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const messages = [];
|
|
435
|
+
let currentAgent = agent;
|
|
436
|
+
const systemPrompt = await currentAgent.getSystemPrompt(ctx.context);
|
|
437
|
+
if (systemPrompt) {
|
|
438
|
+
messages.push({ role: "system", content: systemPrompt });
|
|
439
|
+
}
|
|
440
|
+
if (typeof input === "string") {
|
|
441
|
+
messages.push({ role: "user", content: input });
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
messages.push(...input);
|
|
445
|
+
}
|
|
446
|
+
let lastFinishReason;
|
|
447
|
+
let lastResponseId;
|
|
448
|
+
// Fire run-level onAgentStart
|
|
449
|
+
if (runHooks?.onAgentStart) {
|
|
450
|
+
await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
|
|
451
|
+
}
|
|
452
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
453
|
+
checkAborted(signal);
|
|
454
|
+
const toolDefs = await buildToolDefs(currentAgent, ctx.context);
|
|
455
|
+
let request = {
|
|
456
|
+
messages,
|
|
457
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
458
|
+
modelSettings: currentAgent.modelSettings
|
|
459
|
+
? applyResetToolChoice(currentAgent.modelSettings, turn, options?.resetToolChoice)
|
|
460
|
+
: undefined,
|
|
461
|
+
responseFormat: currentAgent.getResponseFormat(),
|
|
462
|
+
previousResponseId: lastResponseId,
|
|
463
|
+
};
|
|
464
|
+
// Apply callModelInputFilter
|
|
465
|
+
if (callModelInputFilter) {
|
|
466
|
+
request = callModelInputFilter({ agent: currentAgent, request, context: ctx.context });
|
|
467
|
+
}
|
|
468
|
+
// Fire onLlmStart hooks
|
|
469
|
+
if (currentAgent.hooks.onLlmStart) {
|
|
470
|
+
await currentAgent.hooks.onLlmStart({
|
|
471
|
+
agent: currentAgent,
|
|
472
|
+
messages,
|
|
473
|
+
context: ctx.context,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (runHooks?.onLlmStart) {
|
|
477
|
+
await runHooks.onLlmStart({ agent: currentAgent, request, context: ctx.context });
|
|
478
|
+
}
|
|
479
|
+
let finalResponse;
|
|
480
|
+
let gotDone = false;
|
|
481
|
+
for await (const event of model.getStreamedResponse(request, { signal })) {
|
|
482
|
+
yield event;
|
|
483
|
+
if (event.type === "done") {
|
|
484
|
+
finalResponse = event.response;
|
|
485
|
+
gotDone = true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!gotDone || !finalResponse) {
|
|
489
|
+
throw new StratusError("Stream ended without a done event");
|
|
490
|
+
}
|
|
491
|
+
// Fire onLlmEnd hooks
|
|
492
|
+
const llmEndInfo = {
|
|
493
|
+
content: finalResponse.content,
|
|
494
|
+
toolCallCount: finalResponse.toolCalls.length,
|
|
495
|
+
};
|
|
496
|
+
if (currentAgent.hooks.onLlmEnd) {
|
|
497
|
+
await currentAgent.hooks.onLlmEnd({
|
|
498
|
+
agent: currentAgent,
|
|
499
|
+
response: llmEndInfo,
|
|
500
|
+
context: ctx.context,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
if (runHooks?.onLlmEnd) {
|
|
504
|
+
await runHooks.onLlmEnd({
|
|
505
|
+
agent: currentAgent,
|
|
506
|
+
response: llmEndInfo,
|
|
507
|
+
context: ctx.context,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
checkAborted(signal);
|
|
511
|
+
lastFinishReason = finalResponse.finishReason;
|
|
512
|
+
if (finalResponse.responseId)
|
|
513
|
+
lastResponseId = finalResponse.responseId;
|
|
514
|
+
ctx.addUsage(finalResponse.usage);
|
|
515
|
+
ctx.numTurns++;
|
|
516
|
+
applyTurnCost(ctx, finalResponse.usage, costEstimator);
|
|
517
|
+
// Check budget after each model call
|
|
518
|
+
try {
|
|
519
|
+
checkBudget(ctx, maxBudgetUsd);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
if (error instanceof MaxBudgetExceededError && currentAgent.hooks.onStop) {
|
|
523
|
+
await currentAgent.hooks.onStop({
|
|
524
|
+
agent: currentAgent,
|
|
525
|
+
context: ctx.context,
|
|
526
|
+
reason: "max_budget",
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
531
|
+
const assistantMsg = {
|
|
532
|
+
role: "assistant",
|
|
533
|
+
content: finalResponse.content ?? null,
|
|
534
|
+
...(finalResponse.toolCalls.length > 0 ? { tool_calls: finalResponse.toolCalls } : {}),
|
|
535
|
+
};
|
|
536
|
+
messages.push(assistantMsg);
|
|
537
|
+
if (finalResponse.toolCalls.length === 0) {
|
|
538
|
+
if (runHooks?.onAgentEnd) {
|
|
539
|
+
await runHooks.onAgentEnd({
|
|
540
|
+
agent: currentAgent,
|
|
541
|
+
output: finalResponse.content ?? "",
|
|
542
|
+
context: ctx.context,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
const result = await buildFinalResult(agent, currentAgent, messages, ctx, trace, lastFinishReason, lastResponseId, inputGuardrailResults);
|
|
546
|
+
resolveResult(result);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const { toolMessages, handoffAgent } = await executeToolCallsWithHandoffs(currentAgent, ctx, finalResponse.toolCalls, trace, signal, toolErrorFmt, runHooks, toolInputGuardrails, toolOutputGuardrails);
|
|
550
|
+
messages.push(...toolMessages);
|
|
551
|
+
// Check toolUseBehavior
|
|
552
|
+
if (await shouldStopAfterToolCalls(currentAgent, finalResponse.toolCalls, toolMessages)) {
|
|
553
|
+
const toolOutput = toolMessages.map((m) => m.content).join("\n");
|
|
554
|
+
resolveResult(new RunResult({
|
|
555
|
+
output: toolOutput,
|
|
556
|
+
messages,
|
|
557
|
+
usage: ctx.usage,
|
|
558
|
+
lastAgent: currentAgent,
|
|
559
|
+
finishReason: lastFinishReason,
|
|
560
|
+
numTurns: ctx.numTurns,
|
|
561
|
+
totalCostUsd: ctx.totalCostUsd,
|
|
562
|
+
responseId: lastResponseId,
|
|
563
|
+
inputGuardrailResults,
|
|
564
|
+
}));
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (handoffAgent) {
|
|
568
|
+
let allowHandoff = true;
|
|
569
|
+
if (currentAgent.hooks.beforeHandoff) {
|
|
570
|
+
const raw = await currentAgent.hooks.beforeHandoff({
|
|
571
|
+
fromAgent: currentAgent,
|
|
572
|
+
toAgent: handoffAgent,
|
|
573
|
+
context: ctx.context,
|
|
574
|
+
});
|
|
575
|
+
const decision = extractHandoffDecision(raw);
|
|
576
|
+
if (decision?.decision === "deny") {
|
|
577
|
+
allowHandoff = false;
|
|
578
|
+
const lastToolMsg = messages[messages.length - 1];
|
|
579
|
+
if (lastToolMsg && lastToolMsg.role === "tool") {
|
|
580
|
+
messages[messages.length - 1] = {
|
|
581
|
+
...lastToolMsg,
|
|
582
|
+
content: decision.reason ?? `Handoff to ${handoffAgent.name} was denied`,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (allowHandoff) {
|
|
588
|
+
if (runHooks?.onAgentEnd) {
|
|
589
|
+
await runHooks.onAgentEnd({
|
|
590
|
+
agent: currentAgent,
|
|
591
|
+
output: finalResponse?.content ?? "",
|
|
592
|
+
context: ctx.context,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
if (runHooks?.onHandoff) {
|
|
596
|
+
await runHooks.onHandoff({
|
|
597
|
+
fromAgent: currentAgent,
|
|
598
|
+
toAgent: handoffAgent,
|
|
599
|
+
context: ctx.context,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
// Apply handoff inputFilter if present
|
|
603
|
+
const matchedHandoff = currentAgent.handoffs.find((h) => h.agent === handoffAgent || h.agent.name === handoffAgent.name);
|
|
604
|
+
if (matchedHandoff?.inputFilter) {
|
|
605
|
+
const filtered = matchedHandoff.inputFilter({ history: [...messages] });
|
|
606
|
+
messages.length = 0;
|
|
607
|
+
messages.push(...filtered);
|
|
608
|
+
}
|
|
609
|
+
currentAgent = handoffAgent;
|
|
610
|
+
const newSystemPrompt = await currentAgent.getSystemPrompt(ctx.context);
|
|
611
|
+
const systemIdx = messages.findIndex((m) => m.role === "system");
|
|
612
|
+
if (newSystemPrompt) {
|
|
613
|
+
if (systemIdx >= 0) {
|
|
614
|
+
messages[systemIdx] = { role: "system", content: newSystemPrompt };
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
messages.unshift({ role: "system", content: newSystemPrompt });
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
else if (systemIdx >= 0) {
|
|
621
|
+
messages.splice(systemIdx, 1);
|
|
622
|
+
}
|
|
623
|
+
if (runHooks?.onAgentStart) {
|
|
624
|
+
await runHooks.onAgentStart({ agent: currentAgent, context: ctx.context });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// Fire onStop before throwing MaxTurnsExceededError
|
|
630
|
+
if (currentAgent.hooks.onStop) {
|
|
631
|
+
await currentAgent.hooks.onStop({
|
|
632
|
+
agent: currentAgent,
|
|
633
|
+
context: ctx.context,
|
|
634
|
+
reason: "max_turns",
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
// Check for error handler
|
|
638
|
+
if (options?.errorHandlers?.maxTurns) {
|
|
639
|
+
resolveResult(await options.errorHandlers.maxTurns({
|
|
640
|
+
agent: currentAgent,
|
|
641
|
+
messages,
|
|
642
|
+
context: ctx.context,
|
|
643
|
+
maxTurns,
|
|
644
|
+
}));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
throw new MaxTurnsExceededError(maxTurns);
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
rejectResult(error);
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function buildFinalResult(entryAgent, currentAgent, messages, ctx, trace, finishReason, responseId, inputGuardrailResults) {
|
|
655
|
+
const lastMessage = messages[messages.length - 1];
|
|
656
|
+
const rawOutput = lastMessage && lastMessage.role === "assistant" ? (lastMessage.content ?? "") : "";
|
|
657
|
+
// Run output guardrails on the current (possibly handed-off) agent
|
|
658
|
+
let outputGuardrailResults = [];
|
|
659
|
+
if (currentAgent.outputGuardrails.length > 0) {
|
|
660
|
+
if (trace) {
|
|
661
|
+
const span = trace.startSpan("output_guardrails", "guardrail");
|
|
662
|
+
try {
|
|
663
|
+
outputGuardrailResults = await runOutputGuardrails(currentAgent.outputGuardrails, rawOutput, ctx.context);
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
trace.endSpan(span);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
outputGuardrailResults = await runOutputGuardrails(currentAgent.outputGuardrails, rawOutput, ctx.context);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Parse structured output if outputType is set
|
|
674
|
+
let finalOutput;
|
|
675
|
+
if (entryAgent.outputType && rawOutput) {
|
|
676
|
+
try {
|
|
677
|
+
const parsed = JSON.parse(rawOutput);
|
|
678
|
+
finalOutput = entryAgent.outputType.parse(parsed);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
throw new OutputParseError(`Failed to parse structured output: ${getErrorMessage(error)}`, {
|
|
682
|
+
cause: error,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const result = new RunResult({
|
|
687
|
+
output: rawOutput,
|
|
688
|
+
messages,
|
|
689
|
+
usage: ctx.usage,
|
|
690
|
+
lastAgent: currentAgent,
|
|
691
|
+
finalOutput,
|
|
692
|
+
finishReason,
|
|
693
|
+
numTurns: ctx.numTurns,
|
|
694
|
+
totalCostUsd: ctx.totalCostUsd,
|
|
695
|
+
responseId,
|
|
696
|
+
inputGuardrailResults,
|
|
697
|
+
outputGuardrailResults,
|
|
698
|
+
});
|
|
699
|
+
// Fire afterRun hook on the entry agent
|
|
700
|
+
if (entryAgent.hooks.afterRun) {
|
|
701
|
+
await entryAgent.hooks.afterRun({ agent: entryAgent, result, context: ctx.context });
|
|
702
|
+
}
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
async function buildToolDefs(agent, context) {
|
|
706
|
+
const defs = [];
|
|
707
|
+
for (const t of agent.tools) {
|
|
708
|
+
if (isHostedTool(t)) {
|
|
709
|
+
defs.push(t.definition);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
// Check isEnabled for function tools
|
|
713
|
+
if (!(await checkEnabled(t.isEnabled, context)))
|
|
714
|
+
continue;
|
|
715
|
+
defs.push(toolToDefinition(t));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const sa of agent.subagents) {
|
|
719
|
+
defs.push(subagentToDefinition(sa));
|
|
720
|
+
}
|
|
721
|
+
for (const h of agent.handoffs) {
|
|
722
|
+
// Check isEnabled for handoffs
|
|
723
|
+
if (!(await checkEnabled(h.isEnabled, context)))
|
|
724
|
+
continue;
|
|
725
|
+
defs.push(handoffToDefinition(h));
|
|
726
|
+
}
|
|
727
|
+
return defs;
|
|
728
|
+
}
|
|
729
|
+
function extractUserText(messages) {
|
|
730
|
+
const texts = [];
|
|
731
|
+
for (const msg of messages) {
|
|
732
|
+
if (msg.role !== "user")
|
|
733
|
+
continue;
|
|
734
|
+
if (typeof msg.content === "string") {
|
|
735
|
+
texts.push(msg.content);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
for (const part of msg.content) {
|
|
739
|
+
if (part.type === "text") {
|
|
740
|
+
texts.push(part.text);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return texts.join("\n");
|
|
746
|
+
}
|
|
747
|
+
async function shouldStopAfterToolCalls(agent, toolCalls, toolMessages) {
|
|
748
|
+
const behavior = agent.toolUseBehavior;
|
|
749
|
+
if (behavior === "run_llm_again")
|
|
750
|
+
return false;
|
|
751
|
+
if (behavior === "stop_on_first_tool")
|
|
752
|
+
return true;
|
|
753
|
+
if (typeof behavior === "function") {
|
|
754
|
+
// Custom function variant
|
|
755
|
+
const results = toolCalls.map((tc, i) => ({
|
|
756
|
+
toolName: tc.function.name,
|
|
757
|
+
result: toolMessages[i]?.content ?? "",
|
|
758
|
+
}));
|
|
759
|
+
return behavior(results);
|
|
760
|
+
}
|
|
761
|
+
if (typeof behavior === "object" && "stopAtToolNames" in behavior) {
|
|
762
|
+
const stopNames = new Set(behavior.stopAtToolNames);
|
|
763
|
+
return toolCalls.some((tc) => stopNames.has(tc.function.name));
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
function applyResetToolChoice(settings, turn, resetToolChoice) {
|
|
768
|
+
if (!resetToolChoice || turn === 0)
|
|
769
|
+
return settings;
|
|
770
|
+
// After the first turn, reset tool_choice to "auto" to prevent infinite loops
|
|
771
|
+
if (settings.toolChoice && settings.toolChoice !== "auto") {
|
|
772
|
+
return { ...settings, toolChoice: "auto" };
|
|
773
|
+
}
|
|
774
|
+
return settings;
|
|
775
|
+
}
|
|
776
|
+
async function executeToolCallsWithHandoffs(agent, ctx, toolCalls, trace, signal, toolErrorFmt, runHooks, toolInputGuardrails, toolOutputGuardrails) {
|
|
777
|
+
let handoffAgent;
|
|
778
|
+
// Build O(1) lookup maps
|
|
779
|
+
const handoffsByName = new Map(agent.handoffs.map((h) => [h.toolName, h]));
|
|
780
|
+
const subagentsByName = new Map(agent.subagents.map((sa) => [sa.toolName, sa]));
|
|
781
|
+
const functionTools = agent.tools.filter(isFunctionTool);
|
|
782
|
+
const toolsByName = new Map(functionTools.map((t) => [t.name, t]));
|
|
783
|
+
const results = await Promise.all(toolCalls.map(async (tc) => {
|
|
784
|
+
const tcName = tc.function.name;
|
|
785
|
+
// Check handoffs first
|
|
786
|
+
const matchedHandoff = handoffsByName.get(tcName);
|
|
787
|
+
if (matchedHandoff) {
|
|
788
|
+
if (matchedHandoff.onHandoff) {
|
|
789
|
+
await matchedHandoff.onHandoff(ctx.context);
|
|
790
|
+
}
|
|
791
|
+
handoffAgent = matchedHandoff.agent;
|
|
792
|
+
return {
|
|
793
|
+
role: "tool",
|
|
794
|
+
tool_call_id: tc.id,
|
|
795
|
+
content: `Transferred to ${matchedHandoff.agent.name}`,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// Check subagents
|
|
799
|
+
const matchedSubagent = subagentsByName.get(tcName);
|
|
800
|
+
if (matchedSubagent) {
|
|
801
|
+
const saTool = subagentToTool(matchedSubagent);
|
|
802
|
+
try {
|
|
803
|
+
const params = JSON.parse(tc.function.arguments);
|
|
804
|
+
const fullToolCall = { id: tc.id, type: "function", function: tc.function };
|
|
805
|
+
const decision = await resolveBeforeToolCallHook(agent.hooks.beforeToolCall, {
|
|
806
|
+
agent,
|
|
807
|
+
toolCall: fullToolCall,
|
|
808
|
+
context: ctx.context,
|
|
809
|
+
});
|
|
810
|
+
if (decision?.decision === "deny") {
|
|
811
|
+
return {
|
|
812
|
+
role: "tool",
|
|
813
|
+
tool_call_id: tc.id,
|
|
814
|
+
content: decision.reason ?? `Tool call "${tcName}" was denied`,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
// Fire onSubagentStart
|
|
818
|
+
if (agent.hooks.onSubagentStart) {
|
|
819
|
+
await agent.hooks.onSubagentStart({
|
|
820
|
+
agent,
|
|
821
|
+
subagent: matchedSubagent,
|
|
822
|
+
context: ctx.context,
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
// Fire run-level onToolStart
|
|
826
|
+
if (runHooks?.onToolStart) {
|
|
827
|
+
await runHooks.onToolStart({ agent, toolName: tcName, context: ctx.context });
|
|
828
|
+
}
|
|
829
|
+
let result;
|
|
830
|
+
if (trace) {
|
|
831
|
+
const span = trace.startSpan(`subagent:${matchedSubagent.agent.name}`, "subagent", {
|
|
832
|
+
toolName: tcName,
|
|
833
|
+
});
|
|
834
|
+
try {
|
|
835
|
+
result = await saTool.execute(ctx.context, params, { signal });
|
|
836
|
+
}
|
|
837
|
+
finally {
|
|
838
|
+
trace.endSpan(span);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else {
|
|
842
|
+
result = await saTool.execute(ctx.context, params, { signal });
|
|
843
|
+
}
|
|
844
|
+
// Fire onSubagentStop
|
|
845
|
+
if (agent.hooks.onSubagentStop) {
|
|
846
|
+
await agent.hooks.onSubagentStop({
|
|
847
|
+
agent,
|
|
848
|
+
subagent: matchedSubagent,
|
|
849
|
+
result,
|
|
850
|
+
context: ctx.context,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
// Fire run-level onToolEnd
|
|
854
|
+
if (runHooks?.onToolEnd) {
|
|
855
|
+
await runHooks.onToolEnd({ agent, toolName: tcName, result, context: ctx.context });
|
|
856
|
+
}
|
|
857
|
+
await resolveAfterToolCallHook(agent.hooks.afterToolCall, {
|
|
858
|
+
agent,
|
|
859
|
+
toolCall: fullToolCall,
|
|
860
|
+
result,
|
|
861
|
+
context: ctx.context,
|
|
862
|
+
});
|
|
863
|
+
return {
|
|
864
|
+
role: "tool",
|
|
865
|
+
tool_call_id: tc.id,
|
|
866
|
+
content: result,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
catch (error) {
|
|
870
|
+
return {
|
|
871
|
+
role: "tool",
|
|
872
|
+
tool_call_id: tc.id,
|
|
873
|
+
content: formatToolError(matchedSubagent.agent.name, error, toolErrorFmt),
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
// Otherwise, execute as normal tool
|
|
878
|
+
const tool = toolsByName.get(tcName);
|
|
879
|
+
if (!tool) {
|
|
880
|
+
return {
|
|
881
|
+
role: "tool",
|
|
882
|
+
tool_call_id: tc.id,
|
|
883
|
+
content: `Error: Unknown tool "${tcName}"`,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
let params = JSON.parse(tc.function.arguments);
|
|
888
|
+
const fullToolCall = { id: tc.id, type: "function", function: tc.function };
|
|
889
|
+
// Fire beforeToolCall hook
|
|
890
|
+
const decision = await resolveBeforeToolCallHook(agent.hooks.beforeToolCall, {
|
|
891
|
+
agent,
|
|
892
|
+
toolCall: fullToolCall,
|
|
893
|
+
context: ctx.context,
|
|
894
|
+
});
|
|
895
|
+
if (decision?.decision === "deny") {
|
|
896
|
+
return {
|
|
897
|
+
role: "tool",
|
|
898
|
+
tool_call_id: tc.id,
|
|
899
|
+
content: decision.reason ?? `Tool call "${tcName}" was denied`,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (decision?.decision === "modify") {
|
|
903
|
+
params = decision.modifiedParams;
|
|
904
|
+
}
|
|
905
|
+
// Run tool input guardrails
|
|
906
|
+
if (toolInputGuardrails && toolInputGuardrails.length > 0) {
|
|
907
|
+
const guardrailResults = await runToolInputGuardrails(toolInputGuardrails, tcName, params, ctx.context);
|
|
908
|
+
const tripped = guardrailResults.find((r) => r.result.tripwireTriggered);
|
|
909
|
+
if (tripped) {
|
|
910
|
+
return {
|
|
911
|
+
role: "tool",
|
|
912
|
+
tool_call_id: tc.id,
|
|
913
|
+
content: `Tool input guardrail "${tripped.guardrailName}" blocked execution of "${tcName}"`,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// Fire run-level onToolStart
|
|
918
|
+
if (runHooks?.onToolStart) {
|
|
919
|
+
await runHooks.onToolStart({ agent, toolName: tcName, context: ctx.context });
|
|
920
|
+
}
|
|
921
|
+
checkAborted(signal);
|
|
922
|
+
let result;
|
|
923
|
+
if (trace) {
|
|
924
|
+
const span = trace.startSpan(`tool:${tcName}`, "tool_execution", {
|
|
925
|
+
toolName: tcName,
|
|
926
|
+
});
|
|
927
|
+
try {
|
|
928
|
+
result = await executeWithTimeout(() => tool.execute(ctx.context, params, { signal }), tool.timeout, tcName);
|
|
929
|
+
}
|
|
930
|
+
finally {
|
|
931
|
+
trace.endSpan(span);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
result = await executeWithTimeout(() => tool.execute(ctx.context, params, { signal }), tool.timeout, tcName);
|
|
936
|
+
}
|
|
937
|
+
// Run tool output guardrails
|
|
938
|
+
if (toolOutputGuardrails && toolOutputGuardrails.length > 0) {
|
|
939
|
+
const guardrailResults = await runToolOutputGuardrails(toolOutputGuardrails, tcName, result, ctx.context);
|
|
940
|
+
const tripped = guardrailResults.find((r) => r.result.tripwireTriggered);
|
|
941
|
+
if (tripped) {
|
|
942
|
+
result = `Tool output guardrail "${tripped.guardrailName}" flagged the output of "${tcName}"`;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// Fire run-level onToolEnd
|
|
946
|
+
if (runHooks?.onToolEnd) {
|
|
947
|
+
await runHooks.onToolEnd({ agent, toolName: tcName, result, context: ctx.context });
|
|
948
|
+
}
|
|
949
|
+
// Fire afterToolCall hook
|
|
950
|
+
await resolveAfterToolCallHook(agent.hooks.afterToolCall, {
|
|
951
|
+
agent,
|
|
952
|
+
toolCall: fullToolCall,
|
|
953
|
+
result,
|
|
954
|
+
context: ctx.context,
|
|
955
|
+
});
|
|
956
|
+
return {
|
|
957
|
+
role: "tool",
|
|
958
|
+
tool_call_id: tc.id,
|
|
959
|
+
content: result,
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
return {
|
|
964
|
+
role: "tool",
|
|
965
|
+
tool_call_id: tc.id,
|
|
966
|
+
content: formatToolError(tcName, error, toolErrorFmt),
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
}));
|
|
970
|
+
return { toolMessages: results, handoffAgent };
|
|
971
|
+
}
|
|
972
|
+
//# sourceMappingURL=run.js.map
|