@zhivex-ai/core 0.20.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +19 -1
  2. package/dist/agent-control-plane.d.ts +4 -1
  3. package/dist/agent-control-plane.d.ts.map +1 -1
  4. package/dist/agent-control-plane.js +61 -32
  5. package/dist/agent-control-plane.js.map +1 -1
  6. package/dist/agent-evaluation.d.ts +5 -1
  7. package/dist/agent-evaluation.d.ts.map +1 -1
  8. package/dist/agent-evaluation.js +17 -0
  9. package/dist/agent-evaluation.js.map +1 -1
  10. package/dist/agent-harness.d.ts +9 -0
  11. package/dist/agent-harness.d.ts.map +1 -0
  12. package/dist/agent-harness.js +66 -0
  13. package/dist/agent-harness.js.map +1 -0
  14. package/dist/agent-state.d.ts.map +1 -1
  15. package/dist/agent-state.js +112 -0
  16. package/dist/agent-state.js.map +1 -1
  17. package/dist/agent.d.ts +24 -15
  18. package/dist/agent.d.ts.map +1 -1
  19. package/dist/agent.js +692 -39
  20. package/dist/agent.js.map +1 -1
  21. package/dist/api-stability.d.ts.map +1 -1
  22. package/dist/api-stability.js +4 -0
  23. package/dist/api-stability.js.map +1 -1
  24. package/dist/errors.d.ts +2 -2
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js.map +1 -1
  27. package/dist/generate-object.d.ts.map +1 -1
  28. package/dist/generate-object.js +7 -2
  29. package/dist/generate-object.js.map +1 -1
  30. package/dist/generate-text.d.ts +5 -4
  31. package/dist/generate-text.d.ts.map +1 -1
  32. package/dist/generate-text.js +482 -70
  33. package/dist/generate-text.js.map +1 -1
  34. package/dist/index.d.ts +2 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp.d.ts +22 -3
  39. package/dist/mcp.d.ts.map +1 -1
  40. package/dist/mcp.js +164 -18
  41. package/dist/mcp.js.map +1 -1
  42. package/dist/messages.d.ts +5 -5
  43. package/dist/messages.d.ts.map +1 -1
  44. package/dist/messages.js.map +1 -1
  45. package/dist/safety-policy.d.ts.map +1 -1
  46. package/dist/safety-policy.js +2 -3
  47. package/dist/safety-policy.js.map +1 -1
  48. package/dist/stream.d.ts +4 -0
  49. package/dist/stream.d.ts.map +1 -1
  50. package/dist/stream.js +29 -0
  51. package/dist/stream.js.map +1 -1
  52. package/dist/structured-output-prompt.d.ts +6 -0
  53. package/dist/structured-output-prompt.d.ts.map +1 -0
  54. package/dist/structured-output-prompt.js +26 -0
  55. package/dist/structured-output-prompt.js.map +1 -0
  56. package/dist/tool-execution-suspension.d.ts +8 -0
  57. package/dist/tool-execution-suspension.d.ts.map +1 -0
  58. package/dist/tool-execution-suspension.js +12 -0
  59. package/dist/tool-execution-suspension.js.map +1 -0
  60. package/dist/types.d.ts +335 -28
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/ui.d.ts +5 -1
  63. package/dist/ui.d.ts.map +1 -1
  64. package/dist/ui.js +68 -2
  65. package/dist/ui.js.map +1 -1
  66. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { BoundedReplayBroadcast } from "./bounded-broadcast.js";
2
- import { ParseError, UnsupportedFeatureError, ValidationError } from "./errors.js";
3
+ import { GuardrailTriggeredError, ParseError, UnsupportedFeatureError, ValidationError } from "./errors.js";
3
4
  import { emitLanguageModelTelemetryEvent } from "./middleware.js";
4
5
  import { createTextMessage, getTextFromMessages, isCallableToolDefinition, normalizeFinishReason, providerDataPart, resultMessages, serializeJsonValue, toolCallPart, toolResultPart, validateMessageParts } from "./messages.js";
5
6
  import { mergeAbortSignals } from "./runtime.js";
6
7
  import { toToolSet } from "./tool-registry.js";
