@ljoukov/llm 3.0.6 → 3.0.8

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/index.cjs CHANGED
@@ -71,6 +71,7 @@ __export(index_exports, {
71
71
  createReadFilesTool: () => createReadFilesTool,
72
72
  createReplaceTool: () => createReplaceTool,
73
73
  createRgSearchTool: () => createRgSearchTool,
74
+ createToolLoopSteeringChannel: () => createToolLoopSteeringChannel,
74
75
  createWriteFileTool: () => createWriteFileTool,
75
76
  customTool: () => customTool,
76
77
  encodeChatGptAuthJson: () => encodeChatGptAuthJson,
@@ -102,8 +103,10 @@ __export(index_exports, {
102
103
  runAgentLoop: () => runAgentLoop,
103
104
  runToolLoop: () => runToolLoop,
104
105
  sanitisePartForLogging: () => sanitisePartForLogging,
106
+ streamAgentLoop: () => streamAgentLoop,
105
107
  streamJson: () => streamJson,
106
108
  streamText: () => streamText,
109
+ streamToolLoop: () => streamToolLoop,
107
110
  stripCodexCitationMarkers: () => stripCodexCitationMarkers,
108
111
  toGeminiJsonSchema: () => toGeminiJsonSchema,
109
112
  tool: () => tool
@@ -4908,6 +4911,102 @@ var DEFAULT_TOOL_LOOP_MAX_STEPS = 8;
4908
4911
  function resolveToolLoopContents(input) {
4909
4912
  return resolveTextContents(input);
4910
4913
  }
4914
+ var toolLoopSteeringInternals = /* @__PURE__ */ new WeakMap();
4915
+ function createToolLoopSteeringChannel() {
4916
+ const pending = [];
4917
+ let closed = false;
4918
+ const channel = {
4919
+ append: (input) => {
4920
+ if (closed) {
4921
+ return { accepted: false, queuedCount: pending.length };
4922
+ }
4923
+ const normalized = normalizeToolLoopSteeringInput(input);
4924
+ if (normalized.length === 0) {
4925
+ return { accepted: false, queuedCount: pending.length };
4926
+ }
4927
+ pending.push(...normalized);
4928
+ return { accepted: true, queuedCount: pending.length };
4929
+ },
4930
+ steer: (input) => channel.append(input),
4931
+ pendingCount: () => pending.length,
4932
+ close: () => {
4933
+ if (closed) {
4934
+ return;
4935
+ }
4936
+ closed = true;
4937
+ pending.length = 0;
4938
+ }
4939
+ };
4940
+ const internalState = {
4941
+ drainPendingContents: () => {
4942
+ if (pending.length === 0) {
4943
+ return [];
4944
+ }
4945
+ return pending.splice(0, pending.length);
4946
+ },
4947
+ close: channel.close
4948
+ };
4949
+ toolLoopSteeringInternals.set(channel, internalState);
4950
+ return channel;
4951
+ }
4952
+ function resolveToolLoopSteeringInternal(steering) {
4953
+ if (!steering) {
4954
+ return void 0;
4955
+ }
4956
+ const internal = toolLoopSteeringInternals.get(steering);
4957
+ if (!internal) {
4958
+ throw new Error(
4959
+ "Invalid tool loop steering channel. Use createToolLoopSteeringChannel() to construct one."
4960
+ );
4961
+ }
4962
+ return internal;
4963
+ }
4964
+ function normalizeToolLoopSteeringInput(input) {
4965
+ const messages = typeof input === "string" ? [{ role: "user", content: input }] : Array.isArray(input) ? input : [input];
4966
+ const normalized = [];
4967
+ for (const message of messages) {
4968
+ const role = message.role ?? "user";
4969
+ if (role !== "user") {
4970
+ throw new Error("Tool loop steering only accepts role='user' messages.");
4971
+ }
4972
+ if (typeof message.content === "string") {
4973
+ if (message.content.length === 0) {
4974
+ continue;
4975
+ }
4976
+ normalized.push({
4977
+ role: "user",
4978
+ parts: [{ type: "text", text: message.content }]
4979
+ });
4980
+ continue;
4981
+ }
4982
+ if (!Array.isArray(message.content) || message.content.length === 0) {
4983
+ continue;
4984
+ }
4985
+ const parts = [];
4986
+ for (const part of message.content) {
4987
+ if (part.type === "text") {
4988
+ parts.push({ type: "text", text: part.text });
4989
+ } else {
4990
+ parts.push({ type: "inlineData", data: part.data, mimeType: part.mimeType });
4991
+ }
4992
+ }
4993
+ if (parts.length > 0) {
4994
+ normalized.push({ role: "user", parts });
4995
+ }
4996
+ }
4997
+ return normalized;
4998
+ }
4999
+ function toChatGptAssistantMessage(text) {
5000
+ if (!text) {
5001
+ return void 0;
5002
+ }
5003
+ return {
5004
+ type: "message",
5005
+ role: "assistant",
5006
+ status: "completed",
5007
+ content: [{ type: "output_text", text }]
5008
+ };
5009
+ }
4911
5010
  function isCustomTool(toolDef) {
4912
5011
  return toolDef.type === "custom";
4913
5012
  }
@@ -5026,451 +5125,817 @@ async function runToolLoop(request) {
5026
5125
  }
5027
5126
  const maxSteps = Math.max(1, Math.floor(request.maxSteps ?? DEFAULT_TOOL_LOOP_MAX_STEPS));
5028
5127
  const providerInfo = resolveProvider(request.model);
5128
+ const steeringInternal = resolveToolLoopSteeringInternal(request.steering);
5029
5129
  const steps = [];
5030
5130
  let totalCostUsd = 0;
5031
5131
  let finalText = "";
5032
5132
  let finalThoughts = "";
5033
- if (providerInfo.provider === "openai") {
5034
- const openAiAgentTools = buildOpenAiToolsFromToolSet(request.tools);
5035
- const openAiNativeTools = toOpenAiTools(request.modelTools);
5036
- const openAiTools = openAiNativeTools ? [...openAiNativeTools, ...openAiAgentTools] : [...openAiAgentTools];
5037
- const reasoningEffort = resolveOpenAiReasoningEffort(
5038
- providerInfo.model,
5039
- request.openAiReasoningEffort
5040
- );
5041
- const textConfig = {
5042
- format: { type: "text" },
5043
- verbosity: resolveOpenAiVerbosity(providerInfo.model)
5044
- };
5045
- const reasoning = {
5046
- effort: toOpenAiReasoningEffort(reasoningEffort),
5047
- summary: "detailed"
5048
- };
5049
- let previousResponseId;
5050
- let input = toOpenAiInput(contents);
5051
- for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5052
- const turn = stepIndex + 1;
5053
- const stepStartedAtMs = Date.now();
5054
- let firstModelEventAtMs;
5055
- let schedulerMetrics;
5056
- const abortController = new AbortController();
5057
- if (request.signal) {
5058
- if (request.signal.aborted) {
5059
- abortController.abort(request.signal.reason);
5060
- } else {
5061
- request.signal.addEventListener(
5062
- "abort",
5063
- () => abortController.abort(request.signal?.reason),
5064
- { once: true }
5065
- );
5066
- }
5067
- }
5068
- const onEvent = request.onEvent;
5069
- let modelVersion = request.model;
5070
- let usageTokens;
5071
- const emitEvent = (ev) => {
5072
- onEvent?.(ev);
5133
+ try {
5134
+ if (providerInfo.provider === "openai") {
5135
+ const openAiAgentTools = buildOpenAiToolsFromToolSet(request.tools);
5136
+ const openAiNativeTools = toOpenAiTools(request.modelTools);
5137
+ const openAiTools = openAiNativeTools ? [...openAiNativeTools, ...openAiAgentTools] : [...openAiAgentTools];
5138
+ const reasoningEffort = resolveOpenAiReasoningEffort(
5139
+ providerInfo.model,
5140
+ request.openAiReasoningEffort
5141
+ );
5142
+ const textConfig = {
5143
+ format: { type: "text" },
5144
+ verbosity: resolveOpenAiVerbosity(providerInfo.model)
5073
5145
  };
5074
- const markFirstModelEvent = () => {
5075
- if (firstModelEventAtMs === void 0) {
5076
- firstModelEventAtMs = Date.now();
5077
- }
5146
+ const reasoning = {
5147
+ effort: toOpenAiReasoningEffort(reasoningEffort),
5148
+ summary: "detailed"
5078
5149
  };
5079
- const finalResponse = await runOpenAiCall(
5080
- async (client) => {
5081
- const stream = client.responses.stream(
5082
- {
5083
- model: providerInfo.model,
5084
- input,
5085
- ...previousResponseId ? { previous_response_id: previousResponseId } : {},
5086
- ...openAiTools.length > 0 ? { tools: openAiTools } : {},
5087
- ...openAiTools.length > 0 ? { parallel_tool_calls: true } : {},
5088
- reasoning,
5089
- text: textConfig,
5090
- include: ["reasoning.encrypted_content"]
5091
- },
5092
- { signal: abortController.signal }
5093
- );
5094
- for await (const event of stream) {
5095
- markFirstModelEvent();
5096
- switch (event.type) {
5097
- case "response.output_text.delta":
5098
- emitEvent({
5099
- type: "delta",
5100
- channel: "response",
5101
- text: typeof event.delta === "string" ? event.delta : ""
5102
- });
5103
- break;
5104
- case "response.reasoning_summary_text.delta":
5105
- emitEvent({
5106
- type: "delta",
5107
- channel: "thought",
5108
- text: typeof event.delta === "string" ? event.delta : ""
5109
- });
5110
- break;
5111
- case "response.refusal.delta":
5112
- emitEvent({ type: "blocked" });
5113
- break;
5114
- default:
5115
- break;
5150
+ let previousResponseId;
5151
+ let input = toOpenAiInput(contents);
5152
+ for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5153
+ const turn = stepIndex + 1;
5154
+ const stepStartedAtMs = Date.now();
5155
+ let firstModelEventAtMs;
5156
+ let schedulerMetrics;
5157
+ const abortController = new AbortController();
5158
+ if (request.signal) {
5159
+ if (request.signal.aborted) {
5160
+ abortController.abort(request.signal.reason);
5161
+ } else {
5162
+ request.signal.addEventListener(
5163
+ "abort",
5164
+ () => abortController.abort(request.signal?.reason),
5165
+ { once: true }
5166
+ );
5167
+ }
5168
+ }
5169
+ const onEvent = request.onEvent;
5170
+ let modelVersion = request.model;
5171
+ let usageTokens;
5172
+ let thoughtDeltaEmitted = false;
5173
+ const emitEvent = (ev) => {
5174
+ onEvent?.(ev);
5175
+ };
5176
+ const markFirstModelEvent = () => {
5177
+ if (firstModelEventAtMs === void 0) {
5178
+ firstModelEventAtMs = Date.now();
5179
+ }
5180
+ };
5181
+ const finalResponse = await runOpenAiCall(
5182
+ async (client) => {
5183
+ const stream = client.responses.stream(
5184
+ {
5185
+ model: providerInfo.model,
5186
+ input,
5187
+ ...previousResponseId ? { previous_response_id: previousResponseId } : {},
5188
+ ...openAiTools.length > 0 ? { tools: openAiTools } : {},
5189
+ ...openAiTools.length > 0 ? { parallel_tool_calls: true } : {},
5190
+ reasoning,
5191
+ text: textConfig,
5192
+ include: ["reasoning.encrypted_content"]
5193
+ },
5194
+ { signal: abortController.signal }
5195
+ );
5196
+ for await (const event of stream) {
5197
+ markFirstModelEvent();
5198
+ switch (event.type) {
5199
+ case "response.output_text.delta":
5200
+ emitEvent({
5201
+ type: "delta",
5202
+ channel: "response",
5203
+ text: typeof event.delta === "string" ? event.delta : ""
5204
+ });
5205
+ break;
5206
+ case "response.reasoning_summary_text.delta":
5207
+ thoughtDeltaEmitted = true;
5208
+ emitEvent({
5209
+ type: "delta",
5210
+ channel: "thought",
5211
+ text: typeof event.delta === "string" ? event.delta : ""
5212
+ });
5213
+ break;
5214
+ case "response.refusal.delta":
5215
+ emitEvent({ type: "blocked" });
5216
+ break;
5217
+ default:
5218
+ break;
5219
+ }
5220
+ }
5221
+ return await stream.finalResponse();
5222
+ },
5223
+ providerInfo.model,
5224
+ {
5225
+ onSettled: (metrics) => {
5226
+ schedulerMetrics = metrics;
5116
5227
  }
5117
5228
  }
5118
- return await stream.finalResponse();
5119
- },
5120
- providerInfo.model,
5121
- {
5122
- onSettled: (metrics) => {
5123
- schedulerMetrics = metrics;
5229
+ );
5230
+ modelVersion = typeof finalResponse.model === "string" ? finalResponse.model : request.model;
5231
+ emitEvent({ type: "model", modelVersion });
5232
+ if (finalResponse.error) {
5233
+ const message = typeof finalResponse.error.message === "string" ? finalResponse.error.message : "OpenAI response failed";
5234
+ throw new Error(message);
5235
+ }
5236
+ usageTokens = extractOpenAiUsageTokens(finalResponse.usage);
5237
+ const responseText = extractOpenAiResponseParts(finalResponse).parts.filter((p) => p.type === "text" && p.thought !== true).map((p) => p.text).join("").trim();
5238
+ const reasoningSummary = extractOpenAiReasoningSummary(finalResponse).trim();
5239
+ if (!thoughtDeltaEmitted && reasoningSummary.length > 0) {
5240
+ emitEvent({ type: "delta", channel: "thought", text: reasoningSummary });
5241
+ }
5242
+ const modelCompletedAtMs = Date.now();
5243
+ const stepCostUsd = estimateCallCostUsd({
5244
+ modelId: modelVersion,
5245
+ tokens: usageTokens,
5246
+ responseImages: 0
5247
+ });
5248
+ totalCostUsd += stepCostUsd;
5249
+ if (usageTokens) {
5250
+ emitEvent({ type: "usage", usage: usageTokens, costUsd: stepCostUsd, modelVersion });
5251
+ }
5252
+ const responseToolCalls = extractOpenAiToolCalls(finalResponse.output);
5253
+ const stepToolCalls = [];
5254
+ if (responseToolCalls.length === 0) {
5255
+ const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
5256
+ const steeringItems2 = steeringInput2.length > 0 ? toOpenAiInput(steeringInput2) : [];
5257
+ finalText = responseText;
5258
+ finalThoughts = reasoningSummary;
5259
+ const stepCompletedAtMs2 = Date.now();
5260
+ const timing2 = buildStepTiming({
5261
+ stepStartedAtMs,
5262
+ stepCompletedAtMs: stepCompletedAtMs2,
5263
+ modelCompletedAtMs,
5264
+ firstModelEventAtMs,
5265
+ schedulerMetrics,
5266
+ toolExecutionMs: 0,
5267
+ waitToolMs: 0
5268
+ });
5269
+ steps.push({
5270
+ step: steps.length + 1,
5271
+ modelVersion,
5272
+ text: responseText || void 0,
5273
+ thoughts: reasoningSummary || void 0,
5274
+ toolCalls: [],
5275
+ usage: usageTokens,
5276
+ costUsd: stepCostUsd,
5277
+ timing: timing2
5278
+ });
5279
+ if (steeringItems2.length === 0) {
5280
+ return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5124
5281
  }
5282
+ previousResponseId = finalResponse.id;
5283
+ input = steeringItems2;
5284
+ continue;
5125
5285
  }
5126
- );
5127
- modelVersion = typeof finalResponse.model === "string" ? finalResponse.model : request.model;
5128
- emitEvent({ type: "model", modelVersion });
5129
- if (finalResponse.error) {
5130
- const message = typeof finalResponse.error.message === "string" ? finalResponse.error.message : "OpenAI response failed";
5131
- throw new Error(message);
5132
- }
5133
- usageTokens = extractOpenAiUsageTokens(finalResponse.usage);
5134
- const responseText = extractOpenAiResponseParts(finalResponse).parts.filter((p) => p.type === "text" && p.thought !== true).map((p) => p.text).join("").trim();
5135
- const reasoningSummary = extractOpenAiReasoningSummary(finalResponse).trim();
5136
- const modelCompletedAtMs = Date.now();
5137
- const stepCostUsd = estimateCallCostUsd({
5138
- modelId: modelVersion,
5139
- tokens: usageTokens,
5140
- responseImages: 0
5141
- });
5142
- totalCostUsd += stepCostUsd;
5143
- if (usageTokens) {
5144
- emitEvent({ type: "usage", usage: usageTokens, costUsd: stepCostUsd, modelVersion });
5145
- }
5146
- const responseToolCalls = extractOpenAiToolCalls(finalResponse.output);
5147
- const stepToolCalls = [];
5148
- if (responseToolCalls.length === 0) {
5149
- finalText = responseText;
5150
- finalThoughts = reasoningSummary;
5151
- const stepCompletedAtMs2 = Date.now();
5152
- const timing2 = buildStepTiming({
5286
+ const callInputs = responseToolCalls.map((call, index) => {
5287
+ const toolIndex = index + 1;
5288
+ const toolId = buildToolLogId(turn, toolIndex);
5289
+ const toolName = call.name;
5290
+ if (call.kind === "custom") {
5291
+ return {
5292
+ call,
5293
+ toolName,
5294
+ value: call.input,
5295
+ parseError: void 0,
5296
+ toolId,
5297
+ turn,
5298
+ toolIndex
5299
+ };
5300
+ }
5301
+ const { value, error: parseError } = parseOpenAiToolArguments(call.arguments);
5302
+ return { call, toolName, value, parseError, toolId, turn, toolIndex };
5303
+ });
5304
+ for (const entry of callInputs) {
5305
+ emitEvent({
5306
+ type: "tool_call",
5307
+ phase: "started",
5308
+ turn: entry.turn,
5309
+ toolIndex: entry.toolIndex,
5310
+ toolName: entry.toolName,
5311
+ toolId: entry.toolId,
5312
+ callKind: entry.call.kind,
5313
+ callId: entry.call.call_id,
5314
+ input: entry.value
5315
+ });
5316
+ }
5317
+ const callResults = await Promise.all(
5318
+ callInputs.map(async (entry) => {
5319
+ return await toolCallContextStorage.run(
5320
+ {
5321
+ toolName: entry.toolName,
5322
+ toolId: entry.toolId,
5323
+ turn: entry.turn,
5324
+ toolIndex: entry.toolIndex
5325
+ },
5326
+ async () => {
5327
+ const { result, outputPayload } = await executeToolCall({
5328
+ callKind: entry.call.kind,
5329
+ toolName: entry.toolName,
5330
+ tool: request.tools[entry.toolName],
5331
+ rawInput: entry.value,
5332
+ parseError: entry.parseError
5333
+ });
5334
+ return { entry, result, outputPayload };
5335
+ }
5336
+ );
5337
+ })
5338
+ );
5339
+ const toolOutputs = [];
5340
+ let toolExecutionMs = 0;
5341
+ let waitToolMs = 0;
5342
+ for (const { entry, result, outputPayload } of callResults) {
5343
+ stepToolCalls.push({ ...result, callId: entry.call.call_id });
5344
+ const callDurationMs = toToolResultDuration(result);
5345
+ toolExecutionMs += callDurationMs;
5346
+ if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5347
+ waitToolMs += callDurationMs;
5348
+ }
5349
+ emitEvent({
5350
+ type: "tool_call",
5351
+ phase: "completed",
5352
+ turn: entry.turn,
5353
+ toolIndex: entry.toolIndex,
5354
+ toolName: entry.toolName,
5355
+ toolId: entry.toolId,
5356
+ callKind: entry.call.kind,
5357
+ callId: entry.call.call_id,
5358
+ input: entry.value,
5359
+ output: result.output,
5360
+ error: result.error,
5361
+ durationMs: result.durationMs
5362
+ });
5363
+ if (entry.call.kind === "custom") {
5364
+ toolOutputs.push({
5365
+ type: "custom_tool_call_output",
5366
+ call_id: entry.call.call_id,
5367
+ output: mergeToolOutput(outputPayload)
5368
+ });
5369
+ } else {
5370
+ toolOutputs.push({
5371
+ type: "function_call_output",
5372
+ call_id: entry.call.call_id,
5373
+ output: mergeToolOutput(outputPayload)
5374
+ });
5375
+ }
5376
+ }
5377
+ const stepCompletedAtMs = Date.now();
5378
+ const timing = buildStepTiming({
5153
5379
  stepStartedAtMs,
5154
- stepCompletedAtMs: stepCompletedAtMs2,
5380
+ stepCompletedAtMs,
5155
5381
  modelCompletedAtMs,
5156
5382
  firstModelEventAtMs,
5157
5383
  schedulerMetrics,
5158
- toolExecutionMs: 0,
5159
- waitToolMs: 0
5384
+ toolExecutionMs,
5385
+ waitToolMs
5160
5386
  });
5161
5387
  steps.push({
5162
5388
  step: steps.length + 1,
5163
5389
  modelVersion,
5164
5390
  text: responseText || void 0,
5165
5391
  thoughts: reasoningSummary || void 0,
5166
- toolCalls: [],
5392
+ toolCalls: stepToolCalls,
5167
5393
  usage: usageTokens,
5168
5394
  costUsd: stepCostUsd,
5169
- timing: timing2
5395
+ timing
5170
5396
  });
5171
- return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5172
- }
5173
- const callInputs = responseToolCalls.map((call, index) => {
5174
- const toolIndex = index + 1;
5175
- const toolId = buildToolLogId(turn, toolIndex);
5176
- const toolName = call.name;
5177
- if (call.kind === "custom") {
5178
- return {
5179
- call,
5180
- toolName,
5181
- value: call.input,
5182
- parseError: void 0,
5183
- toolId,
5184
- turn,
5185
- toolIndex
5186
- };
5187
- }
5188
- const { value, error: parseError } = parseOpenAiToolArguments(call.arguments);
5189
- return { call, toolName, value, parseError, toolId, turn, toolIndex };
5190
- });
5191
- const callResults = await Promise.all(
5192
- callInputs.map(async (entry) => {
5193
- return await toolCallContextStorage.run(
5194
- {
5195
- toolName: entry.toolName,
5196
- toolId: entry.toolId,
5197
- turn: entry.turn,
5198
- toolIndex: entry.toolIndex
5397
+ const steeringInput = steeringInternal?.drainPendingContents() ?? [];
5398
+ const steeringItems = steeringInput.length > 0 ? toOpenAiInput(steeringInput) : [];
5399
+ previousResponseId = finalResponse.id;
5400
+ input = steeringItems.length > 0 ? toolOutputs.concat(steeringItems) : toolOutputs;
5401
+ }
5402
+ throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
5403
+ }
5404
+ if (providerInfo.provider === "chatgpt") {
5405
+ const openAiAgentTools = buildOpenAiToolsFromToolSet(request.tools);
5406
+ const openAiNativeTools = toOpenAiTools(request.modelTools);
5407
+ const openAiTools = openAiNativeTools ? [...openAiNativeTools, ...openAiAgentTools] : [...openAiAgentTools];
5408
+ const reasoningEffort = resolveOpenAiReasoningEffort(
5409
+ request.model,
5410
+ request.openAiReasoningEffort
5411
+ );
5412
+ const toolLoopInput = toChatGptInput(contents);
5413
+ const conversationId = `tool-loop-${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
5414
+ const promptCacheKey = conversationId;
5415
+ let input = [...toolLoopInput.input];
5416
+ for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5417
+ const turn = stepIndex + 1;
5418
+ const stepStartedAtMs = Date.now();
5419
+ let firstModelEventAtMs;
5420
+ let thoughtDeltaEmitted = false;
5421
+ const markFirstModelEvent = () => {
5422
+ if (firstModelEventAtMs === void 0) {
5423
+ firstModelEventAtMs = Date.now();
5424
+ }
5425
+ };
5426
+ const response = await collectChatGptCodexResponseWithRetry({
5427
+ sessionId: conversationId,
5428
+ request: {
5429
+ model: providerInfo.model,
5430
+ store: false,
5431
+ stream: true,
5432
+ instructions: toolLoopInput.instructions ?? "You are a helpful assistant.",
5433
+ input,
5434
+ prompt_cache_key: promptCacheKey,
5435
+ include: ["reasoning.encrypted_content"],
5436
+ tools: openAiTools,
5437
+ tool_choice: "auto",
5438
+ parallel_tool_calls: true,
5439
+ reasoning: {
5440
+ effort: toOpenAiReasoningEffort(reasoningEffort),
5441
+ summary: "detailed"
5199
5442
  },
5200
- async () => {
5201
- const { result, outputPayload } = await executeToolCall({
5202
- callKind: entry.call.kind,
5203
- toolName: entry.toolName,
5204
- tool: request.tools[entry.toolName],
5205
- rawInput: entry.value,
5206
- parseError: entry.parseError
5207
- });
5208
- return { entry, result, outputPayload };
5443
+ text: { verbosity: resolveOpenAiVerbosity(request.model) }
5444
+ },
5445
+ signal: request.signal,
5446
+ onDelta: (delta) => {
5447
+ if (delta.thoughtDelta) {
5448
+ markFirstModelEvent();
5449
+ thoughtDeltaEmitted = true;
5450
+ request.onEvent?.({ type: "delta", channel: "thought", text: delta.thoughtDelta });
5209
5451
  }
5210
- );
5211
- })
5212
- );
5213
- const toolOutputs = [];
5214
- let toolExecutionMs = 0;
5215
- let waitToolMs = 0;
5216
- for (const { entry, result, outputPayload } of callResults) {
5217
- stepToolCalls.push({ ...result, callId: entry.call.call_id });
5218
- const callDurationMs = toToolResultDuration(result);
5219
- toolExecutionMs += callDurationMs;
5220
- if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5221
- waitToolMs += callDurationMs;
5452
+ if (delta.textDelta) {
5453
+ markFirstModelEvent();
5454
+ request.onEvent?.({ type: "delta", channel: "response", text: delta.textDelta });
5455
+ }
5456
+ }
5457
+ });
5458
+ const modelCompletedAtMs = Date.now();
5459
+ const modelVersion = response.model ? `chatgpt-${response.model}` : request.model;
5460
+ const usageTokens = extractChatGptUsageTokens(response.usage);
5461
+ const stepCostUsd = estimateCallCostUsd({
5462
+ modelId: modelVersion,
5463
+ tokens: usageTokens,
5464
+ responseImages: 0
5465
+ });
5466
+ totalCostUsd += stepCostUsd;
5467
+ const responseText = (response.text ?? "").trim();
5468
+ const reasoningSummaryText = (response.reasoningSummaryText ?? "").trim();
5469
+ if (!thoughtDeltaEmitted && reasoningSummaryText.length > 0) {
5470
+ request.onEvent?.({ type: "delta", channel: "thought", text: reasoningSummaryText });
5222
5471
  }
5223
- if (entry.call.kind === "custom") {
5224
- toolOutputs.push({
5225
- type: "custom_tool_call_output",
5226
- call_id: entry.call.call_id,
5227
- output: mergeToolOutput(outputPayload)
5472
+ const responseToolCalls = response.toolCalls ?? [];
5473
+ if (responseToolCalls.length === 0) {
5474
+ const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
5475
+ const steeringItems2 = steeringInput2.length > 0 ? toChatGptInput(steeringInput2).input : [];
5476
+ finalText = responseText;
5477
+ finalThoughts = reasoningSummaryText;
5478
+ const stepCompletedAtMs2 = Date.now();
5479
+ const timing2 = buildStepTiming({
5480
+ stepStartedAtMs,
5481
+ stepCompletedAtMs: stepCompletedAtMs2,
5482
+ modelCompletedAtMs,
5483
+ firstModelEventAtMs,
5484
+ toolExecutionMs: 0,
5485
+ waitToolMs: 0
5228
5486
  });
5229
- } else {
5230
- toolOutputs.push({
5231
- type: "function_call_output",
5232
- call_id: entry.call.call_id,
5233
- output: mergeToolOutput(outputPayload)
5487
+ steps.push({
5488
+ step: steps.length + 1,
5489
+ modelVersion,
5490
+ text: responseText || void 0,
5491
+ thoughts: reasoningSummaryText || void 0,
5492
+ toolCalls: [],
5493
+ usage: usageTokens,
5494
+ costUsd: stepCostUsd,
5495
+ timing: timing2
5234
5496
  });
5497
+ if (steeringItems2.length === 0) {
5498
+ return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5499
+ }
5500
+ const assistantItem = toChatGptAssistantMessage(responseText);
5501
+ input = assistantItem ? input.concat(assistantItem, steeringItems2) : input.concat(steeringItems2);
5502
+ continue;
5235
5503
  }
5236
- }
5237
- const stepCompletedAtMs = Date.now();
5238
- const timing = buildStepTiming({
5239
- stepStartedAtMs,
5240
- stepCompletedAtMs,
5241
- modelCompletedAtMs,
5242
- firstModelEventAtMs,
5243
- schedulerMetrics,
5244
- toolExecutionMs,
5245
- waitToolMs
5246
- });
5247
- steps.push({
5248
- step: steps.length + 1,
5249
- modelVersion,
5250
- text: responseText || void 0,
5251
- thoughts: reasoningSummary || void 0,
5252
- toolCalls: stepToolCalls,
5253
- usage: usageTokens,
5254
- costUsd: stepCostUsd,
5255
- timing
5256
- });
5257
- previousResponseId = finalResponse.id;
5258
- input = toolOutputs;
5259
- }
5260
- throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
5261
- }
5262
- if (providerInfo.provider === "chatgpt") {
5263
- const openAiAgentTools = buildOpenAiToolsFromToolSet(request.tools);
5264
- const openAiNativeTools = toOpenAiTools(request.modelTools);
5265
- const openAiTools = openAiNativeTools ? [...openAiNativeTools, ...openAiAgentTools] : [...openAiAgentTools];
5266
- const reasoningEffort = resolveOpenAiReasoningEffort(
5267
- request.model,
5268
- request.openAiReasoningEffort
5269
- );
5270
- const toolLoopInput = toChatGptInput(contents);
5271
- const conversationId = `tool-loop-${(0, import_node_crypto.randomBytes)(8).toString("hex")}`;
5272
- const promptCacheKey = conversationId;
5273
- let input = [...toolLoopInput.input];
5274
- for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5275
- const turn = stepIndex + 1;
5276
- const stepStartedAtMs = Date.now();
5277
- let firstModelEventAtMs;
5278
- const markFirstModelEvent = () => {
5279
- if (firstModelEventAtMs === void 0) {
5280
- firstModelEventAtMs = Date.now();
5504
+ const toolCalls = [];
5505
+ const toolOutputs = [];
5506
+ const callInputs = responseToolCalls.map((call, index) => {
5507
+ const toolIndex = index + 1;
5508
+ const toolId = buildToolLogId(turn, toolIndex);
5509
+ const toolName = call.name;
5510
+ const { value, error: parseError } = call.kind === "custom" ? { value: call.input, error: void 0 } : parseOpenAiToolArguments(call.arguments);
5511
+ const ids = normalizeChatGptToolIds({
5512
+ callKind: call.kind,
5513
+ callId: call.callId,
5514
+ itemId: call.id
5515
+ });
5516
+ return { call, toolName, value, parseError, ids, toolId, turn, toolIndex };
5517
+ });
5518
+ for (const entry of callInputs) {
5519
+ request.onEvent?.({
5520
+ type: "tool_call",
5521
+ phase: "started",
5522
+ turn: entry.turn,
5523
+ toolIndex: entry.toolIndex,
5524
+ toolName: entry.toolName,
5525
+ toolId: entry.toolId,
5526
+ callKind: entry.call.kind,
5527
+ callId: entry.ids.callId,
5528
+ input: entry.value
5529
+ });
5281
5530
  }
5282
- };
5283
- const response = await collectChatGptCodexResponseWithRetry({
5284
- sessionId: conversationId,
5285
- request: {
5286
- model: providerInfo.model,
5287
- store: false,
5288
- stream: true,
5289
- instructions: toolLoopInput.instructions ?? "You are a helpful assistant.",
5290
- input,
5291
- prompt_cache_key: promptCacheKey,
5292
- include: ["reasoning.encrypted_content"],
5293
- tools: openAiTools,
5294
- tool_choice: "auto",
5295
- parallel_tool_calls: true,
5296
- reasoning: {
5297
- effort: toOpenAiReasoningEffort(reasoningEffort),
5298
- summary: "detailed"
5299
- },
5300
- text: { verbosity: resolveOpenAiVerbosity(request.model) }
5301
- },
5302
- signal: request.signal,
5303
- onDelta: (delta) => {
5304
- if (delta.thoughtDelta) {
5305
- markFirstModelEvent();
5306
- request.onEvent?.({ type: "delta", channel: "thought", text: delta.thoughtDelta });
5531
+ const callResults = await Promise.all(
5532
+ callInputs.map(async (entry) => {
5533
+ return await toolCallContextStorage.run(
5534
+ {
5535
+ toolName: entry.toolName,
5536
+ toolId: entry.toolId,
5537
+ turn: entry.turn,
5538
+ toolIndex: entry.toolIndex
5539
+ },
5540
+ async () => {
5541
+ const { result, outputPayload } = await executeToolCall({
5542
+ callKind: entry.call.kind,
5543
+ toolName: entry.toolName,
5544
+ tool: request.tools[entry.toolName],
5545
+ rawInput: entry.value,
5546
+ parseError: entry.parseError
5547
+ });
5548
+ return { entry, result, outputPayload };
5549
+ }
5550
+ );
5551
+ })
5552
+ );
5553
+ let toolExecutionMs = 0;
5554
+ let waitToolMs = 0;
5555
+ for (const { entry, result, outputPayload } of callResults) {
5556
+ toolCalls.push({ ...result, callId: entry.ids.callId });
5557
+ const callDurationMs = toToolResultDuration(result);
5558
+ toolExecutionMs += callDurationMs;
5559
+ if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5560
+ waitToolMs += callDurationMs;
5307
5561
  }
5308
- if (delta.textDelta) {
5309
- markFirstModelEvent();
5310
- request.onEvent?.({ type: "delta", channel: "response", text: delta.textDelta });
5562
+ request.onEvent?.({
5563
+ type: "tool_call",
5564
+ phase: "completed",
5565
+ turn: entry.turn,
5566
+ toolIndex: entry.toolIndex,
5567
+ toolName: entry.toolName,
5568
+ toolId: entry.toolId,
5569
+ callKind: entry.call.kind,
5570
+ callId: entry.ids.callId,
5571
+ input: entry.value,
5572
+ output: result.output,
5573
+ error: result.error,
5574
+ durationMs: result.durationMs
5575
+ });
5576
+ if (entry.call.kind === "custom") {
5577
+ toolOutputs.push({
5578
+ type: "custom_tool_call",
5579
+ id: entry.ids.itemId,
5580
+ call_id: entry.ids.callId,
5581
+ name: entry.toolName,
5582
+ input: entry.call.input,
5583
+ status: "completed"
5584
+ });
5585
+ toolOutputs.push({
5586
+ type: "custom_tool_call_output",
5587
+ call_id: entry.ids.callId,
5588
+ output: mergeToolOutput(outputPayload)
5589
+ });
5590
+ } else {
5591
+ toolOutputs.push({
5592
+ type: "function_call",
5593
+ id: entry.ids.itemId,
5594
+ call_id: entry.ids.callId,
5595
+ name: entry.toolName,
5596
+ arguments: entry.call.arguments,
5597
+ status: "completed"
5598
+ });
5599
+ toolOutputs.push({
5600
+ type: "function_call_output",
5601
+ call_id: entry.ids.callId,
5602
+ output: mergeToolOutput(outputPayload)
5603
+ });
5311
5604
  }
5312
5605
  }
5313
- });
5314
- const modelCompletedAtMs = Date.now();
5315
- const modelVersion = response.model ? `chatgpt-${response.model}` : request.model;
5316
- const usageTokens = extractChatGptUsageTokens(response.usage);
5317
- const stepCostUsd = estimateCallCostUsd({
5318
- modelId: modelVersion,
5319
- tokens: usageTokens,
5320
- responseImages: 0
5321
- });
5322
- totalCostUsd += stepCostUsd;
5323
- const responseText = (response.text ?? "").trim();
5324
- const reasoningSummaryText = (response.reasoningSummaryText ?? "").trim();
5325
- const responseToolCalls = response.toolCalls ?? [];
5326
- if (responseToolCalls.length === 0) {
5327
- finalText = responseText;
5328
- finalThoughts = reasoningSummaryText;
5329
- const stepCompletedAtMs2 = Date.now();
5330
- const timing2 = buildStepTiming({
5606
+ const stepCompletedAtMs = Date.now();
5607
+ const timing = buildStepTiming({
5331
5608
  stepStartedAtMs,
5332
- stepCompletedAtMs: stepCompletedAtMs2,
5609
+ stepCompletedAtMs,
5333
5610
  modelCompletedAtMs,
5334
5611
  firstModelEventAtMs,
5335
- toolExecutionMs: 0,
5336
- waitToolMs: 0
5612
+ toolExecutionMs,
5613
+ waitToolMs
5337
5614
  });
5338
5615
  steps.push({
5339
5616
  step: steps.length + 1,
5340
5617
  modelVersion,
5341
5618
  text: responseText || void 0,
5342
5619
  thoughts: reasoningSummaryText || void 0,
5343
- toolCalls: [],
5620
+ toolCalls,
5344
5621
  usage: usageTokens,
5345
5622
  costUsd: stepCostUsd,
5346
- timing: timing2
5623
+ timing
5347
5624
  });
5348
- return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5625
+ const steeringInput = steeringInternal?.drainPendingContents() ?? [];
5626
+ const steeringItems = steeringInput.length > 0 ? toChatGptInput(steeringInput).input : [];
5627
+ input = steeringItems.length > 0 ? input.concat(toolOutputs, steeringItems) : input.concat(toolOutputs);
5349
5628
  }
5350
- const toolCalls = [];
5351
- const toolOutputs = [];
5352
- const callInputs = responseToolCalls.map((call, index) => {
5353
- const toolIndex = index + 1;
5354
- const toolId = buildToolLogId(turn, toolIndex);
5355
- const toolName = call.name;
5356
- const { value, error: parseError } = call.kind === "custom" ? { value: call.input, error: void 0 } : parseOpenAiToolArguments(call.arguments);
5357
- const ids = normalizeChatGptToolIds({
5358
- callKind: call.kind,
5359
- callId: call.callId,
5360
- itemId: call.id
5361
- });
5362
- return { call, toolName, value, parseError, ids, toolId, turn, toolIndex };
5363
- });
5364
- const callResults = await Promise.all(
5365
- callInputs.map(async (entry) => {
5366
- return await toolCallContextStorage.run(
5367
- {
5368
- toolName: entry.toolName,
5369
- toolId: entry.toolId,
5370
- turn: entry.turn,
5371
- toolIndex: entry.toolIndex
5372
- },
5373
- async () => {
5374
- const { result, outputPayload } = await executeToolCall({
5375
- callKind: entry.call.kind,
5376
- toolName: entry.toolName,
5377
- tool: request.tools[entry.toolName],
5378
- rawInput: entry.value,
5379
- parseError: entry.parseError
5380
- });
5381
- return { entry, result, outputPayload };
5629
+ throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
5630
+ }
5631
+ if (providerInfo.provider === "fireworks") {
5632
+ if (request.modelTools && request.modelTools.length > 0) {
5633
+ throw new Error(
5634
+ "Fireworks provider does not support provider-native modelTools in runToolLoop."
5635
+ );
5636
+ }
5637
+ const fireworksTools = buildFireworksToolsFromToolSet(request.tools);
5638
+ const messages = toFireworksMessages(contents);
5639
+ for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5640
+ const turn = stepIndex + 1;
5641
+ const stepStartedAtMs = Date.now();
5642
+ let schedulerMetrics;
5643
+ const response = await runFireworksCall(
5644
+ async (client) => {
5645
+ return await client.chat.completions.create(
5646
+ {
5647
+ model: providerInfo.model,
5648
+ messages,
5649
+ tools: fireworksTools,
5650
+ tool_choice: "auto",
5651
+ parallel_tool_calls: true
5652
+ },
5653
+ { signal: request.signal }
5654
+ );
5655
+ },
5656
+ providerInfo.model,
5657
+ {
5658
+ onSettled: (metrics) => {
5659
+ schedulerMetrics = metrics;
5382
5660
  }
5383
- );
5384
- })
5385
- );
5386
- let toolExecutionMs = 0;
5387
- let waitToolMs = 0;
5388
- for (const { entry, result, outputPayload } of callResults) {
5389
- toolCalls.push({ ...result, callId: entry.ids.callId });
5390
- const callDurationMs = toToolResultDuration(result);
5391
- toolExecutionMs += callDurationMs;
5392
- if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5393
- waitToolMs += callDurationMs;
5661
+ }
5662
+ );
5663
+ const modelCompletedAtMs = Date.now();
5664
+ const modelVersion = typeof response.model === "string" ? response.model : request.model;
5665
+ request.onEvent?.({ type: "model", modelVersion });
5666
+ const choice = Array.isArray(response.choices) ? response.choices[0] : void 0;
5667
+ if (choice?.finish_reason === "content_filter") {
5668
+ request.onEvent?.({ type: "blocked" });
5394
5669
  }
5395
- if (entry.call.kind === "custom") {
5396
- toolOutputs.push({
5397
- type: "custom_tool_call",
5398
- id: entry.ids.itemId,
5399
- call_id: entry.ids.callId,
5400
- name: entry.toolName,
5401
- input: entry.call.input,
5402
- status: "completed"
5670
+ const message = choice?.message;
5671
+ const responseText = extractFireworksMessageText(message).trim();
5672
+ if (responseText.length > 0) {
5673
+ request.onEvent?.({ type: "delta", channel: "response", text: responseText });
5674
+ }
5675
+ const usageTokens = extractFireworksUsageTokens(response.usage);
5676
+ const stepCostUsd = estimateCallCostUsd({
5677
+ modelId: modelVersion,
5678
+ tokens: usageTokens,
5679
+ responseImages: 0
5680
+ });
5681
+ totalCostUsd += stepCostUsd;
5682
+ if (usageTokens) {
5683
+ request.onEvent?.({
5684
+ type: "usage",
5685
+ usage: usageTokens,
5686
+ costUsd: stepCostUsd,
5687
+ modelVersion
5403
5688
  });
5404
- toolOutputs.push({
5405
- type: "custom_tool_call_output",
5406
- call_id: entry.ids.callId,
5407
- output: mergeToolOutput(outputPayload)
5689
+ }
5690
+ const responseToolCalls = extractFireworksToolCalls(message);
5691
+ if (responseToolCalls.length === 0) {
5692
+ const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
5693
+ const steeringMessages = steeringInput2.length > 0 ? toFireworksMessages(steeringInput2) : [];
5694
+ finalText = responseText;
5695
+ finalThoughts = "";
5696
+ const stepCompletedAtMs2 = Date.now();
5697
+ const timing2 = buildStepTiming({
5698
+ stepStartedAtMs,
5699
+ stepCompletedAtMs: stepCompletedAtMs2,
5700
+ modelCompletedAtMs,
5701
+ schedulerMetrics,
5702
+ toolExecutionMs: 0,
5703
+ waitToolMs: 0
5408
5704
  });
5409
- } else {
5410
- toolOutputs.push({
5411
- type: "function_call",
5412
- id: entry.ids.itemId,
5413
- call_id: entry.ids.callId,
5414
- name: entry.toolName,
5415
- arguments: entry.call.arguments,
5416
- status: "completed"
5705
+ steps.push({
5706
+ step: steps.length + 1,
5707
+ modelVersion,
5708
+ text: responseText || void 0,
5709
+ thoughts: void 0,
5710
+ toolCalls: [],
5711
+ usage: usageTokens,
5712
+ costUsd: stepCostUsd,
5713
+ timing: timing2
5417
5714
  });
5418
- toolOutputs.push({
5419
- type: "function_call_output",
5420
- call_id: entry.ids.callId,
5421
- output: mergeToolOutput(outputPayload)
5715
+ if (steeringMessages.length === 0) {
5716
+ return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5717
+ }
5718
+ if (responseText.length > 0) {
5719
+ messages.push({ role: "assistant", content: responseText });
5720
+ }
5721
+ messages.push(...steeringMessages);
5722
+ continue;
5723
+ }
5724
+ const stepToolCalls = [];
5725
+ const callInputs = responseToolCalls.map((call, index) => {
5726
+ const toolIndex = index + 1;
5727
+ const toolId = buildToolLogId(turn, toolIndex);
5728
+ const { value, error: parseError } = parseOpenAiToolArguments(call.arguments);
5729
+ return { call, toolName: call.name, value, parseError, toolId, turn, toolIndex };
5730
+ });
5731
+ for (const entry of callInputs) {
5732
+ request.onEvent?.({
5733
+ type: "tool_call",
5734
+ phase: "started",
5735
+ turn: entry.turn,
5736
+ toolIndex: entry.toolIndex,
5737
+ toolName: entry.toolName,
5738
+ toolId: entry.toolId,
5739
+ callKind: "function",
5740
+ callId: entry.call.id,
5741
+ input: entry.value
5422
5742
  });
5423
5743
  }
5744
+ const callResults = await Promise.all(
5745
+ callInputs.map(async (entry) => {
5746
+ return await toolCallContextStorage.run(
5747
+ {
5748
+ toolName: entry.toolName,
5749
+ toolId: entry.toolId,
5750
+ turn: entry.turn,
5751
+ toolIndex: entry.toolIndex
5752
+ },
5753
+ async () => {
5754
+ const { result, outputPayload } = await executeToolCall({
5755
+ callKind: "function",
5756
+ toolName: entry.toolName,
5757
+ tool: request.tools[entry.toolName],
5758
+ rawInput: entry.value,
5759
+ parseError: entry.parseError
5760
+ });
5761
+ return { entry, result, outputPayload };
5762
+ }
5763
+ );
5764
+ })
5765
+ );
5766
+ const assistantToolCalls = [];
5767
+ const toolMessages = [];
5768
+ let toolExecutionMs = 0;
5769
+ let waitToolMs = 0;
5770
+ for (const { entry, result, outputPayload } of callResults) {
5771
+ stepToolCalls.push({ ...result, callId: entry.call.id });
5772
+ const callDurationMs = toToolResultDuration(result);
5773
+ toolExecutionMs += callDurationMs;
5774
+ if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5775
+ waitToolMs += callDurationMs;
5776
+ }
5777
+ request.onEvent?.({
5778
+ type: "tool_call",
5779
+ phase: "completed",
5780
+ turn: entry.turn,
5781
+ toolIndex: entry.toolIndex,
5782
+ toolName: entry.toolName,
5783
+ toolId: entry.toolId,
5784
+ callKind: "function",
5785
+ callId: entry.call.id,
5786
+ input: entry.value,
5787
+ output: result.output,
5788
+ error: result.error,
5789
+ durationMs: result.durationMs
5790
+ });
5791
+ assistantToolCalls.push({
5792
+ id: entry.call.id,
5793
+ type: "function",
5794
+ function: {
5795
+ name: entry.toolName,
5796
+ arguments: entry.call.arguments
5797
+ }
5798
+ });
5799
+ toolMessages.push({
5800
+ role: "tool",
5801
+ tool_call_id: entry.call.id,
5802
+ content: mergeToolOutput(outputPayload)
5803
+ });
5804
+ }
5805
+ const stepCompletedAtMs = Date.now();
5806
+ const timing = buildStepTiming({
5807
+ stepStartedAtMs,
5808
+ stepCompletedAtMs,
5809
+ modelCompletedAtMs,
5810
+ schedulerMetrics,
5811
+ toolExecutionMs,
5812
+ waitToolMs
5813
+ });
5814
+ steps.push({
5815
+ step: steps.length + 1,
5816
+ modelVersion,
5817
+ text: responseText || void 0,
5818
+ thoughts: void 0,
5819
+ toolCalls: stepToolCalls,
5820
+ usage: usageTokens,
5821
+ costUsd: stepCostUsd,
5822
+ timing
5823
+ });
5824
+ messages.push({
5825
+ role: "assistant",
5826
+ ...responseText.length > 0 ? { content: responseText } : {},
5827
+ tool_calls: assistantToolCalls
5828
+ });
5829
+ messages.push(...toolMessages);
5830
+ const steeringInput = steeringInternal?.drainPendingContents() ?? [];
5831
+ if (steeringInput.length > 0) {
5832
+ messages.push(...toFireworksMessages(steeringInput));
5833
+ }
5424
5834
  }
5425
- const stepCompletedAtMs = Date.now();
5426
- const timing = buildStepTiming({
5427
- stepStartedAtMs,
5428
- stepCompletedAtMs,
5429
- modelCompletedAtMs,
5430
- firstModelEventAtMs,
5431
- toolExecutionMs,
5432
- waitToolMs
5433
- });
5434
- steps.push({
5435
- step: steps.length + 1,
5436
- modelVersion,
5437
- text: responseText || void 0,
5438
- thoughts: reasoningSummaryText || void 0,
5439
- toolCalls,
5440
- usage: usageTokens,
5441
- costUsd: stepCostUsd,
5442
- timing
5443
- });
5444
- input = input.concat(toolOutputs);
5445
- }
5446
- throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
5447
- }
5448
- if (providerInfo.provider === "fireworks") {
5449
- if (request.modelTools && request.modelTools.length > 0) {
5450
- throw new Error(
5451
- "Fireworks provider does not support provider-native modelTools in runToolLoop."
5452
- );
5835
+ throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
5453
5836
  }
5454
- const fireworksTools = buildFireworksToolsFromToolSet(request.tools);
5455
- const messages = toFireworksMessages(contents);
5837
+ const geminiFunctionTools = buildGeminiFunctionDeclarations(request.tools);
5838
+ const geminiNativeTools = toGeminiTools(request.modelTools);
5839
+ const geminiTools = geminiNativeTools ? geminiNativeTools.concat(geminiFunctionTools) : geminiFunctionTools;
5840
+ const geminiContents = contents.map(convertLlmContentToGeminiContent);
5456
5841
  for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5457
- const turn = stepIndex + 1;
5458
5842
  const stepStartedAtMs = Date.now();
5843
+ let firstModelEventAtMs;
5459
5844
  let schedulerMetrics;
5460
- const response = await runFireworksCall(
5845
+ const markFirstModelEvent = () => {
5846
+ if (firstModelEventAtMs === void 0) {
5847
+ firstModelEventAtMs = Date.now();
5848
+ }
5849
+ };
5850
+ const config = {
5851
+ maxOutputTokens: 32e3,
5852
+ tools: geminiTools,
5853
+ toolConfig: {
5854
+ functionCallingConfig: {
5855
+ mode: import_genai2.FunctionCallingConfigMode.VALIDATED
5856
+ }
5857
+ },
5858
+ thinkingConfig: resolveGeminiThinkingConfig(request.model)
5859
+ };
5860
+ const onEvent = request.onEvent;
5861
+ const response = await runGeminiCall(
5461
5862
  async (client) => {
5462
- return await client.chat.completions.create(
5463
- {
5464
- model: providerInfo.model,
5465
- messages,
5466
- tools: fireworksTools,
5467
- tool_choice: "auto",
5468
- parallel_tool_calls: true
5469
- },
5470
- { signal: request.signal }
5471
- );
5863
+ const stream = await client.models.generateContentStream({
5864
+ model: request.model,
5865
+ contents: geminiContents,
5866
+ config
5867
+ });
5868
+ let responseText = "";
5869
+ let thoughtsText = "";
5870
+ const modelParts = [];
5871
+ const functionCalls = [];
5872
+ const seenFunctionCallIds = /* @__PURE__ */ new Set();
5873
+ const seenFunctionCallKeys = /* @__PURE__ */ new Set();
5874
+ let latestUsageMetadata;
5875
+ let resolvedModelVersion;
5876
+ for await (const chunk of stream) {
5877
+ markFirstModelEvent();
5878
+ if (chunk.modelVersion) {
5879
+ resolvedModelVersion = chunk.modelVersion;
5880
+ onEvent?.({ type: "model", modelVersion: chunk.modelVersion });
5881
+ }
5882
+ if (chunk.usageMetadata) {
5883
+ latestUsageMetadata = chunk.usageMetadata;
5884
+ }
5885
+ const candidates = chunk.candidates;
5886
+ if (!candidates || candidates.length === 0) {
5887
+ continue;
5888
+ }
5889
+ const primary = candidates[0];
5890
+ const parts = primary?.content?.parts;
5891
+ if (!parts || parts.length === 0) {
5892
+ continue;
5893
+ }
5894
+ for (const part of parts) {
5895
+ modelParts.push(part);
5896
+ const call = part.functionCall;
5897
+ if (call) {
5898
+ const id = typeof call.id === "string" ? call.id : "";
5899
+ const shouldAdd = (() => {
5900
+ if (id.length > 0) {
5901
+ if (seenFunctionCallIds.has(id)) {
5902
+ return false;
5903
+ }
5904
+ seenFunctionCallIds.add(id);
5905
+ return true;
5906
+ }
5907
+ const key = JSON.stringify({ name: call.name ?? "", args: call.args ?? null });
5908
+ if (seenFunctionCallKeys.has(key)) {
5909
+ return false;
5910
+ }
5911
+ seenFunctionCallKeys.add(key);
5912
+ return true;
5913
+ })();
5914
+ if (shouldAdd) {
5915
+ functionCalls.push(call);
5916
+ }
5917
+ }
5918
+ if (typeof part.text === "string" && part.text.length > 0) {
5919
+ if (part.thought) {
5920
+ thoughtsText += part.text;
5921
+ onEvent?.({ type: "delta", channel: "thought", text: part.text });
5922
+ } else {
5923
+ responseText += part.text;
5924
+ onEvent?.({ type: "delta", channel: "response", text: part.text });
5925
+ }
5926
+ }
5927
+ }
5928
+ }
5929
+ return {
5930
+ responseText,
5931
+ thoughtsText,
5932
+ functionCalls,
5933
+ modelParts,
5934
+ usageMetadata: latestUsageMetadata,
5935
+ modelVersion: resolvedModelVersion ?? request.model
5936
+ };
5472
5937
  },
5473
- providerInfo.model,
5938
+ request.model,
5474
5939
  {
5475
5940
  onSettled: (metrics) => {
5476
5941
  schedulerMetrics = metrics;
@@ -5478,41 +5943,24 @@ async function runToolLoop(request) {
5478
5943
  }
5479
5944
  );
5480
5945
  const modelCompletedAtMs = Date.now();
5481
- const modelVersion = typeof response.model === "string" ? response.model : request.model;
5482
- request.onEvent?.({ type: "model", modelVersion });
5483
- const choice = Array.isArray(response.choices) ? response.choices[0] : void 0;
5484
- if (choice?.finish_reason === "content_filter") {
5485
- request.onEvent?.({ type: "blocked" });
5486
- }
5487
- const message = choice?.message;
5488
- const responseText = extractFireworksMessageText(message).trim();
5489
- if (responseText.length > 0) {
5490
- request.onEvent?.({ type: "delta", channel: "response", text: responseText });
5491
- }
5492
- const usageTokens = extractFireworksUsageTokens(response.usage);
5946
+ const usageTokens = extractGeminiUsageTokens(response.usageMetadata);
5947
+ const modelVersion = response.modelVersion ?? request.model;
5493
5948
  const stepCostUsd = estimateCallCostUsd({
5494
5949
  modelId: modelVersion,
5495
5950
  tokens: usageTokens,
5496
5951
  responseImages: 0
5497
5952
  });
5498
5953
  totalCostUsd += stepCostUsd;
5499
- if (usageTokens) {
5500
- request.onEvent?.({
5501
- type: "usage",
5502
- usage: usageTokens,
5503
- costUsd: stepCostUsd,
5504
- modelVersion
5505
- });
5506
- }
5507
- const responseToolCalls = extractFireworksToolCalls(message);
5508
- if (responseToolCalls.length === 0) {
5509
- finalText = responseText;
5510
- finalThoughts = "";
5954
+ if (response.functionCalls.length === 0) {
5955
+ const steeringInput2 = steeringInternal?.drainPendingContents() ?? [];
5956
+ finalText = response.responseText.trim();
5957
+ finalThoughts = response.thoughtsText.trim();
5511
5958
  const stepCompletedAtMs2 = Date.now();
5512
5959
  const timing2 = buildStepTiming({
5513
5960
  stepStartedAtMs,
5514
5961
  stepCompletedAtMs: stepCompletedAtMs2,
5515
5962
  modelCompletedAtMs,
5963
+ firstModelEventAtMs,
5516
5964
  schedulerMetrics,
5517
5965
  toolExecutionMs: 0,
5518
5966
  waitToolMs: 0
@@ -5520,22 +5968,65 @@ async function runToolLoop(request) {
5520
5968
  steps.push({
5521
5969
  step: steps.length + 1,
5522
5970
  modelVersion,
5523
- text: responseText || void 0,
5524
- thoughts: void 0,
5971
+ text: finalText || void 0,
5972
+ thoughts: finalThoughts || void 0,
5525
5973
  toolCalls: [],
5526
5974
  usage: usageTokens,
5527
5975
  costUsd: stepCostUsd,
5528
5976
  timing: timing2
5529
5977
  });
5530
- return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5978
+ if (steeringInput2.length === 0) {
5979
+ return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
5980
+ }
5981
+ const modelPartsForHistory2 = response.modelParts.filter(
5982
+ (part) => !(typeof part.text === "string" && part.thought === true)
5983
+ );
5984
+ if (modelPartsForHistory2.length > 0) {
5985
+ geminiContents.push({ role: "model", parts: modelPartsForHistory2 });
5986
+ } else if (response.responseText.length > 0) {
5987
+ geminiContents.push({ role: "model", parts: [{ text: response.responseText }] });
5988
+ }
5989
+ geminiContents.push(...steeringInput2.map(convertLlmContentToGeminiContent));
5990
+ continue;
5991
+ }
5992
+ const toolCalls = [];
5993
+ const modelPartsForHistory = response.modelParts.filter(
5994
+ (part) => !(typeof part.text === "string" && part.thought === true)
5995
+ );
5996
+ if (modelPartsForHistory.length > 0) {
5997
+ geminiContents.push({ role: "model", parts: modelPartsForHistory });
5998
+ } else {
5999
+ const parts = [];
6000
+ if (response.responseText) {
6001
+ parts.push({ text: response.responseText });
6002
+ }
6003
+ for (const call of response.functionCalls) {
6004
+ parts.push({ functionCall: call });
6005
+ }
6006
+ geminiContents.push({ role: "model", parts });
5531
6007
  }
5532
- const stepToolCalls = [];
5533
- const callInputs = responseToolCalls.map((call, index) => {
6008
+ const responseParts = [];
6009
+ const callInputs = response.functionCalls.map((call, index) => {
6010
+ const turn = stepIndex + 1;
5534
6011
  const toolIndex = index + 1;
5535
6012
  const toolId = buildToolLogId(turn, toolIndex);
5536
- const { value, error: parseError } = parseOpenAiToolArguments(call.arguments);
5537
- return { call, toolName: call.name, value, parseError, toolId, turn, toolIndex };
6013
+ const toolName = call.name ?? "unknown";
6014
+ const rawInput = call.args ?? {};
6015
+ return { call, toolName, rawInput, toolId, turn, toolIndex };
5538
6016
  });
6017
+ for (const entry of callInputs) {
6018
+ onEvent?.({
6019
+ type: "tool_call",
6020
+ phase: "started",
6021
+ turn: entry.turn,
6022
+ toolIndex: entry.toolIndex,
6023
+ toolName: entry.toolName,
6024
+ toolId: entry.toolId,
6025
+ callKind: "function",
6026
+ callId: entry.call.id,
6027
+ input: entry.rawInput
6028
+ });
6029
+ }
5539
6030
  const callResults = await Promise.all(
5540
6031
  callInputs.map(async (entry) => {
5541
6032
  return await toolCallContextStorage.run(
@@ -5550,44 +6041,51 @@ async function runToolLoop(request) {
5550
6041
  callKind: "function",
5551
6042
  toolName: entry.toolName,
5552
6043
  tool: request.tools[entry.toolName],
5553
- rawInput: entry.value,
5554
- parseError: entry.parseError
6044
+ rawInput: entry.rawInput
5555
6045
  });
5556
6046
  return { entry, result, outputPayload };
5557
6047
  }
5558
6048
  );
5559
6049
  })
5560
6050
  );
5561
- const assistantToolCalls = [];
5562
- const toolMessages = [];
5563
6051
  let toolExecutionMs = 0;
5564
6052
  let waitToolMs = 0;
5565
6053
  for (const { entry, result, outputPayload } of callResults) {
5566
- stepToolCalls.push({ ...result, callId: entry.call.id });
6054
+ toolCalls.push({ ...result, callId: entry.call.id });
5567
6055
  const callDurationMs = toToolResultDuration(result);
5568
6056
  toolExecutionMs += callDurationMs;
5569
6057
  if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5570
6058
  waitToolMs += callDurationMs;
5571
6059
  }
5572
- assistantToolCalls.push({
5573
- id: entry.call.id,
5574
- type: "function",
5575
- function: {
6060
+ onEvent?.({
6061
+ type: "tool_call",
6062
+ phase: "completed",
6063
+ turn: entry.turn,
6064
+ toolIndex: entry.toolIndex,
6065
+ toolName: entry.toolName,
6066
+ toolId: entry.toolId,
6067
+ callKind: "function",
6068
+ callId: entry.call.id,
6069
+ input: entry.rawInput,
6070
+ output: result.output,
6071
+ error: result.error,
6072
+ durationMs: result.durationMs
6073
+ });
6074
+ const responsePayload = isPlainRecord(outputPayload) ? outputPayload : { output: outputPayload };
6075
+ responseParts.push({
6076
+ functionResponse: {
5576
6077
  name: entry.toolName,
5577
- arguments: entry.call.arguments
6078
+ response: responsePayload,
6079
+ ...entry.call.id ? { id: entry.call.id } : {}
5578
6080
  }
5579
6081
  });
5580
- toolMessages.push({
5581
- role: "tool",
5582
- tool_call_id: entry.call.id,
5583
- content: mergeToolOutput(outputPayload)
5584
- });
5585
6082
  }
5586
6083
  const stepCompletedAtMs = Date.now();
5587
6084
  const timing = buildStepTiming({
5588
6085
  stepStartedAtMs,
5589
6086
  stepCompletedAtMs,
5590
6087
  modelCompletedAtMs,
6088
+ firstModelEventAtMs,
5591
6089
  schedulerMetrics,
5592
6090
  toolExecutionMs,
5593
6091
  waitToolMs
@@ -5595,251 +6093,82 @@ async function runToolLoop(request) {
5595
6093
  steps.push({
5596
6094
  step: steps.length + 1,
5597
6095
  modelVersion,
5598
- text: responseText || void 0,
5599
- thoughts: void 0,
5600
- toolCalls: stepToolCalls,
6096
+ text: response.responseText.trim() || void 0,
6097
+ thoughts: response.thoughtsText.trim() || void 0,
6098
+ toolCalls,
5601
6099
  usage: usageTokens,
5602
6100
  costUsd: stepCostUsd,
5603
6101
  timing
5604
6102
  });
5605
- messages.push({
5606
- role: "assistant",
5607
- ...responseText.length > 0 ? { content: responseText } : {},
5608
- tool_calls: assistantToolCalls
5609
- });
5610
- messages.push(...toolMessages);
6103
+ geminiContents.push({ role: "user", parts: responseParts });
6104
+ const steeringInput = steeringInternal?.drainPendingContents() ?? [];
6105
+ if (steeringInput.length > 0) {
6106
+ geminiContents.push(...steeringInput.map(convertLlmContentToGeminiContent));
6107
+ }
5611
6108
  }
5612
6109
  throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
6110
+ } finally {
6111
+ steeringInternal?.close();
5613
6112
  }
5614
- const geminiFunctionTools = buildGeminiFunctionDeclarations(request.tools);
5615
- const geminiNativeTools = toGeminiTools(request.modelTools);
5616
- const geminiTools = geminiNativeTools ? geminiNativeTools.concat(geminiFunctionTools) : geminiFunctionTools;
5617
- const geminiContents = contents.map(convertLlmContentToGeminiContent);
5618
- for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
5619
- const stepStartedAtMs = Date.now();
5620
- let firstModelEventAtMs;
5621
- let schedulerMetrics;
5622
- const markFirstModelEvent = () => {
5623
- if (firstModelEventAtMs === void 0) {
5624
- firstModelEventAtMs = Date.now();
5625
- }
5626
- };
5627
- const config = {
5628
- maxOutputTokens: 32e3,
5629
- tools: geminiTools,
5630
- toolConfig: {
5631
- functionCallingConfig: {
5632
- mode: import_genai2.FunctionCallingConfigMode.VALIDATED
5633
- }
5634
- },
5635
- thinkingConfig: resolveGeminiThinkingConfig(request.model)
5636
- };
5637
- const onEvent = request.onEvent;
5638
- const response = await runGeminiCall(
5639
- async (client) => {
5640
- const stream = await client.models.generateContentStream({
5641
- model: request.model,
5642
- contents: geminiContents,
5643
- config
5644
- });
5645
- let responseText = "";
5646
- let thoughtsText = "";
5647
- const modelParts = [];
5648
- const functionCalls = [];
5649
- const seenFunctionCallIds = /* @__PURE__ */ new Set();
5650
- const seenFunctionCallKeys = /* @__PURE__ */ new Set();
5651
- let latestUsageMetadata;
5652
- let resolvedModelVersion;
5653
- for await (const chunk of stream) {
5654
- markFirstModelEvent();
5655
- if (chunk.modelVersion) {
5656
- resolvedModelVersion = chunk.modelVersion;
5657
- onEvent?.({ type: "model", modelVersion: chunk.modelVersion });
5658
- }
5659
- if (chunk.usageMetadata) {
5660
- latestUsageMetadata = chunk.usageMetadata;
5661
- }
5662
- const candidates = chunk.candidates;
5663
- if (!candidates || candidates.length === 0) {
5664
- continue;
5665
- }
5666
- const primary = candidates[0];
5667
- const parts = primary?.content?.parts;
5668
- if (!parts || parts.length === 0) {
5669
- continue;
5670
- }
5671
- for (const part of parts) {
5672
- modelParts.push(part);
5673
- const call = part.functionCall;
5674
- if (call) {
5675
- const id = typeof call.id === "string" ? call.id : "";
5676
- const shouldAdd = (() => {
5677
- if (id.length > 0) {
5678
- if (seenFunctionCallIds.has(id)) {
5679
- return false;
5680
- }
5681
- seenFunctionCallIds.add(id);
5682
- return true;
5683
- }
5684
- const key = JSON.stringify({ name: call.name ?? "", args: call.args ?? null });
5685
- if (seenFunctionCallKeys.has(key)) {
5686
- return false;
5687
- }
5688
- seenFunctionCallKeys.add(key);
5689
- return true;
5690
- })();
5691
- if (shouldAdd) {
5692
- functionCalls.push(call);
5693
- }
5694
- }
5695
- if (typeof part.text === "string" && part.text.length > 0) {
5696
- if (part.thought) {
5697
- thoughtsText += part.text;
5698
- onEvent?.({ type: "delta", channel: "thought", text: part.text });
5699
- } else {
5700
- responseText += part.text;
5701
- onEvent?.({ type: "delta", channel: "response", text: part.text });
5702
- }
5703
- }
5704
- }
5705
- }
5706
- return {
5707
- responseText,
5708
- thoughtsText,
5709
- functionCalls,
5710
- modelParts,
5711
- usageMetadata: latestUsageMetadata,
5712
- modelVersion: resolvedModelVersion ?? request.model
5713
- };
5714
- },
5715
- request.model,
5716
- {
5717
- onSettled: (metrics) => {
5718
- schedulerMetrics = metrics;
5719
- }
5720
- }
5721
- );
5722
- const modelCompletedAtMs = Date.now();
5723
- const usageTokens = extractGeminiUsageTokens(response.usageMetadata);
5724
- const modelVersion = response.modelVersion ?? request.model;
5725
- const stepCostUsd = estimateCallCostUsd({
5726
- modelId: modelVersion,
5727
- tokens: usageTokens,
5728
- responseImages: 0
5729
- });
5730
- totalCostUsd += stepCostUsd;
5731
- if (response.functionCalls.length === 0) {
5732
- finalText = response.responseText.trim();
5733
- finalThoughts = response.thoughtsText.trim();
5734
- const stepCompletedAtMs2 = Date.now();
5735
- const timing2 = buildStepTiming({
5736
- stepStartedAtMs,
5737
- stepCompletedAtMs: stepCompletedAtMs2,
5738
- modelCompletedAtMs,
5739
- firstModelEventAtMs,
5740
- schedulerMetrics,
5741
- toolExecutionMs: 0,
5742
- waitToolMs: 0
5743
- });
5744
- steps.push({
5745
- step: steps.length + 1,
5746
- modelVersion,
5747
- text: finalText || void 0,
5748
- thoughts: finalThoughts || void 0,
5749
- toolCalls: [],
5750
- usage: usageTokens,
5751
- costUsd: stepCostUsd,
5752
- timing: timing2
5753
- });
5754
- return { text: finalText, thoughts: finalThoughts, steps, totalCostUsd };
6113
+ }
6114
+ function mergeAbortSignals(first, second) {
6115
+ if (!first) {
6116
+ return second;
6117
+ }
6118
+ if (!second) {
6119
+ return first;
6120
+ }
6121
+ const controller = new AbortController();
6122
+ const abortFrom = (signal) => {
6123
+ if (!controller.signal.aborted) {
6124
+ controller.abort(signal.reason);
5755
6125
  }
5756
- const toolCalls = [];
5757
- const modelPartsForHistory = response.modelParts.filter(
5758
- (part) => !(typeof part.text === "string" && part.thought === true)
5759
- );
5760
- if (modelPartsForHistory.length > 0) {
5761
- geminiContents.push({ role: "model", parts: modelPartsForHistory });
5762
- } else {
5763
- const parts = [];
5764
- if (response.responseText) {
5765
- parts.push({ text: response.responseText });
5766
- }
5767
- for (const call of response.functionCalls) {
5768
- parts.push({ functionCall: call });
5769
- }
5770
- geminiContents.push({ role: "model", parts });
5771
- }
5772
- const responseParts = [];
5773
- const callInputs = response.functionCalls.map((call, index) => {
5774
- const turn = stepIndex + 1;
5775
- const toolIndex = index + 1;
5776
- const toolId = buildToolLogId(turn, toolIndex);
5777
- const toolName = call.name ?? "unknown";
5778
- const rawInput = call.args ?? {};
5779
- return { call, toolName, rawInput, toolId, turn, toolIndex };
5780
- });
5781
- const callResults = await Promise.all(
5782
- callInputs.map(async (entry) => {
5783
- return await toolCallContextStorage.run(
5784
- {
5785
- toolName: entry.toolName,
5786
- toolId: entry.toolId,
5787
- turn: entry.turn,
5788
- toolIndex: entry.toolIndex
5789
- },
5790
- async () => {
5791
- const { result, outputPayload } = await executeToolCall({
5792
- callKind: "function",
5793
- toolName: entry.toolName,
5794
- tool: request.tools[entry.toolName],
5795
- rawInput: entry.rawInput
5796
- });
5797
- return { entry, result, outputPayload };
5798
- }
5799
- );
5800
- })
5801
- );
5802
- let toolExecutionMs = 0;
5803
- let waitToolMs = 0;
5804
- for (const { entry, result, outputPayload } of callResults) {
5805
- toolCalls.push({ ...result, callId: entry.call.id });
5806
- const callDurationMs = toToolResultDuration(result);
5807
- toolExecutionMs += callDurationMs;
5808
- if (entry.toolName.toLowerCase() === SUBAGENT_WAIT_TOOL_NAME) {
5809
- waitToolMs += callDurationMs;
5810
- }
5811
- const responsePayload = isPlainRecord(outputPayload) ? outputPayload : { output: outputPayload };
5812
- responseParts.push({
5813
- functionResponse: {
5814
- name: entry.toolName,
5815
- response: responsePayload,
5816
- ...entry.call.id ? { id: entry.call.id } : {}
6126
+ };
6127
+ if (first.aborted) {
6128
+ abortFrom(first);
6129
+ } else {
6130
+ first.addEventListener("abort", () => abortFrom(first), { once: true });
6131
+ }
6132
+ if (second.aborted) {
6133
+ abortFrom(second);
6134
+ } else {
6135
+ second.addEventListener("abort", () => abortFrom(second), { once: true });
6136
+ }
6137
+ return controller.signal;
6138
+ }
6139
+ function streamToolLoop(request) {
6140
+ const queue = createAsyncQueue();
6141
+ const abortController = new AbortController();
6142
+ const steering = request.steering ?? createToolLoopSteeringChannel();
6143
+ const signal = mergeAbortSignals(request.signal, abortController.signal);
6144
+ const sourceOnEvent = request.onEvent;
6145
+ const result = (async () => {
6146
+ try {
6147
+ const output = await runToolLoop({
6148
+ ...request,
6149
+ steering,
6150
+ ...signal ? { signal } : {},
6151
+ onEvent: (event) => {
6152
+ sourceOnEvent?.(event);
6153
+ queue.push(event);
5817
6154
  }
5818
6155
  });
6156
+ queue.close();
6157
+ return output;
6158
+ } catch (error) {
6159
+ const err = error instanceof Error ? error : new Error(String(error));
6160
+ queue.fail(err);
6161
+ throw err;
5819
6162
  }
5820
- const stepCompletedAtMs = Date.now();
5821
- const timing = buildStepTiming({
5822
- stepStartedAtMs,
5823
- stepCompletedAtMs,
5824
- modelCompletedAtMs,
5825
- firstModelEventAtMs,
5826
- schedulerMetrics,
5827
- toolExecutionMs,
5828
- waitToolMs
5829
- });
5830
- steps.push({
5831
- step: steps.length + 1,
5832
- modelVersion,
5833
- text: response.responseText.trim() || void 0,
5834
- thoughts: response.thoughtsText.trim() || void 0,
5835
- toolCalls,
5836
- usage: usageTokens,
5837
- costUsd: stepCostUsd,
5838
- timing
5839
- });
5840
- geminiContents.push({ role: "user", parts: responseParts });
5841
- }
5842
- throw new Error(`Tool loop exceeded max steps (${maxSteps}) without final response.`);
6163
+ })();
6164
+ return {
6165
+ events: queue.iterable,
6166
+ result,
6167
+ append: steering.append,
6168
+ steer: steering.steer,
6169
+ pendingSteeringCount: steering.pendingCount,
6170
+ abort: () => abortController.abort()
6171
+ };
5843
6172
  }
5844
6173
  var IMAGE_GRADE_SCHEMA = import_zod3.z.enum(["pass", "fail"]);
5845
6174
  async function gradeGeneratedImage(params) {
@@ -6092,65 +6421,106 @@ var import_node_crypto3 = require("crypto");
6092
6421
  // src/agent/subagents.ts
6093
6422
  var import_node_crypto2 = require("crypto");
6094
6423
  var import_zod4 = require("zod");
6095
- var DEFAULT_SUBAGENT_MAX_AGENTS = 4;
6096
- var DEFAULT_SUBAGENT_MAX_DEPTH = 2;
6097
- var DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS = 1500;
6098
- var DEFAULT_SUBAGENT_MAX_WAIT_TIMEOUT_MS = 9e4;
6424
+ var DEFAULT_SUBAGENT_MAX_AGENTS = 6;
6425
+ var DEFAULT_SUBAGENT_MAX_DEPTH = 1;
6426
+ var DEFAULT_SUBAGENT_MIN_WAIT_TIMEOUT_MS = 1e4;
6427
+ var DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS = 3e4;
6428
+ var DEFAULT_SUBAGENT_MAX_WAIT_TIMEOUT_MS = 36e5;
6099
6429
  var MAX_SUBAGENT_MAX_AGENTS = 64;
6100
6430
  var MAX_SUBAGENT_MAX_DEPTH = 12;
6101
6431
  var MAX_SUBAGENT_MAX_STEPS = 64;
6102
- var MAX_SUBAGENT_WAIT_TIMEOUT_MS = 6e5;
6432
+ var MAX_SUBAGENT_WAIT_TIMEOUT_MS = 36e5;
6103
6433
  var SUBAGENT_CONTROL_TOOL_NAMES = ["send_input", "resume_agent", "wait", "close_agent"];
6434
+ var DEFAULT_AGENT_TYPE = "default";
6435
+ var BUILT_IN_AGENT_TYPES = ["default", "researcher", "worker", "reviewer"];
6436
+ var RESEARCHER_ROLE_DESCRIPTION = `Use \`researcher\` for focused discovery and fact-finding work.
6437
+ Researchers are fast and authoritative.
6438
+ They should be used for specific, well-scoped research questions.
6439
+ Rules:
6440
+ - Do not repeat searches they have already completed.
6441
+ - Trust researcher findings unless there is a clear contradiction.
6442
+ - Run researchers in parallel when useful.
6443
+ - Reuse existing researchers for related follow-up questions.`;
6444
+ var WORKER_ROLE_DESCRIPTION = `Use for execution and production work across domains.
6445
+ Typical tasks:
6446
+ - Build part of a deliverable
6447
+ - Implement requested changes
6448
+ - Produce concrete outputs (documents, plans, analyses, artifacts)
6449
+ Rules:
6450
+ - Explicitly assign **ownership** of the task (scope / responsibility).
6451
+ - Always tell workers they are **not alone in the workspace**, and they should ignore edits made by others without touching them unless asked.`;
6452
+ var REVIEWER_ROLE_DESCRIPTION = `Use \`reviewer\` to evaluate completed work and provide feedback.
6453
+ Reviewers focus on quality, correctness, risk, and clarity.
6454
+ Rules:
6455
+ - Review critically and prioritize issues by severity.
6456
+ - Call out gaps, assumptions, and edge cases explicitly.
6457
+ - Provide actionable, concrete feedback to improve the result.
6458
+ - Do not redo the entire task unless explicitly requested; evaluate first.`;
6459
+ var BUILT_IN_AGENT_TYPE_DESCRIPTIONS = {
6460
+ default: "Default agent.",
6461
+ researcher: RESEARCHER_ROLE_DESCRIPTION,
6462
+ worker: WORKER_ROLE_DESCRIPTION,
6463
+ reviewer: REVIEWER_ROLE_DESCRIPTION
6464
+ };
6465
+ var BUILT_IN_AGENT_TYPE_INSTRUCTIONS = {
6466
+ default: void 0,
6467
+ researcher: RESEARCHER_ROLE_DESCRIPTION,
6468
+ worker: WORKER_ROLE_DESCRIPTION,
6469
+ reviewer: REVIEWER_ROLE_DESCRIPTION
6470
+ };
6471
+ var SUBAGENT_NOTIFICATION_OPEN_TAG = "<subagent_notification>";
6472
+ var SUBAGENT_NOTIFICATION_CLOSE_TAG = "</subagent_notification>";
6473
+ var SPAWN_AGENT_TYPE_DESCRIPTION = buildSpawnAgentTypeDescription();
6104
6474
  var subagentInputItemSchema = import_zod4.z.object({
6105
- text: import_zod4.z.string().optional(),
6106
- image_url: import_zod4.z.string().optional(),
6107
- name: import_zod4.z.string().optional(),
6108
- path: import_zod4.z.string().optional(),
6109
- type: import_zod4.z.string().optional()
6475
+ text: import_zod4.z.string().nullish(),
6476
+ image_url: import_zod4.z.string().nullish(),
6477
+ name: import_zod4.z.string().nullish(),
6478
+ path: import_zod4.z.string().nullish(),
6479
+ type: import_zod4.z.string().nullish()
6110
6480
  }).passthrough();
6111
6481
  var spawnAgentInputSchema = import_zod4.z.object({
6112
- prompt: import_zod4.z.string().optional().describe("Initial prompt for the subagent."),
6113
- message: import_zod4.z.string().optional().describe("Codex-style alias for prompt."),
6114
- items: import_zod4.z.array(subagentInputItemSchema).optional().describe("Optional Codex-style input items."),
6115
- agent_type: import_zod4.z.string().optional().describe("Codex-style agent type hint."),
6116
- instructions: import_zod4.z.string().optional().describe("Optional extra instructions for this subagent instance."),
6117
- model: import_zod4.z.string().optional().describe("Optional model override. Must be one of this package's supported text model ids."),
6118
- max_steps: import_zod4.z.number().int().min(1).max(MAX_SUBAGENT_MAX_STEPS).optional().describe("Optional max step budget for each subagent run.")
6119
- }).refine((value) => Boolean(resolvePromptValue(value.prompt, value.message, value.items)), {
6120
- message: "Either prompt, message, or items must contain non-empty input."
6482
+ prompt: import_zod4.z.string().nullish().describe("Alias for message. Initial plain-text task for the new agent."),
6483
+ message: import_zod4.z.string().nullish().describe("Initial plain-text task for the new agent. Use either message or items."),
6484
+ items: import_zod4.z.array(subagentInputItemSchema).nullish().describe(
6485
+ "Structured input items. Use this to pass explicit mentions (for example app:// connector paths)."
6486
+ ),
6487
+ agent_type: import_zod4.z.string().nullish().describe(SPAWN_AGENT_TYPE_DESCRIPTION),
6488
+ fork_context: import_zod4.z.boolean().nullish().describe(
6489
+ "When true, fork the current thread history into the new agent before sending the initial prompt. This must be used when you want the new agent to have exactly the same context as you."
6490
+ ),
6491
+ instructions: import_zod4.z.string().nullish().describe("Optional extra instructions for this subagent instance."),
6492
+ model: import_zod4.z.string().nullish().describe("Optional model override. Must be one of this package's supported text model ids."),
6493
+ max_steps: import_zod4.z.number().int().min(1).max(MAX_SUBAGENT_MAX_STEPS).nullish().describe("Optional max step budget for each subagent run.")
6121
6494
  });
6122
6495
  var sendInputSchema = import_zod4.z.object({
6123
- agent_id: import_zod4.z.string().optional().describe("Target subagent id."),
6124
- id: import_zod4.z.string().optional().describe("Codex-style alias for agent_id."),
6125
- input: import_zod4.z.string().optional().describe("New user input queued for the subagent."),
6126
- message: import_zod4.z.string().optional().describe("Codex-style alias for input."),
6127
- items: import_zod4.z.array(subagentInputItemSchema).optional().describe("Optional Codex-style input items."),
6128
- interrupt: import_zod4.z.boolean().optional().describe("If true and currently running, aborts active run before queuing input.")
6496
+ agent_id: import_zod4.z.string().nullish().describe("Target subagent id."),
6497
+ id: import_zod4.z.string().nullish().describe("Agent id to message (from spawn_agent)."),
6498
+ input: import_zod4.z.string().nullish().describe("New user input queued for the subagent."),
6499
+ message: import_zod4.z.string().nullish().describe("Legacy plain-text message to send to the agent. Use either message or items."),
6500
+ items: import_zod4.z.array(subagentInputItemSchema).nullish().describe(
6501
+ "Structured input items. Use this to pass explicit mentions (for example app:// connector paths)."
6502
+ ),
6503
+ interrupt: import_zod4.z.boolean().nullish().describe("If true and currently running, aborts active run before queuing input.")
6129
6504
  }).refine((value) => Boolean(resolveAgentIdValue(value.agent_id, value.id)), {
6130
6505
  message: "agent_id (or id) is required."
6131
- }).refine((value) => Boolean(resolvePromptValue(value.input, value.message, value.items)), {
6132
- message: "input (or message/items) is required."
6133
6506
  });
6134
6507
  var resumeAgentSchema = import_zod4.z.object({
6135
- agent_id: import_zod4.z.string().optional().describe("Target subagent id."),
6136
- id: import_zod4.z.string().optional().describe("Codex-style alias for agent_id.")
6508
+ agent_id: import_zod4.z.string().nullish().describe("Target subagent id."),
6509
+ id: import_zod4.z.string().nullish().describe("Agent id to resume.")
6137
6510
  }).refine((value) => Boolean(resolveAgentIdValue(value.agent_id, value.id)), {
6138
6511
  message: "agent_id (or id) is required."
6139
6512
  });
6140
6513
  var waitSchema = import_zod4.z.object({
6141
- agent_id: import_zod4.z.string().optional().describe("Target subagent id."),
6142
- id: import_zod4.z.string().optional().describe("Codex-style alias for agent_id."),
6143
- ids: import_zod4.z.array(import_zod4.z.string().min(1)).optional().describe("Codex-style list of agent ids."),
6144
- timeout_ms: import_zod4.z.number().int().min(1).optional().describe("Optional wait timeout in milliseconds.")
6145
- }).refine(
6146
- (value) => Boolean(resolveAgentIdValue(value.agent_id, value.id)) || Array.isArray(value.ids) && value.ids.length > 0,
6147
- {
6148
- message: "agent_id/id or ids is required."
6149
- }
6150
- );
6514
+ agent_id: import_zod4.z.string().nullish().describe("Target subagent id."),
6515
+ id: import_zod4.z.string().nullish().describe("Codex-style alias for agent_id."),
6516
+ ids: import_zod4.z.array(import_zod4.z.string().min(1)).nullish().describe("Agent ids to wait on. Pass multiple ids to wait for whichever finishes first."),
6517
+ timeout_ms: import_zod4.z.number().int().nullish().describe(
6518
+ "Optional timeout in milliseconds. Defaults to 30000, min 10000, max 3600000. Prefer longer waits (minutes) to avoid busy polling."
6519
+ )
6520
+ });
6151
6521
  var closeSchema = import_zod4.z.object({
6152
- agent_id: import_zod4.z.string().optional().describe("Target subagent id."),
6153
- id: import_zod4.z.string().optional().describe("Codex-style alias for agent_id.")
6522
+ agent_id: import_zod4.z.string().nullish().describe("Target subagent id."),
6523
+ id: import_zod4.z.string().nullish().describe("Agent id to close (from spawn_agent).")
6154
6524
  }).refine((value) => Boolean(resolveAgentIdValue(value.agent_id, value.id)), {
6155
6525
  message: "agent_id (or id) is required."
6156
6526
  });
@@ -6158,6 +6528,7 @@ function resolveSubagentToolConfig(selection, currentDepth) {
6158
6528
  const defaults = {
6159
6529
  maxAgents: DEFAULT_SUBAGENT_MAX_AGENTS,
6160
6530
  maxDepth: DEFAULT_SUBAGENT_MAX_DEPTH,
6531
+ minWaitTimeoutMs: DEFAULT_SUBAGENT_MIN_WAIT_TIMEOUT_MS,
6161
6532
  defaultWaitTimeoutMs: DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS,
6162
6533
  maxWaitTimeoutMs: DEFAULT_SUBAGENT_MAX_WAIT_TIMEOUT_MS,
6163
6534
  promptPattern: "codex",
@@ -6178,10 +6549,16 @@ function resolveSubagentToolConfig(selection, currentDepth) {
6178
6549
  MAX_SUBAGENT_MAX_AGENTS
6179
6550
  );
6180
6551
  const maxDepth = normalizeInteger(config.maxDepth, defaults.maxDepth, 1, MAX_SUBAGENT_MAX_DEPTH);
6552
+ const minWaitTimeoutMs = normalizeInteger(
6553
+ config.minWaitTimeoutMs,
6554
+ defaults.minWaitTimeoutMs,
6555
+ 1,
6556
+ MAX_SUBAGENT_WAIT_TIMEOUT_MS
6557
+ );
6181
6558
  const defaultWaitTimeoutMs = normalizeInteger(
6182
6559
  config.defaultWaitTimeoutMs,
6183
6560
  defaults.defaultWaitTimeoutMs,
6184
- 1,
6561
+ minWaitTimeoutMs,
6185
6562
  MAX_SUBAGENT_WAIT_TIMEOUT_MS
6186
6563
  );
6187
6564
  const maxWaitTimeoutMs = normalizeInteger(
@@ -6198,6 +6575,7 @@ function resolveSubagentToolConfig(selection, currentDepth) {
6198
6575
  enabled,
6199
6576
  maxAgents,
6200
6577
  maxDepth,
6578
+ minWaitTimeoutMs,
6201
6579
  defaultWaitTimeoutMs,
6202
6580
  maxWaitTimeoutMs,
6203
6581
  promptPattern,
@@ -6211,9 +6589,11 @@ function resolveSubagentToolConfig(selection, currentDepth) {
6211
6589
  function buildCodexSubagentOrchestratorInstructions(params) {
6212
6590
  return [
6213
6591
  "Subagent orchestration tools are available: spawn_agent, send_input, resume_agent, wait, close_agent.",
6592
+ "Background updates may appear as <subagent_notification>{...}</subagent_notification>; treat them as status updates, not new user intent.",
6593
+ "Available spawn_agent agent_type values: default, researcher, worker, reviewer.",
6214
6594
  "Use this control pattern:",
6215
6595
  "1. spawn_agent with a focused prompt.",
6216
- "2. wait on that agent_id until it is no longer running.",
6596
+ "2. wait with ids=[agent_id] until the agent reaches a non-running state. Prefer long waits (minutes).",
6217
6597
  "3. For follow-up turns, send_input then resume_agent.",
6218
6598
  "4. close_agent when delegation is complete.",
6219
6599
  `Limits: max active subagents ${params.maxAgents}, max depth ${params.maxDepth}, current depth ${params.currentDepth}.`
@@ -6235,9 +6615,10 @@ function createSubagentToolController(options) {
6235
6615
  };
6236
6616
  }
6237
6617
  const agents = /* @__PURE__ */ new Map();
6618
+ const roleNicknameCounts = /* @__PURE__ */ new Map();
6238
6619
  const tools = {
6239
6620
  spawn_agent: tool({
6240
- description: "Spawns a subagent asynchronously. Returns immediately with agent status and id.",
6621
+ description: "Spawn a sub-agent for a well-scoped task. Returns the agent id (and user-facing nickname when available) to use to communicate with this agent.",
6241
6622
  inputSchema: spawnAgentInputSchema,
6242
6623
  execute: async (input) => {
6243
6624
  if (countActiveAgents(agents) >= options.config.maxAgents) {
@@ -6260,24 +6641,36 @@ function createSubagentToolController(options) {
6260
6641
  }
6261
6642
  const id = `agent_${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
6262
6643
  const now = Date.now();
6263
- const initialPrompt = resolvePromptValue(input.prompt, input.message, input.items);
6264
- if (!initialPrompt) {
6265
- throw new Error("spawn_agent requires prompt/message/items with non-empty text.");
6266
- }
6644
+ const { roleName, roleInstructions } = resolveAgentType(input.agent_type);
6645
+ const nickname = reserveAgentNickname(roleName, roleNicknameCounts);
6646
+ const perSpawnInstructions = joinInstructionBlocks(
6647
+ roleInstructions,
6648
+ trimToUndefined(input.instructions)
6649
+ );
6650
+ const initialPrompt = resolveCollabInputText({
6651
+ textCandidates: [{ value: input.prompt }, { value: input.message }],
6652
+ items: input.items,
6653
+ bothError: "Provide either prompt/message or items, but not both.",
6654
+ missingError: "Provide one of: prompt/message or items.",
6655
+ emptyTextError: "Empty message can't be sent to an agent.",
6656
+ emptyItemsError: "Items can't be empty."
6657
+ });
6267
6658
  const agent = {
6268
6659
  id,
6269
6660
  depth: childDepth,
6270
6661
  model,
6662
+ ...nickname ? { nickname } : {},
6663
+ agentRole: roleName,
6271
6664
  status: "idle",
6272
6665
  createdAtMs: now,
6273
6666
  updatedAtMs: now,
6274
6667
  pendingInputs: [initialPrompt],
6275
- history: [],
6668
+ history: input.fork_context && options.forkContextMessages ? [...options.forkContextMessages] : [],
6276
6669
  ...options.buildChildInstructions ? {
6277
6670
  instructions: trimToUndefined(
6278
- options.buildChildInstructions(input.instructions, childDepth)
6671
+ options.buildChildInstructions(perSpawnInstructions, childDepth)
6279
6672
  )
6280
- } : input.instructions ? { instructions: trimToUndefined(input.instructions) } : {},
6673
+ } : perSpawnInstructions ? { instructions: perSpawnInstructions } : {},
6281
6674
  ...input.max_steps ? { maxSteps: input.max_steps } : options.config.maxSteps ? { maxSteps: options.config.maxSteps } : {},
6282
6675
  turns: 0,
6283
6676
  notification: "spawned",
@@ -6287,41 +6680,50 @@ function createSubagentToolController(options) {
6287
6680
  };
6288
6681
  agents.set(id, agent);
6289
6682
  startRun(agent, options);
6290
- return buildToolResponse(agent, {
6291
- notification: "spawned",
6292
- message: `Spawned subagent ${id}.`
6293
- });
6683
+ return buildToolResponse(
6684
+ agent,
6685
+ {
6686
+ notification: "spawned",
6687
+ message: `Spawned subagent ${id}.`
6688
+ },
6689
+ { nickname: agent.nickname }
6690
+ );
6294
6691
  }
6295
6692
  }),
6296
6693
  send_input: tool({
6297
- description: "Queues new input for an existing subagent.",
6694
+ description: "Send a message to an existing agent. Use interrupt=true to redirect work immediately.",
6298
6695
  inputSchema: sendInputSchema,
6299
6696
  execute: async (input) => {
6697
+ const submissionId = randomSubmissionId();
6300
6698
  const agentId = resolveAgentIdValue(input.agent_id, input.id);
6301
6699
  if (!agentId) {
6302
6700
  throw new Error("send_input requires agent_id or id.");
6303
6701
  }
6304
6702
  const agent = requireAgent(agents, agentId);
6305
- const nextInput = resolvePromptValue(input.input, input.message, input.items);
6306
- if (!nextInput) {
6307
- throw new Error("send_input requires input/message/items with non-empty text.");
6308
- }
6703
+ const nextInput = resolveCollabInputText({
6704
+ textCandidates: [{ value: input.input }, { value: input.message }],
6705
+ items: input.items,
6706
+ bothError: "Provide either input/message or items, but not both.",
6707
+ missingError: "Provide one of: input/message or items.",
6708
+ emptyTextError: "Empty message can't be sent to an agent.",
6709
+ emptyItemsError: "Items can't be empty."
6710
+ });
6309
6711
  if (agent.status === "closed") {
6310
- throw new Error(`Subagent ${agent.id} is closed.`);
6712
+ throw new Error(`agent with id ${agent.id} is closed`);
6311
6713
  }
6312
6714
  if (input.interrupt && agent.abortController) {
6313
6715
  agent.abortController.abort("send_input_interrupt");
6314
6716
  agent.pendingInputs.unshift(nextInput);
6315
6717
  setNotification(agent, "input_queued", `Interrupted ${agent.id} and queued new input.`);
6316
- return buildToolResponse(agent);
6718
+ return buildToolResponse(agent, void 0, { submission_id: submissionId });
6317
6719
  }
6318
6720
  agent.pendingInputs.push(nextInput);
6319
6721
  setNotification(agent, "input_queued", `Queued input for ${agent.id}.`);
6320
- return buildToolResponse(agent);
6722
+ return buildToolResponse(agent, void 0, { submission_id: submissionId });
6321
6723
  }
6322
6724
  }),
6323
6725
  resume_agent: tool({
6324
- description: "Resumes a subagent run when queued input is available.",
6726
+ description: "Resume a previously closed agent by id so it can receive send_input and wait calls.",
6325
6727
  inputSchema: resumeAgentSchema,
6326
6728
  execute: async (input) => {
6327
6729
  const agentId = resolveAgentIdValue(input.agent_id, input.id);
@@ -6330,10 +6732,11 @@ function createSubagentToolController(options) {
6330
6732
  }
6331
6733
  const agent = requireAgent(agents, agentId);
6332
6734
  if (agent.status === "closed") {
6333
- setNotification(agent, "already_closed", `Subagent ${agent.id} is already closed.`);
6735
+ agent.status = "idle";
6736
+ setNotification(agent, "resumed", `Resumed subagent ${agent.id}.`);
6334
6737
  return buildToolResponse(agent, {
6335
- notification: "already_closed",
6336
- message: `Subagent ${agent.id} is already closed.`
6738
+ notification: "resumed",
6739
+ message: `Resumed subagent ${agent.id}.`
6337
6740
  });
6338
6741
  }
6339
6742
  const outcome = startRun(agent, options);
@@ -6352,41 +6755,42 @@ function createSubagentToolController(options) {
6352
6755
  }
6353
6756
  }),
6354
6757
  wait: tool({
6355
- description: "Waits for a running subagent to change state or until timeout. Returns current status.",
6758
+ description: "Wait for agents to reach a final status. Completed statuses may include the agent's final message. Returns empty status when timed out. Once the agent reaches a final status, a notification message will be received containing the same completed status.",
6356
6759
  inputSchema: waitSchema,
6357
6760
  execute: async (input) => {
6358
- const usesIdsArray = Array.isArray(input.ids) && input.ids.length > 0;
6359
6761
  const ids = resolveAgentIdList(input.agent_id, input.id, input.ids);
6360
6762
  if (ids.length === 0) {
6361
- throw new Error("wait requires agent_id/id or ids.");
6763
+ throw new Error("ids must be non-empty");
6764
+ }
6765
+ if (typeof input.timeout_ms === "number" && input.timeout_ms <= 0) {
6766
+ throw new Error("timeout_ms must be greater than zero");
6362
6767
  }
6363
6768
  const timeoutMs = normalizeInteger(
6364
6769
  input.timeout_ms,
6365
6770
  options.config.defaultWaitTimeoutMs,
6366
- 1,
6771
+ options.config.minWaitTimeoutMs,
6367
6772
  options.config.maxWaitTimeoutMs
6368
6773
  );
6369
- if (usesIdsArray) {
6370
- const status = await waitForAnyAgentStatus(agents, ids, timeoutMs);
6371
- return { status, timed_out: Object.keys(status).length === 0, timeout_ms: timeoutMs };
6372
- }
6373
- const agent = requireAgent(agents, ids[0]);
6374
- if (agent.status === "running") {
6375
- const completed = await waitUntilNotRunning(agent, timeoutMs);
6376
- if (!completed) {
6377
- setNotification(
6378
- agent,
6379
- "timeout",
6380
- `Timed out after ${timeoutMs}ms while waiting for ${agent.id}.`
6381
- );
6382
- return buildToolResponse(agent, void 0, { timed_out: true, timeout_ms: timeoutMs });
6383
- }
6774
+ const status = await waitForAnyAgentStatus(agents, ids, timeoutMs);
6775
+ const timedOut = Object.keys(status).length === 0;
6776
+ if (timedOut && ids.length === 1) {
6777
+ const agent = requireAgent(agents, ids[0]);
6778
+ setNotification(
6779
+ agent,
6780
+ "timeout",
6781
+ `Timed out after ${timeoutMs}ms while waiting for ${agent.id}.`
6782
+ );
6384
6783
  }
6385
- return buildToolResponse(agent, void 0, { timed_out: false, timeout_ms: timeoutMs });
6784
+ return {
6785
+ status,
6786
+ status_summary: summarizeAgentStatuses(status),
6787
+ timed_out: timedOut,
6788
+ timeout_ms: timeoutMs
6789
+ };
6386
6790
  }
6387
6791
  }),
6388
6792
  close_agent: tool({
6389
- description: "Closes a subagent and aborts its current run if it is still running.",
6793
+ description: "Close an agent when it is no longer needed and return its last known status.",
6390
6794
  inputSchema: closeSchema,
6391
6795
  execute: async (input) => {
6392
6796
  const agentId = resolveAgentIdValue(input.agent_id, input.id);
@@ -6398,7 +6802,7 @@ function createSubagentToolController(options) {
6398
6802
  setNotification(agent, "already_closed", `Subagent ${agent.id} is already closed.`);
6399
6803
  return buildToolResponse(agent, void 0, { cancelled: false });
6400
6804
  }
6401
- const cancelled = closeSubagent(agent, `Closed ${agent.id}.`);
6805
+ const cancelled = closeSubagent(agent, `Closed ${agent.id}.`, options);
6402
6806
  return buildToolResponse(
6403
6807
  agent,
6404
6808
  { notification: "closed", message: `Closed ${agent.id}.` },
@@ -6413,7 +6817,7 @@ function createSubagentToolController(options) {
6413
6817
  const running = [];
6414
6818
  for (const agent of agents.values()) {
6415
6819
  if (agent.status !== "closed") {
6416
- closeSubagent(agent, `Parent agent loop closed ${agent.id}.`);
6820
+ closeSubagent(agent, `Parent agent loop closed ${agent.id}.`, options);
6417
6821
  }
6418
6822
  if (agent.runningPromise) {
6419
6823
  running.push(agent.runningPromise);
@@ -6428,7 +6832,7 @@ function createSubagentToolController(options) {
6428
6832
  function requireAgent(agents, id) {
6429
6833
  const agent = agents.get(id);
6430
6834
  if (!agent) {
6431
- throw new Error(`Unknown subagent id: ${id}`);
6835
+ throw new Error(`agent with id ${id} not found`);
6432
6836
  }
6433
6837
  return agent;
6434
6838
  }
@@ -6447,17 +6851,33 @@ function resolveAgentIdList(agentId, idAlias, ids) {
6447
6851
  const single = resolveAgentIdValue(agentId, idAlias);
6448
6852
  return single ? [single] : [];
6449
6853
  }
6450
- function resolvePromptValue(prompt, message, items) {
6451
- const promptValue = prompt?.trim();
6452
- if (promptValue) {
6453
- return promptValue;
6854
+ function resolveCollabInputText(params) {
6855
+ const textCandidate = params.textCandidates.find(
6856
+ (candidate) => candidate.value !== void 0 && candidate.value !== null
6857
+ );
6858
+ const hasText = Boolean(textCandidate);
6859
+ const hasItems = params.items !== void 0 && params.items !== null;
6860
+ if (hasText && hasItems) {
6861
+ throw new Error(params.bothError);
6862
+ }
6863
+ if (!hasText && !hasItems) {
6864
+ throw new Error(params.missingError);
6865
+ }
6866
+ if (hasText) {
6867
+ const value = textCandidate?.value?.trim();
6868
+ if (!value) {
6869
+ throw new Error(params.emptyTextError);
6870
+ }
6871
+ return value;
6872
+ }
6873
+ if (!params.items || params.items.length === 0) {
6874
+ throw new Error(params.emptyItemsError);
6454
6875
  }
6455
- const messageValue = message?.trim();
6456
- if (messageValue) {
6457
- return messageValue;
6876
+ const itemText = resolveInputItemsText(params.items);
6877
+ if (!itemText) {
6878
+ throw new Error(params.emptyItemsError);
6458
6879
  }
6459
- const itemText = resolveInputItemsText(items);
6460
- return itemText ?? "";
6880
+ return itemText;
6461
6881
  }
6462
6882
  function resolveInputItemsText(items) {
6463
6883
  if (!items || items.length === 0) {
@@ -6473,9 +6893,28 @@ function resolveInputItemsText(items) {
6473
6893
  const name = typeof item.name === "string" ? item.name.trim() : "";
6474
6894
  const path6 = typeof item.path === "string" ? item.path.trim() : "";
6475
6895
  const imageUrl = typeof item.image_url === "string" ? item.image_url.trim() : "";
6476
- const compact = [itemType, name, path6 || imageUrl].filter(Boolean).join(" ");
6477
- if (compact) {
6478
- lines.push(compact);
6896
+ if (itemType === "image") {
6897
+ lines.push("[image]");
6898
+ continue;
6899
+ }
6900
+ if (itemType === "local_image" && path6) {
6901
+ lines.push(`[local_image:${path6}]`);
6902
+ continue;
6903
+ }
6904
+ if (itemType === "skill" && name && path6) {
6905
+ lines.push(`[skill:$${name}](${path6})`);
6906
+ continue;
6907
+ }
6908
+ if (itemType === "mention" && name && path6) {
6909
+ lines.push(`[mention:$${name}](${path6})`);
6910
+ continue;
6911
+ }
6912
+ if (path6 || imageUrl) {
6913
+ lines.push(`[${itemType || "input"}:${path6 || imageUrl}]`);
6914
+ continue;
6915
+ }
6916
+ if (name) {
6917
+ lines.push(`[${itemType || "input"}:${name}]`);
6479
6918
  }
6480
6919
  }
6481
6920
  if (lines.length === 0) {
@@ -6587,6 +7026,7 @@ function startRun(agent, options) {
6587
7026
  "run_completed",
6588
7027
  `Subagent ${agent.id} completed run ${agent.turns}.`
6589
7028
  );
7029
+ emitBackgroundNotification(agent, options);
6590
7030
  } catch (error) {
6591
7031
  if (agent.status === "closed") {
6592
7032
  return;
@@ -6598,6 +7038,7 @@ function startRun(agent, options) {
6598
7038
  const message = toErrorMessage(error);
6599
7039
  agent.lastError = message;
6600
7040
  setLifecycle(agent, "failed", "run_failed", `Subagent ${agent.id} failed: ${message}`);
7041
+ emitBackgroundNotification(agent, options);
6601
7042
  } finally {
6602
7043
  const runCompletedAtMs = Date.now();
6603
7044
  agent.lastRunCompletedAtMs = runCompletedAtMs;
@@ -6609,30 +7050,16 @@ function startRun(agent, options) {
6609
7050
  agent.runningPromise = runPromise;
6610
7051
  return "started";
6611
7052
  }
6612
- function closeSubagent(agent, message) {
7053
+ function closeSubagent(agent, message, options) {
6613
7054
  const cancelled = Boolean(agent.runningPromise);
6614
7055
  agent.pendingInputs = [];
6615
7056
  if (agent.abortController) {
6616
7057
  agent.abortController.abort("close_agent");
6617
7058
  }
6618
7059
  setLifecycle(agent, "closed", "closed", message);
7060
+ emitBackgroundNotification(agent, options);
6619
7061
  return cancelled;
6620
7062
  }
6621
- async function waitUntilNotRunning(agent, timeoutMs) {
6622
- const deadline = Date.now() + timeoutMs;
6623
- while (agent.status === "running") {
6624
- const remaining = deadline - Date.now();
6625
- if (remaining <= 0) {
6626
- return false;
6627
- }
6628
- const currentVersion = agent.version;
6629
- const changed = await waitForVersionChange(agent, currentVersion, remaining);
6630
- if (!changed) {
6631
- return false;
6632
- }
6633
- }
6634
- return true;
6635
- }
6636
7063
  async function waitForVersionChange(agent, version, timeoutMs) {
6637
7064
  if (agent.version !== version) {
6638
7065
  return true;
@@ -6670,6 +7097,8 @@ function buildToolResponse(agent, override, extra = {}) {
6670
7097
  function buildSnapshot(agent) {
6671
7098
  return {
6672
7099
  agent_id: agent.id,
7100
+ ...agent.nickname ? { nickname: agent.nickname } : {},
7101
+ agent_role: agent.agentRole,
6673
7102
  status: agent.status,
6674
7103
  depth: agent.depth,
6675
7104
  model: agent.model,
@@ -6695,6 +7124,83 @@ function buildSnapshot(agent) {
6695
7124
  } : {}
6696
7125
  };
6697
7126
  }
7127
+ function emitBackgroundNotification(agent, options) {
7128
+ if (!options?.onBackgroundMessage) {
7129
+ return;
7130
+ }
7131
+ if (!isBackgroundNotification(agent.notification)) {
7132
+ return;
7133
+ }
7134
+ const payload = {
7135
+ agent_id: agent.id,
7136
+ status: buildSnapshot(agent)
7137
+ };
7138
+ const body = JSON.stringify(payload);
7139
+ try {
7140
+ options.onBackgroundMessage(
7141
+ `${SUBAGENT_NOTIFICATION_OPEN_TAG}${body}${SUBAGENT_NOTIFICATION_CLOSE_TAG}`
7142
+ );
7143
+ } catch {
7144
+ }
7145
+ }
7146
+ function isBackgroundNotification(notification) {
7147
+ return notification === "run_completed" || notification === "run_failed" || notification === "closed";
7148
+ }
7149
+ function summarizeAgentStatuses(status) {
7150
+ const summary = {};
7151
+ for (const [agentId, snapshot] of Object.entries(status)) {
7152
+ const value = snapshot.status;
7153
+ summary[agentId] = typeof value === "string" ? value : "unknown";
7154
+ }
7155
+ return summary;
7156
+ }
7157
+ function buildSpawnAgentTypeDescription() {
7158
+ const sections = BUILT_IN_AGENT_TYPES.map((name) => {
7159
+ const description = BUILT_IN_AGENT_TYPE_DESCRIPTIONS[name];
7160
+ return `${name}: {
7161
+ ${description}
7162
+ }`;
7163
+ });
7164
+ return [
7165
+ `Optional type name for the new agent. If omitted, \`${DEFAULT_AGENT_TYPE}\` is used.`,
7166
+ "Available roles:",
7167
+ ...sections
7168
+ ].join("\n");
7169
+ }
7170
+ function resolveAgentType(agentType) {
7171
+ const requestedRoleName = trimToUndefined(agentType) ?? DEFAULT_AGENT_TYPE;
7172
+ const roleName = requestedRoleName;
7173
+ const description = BUILT_IN_AGENT_TYPE_DESCRIPTIONS[roleName];
7174
+ if (!description) {
7175
+ throw new Error(`unknown agent_type '${requestedRoleName}'`);
7176
+ }
7177
+ return {
7178
+ roleName,
7179
+ roleInstructions: BUILT_IN_AGENT_TYPE_INSTRUCTIONS[roleName]
7180
+ };
7181
+ }
7182
+ function reserveAgentNickname(roleName, counts) {
7183
+ const prefixByRole = {
7184
+ default: "Agent",
7185
+ researcher: "Researcher",
7186
+ worker: "Worker",
7187
+ reviewer: "Reviewer"
7188
+ };
7189
+ const prefix = prefixByRole[roleName] ?? "Agent";
7190
+ const next = (counts.get(prefix) ?? 0) + 1;
7191
+ counts.set(prefix, next);
7192
+ return `${prefix}_${next}`;
7193
+ }
7194
+ function joinInstructionBlocks(...blocks) {
7195
+ const parts = blocks.map(trimToUndefined).filter((value) => Boolean(value));
7196
+ if (parts.length === 0) {
7197
+ return void 0;
7198
+ }
7199
+ return parts.join("\n\n");
7200
+ }
7201
+ function randomSubmissionId() {
7202
+ return `sub_${(0, import_node_crypto2.randomBytes)(6).toString("hex")}`;
7203
+ }
6698
7204
  function normalizeInteger(value, fallback, min, max) {
6699
7205
  const parsed = Number.isFinite(value) ? Math.floor(value) : fallback;
6700
7206
  return Math.max(min, Math.min(max, parsed));
@@ -7433,29 +7939,33 @@ var DEFAULT_MAX_LINE_LENGTH = 500;
7433
7939
  var DEFAULT_GREP_MAX_SCANNED_FILES = 2e4;
7434
7940
  var DEFAULT_TAB_WIDTH = 4;
7435
7941
  var codexReadFileInputSchema = import_zod6.z.object({
7436
- file_path: import_zod6.z.string().min(1).describe("Absolute path to the file"),
7437
- offset: import_zod6.z.number().int().min(1).optional().describe("The line number to start reading from. Must be 1 or greater."),
7438
- limit: import_zod6.z.number().int().min(1).optional().describe("The maximum number of lines to return."),
7439
- mode: import_zod6.z.enum(["slice", "indentation"]).optional().describe('Optional mode selector: "slice" (default) or "indentation".'),
7942
+ file_path: import_zod6.z.string().min(1).describe(
7943
+ "Path to the file (relative to cwd, or absolute. In sandbox mode, / maps to the sandbox root)."
7944
+ ),
7945
+ offset: import_zod6.z.number().int().min(1).nullish().describe("The line number to start reading from. Must be 1 or greater."),
7946
+ limit: import_zod6.z.number().int().min(1).nullish().describe("The maximum number of lines to return."),
7947
+ mode: import_zod6.z.enum(["slice", "indentation"]).nullish().describe('Optional mode selector: "slice" (default) or "indentation".'),
7440
7948
  indentation: import_zod6.z.object({
7441
- anchor_line: import_zod6.z.number().int().min(1).optional(),
7442
- max_levels: import_zod6.z.number().int().min(0).optional(),
7443
- include_siblings: import_zod6.z.boolean().optional(),
7444
- include_header: import_zod6.z.boolean().optional(),
7445
- max_lines: import_zod6.z.number().int().min(1).optional()
7446
- }).optional()
7949
+ anchor_line: import_zod6.z.number().int().min(1).nullish(),
7950
+ max_levels: import_zod6.z.number().int().min(0).nullish(),
7951
+ include_siblings: import_zod6.z.boolean().nullish(),
7952
+ include_header: import_zod6.z.boolean().nullish(),
7953
+ max_lines: import_zod6.z.number().int().min(1).nullish()
7954
+ }).nullish()
7447
7955
  });
7448
7956
  var codexListDirInputSchema = import_zod6.z.object({
7449
- dir_path: import_zod6.z.string().min(1).describe("Absolute path to the directory to list."),
7450
- offset: import_zod6.z.number().int().min(1).optional().describe("The entry number to start listing from. Must be 1 or greater."),
7451
- limit: import_zod6.z.number().int().min(1).optional().describe("The maximum number of entries to return."),
7452
- depth: import_zod6.z.number().int().min(1).optional().describe("The maximum directory depth to traverse. Must be 1 or greater.")
7957
+ dir_path: import_zod6.z.string().min(1).describe(
7958
+ "Path to the directory to list (relative to cwd, or absolute. In sandbox mode, / maps to the sandbox root)."
7959
+ ),
7960
+ offset: import_zod6.z.number().int().min(1).nullish().describe("The entry number to start listing from. Must be 1 or greater."),
7961
+ limit: import_zod6.z.number().int().min(1).nullish().describe("The maximum number of entries to return."),
7962
+ depth: import_zod6.z.number().int().min(1).nullish().describe("The maximum directory depth to traverse. Must be 1 or greater.")
7453
7963
  });
7454
7964
  var codexGrepFilesInputSchema = import_zod6.z.object({
7455
7965
  pattern: import_zod6.z.string().min(1).describe("Regular expression pattern to search for."),
7456
- include: import_zod6.z.string().optional().describe('Optional glob limiting searched files (for example "*.rs").'),
7457
- path: import_zod6.z.string().optional().describe("Directory or file path to search. Defaults to cwd."),
7458
- limit: import_zod6.z.number().int().min(1).optional().describe("Maximum number of file paths to return (defaults to 100).")
7966
+ include: import_zod6.z.string().nullish().describe('Optional glob limiting searched files (for example "*.rs").'),
7967
+ path: import_zod6.z.string().nullish().describe("Directory or file path to search. Defaults to cwd."),
7968
+ limit: import_zod6.z.number().int().min(1).nullish().describe("Maximum number of file paths to return (defaults to 100).")
7459
7969
  });
7460
7970
  var applyPatchInputSchema = import_zod6.z.object({
7461
7971
  input: import_zod6.z.string().min(1).describe(CODEX_APPLY_PATCH_INPUT_DESCRIPTION)
@@ -7690,9 +8200,6 @@ function createGlobTool(options = {}) {
7690
8200
  }
7691
8201
  async function readFileCodex(input, options) {
7692
8202
  const runtime = resolveRuntime(options);
7693
- if (!import_node_path5.default.isAbsolute(input.file_path)) {
7694
- throw new Error("file_path must be an absolute path");
7695
- }
7696
8203
  const filePath = resolvePathWithPolicy(input.file_path, runtime.cwd, runtime.allowOutsideCwd);
7697
8204
  await runAccessHook2(runtime, {
7698
8205
  cwd: runtime.cwd,
@@ -7735,15 +8242,12 @@ async function readFileCodex(input, options) {
7735
8242
  maxLevels: indentation.max_levels ?? 0,
7736
8243
  includeSiblings: indentation.include_siblings ?? false,
7737
8244
  includeHeader: indentation.include_header ?? true,
7738
- maxLines: indentation.max_lines
8245
+ maxLines: indentation.max_lines ?? void 0
7739
8246
  });
7740
8247
  return selected.map((record) => `L${record.number}: ${record.display}`).join("\n");
7741
8248
  }
7742
8249
  async function listDirectoryCodex(input, options) {
7743
8250
  const runtime = resolveRuntime(options);
7744
- if (!import_node_path5.default.isAbsolute(input.dir_path)) {
7745
- throw new Error("dir_path must be an absolute path");
7746
- }
7747
8251
  const dirPath = resolvePathWithPolicy(input.dir_path, runtime.cwd, runtime.allowOutsideCwd);
7748
8252
  await runAccessHook2(runtime, {
7749
8253
  cwd: runtime.cwd,
@@ -7771,7 +8275,7 @@ async function listDirectoryCodex(input, options) {
7771
8275
  const remaining = entries.length - startIndex;
7772
8276
  const cappedLimit = Math.min(limit, remaining);
7773
8277
  const selected = entries.slice(startIndex, startIndex + cappedLimit);
7774
- const output = [`Absolute path: ${dirPath}`];
8278
+ const output = [`Absolute path: ${toSandboxDisplayPath(dirPath, runtime.cwd)}`];
7775
8279
  for (const entry of selected) {
7776
8280
  output.push(formatListEntry(entry));
7777
8281
  }
@@ -8157,10 +8661,17 @@ function mapApplyPatchAction(action) {
8157
8661
  }
8158
8662
  function resolvePathWithPolicy(inputPath, cwd, allowOutsideCwd) {
8159
8663
  const absolutePath = import_node_path5.default.isAbsolute(inputPath) ? import_node_path5.default.resolve(inputPath) : import_node_path5.default.resolve(cwd, inputPath);
8160
- if (!allowOutsideCwd && !isPathInsideCwd2(absolutePath, cwd)) {
8161
- throw new Error(`path "${inputPath}" resolves outside cwd "${cwd}"`);
8664
+ if (allowOutsideCwd || isPathInsideCwd2(absolutePath, cwd)) {
8665
+ return absolutePath;
8162
8666
  }
8163
- return absolutePath;
8667
+ if (import_node_path5.default.isAbsolute(inputPath)) {
8668
+ const sandboxRelativePath = inputPath.replace(/^[/\\]+/, "");
8669
+ const sandboxRootedPath = import_node_path5.default.resolve(cwd, sandboxRelativePath);
8670
+ if (isPathInsideCwd2(sandboxRootedPath, cwd)) {
8671
+ return sandboxRootedPath;
8672
+ }
8673
+ }
8674
+ throw new Error(`path "${inputPath}" resolves outside cwd "${cwd}"`);
8164
8675
  }
8165
8676
  function isPathInsideCwd2(candidatePath, cwd) {
8166
8677
  const relative = import_node_path5.default.relative(cwd, candidatePath);
@@ -8176,6 +8687,16 @@ function toDisplayPath2(absolutePath, cwd) {
8176
8687
  }
8177
8688
  return absolutePath;
8178
8689
  }
8690
+ function toSandboxDisplayPath(absolutePath, cwd) {
8691
+ const relative = import_node_path5.default.relative(cwd, absolutePath);
8692
+ if (relative === "") {
8693
+ return "/";
8694
+ }
8695
+ if (!relative.startsWith("..") && !import_node_path5.default.isAbsolute(relative)) {
8696
+ return `/${normalizeSlashes(relative)}`;
8697
+ }
8698
+ return normalizeSlashes(absolutePath);
8699
+ }
8179
8700
  function splitLines(content) {
8180
8701
  const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
8181
8702
  const lines = normalized.split("\n");
@@ -8520,6 +9041,65 @@ async function runAgentLoop(request) {
8520
9041
  await telemetry?.flush();
8521
9042
  }
8522
9043
  }
9044
+ function mergeAbortSignals2(first, second) {
9045
+ if (!first) {
9046
+ return second;
9047
+ }
9048
+ if (!second) {
9049
+ return first;
9050
+ }
9051
+ const controller = new AbortController();
9052
+ const abortFrom = (signal) => {
9053
+ if (!controller.signal.aborted) {
9054
+ controller.abort(signal.reason);
9055
+ }
9056
+ };
9057
+ if (first.aborted) {
9058
+ abortFrom(first);
9059
+ } else {
9060
+ first.addEventListener("abort", () => abortFrom(first), { once: true });
9061
+ }
9062
+ if (second.aborted) {
9063
+ abortFrom(second);
9064
+ } else {
9065
+ second.addEventListener("abort", () => abortFrom(second), { once: true });
9066
+ }
9067
+ return controller.signal;
9068
+ }
9069
+ function streamAgentLoop(request) {
9070
+ const queue = createAsyncQueue();
9071
+ const abortController = new AbortController();
9072
+ const steering = request.steering ?? createToolLoopSteeringChannel();
9073
+ const signal = mergeAbortSignals2(request.signal, abortController.signal);
9074
+ const sourceOnEvent = request.onEvent;
9075
+ const result = (async () => {
9076
+ try {
9077
+ const output = await runAgentLoop({
9078
+ ...request,
9079
+ steering,
9080
+ ...signal ? { signal } : {},
9081
+ onEvent: (event) => {
9082
+ sourceOnEvent?.(event);
9083
+ queue.push(event);
9084
+ }
9085
+ });
9086
+ queue.close();
9087
+ return output;
9088
+ } catch (error) {
9089
+ const err = error instanceof Error ? error : new Error(String(error));
9090
+ queue.fail(err);
9091
+ throw err;
9092
+ }
9093
+ })();
9094
+ return {
9095
+ events: queue.iterable,
9096
+ result,
9097
+ append: steering.append,
9098
+ steer: steering.steer,
9099
+ pendingSteeringCount: steering.pendingCount,
9100
+ abort: () => abortController.abort()
9101
+ };
9102
+ }
8523
9103
  async function runAgentLoopInternal(request, context) {
8524
9104
  const {
8525
9105
  tools: customTools,
@@ -8534,6 +9114,8 @@ async function runAgentLoopInternal(request, context) {
8534
9114
  const telemetrySession = context.telemetry ?? createAgentTelemetrySession(telemetry);
8535
9115
  const runId = randomRunId();
8536
9116
  const startedAtMs = Date.now();
9117
+ const steeringChannel = toolLoopRequest.steering ?? createToolLoopSteeringChannel();
9118
+ const toolLoopRequestWithSteering = toolLoopRequest.steering === steeringChannel ? toolLoopRequest : { ...toolLoopRequest, steering: steeringChannel };
8537
9119
  const filesystemSelection = filesystemTool ?? filesystem_tool;
8538
9120
  const subagentSelection = subagentTool ?? subagent_tool ?? subagents;
8539
9121
  const filesystemTools = resolveFilesystemTools(request.model, filesystemSelection);
@@ -8546,7 +9128,8 @@ async function runAgentLoopInternal(request, context) {
8546
9128
  customTools: customTools ?? {},
8547
9129
  filesystemSelection,
8548
9130
  subagentSelection,
8549
- toolLoopRequest,
9131
+ toolLoopRequest: toolLoopRequestWithSteering,
9132
+ steering: steeringChannel,
8550
9133
  resolvedSubagentConfig
8551
9134
  });
8552
9135
  const mergedTools = mergeToolSets(
@@ -8559,7 +9142,7 @@ async function runAgentLoopInternal(request, context) {
8559
9142
  );
8560
9143
  }
8561
9144
  const instructions = buildLoopInstructions(
8562
- toolLoopRequest.instructions,
9145
+ toolLoopRequestWithSteering.instructions,
8563
9146
  resolvedSubagentConfig,
8564
9147
  context.depth
8565
9148
  );
@@ -8578,7 +9161,7 @@ async function runAgentLoopInternal(request, context) {
8578
9161
  filesystemToolsEnabled: Object.keys(filesystemTools).length > 0,
8579
9162
  subagentToolsEnabled: resolvedSubagentConfig.enabled
8580
9163
  });
8581
- const sourceOnEvent = toolLoopRequest.onEvent;
9164
+ const sourceOnEvent = toolLoopRequestWithSteering.onEvent;
8582
9165
  const includeLlmStreamEvents = telemetrySession?.includeLlmStreamEvents === true;
8583
9166
  const wrappedOnEvent = sourceOnEvent || includeLlmStreamEvents ? (event) => {
8584
9167
  sourceOnEvent?.(event);
@@ -8588,7 +9171,7 @@ async function runAgentLoopInternal(request, context) {
8588
9171
  } : void 0;
8589
9172
  try {
8590
9173
  const result = await runToolLoop({
8591
- ...toolLoopRequest,
9174
+ ...toolLoopRequestWithSteering,
8592
9175
  ...instructions ? { instructions } : {},
8593
9176
  ...wrappedOnEvent ? { onEvent: wrappedOnEvent } : {},
8594
9177
  tools: mergedTools
@@ -8656,6 +9239,10 @@ function createSubagentController(params) {
8656
9239
  config: params.resolvedSubagentConfig,
8657
9240
  parentDepth: params.depth,
8658
9241
  parentModel: params.resolvedSubagentConfig.model ?? params.model,
9242
+ forkContextMessages: normalizeForkContextMessages(params.toolLoopRequest.input),
9243
+ onBackgroundMessage: (message) => {
9244
+ params.steering?.append({ role: "user", content: message });
9245
+ },
8659
9246
  buildChildInstructions: (spawnInstructions, childDepth) => buildChildInstructions(spawnInstructions, params.resolvedSubagentConfig, childDepth),
8660
9247
  runSubagent: async (subagentRequest) => {
8661
9248
  const childCustomTools = params.resolvedSubagentConfig.inheritTools ? params.customTools : {};
@@ -8724,6 +9311,15 @@ function buildChildInstructions(spawnInstructions, config, childDepth) {
8724
9311
  }
8725
9312
  return blocks.length > 0 ? blocks.join("\n\n") : void 0;
8726
9313
  }
9314
+ function normalizeForkContextMessages(input) {
9315
+ if (typeof input === "string") {
9316
+ return [{ role: "user", content: input }];
9317
+ }
9318
+ return input.map((message) => ({
9319
+ role: message.role,
9320
+ content: Array.isArray(message.content) ? [...message.content] : message.content
9321
+ }));
9322
+ }
8727
9323
  function trimToUndefined2(value) {
8728
9324
  const trimmed = value?.trim();
8729
9325
  return trimmed && trimmed.length > 0 ? trimmed : void 0;
@@ -8894,6 +9490,7 @@ function createAgentTelemetryEmitter(params) {
8894
9490
  createReadFilesTool,
8895
9491
  createReplaceTool,
8896
9492
  createRgSearchTool,
9493
+ createToolLoopSteeringChannel,
8897
9494
  createWriteFileTool,
8898
9495
  customTool,
8899
9496
  encodeChatGptAuthJson,
@@ -8925,8 +9522,10 @@ function createAgentTelemetryEmitter(params) {
8925
9522
  runAgentLoop,
8926
9523
  runToolLoop,
8927
9524
  sanitisePartForLogging,
9525
+ streamAgentLoop,
8928
9526
  streamJson,
8929
9527
  streamText,
9528
+ streamToolLoop,
8930
9529
  stripCodexCitationMarkers,
8931
9530
  toGeminiJsonSchema,
8932
9531
  tool