@framers/agentos 0.9.26 → 0.9.28

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 (33) hide show
  1. package/dist/api/generateText.d.ts +18 -0
  2. package/dist/api/generateText.d.ts.map +1 -1
  3. package/dist/api/generateText.js +280 -207
  4. package/dist/api/generateText.js.map +1 -1
  5. package/dist/api/runtime/tool-emulation/activation.d.ts +7 -0
  6. package/dist/api/runtime/tool-emulation/activation.d.ts.map +1 -0
  7. package/dist/api/runtime/tool-emulation/activation.js +17 -0
  8. package/dist/api/runtime/tool-emulation/activation.js.map +1 -0
  9. package/dist/api/runtime/tool-emulation/index.d.ts +5 -0
  10. package/dist/api/runtime/tool-emulation/index.d.ts.map +1 -0
  11. package/dist/api/runtime/tool-emulation/index.js +5 -0
  12. package/dist/api/runtime/tool-emulation/index.js.map +1 -0
  13. package/dist/api/runtime/tool-emulation/loop.d.ts +38 -0
  14. package/dist/api/runtime/tool-emulation/loop.d.ts.map +1 -0
  15. package/dist/api/runtime/tool-emulation/loop.js +53 -0
  16. package/dist/api/runtime/tool-emulation/loop.js.map +1 -0
  17. package/dist/api/runtime/tool-emulation/parser.d.ts +23 -0
  18. package/dist/api/runtime/tool-emulation/parser.d.ts.map +1 -0
  19. package/dist/api/runtime/tool-emulation/parser.js +38 -0
  20. package/dist/api/runtime/tool-emulation/parser.js.map +1 -0
  21. package/dist/api/runtime/tool-emulation/renderer.d.ts +8 -0
  22. package/dist/api/runtime/tool-emulation/renderer.d.ts.map +1 -0
  23. package/dist/api/runtime/tool-emulation/renderer.js +26 -0
  24. package/dist/api/runtime/tool-emulation/renderer.js.map +1 -0
  25. package/dist/api/streamText.d.ts.map +1 -1
  26. package/dist/api/streamText.js +324 -256
  27. package/dist/api/streamText.js.map +1 -1
  28. package/dist/cognition/rag/vector_stores/PostgresVectorStore.js +3 -3
  29. package/dist/cognition/rag/vector_stores/PostgresVectorStore.js.map +1 -1
  30. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts.map +1 -1
  31. package/dist/core/llm/providers/implementations/AnthropicProvider.js +18 -4
  32. package/dist/core/llm/providers/implementations/AnthropicProvider.js.map +1 -1
  33. package/package.json +1 -1
@@ -13,6 +13,7 @@ import { attachUsageAttributes, toTurnMetricUsage } from './observability.js';
13
13
  import { fireLlmUsageObserver } from './observers.js';
14
14
  import { hostPolicyToRouteParams, mergeRequiredCapabilities } from './runtime/hostPolicy.js';
15
15
  import { adaptTools } from './runtime/toolAdapter.js';
16
+ import { runEmulatedToolLoop } from './runtime/tool-emulation/index.js';
16
17
  import { buildFallbackChain, createPlan, isRetryableError, resolveChainOfThought, } from './generateText.js';
17
18
  import { resolveDynamicToolCalls } from './runtime/dynamicToolCalling.js';
18
19
  import { StreamingReconstructor } from '../core/llm/streaming/StreamingReconstructor.js';
@@ -229,287 +230,354 @@ export function streamText(opts) {
229
230
  rootSpan?.setAttribute('agentos.api.plan_steps', resolvedPlan.steps.length);
230
231
  }
231
232
  }