8
+ import { ToolExecutionSuspendedError } from "./tool-execution-suspension.js";
7
9
  const withToolTimeout = async (operation, timeoutMs, abortSignal) => {
8
10
  if (!timeoutMs) {
9
11
  return operation(abortSignal);
@@ -121,8 +123,52 @@ const toRequest = (options, messages) => ({
121
123
  maxRetries: options.maxRetries,
122
124
  retryBackoffMs: options.retryBackoffMs
123
125
  });
124
- const executeTools = async (toolCalls, options, context) => {
125
- const validatedCalls = toolCalls.map((call) => {
126
+ const canonicalJson = (value) => {
127
+ if (value === null || typeof value !== "object") {
128
+ return JSON.stringify(value);
129
+ }
130
+ if (Array.isArray(value)) {
131
+ return `[${value.map(canonicalJson).join(",")}]`;
132
+ }
133
+ return `{${Object.keys(value)
134
+ .sort()
135
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
136
+ .join(",")}}`;
137
+ };
138
+ const localApprovalBinding = (options, item, step) => {
139
+ const input = serializeJsonValue(item.parsedInput);
140
+ const toolVersion = item.tool.approvalVersion ?? "1";
141
+ const payload = canonicalJson({
142
+ runId: options.toolContext?.runId ?? null,
143
+ step,
144
+ toolCallId: item.call.id,
145
+ toolName: item.call.name,
146
+ input,
147
+ toolVersion
148
+ });
149
+ const inputDigest = createHash("sha256").update(payload).digest("hex");
150
+ return {
151
+ id: `approval_${inputDigest}`,
152
+ input,
153
+ inputDigest,
154
+ payload,
155
+ toolVersion
156
+ };
157
+ };
158
+ const localApprovalResolutionPayload = (inputDigest, approve, reason) => JSON.stringify({
159
+ inputDigest,
160
+ approve,
161
+ reason: reason ?? null
162
+ });
163
+ const normalizeApprovalDecision = (rawDecision, toolName) => typeof rawDecision === "boolean"
164
+ ? {
165
+ approved: rawDecision,
166
+ reason: rawDecision ? undefined : `Tool "${toolName}" was denied by the approval policy.`
167
+ }
168
+ : rawDecision ?? { approved: true };
169
+ const validateToolCalls = async (toolCalls, options, context) => {
170
+ const validated = [];
171
+ for (const call of toolCalls) {
126
172
  const tool = context.tools[call.name];
127
173
  if (!tool) {
128
174
  throw new ValidationError(`Tool "${call.name}" was requested by the model but is not registered.`);
@@ -134,56 +180,165 @@ const executeTools = async (toolCalls, options, context) => {
134
180
  if (!parsed.success) {
135
181
  throw new ValidationError(`Invalid input for tool "${call.name}": ${parsed.error.message}`);
136
182
  }
137
- return {
138
- call,
139
- tool,
140
- parsedInput: parsed.data
141
- };
142
- });
143
- const parallel = options.toolExecution?.parallel ?? options.model.capabilities.parallelToolCalls;
144
- const maxConcurrency = Math.max(1, options.toolExecution?.maxConcurrency ?? validatedCalls.length ?? 1);
145
- const timeoutMs = options.toolExecution?.timeoutMs;
146
- const stopOnError = options.toolExecution?.stopOnError ?? false;
147
- const results = new Array(validatedCalls.length);
148
- const evaluateApproval = async (item) => {
149
- const request = {
150
- toolCall: item.call,
151
- tool: item.tool,
152
- input: serializeJsonValue(item.parsedInput),
183
+ const executionContext = {
184
+ ...options.toolContext,
185
+ abortSignal: context.request.abortSignal,
186
+ toolCall: call,
153
187
  step: context.step,
154
188
  model: options.model,
155
189
  request: context.request
156
190
  };
157
- if (!options.toolApprovalPolicy) {
158
- const decision = item.tool.requiresApproval
191
+ if (tool.isEnabled && !(await tool.isEnabled(parsed.data, executionContext))) {
192
+ throw new ValidationError(`Tool "${call.name}" is disabled for this execution context.`);
193
+ }
194
+ validated.push({
195
+ call,
196
+ tool,
197
+ parsedInput: parsed.data,
198
+ executionContext
199
+ });
200
+ }
201
+ return validated;
202
+ };
203
+ const preflightTools = async (toolCalls, options, context) => {
204
+ const validatedCalls = await validateToolCalls(toolCalls, options, context);
205
+ const decisions = new Map();
206
+ const approvalRequests = [];
207
+ for (const item of validatedCalls) {
208
+ await runToolGuardrails(item, "input");
209
+ const binding = localApprovalBinding(options, item, context.step);
210
+ const resolution = options.toolApprovalResolutions?.find((candidate) => candidate.kind === "local-tool" &&
211
+ candidate.requestId === binding.id &&
212
+ candidate.toolCallId === item.call.id);
213
+ let decision;
214
+ if (resolution) {
215
+ if (resolution.inputDigest !== binding.inputDigest ||
216
+ resolution.toolVersion !== binding.toolVersion ||
217
+ resolution.step !== context.step) {
218
+ throw new ValidationError(`Approval request "${resolution.requestId}" no longer matches tool "${item.call.name}".`);
219
+ }
220
+ if (options.toolApprovalSigner) {
221
+ if (!resolution.signature) {
222
+ throw new ValidationError(`Approval request "${resolution.requestId}" is missing its required signature.`);
223
+ }
224
+ const resolutionPayload = localApprovalResolutionPayload(resolution.inputDigest, resolution.approve, resolution.reason);
225
+ const validSignature = options.toolApprovalSigner.verify
226
+ ? await options.toolApprovalSigner.verify(resolutionPayload, resolution.signature)
227
+ : (await options.toolApprovalSigner.sign(resolutionPayload)) === resolution.signature;
228
+ if (!validSignature) {
229
+ throw new ValidationError(`Approval request "${resolution.requestId}" has an invalid signature.`);
230
+ }
231
+ }
232
+ decision = {
233
+ approved: resolution.approve,
234
+ reason: resolution.reason
235
+ };
236
+ }
237
+ else if (item.tool.requiresApproval && item.tool.approvalMode === "interrupt") {
238
+ decision = {
239
+ approved: false,
240
+ approvalRequired: true,
241
+ reason: `Tool "${item.call.name}" requires human approval.`
242
+ };
243
+ }
244
+ else if (!options.toolApprovalPolicy) {
245
+ decision = item.tool.requiresApproval
159
246
  ? {
160
247
  approved: false,
161
248
  reason: `Tool "${item.call.name}" requires approval, but no toolApprovalPolicy is configured.`
162
249
  }
163
250
  : { approved: true };
164
- await options.onToolApprovalDecision?.({
165
- request,
166
- decision
167
- });
168
- return decision;
169
251
  }
170
- const rawDecision = await options.toolApprovalPolicy(request);
171
- const decision = typeof rawDecision === "boolean"
172
- ? {
173
- approved: rawDecision,
174
- reason: rawDecision ? undefined : `Tool "${item.call.name}" was denied by the approval policy.`
175
- }
176
- : rawDecision;
177
- const normalizedDecision = decision ?? { approved: true };
252
+ else {
253
+ const approvalRequest = {
254
+ toolCall: item.call,
255
+ tool: item.tool,
256
+ input: binding.input,
257
+ step: context.step,
258
+ model: options.model,
259
+ request: context.request,
260
+ executionContext: item.executionContext
261
+ };
262
+ decision = normalizeApprovalDecision(await options.toolApprovalPolicy(approvalRequest), item.call.name);
263
+ }
264
+ const approvalRequest = {
265
+ toolCall: item.call,
266
+ tool: item.tool,
267
+ input: binding.input,
268
+ step: context.step,
269
+ model: options.model,
270
+ request: context.request,
271
+ executionContext: item.executionContext
272
+ };
273
+ if (decision.approved && decision.approvalRequired) {
274
+ throw new ValidationError(`Tool approval decision for "${item.call.name}" cannot be both approved and approvalRequired.`);
275
+ }
178
276
  await options.onToolApprovalDecision?.({
179
- request,
180
- decision: normalizedDecision
277
+ request: approvalRequest,
278
+ decision
181
279
  });
182
- return normalizedDecision;
280
+ if (decision.approvalRequired) {
281
+ const signature = options.toolApprovalSigner
282
+ ? await options.toolApprovalSigner.sign(binding.inputDigest)
283
+ : undefined;
284
+ approvalRequests.push({
285
+ kind: "local-tool",
286
+ provider: "zhivex",
287
+ id: binding.id,
288
+ name: item.call.name,
289
+ arguments: canonicalJson(binding.input),
290
+ toolCallId: item.call.id,
291
+ step: context.step,
292
+ inputDigest: binding.inputDigest,
293
+ toolVersion: binding.toolVersion,
294
+ signature,
295
+ rawData: {
296
+ type: "tool_approval_request",
297
+ id: binding.id,
298
+ name: item.call.name,
299
+ arguments: canonicalJson(binding.input),
300
+ tool_call_id: item.call.id,
301
+ step: context.step,
302
+ input_digest: binding.inputDigest,
303
+ tool_version: binding.toolVersion,
304
+ ...(signature ? { signature } : {})
305
+ }
306
+ });
307
+ }
308
+ else {
309
+ decisions.set(item.call.id, decision);
310
+ }
311
+ }
312
+ return {
313
+ validatedCalls,
314
+ decisions,
315
+ approvalRequests
183
316
  };
317
+ };
318
+ const runToolGuardrails = async (item, stage, output) => {
319
+ const guardrails = stage === "input" ? item.tool.inputGuardrails : item.tool.outputGuardrails;
320
+ for (const guardrail of guardrails ?? []) {
321
+ const trigger = await guardrail({
322
+ tool: item.tool,
323
+ input: item.parsedInput,
324
+ context: item.executionContext,
325
+ ...(stage === "output" ? { output } : {})
326
+ });
327
+ if (trigger?.triggered) {
328
+ throw new GuardrailTriggeredError(stage === "input" ? "tool-input" : "tool-output", trigger.reason ?? `Tool "${item.call.name}" ${stage} guardrail triggered.`, { metadata: trigger.metadata });
329
+ }
330
+ }
331
+ };
332
+ const executeTools = async (preflight, options, context) => {
333
+ const { validatedCalls, decisions } = preflight;
334
+ const parallel = options.toolExecution?.parallel ?? options.model.capabilities.parallelToolCalls;
335
+ const maxConcurrency = Math.max(1, options.toolExecution?.maxConcurrency ?? validatedCalls.length ?? 1);
336
+ const timeoutMs = options.toolExecution?.timeoutMs;
337
+ const stopOnError = options.toolExecution?.stopOnError ?? false;
338
+ const results = new Array(validatedCalls.length);
184
339
  const executeSingleTool = async (item, index) => {
185
- const { call, tool, parsedInput } = item;
186
- const approval = await evaluateApproval(item);
340
+ const { call, tool } = item;
341
+ const approval = decisions.get(call.id) ?? { approved: true };
187
342
  if (!approval.approved) {
188
343
  results[index] = {
189
344
  toolCallId: call.id,
@@ -206,13 +361,23 @@ const executeTools = async (toolCalls, options, context) => {
206
361
  startedAt
207
362
  });
208
363
  try {
209
- const output = serializeJsonValue(await withToolTimeout(async (abortSignal) => tool.execute(parsedInput, {
210
- abortSignal,
211
- toolCall: call,
212
- step: context.step,
213
- model: options.model,
214
- request: context.request
215
- }), timeoutMs, context.request.abortSignal));
364
+ // Schema and availability are deliberately rechecked immediately before
365
+ // the side effect. Input guardrails already ran in this preflight and run
366
+ // again when a persisted approval is resumed.
367
+ const reparsed = tool.schema.safeParse(call.input);
368
+ if (!reparsed.success) {
369
+ throw new ValidationError(`Invalid input for tool "${call.name}": ${reparsed.error.message}`);
370
+ }
371
+ item.parsedInput = reparsed.data;
372
+ if (tool.isEnabled && !(await tool.isEnabled(reparsed.data, item.executionContext))) {
373
+ throw new ValidationError(`Tool "${call.name}" is disabled for this execution context.`);
374
+ }
375
+ const rawOutput = await withToolTimeout(async (abortSignal) => tool.execute(item.parsedInput, {
376
+ ...item.executionContext,
377
+ abortSignal
378
+ }), timeoutMs, context.request.abortSignal);
379
+ await runToolGuardrails(item, "output", rawOutput);
380
+ const output = serializeJsonValue(rawOutput);
216
381
  const result = {
217
382
  toolCallId: call.id,
218
383
  toolName: call.name,
@@ -235,10 +400,49 @@ const executeTools = async (toolCalls, options, context) => {
235
400
  });
236
401
  }
237
402
  catch (error) {
403
+ if (error instanceof ToolExecutionSuspendedError) {
404
+ throw error;
405
+ }
406
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
407
+ let recoveredOutput;
408
+ if (tool.onError && !(normalizedError instanceof GuardrailTriggeredError)) {
409
+ recoveredOutput = await tool.onError(normalizedError, {
410
+ tool,
411
+ input: item.parsedInput,
412
+ context: item.executionContext
413
+ });
414
+ }
415
+ if (recoveredOutput !== undefined) {
416
+ await runToolGuardrails(item, "output", recoveredOutput);
417
+ const recoveredResult = {
418
+ toolCallId: call.id,
419
+ toolName: call.name,
420
+ output: serializeJsonValue(recoveredOutput),
421
+ isError: false,
422
+ providerMetadata: call.providerMetadata
423
+ };
424
+ results[index] = recoveredResult;
425
+ const finishedAt = Date.now();
426
+ await emitLanguageModelTelemetryEvent(options.model, {
427
+ type: "tool-execution-finish",
428
+ model: options.model,
429
+ input: context.request,
430
+ step: context.step,
431
+ toolCall: call,
432
+ toolResult: recoveredResult,
433
+ startedAt,
434
+ finishedAt,
435
+ latencyMs: finishedAt - startedAt
436
+ });
437
+ return;
438
+ }
439
+ if (normalizedError instanceof GuardrailTriggeredError) {
440
+ throw normalizedError;
441
+ }
238
442
  const result = {
239
443
  toolCallId: call.id,
240
444
  toolName: call.name,
241
- error: { message: error instanceof Error ? error.message : "Tool execution failed." },
445
+ error: { message: normalizedError.message },
242
446
  isError: true,
243
447
  providerMetadata: call.providerMetadata
244
448
  };
@@ -250,7 +454,7 @@ const executeTools = async (toolCalls, options, context) => {
250
454
  input: context.request,
251
455
  step: context.step,
252
456
  toolCall: call,
253
- error: error instanceof Error ? error : new Error(String(error)),
457
+ error: normalizedError,
254
458
  startedAt,
255
459
  finishedAt,
256
460
  latencyMs: finishedAt - startedAt
@@ -259,7 +463,15 @@ const executeTools = async (toolCalls, options, context) => {
259
463
  };
260
464
  if (!parallel || validatedCalls.length <= 1) {
261
465
  for (const [index, item] of validatedCalls.entries()) {
262
- await executeSingleTool(item, index);
466
+ try {
467
+ await executeSingleTool(item, index);
468
+ }
469
+ catch (error) {
470
+ if (error instanceof ToolExecutionSuspendedError) {
471
+ throw new ToolExecutionSuspendedError(error.approvals, results.filter((result) => Boolean(result)));
472
+ }
473
+ throw error;
474
+ }
263
475
  if (stopOnError && results[index]?.isError) {
264
476
  throw new Error(`Tool "${item.call.name}" failed: ${results[index]?.error?.message ?? "Unknown tool error."}`);
265
477
  }
@@ -328,17 +540,61 @@ export const generateText = async (options) => {
328
540
  }
329
541
  const toolResults = [];
330
542
  const generatedMessages = [];
543
+ const approvalRequests = [];
331
544
  let finalResult;
332
545
  const pendingToolCalls = extractUnresolvedToolCalls(allMessages);
333
546
  if (pendingToolCalls.length) {
334
547
  const request = toRequest(options, allMessages);
335
548
  const step = options.stepOffset ?? 0;
336
- await options.onBeforeToolExecution?.({ request, step, toolCalls: pendingToolCalls });
337
- const recoveredToolResults = await executeTools(pendingToolCalls, options, {
549
+ const preflight = await preflightTools(pendingToolCalls, options, {
338
550
  request,
339
551
  step,
340
552
  tools: resolvedTools
341
553
  });
554
+ if (preflight.approvalRequests.length) {
555
+ approvalRequests.push(...preflight.approvalRequests);
556
+ return {
557
+ text: "",
558
+ finishReason: "tool-calls",
559
+ steps,
560
+ messages: allMessages,
561
+ toolResults,
562
+ approvalRequests
563
+ };
564
+ }
565
+ await options.onBeforeToolExecution?.({ request, step, toolCalls: pendingToolCalls });
566
+ let recoveredToolResults;
567
+ try {
568
+ recoveredToolResults = await executeTools(preflight, options, {
569
+ request,
570
+ step
571
+ });
572
+ }
573
+ catch (error) {
574
+ if (!(error instanceof ToolExecutionSuspendedError)) {
575
+ throw error;
576
+ }
577
+ toolResults.push(...error.completedResults);
578
+ for (const result of error.completedResults) {
579
+ allMessages.push({ role: "tool", parts: [toolResultPart(result)] });
580
+ }
581
+ if (error.completedResults.length) {
582
+ await options.onToolExecutionComplete?.({
583
+ request,
584
+ step,
585
+ toolResults: error.completedResults
586
+ });
587
+ }
588
+ approvalRequests.push(...error.approvals);
589
+ return {
590
+ text: "",
591
+ finishReason: "tool-calls",
592
+ steps,
593
+ messages: allMessages,
594
+ toolResults,
595
+ approvalRequests
596
+ };
597
+ }
342
598
  toolResults.push(...recoveredToolResults);
343
599
  for (const result of recoveredToolResults) {
344
600
  allMessages.push({ role: "tool", parts: [toolResultPart(result)] });
@@ -346,11 +602,18 @@ export const generateText = async (options) => {
346
602
  await options.onToolExecutionComplete?.({ request, step, toolResults: recoveredToolResults });
347
603
  }
348
604
  for (let step = 0; step < maxSteps; step += 1) {
349
- const request = toRequest(options, allMessages);
350
605
  const absoluteStep = (options.stepOffset ?? 0) + step + 1;
606
+ const preparedMessages = await options.prepareModelMessages?.({
607
+ messages: structuredClone(allMessages),
608
+ step: absoluteStep
609
+ });
610
+ if (preparedMessages) {
611
+ allMessages.splice(0, allMessages.length, ...structuredClone(preparedMessages));
612
+ }
613
+ const request = toRequest(options, allMessages);
351
614
  await options.onBeforeModelStep?.({ request, step: absoluteStep });
352
615
  const startedAt = Date.now();
353
- const response = await options.model.generate(request);
616
+ let response = await options.model.generate(request);
354
617
  stepTimings.set(request, { startedAt, finishedAt: Date.now() });
355
618
  steps.push({ request, response });
356
619
  finalResult = response;
@@ -360,16 +623,55 @@ export const generateText = async (options) => {
360
623
  generatedMessages.push(...responseMessages);
361
624
  }
362
625
  const toolCalls = extractToolCalls(responseMessages);
363
- await options.onModelStep?.({ request, response, step: absoluteStep, toolCalls });
364
- if (!toolCalls.length) {
365
- break;
626
+ const preflight = toolCalls.length
627
+ ? await preflightTools(toolCalls, options, {
628
+ request,
629
+ step: absoluteStep,
630
+ tools: resolvedTools
631
+ })
632
+ : undefined;
633
+ if (preflight?.approvalRequests.length) {
634
+ approvalRequests.push(...preflight.approvalRequests);
366
635
  }
367
- await options.onBeforeToolExecution?.({ request, step: absoluteStep, toolCalls });
368
- const currentToolResults = await executeTools(toolCalls, options, {
636
+ await options.onModelStep?.({
369
637
  request,
638
+ response,
370
639
  step: absoluteStep,
371
- tools: resolvedTools
640
+ toolCalls,
641
+ approvalRequests: preflight?.approvalRequests ?? []
372
642
  });
643
+ if (!toolCalls.length || preflight?.approvalRequests.length) {
644
+ break;
645
+ }
646
+ await options.onBeforeToolExecution?.({ request, step: absoluteStep, toolCalls });
647
+ let currentToolResults;
648
+ try {
649
+ currentToolResults = await executeTools(preflight, options, {
650
+ request,
651
+ step: absoluteStep
652
+ });
653
+ }
654
+ catch (error) {
655
+ if (!(error instanceof ToolExecutionSuspendedError)) {
656
+ throw error;
657
+ }
658
+ toolResults.push(...error.completedResults);
659
+ for (const result of error.completedResults) {
660
+ allMessages.push({
661
+ role: "tool",
662
+ parts: [toolResultPart(result)]
663
+ });
664
+ }
665
+ if (error.completedResults.length) {
666
+ await options.onToolExecutionComplete?.({
667
+ request,
668
+ step: absoluteStep,
669
+ toolResults: error.completedResults
670
+ });
671
+ }
672
+ approvalRequests.push(...error.approvals);
673
+ break;
674
+ }
373
675
  toolResults.push(...currentToolResults);
374
676
  for (const result of currentToolResults) {
375
677
  allMessages.push({
@@ -389,7 +691,8 @@ export const generateText = async (options) => {
389
691
  usage: aggregateTokenUsage(steps.map((step) => step.response.usage)),
390
692
  steps,
391
693
  messages: allMessages,
392
- toolResults
694
+ toolResults,
695
+ approvalRequests
393
696
  };
394
697
  };
395
698
  export const streamText = (options) => {
@@ -428,17 +731,72 @@ export const streamText = (options) => {
428
731
  const generatedMessages = [];
429
732
  const steps = [];
430
733
  const toolResults = [];
734
+ const approvalRequests = [];
431
735
  let finalResult;
432
736
  const pendingToolCalls = extractUnresolvedToolCalls(allMessages);
433
737
  if (pendingToolCalls.length) {
434
738
  const request = toRequest(options, allMessages);
435
739
  const step = options.stepOffset ?? 0;
436
- await options.onBeforeToolExecution?.({ request, step, toolCalls: pendingToolCalls });
437
- const recoveredToolResults = await executeTools(pendingToolCalls, options, {
740
+ const preflight = await preflightTools(pendingToolCalls, options, {
438
741
  request,
439
742
  step,
440
743
  tools: resolvedTools
441
744
  });
745
+ if (preflight.approvalRequests.length) {
746
+ approvalRequests.push(...preflight.approvalRequests);
747
+ for (const approval of preflight.approvalRequests) {
748
+ await publish({ type: "tool-approval-request", approval });
749
+ }
750
+ await publish({ type: "finish", finishReason: "tool-calls" }, true);
751
+ broadcast.close();
752
+ return {
753
+ text: "",
754
+ finishReason: "tool-calls",
755
+ steps,
756
+ messages: allMessages,
757
+ toolResults,
758
+ approvalRequests
759
+ };
760
+ }
761
+ await options.onBeforeToolExecution?.({ request, step, toolCalls: pendingToolCalls });
762
+ let recoveredToolResults;
763
+ try {
764
+ recoveredToolResults = await executeTools(preflight, options, {
765
+ request,
766
+ step
767
+ });
768
+ }
769
+ catch (error) {
770
+ if (!(error instanceof ToolExecutionSuspendedError)) {
771
+ throw error;
772
+ }
773
+ toolResults.push(...error.completedResults);
774
+ for (const result of error.completedResults) {
775
+ await publish({ type: "tool-result", toolResult: result });
776
+ allMessages.push({ role: "tool", parts: [toolResultPart(result)] });
777
+ }
778
+ if (error.completedResults.length) {
779
+ await options.onToolExecutionComplete?.({
780
+ request,
781
+ step,
782
+ toolResults: error.completedResults
783
+ });
784
+ }
785
+ approvalRequests.push(...error.approvals);
786
+ for (const approval of error.approvals) {
787
+ await publish({ type: "tool-approval-request", approval });
788
+ }
789
+ await publish({ type: "finish", finishReason: "tool-calls" }, true);
790
+ broadcast.close();
791
+ return {
792
+ text: "",
793
+ finishReason: "tool-calls",
794
+ steps,
795
+ messages: allMessages,
796
+ toolResults,
797
+ approvalRequests
798
+ };
799
+ }
442
800
  toolResults.push(...recoveredToolResults);
443
801
  for (const result of recoveredToolResults) {
444
802
  await publish({ type: "tool-result", toolResult: result });
@@ -447,8 +805,15 @@ export const streamText = (options) => {
447
805
  await options.onToolExecutionComplete?.({ request, step, toolResults: recoveredToolResults });
448
806
  }
449
807
  for (let step = 0; step < maxSteps; step += 1) {
450
- const request = toRequest(options, allMessages);
451
808
  const absoluteStep = (options.stepOffset ?? 0) + step + 1;
809
+ const preparedMessages = await options.prepareModelMessages?.({
810
+ messages: structuredClone(allMessages),
811
+ step: absoluteStep
812
+ });
813
+ if (preparedMessages) {
814
+ allMessages.splice(0, allMessages.length, ...structuredClone(preparedMessages));
815
+ }
816
+ const request = toRequest(options, allMessages);
452
817
  await options.onBeforeModelStep?.({ request, step: absoluteStep });
453
818
  const startedAt = Date.now();
454
819
  const stream = await streamModel(request);
@@ -518,16 +883,62 @@ export const streamText = (options) => {
518
883
  allMessages.push(...stepMessages);
519
884
  generatedMessages.push(...stepMessages);
520
885
  const toolCalls = extractToolCalls(stepMessages);
521
- await options.onModelStep?.({ request, response: finalResult, step: absoluteStep, toolCalls });
522
- if (!toolCalls.length) {
523
- break;
886
+ const preflight = toolCalls.length
887
+ ? await preflightTools(toolCalls, options, {
888
+ request,
889
+ step: absoluteStep,
890
+ tools: resolvedTools
891
+ })
892
+ : undefined;
893
+ if (preflight?.approvalRequests.length) {
894
+ approvalRequests.push(...preflight.approvalRequests);
895
+ for (const approval of preflight.approvalRequests) {
896
+ await publish({ type: "tool-approval-request", approval });
897
+ }
524
898
  }
525
- await options.onBeforeToolExecution?.({ request, step: absoluteStep, toolCalls });
526
- const currentToolResults = await executeTools(toolCalls, options, {
899
+ await options.onModelStep?.({
527
900
  request,
901
+ response: finalResult,
528
902
  step: absoluteStep,
529
- tools: resolvedTools
903
+ toolCalls,
904
+ approvalRequests: preflight?.approvalRequests ?? []
530
905
  });
906
+ if (!toolCalls.length || preflight?.approvalRequests.length) {
907
+ break;
908
+ }
909
+ await options.onBeforeToolExecution?.({ request, step: absoluteStep, toolCalls });
910
+ let currentToolResults;
911
+ try {
912
+ currentToolResults = await executeTools(preflight, options, {
913
+ request,
914
+ step: absoluteStep
915
+ });
916
+ }
917
+ catch (error) {
918
+ if (!(error instanceof ToolExecutionSuspendedError)) {
919
+ throw error;
920
+ }
921
+ toolResults.push(...error.completedResults);
922
+ for (const result of error.completedResults) {
923
+ await publish({ type: "tool-result", toolResult: result });
924
+ allMessages.push({
925
+ role: "tool",
926
+ parts: [toolResultPart(result)]
927
+ });
928
+ }
929
+ if (error.completedResults.length) {
930
+ await options.onToolExecutionComplete?.({
931
+ request,
932
+ step: absoluteStep,
933
+ toolResults: error.completedResults
934
+ });
935
+ }
936
+ approvalRequests.push(...error.approvals);
937
+ for (const approval of error.approvals) {
938
+ await publish({ type: "tool-approval-request", approval });
939
+ }
940
+ break;
941
+ }
531
942
  toolResults.push(...currentToolResults);
532
943
  for (const toolResult of currentToolResults) {
533
944
  await publish({ type: "tool-result", toolResult });
@@ -556,7 +967,8 @@ export const streamText = (options) => {
556
967
  usage,
557
968
  steps,
558
969
  messages: allMessages,
559
- toolResults
970
+ toolResults,
971
+ approvalRequests
560
972
  };
561
973
  };
562
974
  finalResultPromise = runner().catch(async (error) => {