232
- for (let step = 0; step < maxSteps; step++) {
233
- // --- onBeforeGeneration hook ---
234
- let effectiveMessages = messages;
235
- if (opts.onBeforeGeneration) {
236
- try {
237
- const hookCtx = {
238
- messages: [...messages],
239
- system: opts.system,
240
- tools: Array.from(toolMap.values()),
241
- model: resolved.modelId,
242
- provider: resolved.providerId,
243
- step,
244
- prompt: opts.prompt,
233
+ // --- Prompt-based tool-calling shim (toolMode) ---
234
+ // For models without native tool-use, buffer the tool roundtrips and emit
235
+ // the final answer as a single text part through the stream contract (spec
236
+ // decision #1 — replay from buffer, no extra model call). 'prompt' forces
237
+ // it up front; 'auto' tries native streaming first and falls back here on
238
+ // the provider's tool-unsupported error (see the catch around the loop).
239
+ const toolMode = opts.toolMode ?? 'auto';
240
+ const toolUnsupportedErr = (e) => e instanceof Error &&
241
+ /support tool use|does not support (tools|function)|no endpoints found that support/i.test(e.message);
242
+ async function* runShimStream() {
243
+ const loopResult = await runEmulatedToolLoop({
244
+ tools: Array.from(toolMap.values()),
245
+ messages: messages.map((m) => ({
246
+ role: String(m.role),
247
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? ''),
248
+ })),
249
+ maxRoundtrips: opts.maxSteps ?? 5,
250
+ callModel: async (msgs) => {
251
+ // provider is guaranteed non-undefined by the `if (!provider) throw`
252
+ // guard above; the closure just loses TS's flow-narrowing.
253
+ const r = await provider.generateCompletion(resolved.modelId, msgs, {
254
+ temperature: opts.temperature,
255
+ maxTokens: opts.maxTokens,
256
+ });
257
+ const cc = r.choices?.[0]?.message?.content;
258
+ return {
259
+ text: typeof cc === 'string' ? cc : (cc?.text ?? ''),
260
+ usage: { totalTokens: r.usage?.totalTokens ?? 0 },
245
261
  };
246
- const modified = await opts.onBeforeGeneration(hookCtx);
247
- if (modified) {
248
- effectiveMessages = modified.messages;
249
- }
250
- }
251
- catch (hookErr) {
252
- console.warn('[agentos] onBeforeGeneration hook error:', hookErr);
253
- }
254
- }
255
- const stepSpan = startAgentOSSpan('agentos.api.stream_text.step', {
256
- attributes: {
257
- 'llm.provider': resolved.providerId,
258
- 'llm.model': resolved.modelId,
259
- 'agentos.api.step': step + 1,
260
- 'agentos.api.tool_count': tools.length,
261
262
  },
262
263
  });
263
- const stream = provider.generateCompletionStream(resolved.modelId, effectiveMessages, {
264
- tools: toolSchemas,
265
- temperature: opts.temperature,
266
- maxTokens: opts.maxTokens,
267
- });
268
- const reconstructor = new StreamingReconstructor();
269
- try {
270
- for await (const chunk of stream) {
271
- reconstructor.push(chunk);
272
- const textDelta = chunk.responseTextDelta ?? '';
273
- if (textDelta) {
274
- const part = { type: 'text', text: textDelta };
275
- parts.push(part);
276
- yield part;
277
- }
278
- if (chunk.error) {
279
- const error = new Error(chunk.error.message);
280
- const part = { type: 'error', error };
281
- parts.push(part);
282
- yield part;
283
- metricStatus = 'error';
284
- resolveText(finalText);
285
- resolveUsage(usage);
286
- resolveToolCalls(allToolCalls);
287
- return;
288
- }
289
- if (chunk.isFinal && chunk.usage) {
290
- usage.promptTokens += chunk.usage.promptTokens ?? 0;
291
- usage.completionTokens += chunk.usage.completionTokens ?? 0;
292
- usage.totalTokens += chunk.usage.totalTokens ?? 0;
293
- if (typeof chunk.usage.costUSD === 'number') {
294
- usage.costUSD = (usage.costUSD ?? 0) + chunk.usage.costUSD;
295
- }
296
- // Prompt-cache metrics from the provider layer. Provider
297
- // surfaces these as cacheReadInputTokens /
298
- // cacheCreationInputTokens on the final chunk's usage
299
- // (mirrors Anthropic's cache_read_input_tokens /
300
- // cache_creation_input_tokens); forward them onto the
301
- // aggregated TokenUsage so downstream cost trackers see
302
- // cache hits.
303
- const cacheRead = chunk.usage.cacheReadInputTokens;
304
- const cacheCreate = chunk.usage.cacheCreationInputTokens;
305
- if (typeof cacheRead === 'number') {
306
- usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cacheRead;
307
- }
308
- if (typeof cacheCreate === 'number') {
309
- usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + cacheCreate;
264
+ usage.totalTokens = (usage.totalTokens ?? 0) + loopResult.totalTokens;
265
+ finalText = loopResult.text;
266
+ const shimToolCalls = loopResult.toolCalls.map((c) => ({
267
+ name: c.name,
268
+ args: c.args,
269
+ ...(c.error ? { error: c.error } : {}),
270
+ }));
271
+ if (loopResult.text) {
272
+ const part = { type: 'text', text: loopResult.text };
273
+ parts.push(part);
274
+ yield part;
275
+ }
276
+ resolveText(finalText);
277
+ resolveUsage(usage);
278
+ resolveToolCalls(shimToolCalls);
279
+ }
280
+ if (tools.length > 0 && toolMode === 'prompt') {
281
+ yield* runShimStream();
282
+ return;
283
+ }
284
+ let streamedAnyText = false;
285
+ try {
286
+ for (let step = 0; step < maxSteps; step++) {
287
+ // --- onBeforeGeneration hook ---
288
+ let effectiveMessages = messages;
289
+ if (opts.onBeforeGeneration) {
290
+ try {
291
+ const hookCtx = {
292
+ messages: [...messages],
293
+ system: opts.system,
294
+ tools: Array.from(toolMap.values()),
295
+ model: resolved.modelId,
296
+ provider: resolved.providerId,
297
+ step,
298
+ prompt: opts.prompt,
299
+ };
300
+ const modified = await opts.onBeforeGeneration(hookCtx);
301
+ if (modified) {
302
+ effectiveMessages = modified.messages;
310
303
  }
311
- attachUsageAttributes(stepSpan, {
312
- promptTokens: chunk.usage.promptTokens,
313
- completionTokens: chunk.usage.completionTokens,
314
- totalTokens: chunk.usage.totalTokens,
315
- costUSD: chunk.usage.costUSD,
316
- });
304
+ }
305
+ catch (hookErr) {
306
+ console.warn('[agentos] onBeforeGeneration hook error:', hookErr);
317
307
  }
318
308
  }
319
- }
320
- finally {
321
- stepSpan?.end();
322
- }
323
- const stepText = reconstructor.getFullText();
324
- const finalChunk = reconstructor.getFinalChunk();
325
- let streamedToolCalls = resolveDynamicToolCalls(finalChunk?.choices?.[0]?.message?.tool_calls ??
326
- reconstructor
327
- .getToolCalls()
328
- .filter((toolCall) => toolCall.id && toolCall.name)
329
- .map((toolCall) => ({
330
- id: toolCall.id,
331
- type: 'function',
332
- function: {
333
- name: toolCall.name,
334
- arguments: toolCall.rawArguments || JSON.stringify(toolCall.arguments ?? {}),
309
+ const stepSpan = startAgentOSSpan('agentos.api.stream_text.step', {
310
+ attributes: {
311
+ 'llm.provider': resolved.providerId,
312
+ 'llm.model': resolved.modelId,
313
+ 'agentos.api.step': step + 1,
314
+ 'agentos.api.tool_count': tools.length,
335
315
  },
336
- })), {
337
- text: stepText,
338
- step,
339
- toolsAvailable: tools.length > 0,
340
- });
341
- // --- onAfterGeneration hook ---
342
- let effectiveStepText = stepText;
343
- if (opts.onAfterGeneration) {
316
+ });
317
+ const stream = provider.generateCompletionStream(resolved.modelId, effectiveMessages, {
318
+ tools: toolSchemas,
319
+ temperature: opts.temperature,
320
+ maxTokens: opts.maxTokens,
321
+ });
322
+ const reconstructor = new StreamingReconstructor();
344
323
  try {
345
- const stepUsage = {
346
- promptTokens: usage.promptTokens,
347
- completionTokens: usage.completionTokens,
348
- totalTokens: usage.totalTokens,
349
- costUSD: usage.costUSD,
350
- cacheReadTokens: usage.cacheReadTokens,
351
- cacheCreationTokens: usage.cacheCreationTokens,
352
- };
353
- const toolCallRecords = (streamedToolCalls ?? []).map((tc) => ({
354
- name: tc.function?.name ?? '',
355
- args: tc.function?.arguments ?? '{}',
356
- }));
357
- const hookResult = {
358
- text: stepText,
359
- toolCalls: toolCallRecords,
360
- usage: stepUsage,
361
- step,
362
- };
363
- const modified = await opts.onAfterGeneration(hookResult);
364
- if (modified) {
365
- effectiveStepText = modified.text;
366
- if (modified.toolCalls.length === 0 && streamedToolCalls && streamedToolCalls.length > 0) {
367
- streamedToolCalls = [];
324
+ for await (const chunk of stream) {
325
+ reconstructor.push(chunk);
326
+ const textDelta = chunk.responseTextDelta ?? '';
327
+ if (textDelta) {
328
+ const part = { type: 'text', text: textDelta };
329
+ parts.push(part);
330
+ yield part;
331
+ streamedAnyText = true;
332
+ }
333
+ if (chunk.error) {
334
+ const error = new Error(chunk.error.message);
335
+ const part = { type: 'error', error };
336
+ parts.push(part);
337
+ yield part;
338
+ metricStatus = 'error';
339
+ resolveText(finalText);
340
+ resolveUsage(usage);
341
+ resolveToolCalls(allToolCalls);
342
+ return;
343
+ }
344
+ if (chunk.isFinal && chunk.usage) {
345
+ usage.promptTokens += chunk.usage.promptTokens ?? 0;
346
+ usage.completionTokens += chunk.usage.completionTokens ?? 0;
347
+ usage.totalTokens += chunk.usage.totalTokens ?? 0;
348
+ if (typeof chunk.usage.costUSD === 'number') {
349
+ usage.costUSD = (usage.costUSD ?? 0) + chunk.usage.costUSD;
350
+ }
351
+ // Prompt-cache metrics from the provider layer. Provider
352
+ // surfaces these as cacheReadInputTokens /
353
+ // cacheCreationInputTokens on the final chunk's usage
354
+ // (mirrors Anthropic's cache_read_input_tokens /
355
+ // cache_creation_input_tokens); forward them onto the
356
+ // aggregated TokenUsage so downstream cost trackers see
357
+ // cache hits.
358
+ const cacheRead = chunk.usage.cacheReadInputTokens;
359
+ const cacheCreate = chunk.usage.cacheCreationInputTokens;
360
+ if (typeof cacheRead === 'number') {
361
+ usage.cacheReadTokens = (usage.cacheReadTokens ?? 0) + cacheRead;
362
+ }
363
+ if (typeof cacheCreate === 'number') {
364
+ usage.cacheCreationTokens = (usage.cacheCreationTokens ?? 0) + cacheCreate;
365
+ }
366
+ attachUsageAttributes(stepSpan, {
367
+ promptTokens: chunk.usage.promptTokens,
368
+ completionTokens: chunk.usage.completionTokens,
369
+ totalTokens: chunk.usage.totalTokens,
370
+ costUSD: chunk.usage.costUSD,
371
+ });
368
372
  }
369
373
  }
370
374
  }
371
- catch (hookErr) {
372
- console.warn('[agentos] onAfterGeneration hook error:', hookErr);
375
+ finally {
376
+ stepSpan?.end();
373
377
  }
374
- }
375
- // Always track the latest step's text so finalText is available even
376
- // when maxSteps is exhausted with outstanding tool calls.
377
- if (effectiveStepText) {
378
- finalText = effectiveStepText;
379
- }
380
- if (!streamedToolCalls || streamedToolCalls.length === 0) {
381
- rootSpan?.setAttribute('agentos.api.finish_reason', 'stop');
382
- rootSpan?.setAttribute('agentos.api.tool_calls', allToolCalls.length);
383
- attachUsageAttributes(rootSpan, usage);
384
- resolveText(finalText);
385
- resolveUsage(usage);
386
- resolveToolCalls(allToolCalls);
387
- return;
388
- }
389
- messages.push({
390
- role: 'assistant',
391
- content: effectiveStepText || null,
392
- tool_calls: streamedToolCalls,
393
- });
394
- for (const toolCall of streamedToolCalls) {
395
- const fnName = toolCall.function?.name ?? '';
396
- const rawArgs = toolCall.function?.arguments ?? '{}';
397
- const toolCallId = toolCall.id ?? '';
398
- const toolCallRecord = {
399
- name: fnName,
400
- args: rawArgs,
401
- };
402
- let parsedArgs;
403
- try {
404
- parsedArgs = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : rawArgs;
405
- toolCallRecord.args = parsedArgs;
406
- }
407
- catch {
408
- toolCallRecord.error = `Tool "${fnName}" arguments were not valid JSON.`;
409
- const resultPart = {
410
- type: 'tool-result',
411
- toolName: fnName,
412
- result: { error: toolCallRecord.error },
413
- };
414
- parts.push(resultPart);
415
- yield resultPart;
416
- messages.push({
417
- role: 'tool',
418
- tool_call_id: toolCallId,
419
- content: JSON.stringify({ error: toolCallRecord.error }),
420
- });
421
- allToolCalls.push(toolCallRecord);
422
- continue;
423
- }
424
- const requestPart = { type: 'tool-call', toolName: fnName, args: parsedArgs };
425
- parts.push(requestPart);
426
- yield requestPart;
427
- const tool = toolMap.get(fnName);
428
- if (!tool) {
429
- toolCallRecord.error = `Tool "${fnName}" not found.`;
430
- const resultPart = {
431
- type: 'tool-result',
432
- toolName: fnName,
433
- result: { error: toolCallRecord.error },
434
- };
435
- parts.push(resultPart);
436
- yield resultPart;
437
- messages.push({
438
- role: 'tool',
439
- tool_call_id: toolCallId,
440
- content: JSON.stringify({ error: toolCallRecord.error }),
441
- });
442
- allToolCalls.push(toolCallRecord);
443
- continue;
444
- }
445
- // --- onBeforeToolExecution hook ---
446
- if (opts.onBeforeToolExecution) {
378
+ const stepText = reconstructor.getFullText();
379
+ const finalChunk = reconstructor.getFinalChunk();
380
+ let streamedToolCalls = resolveDynamicToolCalls(finalChunk?.choices?.[0]?.message?.tool_calls ??
381
+ reconstructor
382
+ .getToolCalls()
383
+ .filter((toolCall) => toolCall.id && toolCall.name)
384
+ .map((toolCall) => ({
385
+ id: toolCall.id,
386
+ type: 'function',
387
+ function: {
388
+ name: toolCall.name,
389
+ arguments: toolCall.rawArguments || JSON.stringify(toolCall.arguments ?? {}),
390
+ },
391
+ })), {
392
+ text: stepText,
393
+ step,
394
+ toolsAvailable: tools.length > 0,
395
+ });
396
+ // --- onAfterGeneration hook ---
397
+ let effectiveStepText = stepText;
398
+ if (opts.onAfterGeneration) {
447
399
  try {
448
- const hookInfo = {
449
- name: fnName,
450
- args: parsedArgs,
451
- id: toolCallId || '',
400
+ const stepUsage = {
401
+ promptTokens: usage.promptTokens,
402
+ completionTokens: usage.completionTokens,
403
+ totalTokens: usage.totalTokens,
404
+ costUSD: usage.costUSD,
405
+ cacheReadTokens: usage.cacheReadTokens,
406
+ cacheCreationTokens: usage.cacheCreationTokens,
407
+ };
408
+ const toolCallRecords = (streamedToolCalls ?? []).map((tc) => ({
409
+ name: tc.function?.name ?? '',
410
+ args: tc.function?.arguments ?? '{}',
411
+ }));
412
+ const hookResult = {
413
+ text: stepText,
414
+ toolCalls: toolCallRecords,
415
+ usage: stepUsage,
452
416
  step,
453
417
  };
454
- const hookResult = await opts.onBeforeToolExecution(hookInfo);
455
- if (hookResult === null) {
456
- toolCallRecord.error = 'Skipped by onBeforeToolExecution hook';
457
- const resultPart = {
458
- type: 'tool-result',
459
- toolName: fnName,
460
- result: { skipped: true },
461
- };
462
- parts.push(resultPart);
463
- yield resultPart;
464
- messages.push({
465
- role: 'tool',
466
- tool_call_id: toolCallId,
467
- content: JSON.stringify({ skipped: true }),
468
- });
469
- allToolCalls.push(toolCallRecord);
470
- continue;
418
+ const modified = await opts.onAfterGeneration(hookResult);
419
+ if (modified) {
420
+ effectiveStepText = modified.text;
421
+ if (modified.toolCalls.length === 0 && streamedToolCalls && streamedToolCalls.length > 0) {
422
+ streamedToolCalls = [];
423
+ }
471
424
  }
472
- parsedArgs = hookResult.args;
473
425
  }
474
426
  catch (hookErr) {
475
- console.warn('[agentos] onBeforeToolExecution hook error:', hookErr);
427
+ console.warn('[agentos] onAfterGeneration hook error:', hookErr);
476
428
  }
477
429
  }
478
- try {
479
- const result = await tool.execute(parsedArgs, buildHelperToolExecutionContext('streamText', helperToolRunId, step, toolCallId || undefined));
480
- toolCallRecord.result = result.output;
481
- toolCallRecord.error = result.success ? undefined : result.error;
482
- const resultPart = {
483
- type: 'tool-result',
484
- toolName: fnName,
485
- result: result.output ?? { error: result.error },
486
- };
487
- parts.push(resultPart);
488
- yield resultPart;
489
- messages.push({
490
- role: 'tool',
491
- tool_call_id: toolCallId,
492
- content: JSON.stringify(result.output ?? { error: result.error ?? 'Tool execution failed.' }),
493
- });
430
+ // Always track the latest step's text so finalText is available even
431
+ // when maxSteps is exhausted with outstanding tool calls.
432
+ if (effectiveStepText) {
433
+ finalText = effectiveStepText;
434
+ }
435
+ if (!streamedToolCalls || streamedToolCalls.length === 0) {
436
+ rootSpan?.setAttribute('agentos.api.finish_reason', 'stop');
437
+ rootSpan?.setAttribute('agentos.api.tool_calls', allToolCalls.length);
438
+ attachUsageAttributes(rootSpan, usage);
439
+ resolveText(finalText);
440
+ resolveUsage(usage);
441
+ resolveToolCalls(allToolCalls);
442
+ return;
494
443
  }
495
- catch (err) {
496
- toolCallRecord.error = err?.message ?? String(err);
497
- const resultPart = {
498
- type: 'tool-result',
499
- toolName: fnName,
500
- result: { error: toolCallRecord.error },
444
+ messages.push({
445
+ role: 'assistant',
446
+ content: effectiveStepText || null,
447
+ tool_calls: streamedToolCalls,
448
+ });
449
+ for (const toolCall of streamedToolCalls) {
450
+ const fnName = toolCall.function?.name ?? '';
451
+ const rawArgs = toolCall.function?.arguments ?? '{}';
452
+ const toolCallId = toolCall.id ?? '';
453
+ const toolCallRecord = {
454
+ name: fnName,
455
+ args: rawArgs,
501
456
  };
502
- parts.push(resultPart);
503
- yield resultPart;
504
- messages.push({
505
- role: 'tool',
506
- tool_call_id: toolCallId,
507
- content: JSON.stringify({ error: toolCallRecord.error }),
508
- });
457
+ let parsedArgs;
458
+ try {
459
+ parsedArgs = typeof rawArgs === 'string' ? JSON.parse(rawArgs) : rawArgs;
460
+ toolCallRecord.args = parsedArgs;
461
+ }
462
+ catch {
463
+ toolCallRecord.error = `Tool "${fnName}" arguments were not valid JSON.`;
464
+ const resultPart = {
465
+ type: 'tool-result',
466
+ toolName: fnName,
467
+ result: { error: toolCallRecord.error },
468
+ };
469
+ parts.push(resultPart);
470
+ yield resultPart;
471
+ messages.push({
472
+ role: 'tool',
473
+ tool_call_id: toolCallId,
474
+ content: JSON.stringify({ error: toolCallRecord.error }),
475
+ });
476
+ allToolCalls.push(toolCallRecord);
477
+ continue;
478
+ }
479
+ const requestPart = { type: 'tool-call', toolName: fnName, args: parsedArgs };
480
+ parts.push(requestPart);
481
+ yield requestPart;
482
+ const tool = toolMap.get(fnName);
483
+ if (!tool) {
484
+ toolCallRecord.error = `Tool "${fnName}" not found.`;
485
+ const resultPart = {
486
+ type: 'tool-result',
487
+ toolName: fnName,
488
+ result: { error: toolCallRecord.error },
489
+ };
490
+ parts.push(resultPart);
491
+ yield resultPart;
492
+ messages.push({
493
+ role: 'tool',
494
+ tool_call_id: toolCallId,
495
+ content: JSON.stringify({ error: toolCallRecord.error }),
496
+ });
497
+ allToolCalls.push(toolCallRecord);
498
+ continue;
499
+ }
500
+ // --- onBeforeToolExecution hook ---
501
+ if (opts.onBeforeToolExecution) {
502
+ try {
503
+ const hookInfo = {
504
+ name: fnName,
505
+ args: parsedArgs,
506
+ id: toolCallId || '',
507
+ step,
508
+ };
509
+ const hookResult = await opts.onBeforeToolExecution(hookInfo);
510
+ if (hookResult === null) {
511
+ toolCallRecord.error = 'Skipped by onBeforeToolExecution hook';
512
+ const resultPart = {
513
+ type: 'tool-result',
514
+ toolName: fnName,
515
+ result: { skipped: true },
516
+ };
517
+ parts.push(resultPart);
518
+ yield resultPart;
519
+ messages.push({
520
+ role: 'tool',
521
+ tool_call_id: toolCallId,
522
+ content: JSON.stringify({ skipped: true }),
523
+ });
524
+ allToolCalls.push(toolCallRecord);
525
+ continue;
526
+ }
527
+ parsedArgs = hookResult.args;
528
+ }
529
+ catch (hookErr) {
530
+ console.warn('[agentos] onBeforeToolExecution hook error:', hookErr);
531
+ }
532
+ }
533
+ try {
534
+ const result = await tool.execute(parsedArgs, buildHelperToolExecutionContext('streamText', helperToolRunId, step, toolCallId || undefined));
535
+ toolCallRecord.result = result.output;
536
+ toolCallRecord.error = result.success ? undefined : result.error;
537
+ const resultPart = {
538
+ type: 'tool-result',
539
+ toolName: fnName,
540
+ result: result.output ?? { error: result.error },
541
+ };
542
+ parts.push(resultPart);
543
+ yield resultPart;
544
+ messages.push({
545
+ role: 'tool',
546
+ tool_call_id: toolCallId,
547
+ content: JSON.stringify(result.output ?? { error: result.error ?? 'Tool execution failed.' }),
548
+ });
549
+ }
550
+ catch (err) {
551
+ toolCallRecord.error = err?.message ?? String(err);
552
+ const resultPart = {
553
+ type: 'tool-result',
554
+ toolName: fnName,
555
+ result: { error: toolCallRecord.error },
556
+ };
557
+ parts.push(resultPart);
558
+ yield resultPart;
559
+ messages.push({
560
+ role: 'tool',
561
+ tool_call_id: toolCallId,
562
+ content: JSON.stringify({ error: toolCallRecord.error }),
563
+ });
564
+ }
565
+ allToolCalls.push(toolCallRecord);
509
566
  }
510
- allToolCalls.push(toolCallRecord);
511
567
  }
512
568
  }
569
+ catch (loopErr) {
570
+ // 'auto' reactive fallback: if the provider rejected native tool-use
571
+ // before any text streamed, re-run the turn through the prompt shim.
572
+ if (!streamedAnyText &&
573
+ tools.length > 0 &&
574
+ toolMode === 'auto' &&
575
+ toolUnsupportedErr(loopErr)) {
576
+ yield* runShimStream();
577
+ return;
578
+ }
579
+ throw loopErr;
580
+ }
513
581
  resolveText(finalText);
514
582
  resolveUsage(usage);
515
583
  resolveToolCalls(allToolCalls);