@juspay/neurolink 11.16.3 → 11.17.1
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +428 -426
- package/dist/core/loopEngine.js +2 -0
- package/dist/providers/anthropic/client.js +5 -2
- package/dist/providers/anthropic/loopAdapter.d.ts +1 -1
- package/dist/providers/anthropic/loopAdapter.js +48 -6
- package/dist/providers/googleNativeGemini3/utils.js +5 -1
- package/dist/providers/googleVertex/client.js +543 -848
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +6 -1
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.js +9 -1
- package/dist/types/loopEngine.d.ts +63 -4
- package/dist/types/server.d.ts +9 -1
- package/package.json +3 -1
|
@@ -27,11 +27,14 @@ import { resolveClaudeMaxTokens } from "../../utils/tokenLimits.js";
|
|
|
27
27
|
import { validateApiKey, createVertexProjectConfig, createGoogleAuthConfig, } from "../../utils/providerConfig.js";
|
|
28
28
|
import { convertZodToJsonSchema, inlineJsonSchema, ensureNestedSchemaTypes, } from "../../utils/schemaConversion.js";
|
|
29
29
|
import { createNativeThinkingConfig } from "../../utils/thinkingConfig.js";
|
|
30
|
-
import { TimeoutError,
|
|
30
|
+
import { TimeoutError, withTimeout } from "../../utils/async/index.js";
|
|
31
31
|
import { parseTimeout } from "../../utils/timeout.js";
|
|
32
32
|
import { appendStepText, buildAbortedTurnMessage, buildContextCapMessage, buildDedupedEngineTools, buildToolLoopCapMessage, buildTurnStalledMessage, buildTurnTimeoutMessage, buildWrapupNudgeText, createContextGuard, createTurnClock, extractThoughtSignature, isAbortError, mapGeminiFinishReason, prependConversationMessages, resolveTurnStopReason, DedupExecuteMap, } from "../googleNativeGemini3/index.js";
|
|
33
33
|
import { createGeminiLoopAdapter } from "../../core/geminiLoopAdapter.js";
|
|
34
34
|
import { runAgenticLoop } from "../../core/loopEngine.js";
|
|
35
|
+
import { createAnthropicLoopAdapter } from "../anthropic/loopAdapter.js";
|
|
36
|
+
import { guardToolExecutor } from "../googleNativeGemini3/utils.js";
|
|
37
|
+
import { extractMcpToolErrorMessage } from "../../utils/mcpErrorText.js";
|
|
35
38
|
import { createStreamChannel } from "../../core/streamChannel.js";
|
|
36
39
|
import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
|
|
37
40
|
import { getAvailableInputTokens, getContextWindowSize, } from "../../constants/contextWindows.js";
|
|
@@ -43,7 +46,7 @@ import { transformToolExecutions } from "../../utils/transformationUtils.js";
|
|
|
43
46
|
import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
|
|
44
47
|
import { resolveSamplingParams } from "../../models/modelRegistry.js";
|
|
45
48
|
import { sanitizeAnthropicMessagesForTrace } from "../../utils/anthropicTraceSanitizer.js";
|
|
46
|
-
import {
|
|
49
|
+
import { extractToolFailureText } from "../../utils/mcpErrorText.js";
|
|
47
50
|
// Import proper types for multimodal message handling
|
|
48
51
|
// Dynamic import helper for native Anthropic Vertex SDK
|
|
49
52
|
let anthropicVertexModule = null;
|
|
@@ -3334,514 +3337,379 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
3334
3337
|
const contextGuard = createContextGuard(getContextWindowSize("vertex", modelName));
|
|
3335
3338
|
const failedTools = new Map();
|
|
3336
3339
|
try {
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
// Context guard: stop the tool loop before the accumulated
|
|
3348
|
-
// conversation crosses the window threshold (see generate twin).
|
|
3349
|
-
if (contextGuard.shouldStop()) {
|
|
3350
|
-
// Parity upgrade: reclaim and continue where possible; the
|
|
3351
|
-
// historic stop-only behaviour remains the fallback.
|
|
3352
|
-
const reclaimed = reclaimVertexAnthropicContext(currentMessages, modelName, contextGuard.projectedNextPromptTokens);
|
|
3353
|
-
if (reclaimed) {
|
|
3354
|
-
contextGuard.resetAfterReclaim();
|
|
3355
|
-
}
|
|
3356
|
-
else {
|
|
3357
|
-
hitContextLimit = true;
|
|
3358
|
-
logger.warn(`[GoogleVertex] Anthropic stream turn stopped by the context guard: ` +
|
|
3359
|
-
`projected prompt ~${contextGuard.projectedNextPromptTokens} tokens ` +
|
|
3360
|
-
`>= threshold ${contextGuard.thresholdTokens} (step ${step}) — synthesizing a final answer.`);
|
|
3361
|
-
break;
|
|
3362
|
-
}
|
|
3340
|
+
// Executors handed to the engine, taken through the turn's
|
|
3341
|
+
// DedupExecuteMap so an identical repeated call is answered from the
|
|
3342
|
+
// per-turn cache rather than run again (BZ-3327). `.get()` returns the
|
|
3343
|
+
// wrapper; iterating the map yields the raw functions, which is why the
|
|
3344
|
+
// record is built by name rather than from entries.
|
|
3345
|
+
const engineTools = {};
|
|
3346
|
+
for (const toolName of executeMap.keys()) {
|
|
3347
|
+
const wrapped = executeMap.get(toolName);
|
|
3348
|
+
if (!wrapped) {
|
|
3349
|
+
continue;
|
|
3363
3350
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3351
|
+
engineTools[toolName] = {
|
|
3352
|
+
// Guarded exactly as the hand-rolled loop guarded it: a per-tool
|
|
3353
|
+
// bound so a wedged tool costs ONE STEP rather than the whole turn,
|
|
3354
|
+
// raced against the turn's abort so a deadline is observed at once
|
|
3355
|
+
// instead of after the tool settles, and a stall-clock ping either
|
|
3356
|
+
// side so a slow-but-healthy tool is not read as a stalled turn.
|
|
3357
|
+
execute: guardToolExecutor(toolName, wrapped, {
|
|
3358
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
3359
|
+
abortSignal: internalAbort.signal,
|
|
3360
|
+
onProgress: () => turnClock.noteProgress(),
|
|
3361
|
+
// Restores the per-tool observation the hand-rolled dispatch had.
|
|
3362
|
+
// The execution runs INSIDE the span's context so spans the tool opens
|
|
3363
|
+
// itself nest under this call rather than dangling beside the turn, and
|
|
3364
|
+
// the settled RESULT is inspected — an MCP tool reports failure in its
|
|
3365
|
+
// payload, so an observation that only watches for throws records a
|
|
3366
|
+
// failed call as successful.
|
|
3367
|
+
//
|
|
3368
|
+
// Stream path only: the generate twin never had per-tool spans, and
|
|
3369
|
+
// giving it them here would be behaviour GAINED under cover of a
|
|
3370
|
+
// migration.
|
|
3371
|
+
withToolSpan: (toolCallName, run) => {
|
|
3372
|
+
const toolSpan = tracers.mcp.startSpan("ai.toolCall", {
|
|
3373
|
+
kind: SpanKind.INTERNAL,
|
|
3374
|
+
attributes: {
|
|
3375
|
+
[LANGFUSE_ATTR.OBSERVATION_TYPE]: "tool",
|
|
3376
|
+
[ATTR.GEN_AI_TOOL_NAME]: toolCallName,
|
|
3377
|
+
"ai.toolCall.name": toolCallName,
|
|
3378
|
+
},
|
|
3379
|
+
}, turnContext);
|
|
3380
|
+
const finish = (output, errorMessage) => {
|
|
3381
|
+
toolSpan.setAttribute("ai.toolCall.result", spanJsonAttribute(output));
|
|
3382
|
+
toolSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanJsonAttribute(output));
|
|
3383
|
+
if (errorMessage) {
|
|
3384
|
+
toolSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_LEVEL, "ERROR");
|
|
3385
|
+
toolSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_STATUS_MESSAGE, errorMessage);
|
|
3386
|
+
toolSpan.setStatus({
|
|
3387
|
+
code: SpanStatusCode.ERROR,
|
|
3388
|
+
message: errorMessage,
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
else {
|
|
3392
|
+
toolSpan.setStatus({ code: SpanStatusCode.OK });
|
|
3393
|
+
}
|
|
3394
|
+
toolSpan.end();
|
|
3395
|
+
};
|
|
3396
|
+
return otelContext.with(otelTrace.setSpan(turnContext, toolSpan), async () => {
|
|
3397
|
+
try {
|
|
3398
|
+
const result = await run();
|
|
3399
|
+
finish(result, extractMcpToolErrorMessage(result));
|
|
3400
|
+
return result;
|
|
3401
|
+
}
|
|
3402
|
+
catch (error) {
|
|
3403
|
+
finish({ error: true }, error instanceof Error ? error.message : String(error));
|
|
3404
|
+
throw error;
|
|
3405
|
+
}
|
|
3406
|
+
});
|
|
3407
|
+
},
|
|
3408
|
+
}),
|
|
3409
|
+
};
|
|
3410
|
+
}
|
|
3411
|
+
// The turn runs on the shared engine. The step cap, tool dispatch, the
|
|
3412
|
+
// failure breaker, per-step usage accumulation and the pre-first-chunk
|
|
3413
|
+
// provider retry all live there now. What stays here is everything the
|
|
3414
|
+
// engine has no opinion about: the turn clock, the context guard, the
|
|
3415
|
+
// per-step Langfuse generation span, conversation-memory storage, the
|
|
3416
|
+
// wrap-up nudge, and the reserved finalization in the terminal block.
|
|
3417
|
+
//
|
|
3418
|
+
// maxSteps is agenticStepBudget, NOT maxSteps: when structured output is
|
|
3419
|
+
// active the last slot is reserved for the forced final_result call, and
|
|
3420
|
+
// the engine must never spend it.
|
|
3421
|
+
// The type argument is explicit: without it TMessage infers as the SDK's
|
|
3422
|
+
// MessageParam and every hook here is typed against the wrong shape.
|
|
3423
|
+
const baseAdapter = createAnthropicLoopAdapter({
|
|
3424
|
+
client,
|
|
3425
|
+
maxSteps: agenticStepBudget,
|
|
3426
|
+
toolsRecord: options.tools ?? {},
|
|
3427
|
+
// Set here and NOT for native Anthropic: these loops have always had
|
|
3428
|
+
// the consecutive-failure strike breaker, and native Anthropic has
|
|
3429
|
+
// never had one. Giving it one under cover of a shared refactor would
|
|
3430
|
+
// be a behaviour change, not a migration.
|
|
3431
|
+
toolFailureBreaker: {
|
|
3432
|
+
maxRetries: DEFAULT_TOOL_MAX_RETRIES,
|
|
3433
|
+
// MCP failures are RETURNED, not thrown. Counting only throws lets
|
|
3434
|
+
// the model grind on a blocked tool for the whole step budget.
|
|
3435
|
+
classifyResultFailure: (output) => extractToolFailureText(output) ?? undefined,
|
|
3436
|
+
},
|
|
3437
|
+
buildParams: (conversation) => {
|
|
3438
|
+
// Mid-turn discovery sync: Claude only calls tools declared in the
|
|
3439
|
+
// request, so tools hydrated by search_tools last step have to be
|
|
3440
|
+
// advertised now. `tools` is held by reference in requestParams.
|
|
3441
|
+
this.refreshAnthropicToolDeclarations(options.tools, tools, executeMap, failedTools);
|
|
3442
|
+
// Vertex has no automatic prompt caching — explicit cache_control
|
|
3443
|
+
// breakpoints (system, tools, rolling history) keep the conversation
|
|
3444
|
+
// prefix cached across turns instead of re-billed as fresh input.
|
|
3445
|
+
// Re-applied per step: the stable prefix stays byte-identical for a
|
|
3446
|
+
// consistent cache key while the rolling breakpoint follows the tail.
|
|
3447
|
+
const cached = applyVertexAnthropicCacheBreakpoints({
|
|
3404
3448
|
system: systemPromptWithSchema,
|
|
3405
3449
|
tools,
|
|
3406
|
-
messages:
|
|
3450
|
+
messages: conversation,
|
|
3407
3451
|
});
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3452
|
+
// `stream` is dropped from the spread: requestParams is typed for
|
|
3453
|
+
// messages.stream so it carries an optional stream flag, and
|
|
3454
|
+
// executeStep sets that itself. Passing it through would type this
|
|
3455
|
+
// return as the streaming variant for a field the adapter owns.
|
|
3456
|
+
const { stream: _ignoredStreamFlag, output_config: _ignoredOutputConfig, ...baseParams } = requestParams;
|
|
3457
|
+
void _ignoredStreamFlag;
|
|
3458
|
+
// output_config differs between the two param types as well — the
|
|
3459
|
+
// streaming variant allows null where the non-streaming one does
|
|
3460
|
+
// not. Nothing here sets it, so it is dropped rather than widened.
|
|
3461
|
+
void _ignoredOutputConfig;
|
|
3462
|
+
return {
|
|
3463
|
+
...baseParams,
|
|
3464
|
+
...(cached.system !== undefined && {
|
|
3465
|
+
system: cached.system,
|
|
3412
3466
|
}),
|
|
3413
|
-
...(
|
|
3414
|
-
|
|
3415
|
-
tools:
|
|
3467
|
+
...(cached.tools &&
|
|
3468
|
+
cached.tools.length > 0 && {
|
|
3469
|
+
tools: cached.tools,
|
|
3416
3470
|
}),
|
|
3417
|
-
messages:
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
// giving Langfuse the generation's time-to-first-token.
|
|
3424
|
-
let firstDeltaSeen = false;
|
|
3425
|
-
stream.on("text", (delta) => {
|
|
3426
|
-
turnClock.noteProgress();
|
|
3427
|
-
if (delta.length > 0) {
|
|
3428
|
-
if (!firstDeltaSeen) {
|
|
3429
|
-
firstDeltaSeen = true;
|
|
3430
|
-
generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_COMPLETION_START_TIME, new Date().toISOString());
|
|
3431
|
-
}
|
|
3432
|
-
channel.push({ content: delta });
|
|
3433
|
-
liveTextPushedLength += delta.length;
|
|
3434
|
-
}
|
|
3435
|
-
});
|
|
3436
|
-
// finalMessage() resolves AFTER message_stop. By then the listener
|
|
3437
|
-
// has already fired for every delta — awaiting here doesn't block
|
|
3438
|
-
// visible streaming, it just gives us the structured response
|
|
3439
|
-
// shape needed for tool_use block extraction.
|
|
3440
|
-
response = await stream.finalMessage();
|
|
3441
|
-
}
|
|
3442
|
-
catch (modelCallError) {
|
|
3443
|
-
generationSpan.setStatus({
|
|
3444
|
-
code: SpanStatusCode.ERROR,
|
|
3445
|
-
message: modelCallError instanceof Error
|
|
3446
|
-
? modelCallError.message
|
|
3447
|
-
: String(modelCallError),
|
|
3448
|
-
});
|
|
3449
|
-
if (modelCallError instanceof Error) {
|
|
3450
|
-
generationSpan.recordException(modelCallError);
|
|
3451
|
-
}
|
|
3452
|
-
generationSpan.end();
|
|
3453
|
-
// A mid-flight abort (caller signal or a turn-clock watchdog
|
|
3454
|
-
// tripping internalAbort/abortHandler) rejects finalMessage()
|
|
3455
|
-
// with an abort-shaped error. Break gracefully into the terminal
|
|
3456
|
-
// handling instead of routing it through channel.error as a
|
|
3457
|
-
// failure.
|
|
3458
|
-
if (internalAbort.signal.aborted || isAbortError(modelCallError)) {
|
|
3459
|
-
activeStream = undefined;
|
|
3460
|
-
wasAborted = true;
|
|
3461
|
-
break;
|
|
3462
|
-
}
|
|
3463
|
-
throw modelCallError;
|
|
3464
|
-
}
|
|
3465
|
-
activeStream = undefined;
|
|
3466
|
-
// End the generation span even if the bookkeeping below throws (else
|
|
3467
|
-
// it leaks). The model-call error path already ended it — no double-end.
|
|
3468
|
-
try {
|
|
3469
|
-
const stepCacheRead = response.usage?.cache_read_input_tokens ?? 0;
|
|
3470
|
-
const stepCacheCreation = response.usage?.cache_creation_input_tokens ?? 0;
|
|
3471
|
-
const stepCacheCreation5m = response.usage?.cache_creation?.ephemeral_5m_input_tokens ?? 0;
|
|
3472
|
-
const stepCacheCreation1h = response.usage?.cache_creation?.ephemeral_1h_input_tokens ?? 0;
|
|
3473
|
-
turnCacheUsage.read += stepCacheRead;
|
|
3474
|
-
turnCacheUsage.creation += stepCacheCreation;
|
|
3475
|
-
turnCacheUsage.creation5m += stepCacheCreation5m;
|
|
3476
|
-
turnCacheUsage.creation1h += stepCacheCreation1h;
|
|
3477
|
-
usage.input += response.usage?.input_tokens || 0;
|
|
3478
|
-
usage.output += response.usage?.output_tokens || 0;
|
|
3479
|
-
// Anthropic's input_tokens is only the UNCACHED remainder; cache
|
|
3480
|
-
// reads/writes are billed tokens reported separately, so the
|
|
3481
|
-
// total must include them (matches proxyTracer + anthropic.ts).
|
|
3482
|
-
usage.total =
|
|
3483
|
-
usage.input +
|
|
3484
|
-
usage.output +
|
|
3485
|
-
turnCacheUsage.read +
|
|
3486
|
-
turnCacheUsage.creation;
|
|
3487
|
-
lastStopReason = response.stop_reason;
|
|
3488
|
-
// Feed the context guard the FULL prompt size of this call
|
|
3489
|
-
// (uncached input + cache reads/writes).
|
|
3490
|
-
contextGuard.noteUsage((response.usage?.input_tokens || 0) +
|
|
3491
|
-
stepCacheRead +
|
|
3492
|
-
stepCacheCreation, response.usage?.output_tokens || 0);
|
|
3493
|
-
for (const block of response.content) {
|
|
3494
|
-
if (block.type === "text" && typeof block.text === "string") {
|
|
3495
|
-
aggregatedTurnText += block.text;
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanJsonAttribute(response.content));
|
|
3499
|
-
// 5m and 1h cache-creation are priced differently, so keep both;
|
|
3500
|
-
// drop the aggregate input_cache_creation (= 5m + 1h) that would
|
|
3501
|
-
// double-count. total sums the per-TTL keys shown here to match them.
|
|
3502
|
-
generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_USAGE_DETAILS, spanJsonAttribute({
|
|
3503
|
-
input: response.usage?.input_tokens ?? 0,
|
|
3504
|
-
output: response.usage?.output_tokens ?? 0,
|
|
3505
|
-
input_cached_tokens: stepCacheRead,
|
|
3506
|
-
input_cache_creation_5m: stepCacheCreation5m,
|
|
3507
|
-
input_cache_creation_1h: stepCacheCreation1h,
|
|
3508
|
-
total: (response.usage?.input_tokens ?? 0) +
|
|
3509
|
-
(response.usage?.output_tokens ?? 0) +
|
|
3510
|
-
stepCacheRead +
|
|
3511
|
-
stepCacheCreation5m +
|
|
3512
|
-
stepCacheCreation1h,
|
|
3513
|
-
}));
|
|
3514
|
-
generationSpan.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, response.usage?.input_tokens ?? 0);
|
|
3515
|
-
generationSpan.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, response.usage?.output_tokens ?? 0);
|
|
3516
|
-
if (response.stop_reason) {
|
|
3517
|
-
generationSpan.setAttribute(ATTR.GEN_AI_FINISH_REASON, response.stop_reason);
|
|
3471
|
+
messages: cached.messages,
|
|
3472
|
+
};
|
|
3473
|
+
},
|
|
3474
|
+
planReclaim: (conversation) => {
|
|
3475
|
+
if (!contextGuard.shouldStop()) {
|
|
3476
|
+
return undefined;
|
|
3518
3477
|
}
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
// Structured-output pattern: when the model returns the
|
|
3526
|
-
// final_result tool call, push its arguments as JSON and stop.
|
|
3527
|
-
// Single-shot yield so callers consuming the stream still see
|
|
3528
|
-
// the structured value.
|
|
3529
|
-
if (useFinalResultTool) {
|
|
3530
|
-
const finalResultCall = toolUseBlocks.find((block) => block.name === "final_result");
|
|
3531
|
-
if (finalResultCall) {
|
|
3532
|
-
structuredOutputRef.value = finalResultCall.input;
|
|
3533
|
-
channel.push({ content: JSON.stringify(finalResultCall.input) });
|
|
3534
|
-
modelFinished = true;
|
|
3535
|
-
logger.debug("[GoogleVertex] Extracted structured output from final_result tool (stream)", { keys: Object.keys(finalResultCall.input) });
|
|
3536
|
-
break;
|
|
3478
|
+
// Reclaim and continue where possible: ending the turn early is safe
|
|
3479
|
+
// but throws away work the model was mid-way through.
|
|
3480
|
+
const working = [...conversation];
|
|
3481
|
+
if (reclaimVertexAnthropicContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
|
|
3482
|
+
contextGuard.resetAfterReclaim();
|
|
3483
|
+
return { conversation: working };
|
|
3537
3484
|
}
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
break;
|
|
3485
|
+
hitContextLimit = true;
|
|
3486
|
+
logger.warn(`[GoogleVertex] Native Anthropic turn stopped by the context guard: ` +
|
|
3487
|
+
`projected prompt ~${contextGuard.projectedNextPromptTokens} tokens ` +
|
|
3488
|
+
`>= threshold ${contextGuard.thresholdTokens} — forcing finalization.`);
|
|
3489
|
+
return undefined;
|
|
3490
|
+
},
|
|
3491
|
+
noteObservedPromptTokens: (tokens) => {
|
|
3492
|
+
contextGuard.noteUsage(tokens, 0);
|
|
3493
|
+
},
|
|
3494
|
+
...(useFinalResultTool
|
|
3495
|
+
? {
|
|
3496
|
+
finalResultToolName: "final_result",
|
|
3497
|
+
onTerminalResult: (text) => {
|
|
3498
|
+
// The engine ends the turn on a terminal call and hands back
|
|
3499
|
+
// the payload as text; this loop also streams it, so the push
|
|
3500
|
+
// stays here rather than in the adapter.
|
|
3501
|
+
try {
|
|
3502
|
+
structuredOutputRef.value = JSON.parse(text);
|
|
3503
|
+
}
|
|
3504
|
+
catch {
|
|
3505
|
+
/* the caller's coercion layer repairs a partial payload */
|
|
3506
|
+
}
|
|
3507
|
+
channel.push({ content: text });
|
|
3508
|
+
liveTextPushedLength += text.length;
|
|
3509
|
+
logger.debug("[GoogleVertex] Extracted structured output from final_result tool (stream)", { chars: text.length });
|
|
3510
|
+
},
|
|
3565
3511
|
}
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
toolResults.push({
|
|
3590
|
-
type: "tool_result",
|
|
3591
|
-
tool_use_id: toolUse.id,
|
|
3592
|
-
content: errMsg,
|
|
3593
|
-
});
|
|
3594
|
-
stepStorageResults.push({
|
|
3595
|
-
toolCallId: toolUse.id,
|
|
3596
|
-
toolName: toolUse.name,
|
|
3597
|
-
output: errorPayload,
|
|
3598
|
-
});
|
|
3599
|
-
continue;
|
|
3512
|
+
: {}),
|
|
3513
|
+
});
|
|
3514
|
+
// Wrapped rather than configured: both of these fire once PER STEP, and
|
|
3515
|
+
// these are the only hooks that see a single step's request and results.
|
|
3516
|
+
// Reading them off the turn's final result would batch every step into
|
|
3517
|
+
// one late write and lose the per-step generation span entirely.
|
|
3518
|
+
const adapter = {
|
|
3519
|
+
...baseAdapter,
|
|
3520
|
+
buildStepRequest: (conversation, engineStep) => {
|
|
3521
|
+
step = engineStep + 1;
|
|
3522
|
+
turnClock.noteProgress();
|
|
3523
|
+
return baseAdapter.buildStepRequest(conversation, engineStep);
|
|
3524
|
+
},
|
|
3525
|
+
// The provider's own miss handler, not the adapter's. The adapter
|
|
3526
|
+
// resolves a deferred tool and hands back its RAW executor; this one
|
|
3527
|
+
// also DECLARES the tool so Claude can call it on later steps, and
|
|
3528
|
+
// registers it in the turn's DedupExecuteMap so a repeat with
|
|
3529
|
+
// identical arguments is served from cache. Without the declaration a
|
|
3530
|
+
// hydrated tool works exactly once and is then invisible again.
|
|
3531
|
+
resolveToolOnMiss: (name) => {
|
|
3532
|
+
const hydrated = this.resolveAnthropicToolOnMiss(name, options.tools, tools, executeMap, failedTools);
|
|
3533
|
+
if (!hydrated) {
|
|
3534
|
+
return undefined;
|
|
3600
3535
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3536
|
+
return {
|
|
3537
|
+
execute: guardToolExecutor(name, hydrated, {
|
|
3538
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
3539
|
+
abortSignal: internalAbort.signal,
|
|
3540
|
+
onProgress: () => turnClock.noteProgress(),
|
|
3541
|
+
}),
|
|
3542
|
+
};
|
|
3543
|
+
},
|
|
3544
|
+
executeStep: async (request, stepChannel, signal) => {
|
|
3545
|
+
// One generation observation per API call: request in, content and
|
|
3546
|
+
// usage out. Started here rather than inside the adapter because the
|
|
3547
|
+
// attributes are this provider's, not the engine's.
|
|
3548
|
+
const generationSpan = tracers.generation.startSpan("anthropic.messages.stream", {
|
|
3549
|
+
kind: SpanKind.CLIENT,
|
|
3605
3550
|
attributes: {
|
|
3606
|
-
[LANGFUSE_ATTR.OBSERVATION_TYPE]: "
|
|
3607
|
-
[
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3551
|
+
[LANGFUSE_ATTR.OBSERVATION_TYPE]: "generation",
|
|
3552
|
+
[LANGFUSE_ATTR.OBSERVATION_MODEL_NAME]: modelName,
|
|
3553
|
+
[LANGFUSE_ATTR.OBSERVATION_MODEL_PARAMETERS]: spanJsonAttribute({
|
|
3554
|
+
max_tokens: requestParams.max_tokens,
|
|
3555
|
+
temperature: requestParams.temperature,
|
|
3556
|
+
top_p: requestParams.top_p,
|
|
3557
|
+
}),
|
|
3558
|
+
[LANGFUSE_ATTR.OBSERVATION_INPUT]: spanJsonAttribute({
|
|
3559
|
+
system: systemPromptWithSchema,
|
|
3560
|
+
messages: sanitizeAnthropicMessagesForTrace(currentMessages),
|
|
3561
|
+
}),
|
|
3612
3562
|
[LANGFUSE_ATTR.OBSERVATION_METADATA]: spanJsonAttribute({
|
|
3613
3563
|
step,
|
|
3564
|
+
toolsOffered: offeredToolNames.length,
|
|
3614
3565
|
}),
|
|
3566
|
+
[ATTR.GEN_AI_SYSTEM]: "anthropic",
|
|
3567
|
+
[ATTR.GEN_AI_MODEL]: modelName,
|
|
3568
|
+
[ATTR.GEN_AI_OPERATION]: "chat",
|
|
3615
3569
|
},
|
|
3616
3570
|
}, turnContext);
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
// a deferred catalog tool called directly by name.
|
|
3637
|
-
execute = this.resolveAnthropicToolOnMiss(toolUse.name, options.tools, tools, executeMap, failedTools);
|
|
3571
|
+
let firstDeltaSeen = false;
|
|
3572
|
+
try {
|
|
3573
|
+
const result = await baseAdapter.executeStep(request, {
|
|
3574
|
+
push: (chunk) => {
|
|
3575
|
+
turnClock.noteProgress();
|
|
3576
|
+
if (chunk.content && !firstDeltaSeen) {
|
|
3577
|
+
firstDeltaSeen = true;
|
|
3578
|
+
// Time-to-first-token for this generation.
|
|
3579
|
+
generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_COMPLETION_START_TIME, new Date().toISOString());
|
|
3580
|
+
}
|
|
3581
|
+
stepChannel.push(chunk);
|
|
3582
|
+
},
|
|
3583
|
+
}, signal);
|
|
3584
|
+
turnCacheUsage.read += result.usage.cacheReadTokens ?? 0;
|
|
3585
|
+
turnCacheUsage.creation += result.usage.cacheWriteTokens ?? 0;
|
|
3586
|
+
turnCacheUsage.creation5m += result.usage.cacheWrite5mTokens ?? 0;
|
|
3587
|
+
turnCacheUsage.creation1h += result.usage.cacheWrite1hTokens ?? 0;
|
|
3588
|
+
generationSpan.setAttribute(LANGFUSE_ATTR.OBSERVATION_OUTPUT, spanJsonAttribute({ text: result.text }));
|
|
3589
|
+
return result;
|
|
3638
3590
|
}
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
turnClock.noteProgress();
|
|
3647
|
-
// Run with toolSpan active so spans inside execute
|
|
3648
|
-
// (neurolink.tool.execute) nest under this observation instead
|
|
3649
|
-
// of becoming disconnected siblings. Bound the await — a
|
|
3650
|
-
// wedged tool costs one step (error tool_result), not the
|
|
3651
|
-
// whole turn — and race it against the turn's abort so a
|
|
3652
|
-
// deadline/caller abort is observed IMMEDIATELY instead of
|
|
3653
|
-
// after the tool settles.
|
|
3654
|
-
const result = await withTimeout(raceWithAbort(otelContext.with(otelTrace.setSpan(turnContext, toolSpan), () => Promise.resolve(execute(toolUse.input, toolOptions))), internalAbort.signal), toolExecTimeoutMs, `Tool "${toolUse.name}" execution timed out after ${toolExecTimeoutMs}ms`);
|
|
3655
|
-
turnClock.noteProgress();
|
|
3656
|
-
// MCP failures are returned, not thrown — surface them on
|
|
3657
|
-
// the span so failed calls show as ERROR in Langfuse.
|
|
3658
|
-
endToolSpan(result, extractMcpToolErrorMessage(result));
|
|
3659
|
-
// Error-shaped success (MCP isError / { error } payloads)
|
|
3660
|
-
// counts toward the breaker too — see the generate twin.
|
|
3661
|
-
const resultErrorText = extractToolFailureText(result);
|
|
3662
|
-
if (resultErrorText) {
|
|
3663
|
-
const info = failedTools.get(toolUse.name) || {
|
|
3664
|
-
count: 0,
|
|
3665
|
-
lastError: "",
|
|
3666
|
-
};
|
|
3667
|
-
info.count++;
|
|
3668
|
-
info.lastError = resultErrorText;
|
|
3669
|
-
failedTools.set(toolUse.name, info);
|
|
3670
|
-
}
|
|
3671
|
-
else {
|
|
3672
|
-
// Genuinely consecutive: a success clears the strike count
|
|
3673
|
-
// (argument-dependent soft errors — file-not-found on
|
|
3674
|
-
// different paths — must not disable a working tool).
|
|
3675
|
-
failedTools.delete(toolUse.name);
|
|
3676
|
-
}
|
|
3677
|
-
toolExecutions.push({
|
|
3678
|
-
name: toolUse.name,
|
|
3679
|
-
input: toolUse.input,
|
|
3680
|
-
output: result,
|
|
3681
|
-
});
|
|
3682
|
-
// Anthropic requires tool_result.content to be a string.
|
|
3683
|
-
// JSON.stringify returns undefined for undefined/function/symbol,
|
|
3684
|
-
// so coerce defensively to keep the follow-up turn valid.
|
|
3685
|
-
const resultContent = typeof result === "string"
|
|
3686
|
-
? result
|
|
3687
|
-
: stringifyContentSafe(result ?? null);
|
|
3688
|
-
toolResults.push({
|
|
3689
|
-
type: "tool_result",
|
|
3690
|
-
tool_use_id: toolUse.id,
|
|
3691
|
-
content: resultContent,
|
|
3692
|
-
});
|
|
3693
|
-
stepStorageResults.push({
|
|
3694
|
-
toolCallId: toolUse.id,
|
|
3695
|
-
toolName: toolUse.name,
|
|
3696
|
-
output: result,
|
|
3697
|
-
});
|
|
3698
|
-
}
|
|
3699
|
-
catch (err) {
|
|
3700
|
-
// An aborted tool call is a cancellation, not a tool failure —
|
|
3701
|
-
// end the span without recording an error execution/result and
|
|
3702
|
-
// break the turn.
|
|
3703
|
-
if (internalAbort.signal.aborted || isAbortError(err)) {
|
|
3704
|
-
endToolSpan({ aborted: true });
|
|
3705
|
-
// Keep persisted tool history paired: the call row was
|
|
3706
|
-
// already pushed above, so record a neutral cancellation
|
|
3707
|
-
// result (NOT an error) — an unpaired tool_use replayed on
|
|
3708
|
-
// the next turn would be rejected by the Anthropic API.
|
|
3709
|
-
stepStorageResults.push({
|
|
3710
|
-
toolCallId: toolUse.id,
|
|
3711
|
-
toolName: toolUse.name,
|
|
3712
|
-
output: { aborted: true },
|
|
3713
|
-
});
|
|
3714
|
-
wasAborted = true;
|
|
3715
|
-
break;
|
|
3716
|
-
}
|
|
3717
|
-
turnClock.noteProgress();
|
|
3718
|
-
if (err instanceof TimeoutError) {
|
|
3719
|
-
this.emitTurnEvent({
|
|
3720
|
-
phase: "tool-timeout",
|
|
3721
|
-
step,
|
|
3722
|
-
maxSteps,
|
|
3723
|
-
toolName: toolUse.name,
|
|
3724
|
-
});
|
|
3725
|
-
}
|
|
3726
|
-
// Count the failure toward the consecutive-failure breaker.
|
|
3727
|
-
const thrownErrorText = err instanceof Error ? err.message : String(err);
|
|
3728
|
-
const info = failedTools.get(toolUse.name) || {
|
|
3729
|
-
count: 0,
|
|
3730
|
-
lastError: "",
|
|
3731
|
-
};
|
|
3732
|
-
info.count++;
|
|
3733
|
-
info.lastError = thrownErrorText;
|
|
3734
|
-
failedTools.set(toolUse.name, info);
|
|
3735
|
-
logger.warn(`[GoogleVertex] Tool "${toolUse.name}" failed (attempt ${info.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${thrownErrorText}`);
|
|
3736
|
-
const errMsg = `Error executing tool "${toolUse.name}": ${thrownErrorText}`;
|
|
3737
|
-
const errorPayload = { error: errMsg };
|
|
3738
|
-
endToolSpan(errorPayload, errMsg);
|
|
3739
|
-
toolExecutions.push({
|
|
3740
|
-
name: toolUse.name,
|
|
3741
|
-
input: toolUse.input,
|
|
3742
|
-
output: errorPayload,
|
|
3743
|
-
});
|
|
3744
|
-
toolResults.push({
|
|
3745
|
-
type: "tool_result",
|
|
3746
|
-
tool_use_id: toolUse.id,
|
|
3747
|
-
content: errMsg,
|
|
3748
|
-
});
|
|
3749
|
-
stepStorageResults.push({
|
|
3750
|
-
toolCallId: toolUse.id,
|
|
3751
|
-
toolName: toolUse.name,
|
|
3752
|
-
output: errorPayload,
|
|
3753
|
-
});
|
|
3591
|
+
catch (error) {
|
|
3592
|
+
generationSpan.setStatus({
|
|
3593
|
+
code: SpanStatusCode.ERROR,
|
|
3594
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3595
|
+
});
|
|
3596
|
+
if (error instanceof Error) {
|
|
3597
|
+
generationSpan.recordException(error);
|
|
3754
3598
|
}
|
|
3599
|
+
throw error;
|
|
3755
3600
|
}
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
lastError: "",
|
|
3765
|
-
};
|
|
3766
|
-
notFoundInfo.count++;
|
|
3767
|
-
notFoundInfo.lastError = errMsg;
|
|
3768
|
-
failedTools.set(toolUse.name, notFoundInfo);
|
|
3769
|
-
endToolSpan(errorPayload, errMsg);
|
|
3601
|
+
finally {
|
|
3602
|
+
generationSpan.end();
|
|
3603
|
+
}
|
|
3604
|
+
},
|
|
3605
|
+
buildToolResultMessages: (conversation, stepResult, toolResults, engineStep) => {
|
|
3606
|
+
for (const result of toolResults) {
|
|
3607
|
+
allToolCalls.push({ toolName: result.name, args: result.args });
|
|
3608
|
+
toolsUsedRef.push(result.name);
|
|
3770
3609
|
toolExecutions.push({
|
|
3771
|
-
name:
|
|
3772
|
-
input:
|
|
3773
|
-
output:
|
|
3774
|
-
});
|
|
3775
|
-
toolResults.push({
|
|
3776
|
-
type: "tool_result",
|
|
3777
|
-
tool_use_id: toolUse.id,
|
|
3778
|
-
content: errMsg,
|
|
3779
|
-
});
|
|
3780
|
-
stepStorageResults.push({
|
|
3781
|
-
toolCallId: toolUse.id,
|
|
3782
|
-
toolName: toolUse.name,
|
|
3783
|
-
output: errorPayload,
|
|
3610
|
+
name: result.name,
|
|
3611
|
+
input: result.args,
|
|
3612
|
+
output: result.output,
|
|
3784
3613
|
});
|
|
3785
3614
|
}
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3615
|
+
metadata.totalToolExecutions += toolResults.length;
|
|
3616
|
+
const next = baseAdapter.buildToolResultMessages(conversation, stepResult, toolResults, engineStep);
|
|
3617
|
+
// Time-budget wrap-up nudge: with the turn deadline approaching,
|
|
3618
|
+
// tell the model to consolidate. Rides as a trailing text block on
|
|
3619
|
+
// the tool_result user turn.
|
|
3620
|
+
if (turnClock.shouldNudgeWrapup()) {
|
|
3621
|
+
const last = next[next.length - 1];
|
|
3622
|
+
if (last && Array.isArray(last.content)) {
|
|
3623
|
+
last.content.push({
|
|
3624
|
+
type: "text",
|
|
3625
|
+
text: buildWrapupNudgeText(useFinalResultTool),
|
|
3626
|
+
});
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
// Tool activity reaches conversation memory per step, not batched at
|
|
3630
|
+
// the end: tools that DID complete in a step later aborted are real
|
|
3631
|
+
// side effects and belong in the chat history.
|
|
3632
|
+
withTimeout(this.handleToolExecutionStorage(toolResults.map((result) => ({
|
|
3633
|
+
toolName: result.name,
|
|
3634
|
+
args: result.args,
|
|
3635
|
+
stepIndex: engineStep + 1,
|
|
3636
|
+
})), toolResults.map((result) => ({
|
|
3637
|
+
toolName: result.name,
|
|
3638
|
+
output: result.output,
|
|
3639
|
+
stepIndex: engineStep + 1,
|
|
3640
|
+
})), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
|
|
3794
3641
|
logger.warn("[GoogleVertex] Failed to store native Anthropic stream tool executions", {
|
|
3795
3642
|
error: error instanceof Error ? error.message : String(error),
|
|
3796
3643
|
});
|
|
3797
3644
|
});
|
|
3645
|
+
// Project this step's growth for the context guard: everything just
|
|
3646
|
+
// appended rides the next prompt.
|
|
3647
|
+
try {
|
|
3648
|
+
const appended = next[next.length - 1];
|
|
3649
|
+
contextGuard.noteAppendedChars(JSON.stringify(appended?.content ?? []).length);
|
|
3650
|
+
}
|
|
3651
|
+
catch {
|
|
3652
|
+
/* estimation is best-effort — never break the loop */
|
|
3653
|
+
}
|
|
3654
|
+
return next;
|
|
3655
|
+
},
|
|
3656
|
+
};
|
|
3657
|
+
const activeSpan = otelTrace.getSpan(turnContext);
|
|
3658
|
+
const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentMessages.slice(), {
|
|
3659
|
+
tools: engineTools,
|
|
3660
|
+
abortSignal: internalAbort.signal,
|
|
3661
|
+
...(activeSpan ? { span: activeSpan } : {}),
|
|
3662
|
+
});
|
|
3663
|
+
const pump = (async () => {
|
|
3664
|
+
for await (const chunk of engineStream) {
|
|
3665
|
+
if (chunk.content) {
|
|
3666
|
+
channel.push({ content: chunk.content });
|
|
3667
|
+
liveTextPushedLength += chunk.content.length;
|
|
3668
|
+
aggregatedTurnText += chunk.content;
|
|
3669
|
+
}
|
|
3798
3670
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
text: `NOTE: Only ${stepsRemaining} tool step(s) remain. Consolidate what you have and ` +
|
|
3816
|
-
(useFinalResultTool
|
|
3817
|
-
? "call final_result with your best answer."
|
|
3818
|
-
: "provide your final answer."),
|
|
3819
|
-
});
|
|
3671
|
+
})();
|
|
3672
|
+
let engineResult;
|
|
3673
|
+
let turnFailure;
|
|
3674
|
+
try {
|
|
3675
|
+
engineResult = await resultPromise;
|
|
3676
|
+
}
|
|
3677
|
+
catch (error) {
|
|
3678
|
+
turnFailure = error;
|
|
3679
|
+
}
|
|
3680
|
+
// Drained tolerantly and exactly once: when a turn ends by abort the
|
|
3681
|
+
// channel rejects too, and re-awaiting a settled rejection would rethrow
|
|
3682
|
+
// the error the branch below has already decided to absorb.
|
|
3683
|
+
await pump.catch(() => { });
|
|
3684
|
+
if (turnFailure !== undefined) {
|
|
3685
|
+
if (internalAbort.signal.aborted || isAbortError(turnFailure)) {
|
|
3686
|
+
wasAborted = true;
|
|
3820
3687
|
}
|
|
3821
|
-
else
|
|
3822
|
-
|
|
3823
|
-
type: "text",
|
|
3824
|
-
text: buildWrapupNudgeText(useFinalResultTool),
|
|
3825
|
-
});
|
|
3688
|
+
else {
|
|
3689
|
+
throw turnFailure;
|
|
3826
3690
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3691
|
+
}
|
|
3692
|
+
if (engineResult) {
|
|
3693
|
+
usage.input += engineResult.usage.inputTokens;
|
|
3694
|
+
usage.output += engineResult.usage.outputTokens;
|
|
3695
|
+
finishReasonRef.value =
|
|
3696
|
+
engineResult.rawStopReason ?? finishReasonRef.value;
|
|
3697
|
+
// NOT `toolCalls.length === 0`: that array accumulates across the
|
|
3698
|
+
// WHOLE turn, so a turn that called a tool in step 1 and answered
|
|
3699
|
+
// with text in step 2 would look unfinished and fall into terminal
|
|
3700
|
+
// handling. The finish reason is the per-turn signal — the engine
|
|
3701
|
+
// reports "tool-calls" only when the cap was hit with tools still
|
|
3702
|
+
// pending.
|
|
3703
|
+
modelFinished =
|
|
3704
|
+
engineResult.toolCalls.length === 0 ||
|
|
3705
|
+
engineResult.finishReason !== "tool-calls";
|
|
3706
|
+
// Replace in place: the terminal block and the finalization call both
|
|
3707
|
+
// read `currentMessages`.
|
|
3708
|
+
currentMessages.length = 0;
|
|
3709
|
+
currentMessages.push(...engineResult.conversation);
|
|
3710
|
+
}
|
|
3711
|
+
if (internalAbort.signal.aborted) {
|
|
3712
|
+
wasAborted = true;
|
|
3845
3713
|
}
|
|
3846
3714
|
// Terminal handling — the loop exited without a model-initiated
|
|
3847
3715
|
// finish (step budget exhausted, or the turn was aborted). Never end
|
|
@@ -4620,398 +4488,225 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
4620
4488
|
turnClock.dispose();
|
|
4621
4489
|
options.abortSignal?.removeEventListener("abort", onCallerAbort);
|
|
4622
4490
|
};
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
// threshold — stop the tool loop and synthesize from what we have.
|
|
4635
|
-
if (contextGuard.shouldStop()) {
|
|
4636
|
-
const reclaimed = reclaimVertexAnthropicContext(currentMessages, modelName, contextGuard.projectedNextPromptTokens);
|
|
4637
|
-
if (reclaimed) {
|
|
4638
|
-
contextGuard.resetAfterReclaim();
|
|
4639
|
-
}
|
|
4640
|
-
else {
|
|
4641
|
-
hitContextLimit = true;
|
|
4642
|
-
logger.warn(`[GoogleVertex] Anthropic generate turn stopped by the context guard: ` +
|
|
4643
|
-
`projected prompt ~${contextGuard.projectedNextPromptTokens} tokens ` +
|
|
4644
|
-
`>= threshold ${contextGuard.thresholdTokens} (step ${step}) — synthesizing a final answer.`);
|
|
4645
|
-
break;
|
|
4646
|
-
}
|
|
4491
|
+
// Executors handed to the engine, taken through the turn's
|
|
4492
|
+
// DedupExecuteMap so an identical repeated call is answered from the
|
|
4493
|
+
// per-turn cache rather than run again (BZ-3327), and guarded exactly as
|
|
4494
|
+
// the hand-rolled loop guarded them: a per-tool bound so a wedged tool
|
|
4495
|
+
// costs ONE STEP rather than the whole turn, raced against the turn's
|
|
4496
|
+
// abort, and a stall-clock ping either side.
|
|
4497
|
+
const engineTools = {};
|
|
4498
|
+
for (const toolName of executeMap.keys()) {
|
|
4499
|
+
const wrapped = executeMap.get(toolName);
|
|
4500
|
+
if (!wrapped) {
|
|
4501
|
+
continue;
|
|
4647
4502
|
}
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4503
|
+
engineTools[toolName] = {
|
|
4504
|
+
execute: guardToolExecutor(toolName, wrapped, {
|
|
4505
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
4506
|
+
abortSignal: internalAbort.signal,
|
|
4507
|
+
onProgress: () => turnClock.noteProgress(),
|
|
4508
|
+
}),
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
// The turn runs on the shared engine, exactly as the streaming twin does.
|
|
4512
|
+
// maxSteps is agenticStepBudget, not maxSteps: the last slot is reserved
|
|
4513
|
+
// for the forced final_result call in the terminal block below, and the
|
|
4514
|
+
// engine must never spend it.
|
|
4515
|
+
const baseAdapter = createAnthropicLoopAdapter({
|
|
4516
|
+
client,
|
|
4517
|
+
maxSteps: agenticStepBudget,
|
|
4518
|
+
toolsRecord: options.tools ?? {},
|
|
4519
|
+
toolFailureBreaker: {
|
|
4520
|
+
maxRetries: DEFAULT_TOOL_MAX_RETRIES,
|
|
4521
|
+
// MCP failures are RETURNED, not thrown. Counting only throws lets
|
|
4522
|
+
// the model grind on a blocked tool for the whole step budget.
|
|
4523
|
+
classifyResultFailure: (output) => extractToolFailureText(output) ?? undefined,
|
|
4524
|
+
},
|
|
4525
|
+
buildParams: (conversation) => {
|
|
4526
|
+
// Mid-turn discovery sync — see the stream twin.
|
|
4527
|
+
this.refreshAnthropicToolDeclarations(options.tools, tools, executeMap, failedTools);
|
|
4528
|
+
const cached = applyVertexAnthropicCacheBreakpoints({
|
|
4663
4529
|
system: systemPromptWithSchema,
|
|
4664
4530
|
tools,
|
|
4665
|
-
messages:
|
|
4531
|
+
messages: conversation,
|
|
4666
4532
|
});
|
|
4667
|
-
|
|
4668
|
-
// mid-flight abort cancels the HTTP call itself (the SDK rejects with
|
|
4669
|
-
// an abort-shaped error the per-step catch below turns into a break).
|
|
4670
|
-
const response = await withTimeout(client.messages.create({
|
|
4533
|
+
return {
|
|
4671
4534
|
...requestParams,
|
|
4672
|
-
...(
|
|
4673
|
-
system:
|
|
4535
|
+
...(cached.system !== undefined && {
|
|
4536
|
+
system: cached.system,
|
|
4674
4537
|
}),
|
|
4675
|
-
...(
|
|
4676
|
-
|
|
4677
|
-
tools:
|
|
4538
|
+
...(cached.tools &&
|
|
4539
|
+
cached.tools.length > 0 && {
|
|
4540
|
+
tools: cached.tools,
|
|
4678
4541
|
}),
|
|
4679
|
-
messages:
|
|
4680
|
-
}
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
totalOutputTokens += response.usage?.output_tokens || 0;
|
|
4686
|
-
totalCacheReadTokens += response.usage?.cache_read_input_tokens || 0;
|
|
4687
|
-
totalCacheCreationTokens +=
|
|
4688
|
-
response.usage?.cache_creation_input_tokens || 0;
|
|
4689
|
-
lastStopReason = response.stop_reason;
|
|
4690
|
-
// Feed the context guard the FULL prompt size of this call (uncached
|
|
4691
|
-
// input + cache reads/writes) — the API reports it every step.
|
|
4692
|
-
contextGuard.noteUsage((response.usage?.input_tokens || 0) +
|
|
4693
|
-
(response.usage?.cache_read_input_tokens || 0) +
|
|
4694
|
-
(response.usage?.cache_creation_input_tokens || 0), response.usage?.output_tokens || 0);
|
|
4695
|
-
// Check if we need to handle tool use
|
|
4696
|
-
const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
|
|
4697
|
-
// Check for final_result tool call (for structured output)
|
|
4698
|
-
if (useFinalResultTool) {
|
|
4699
|
-
const finalResultCall = toolUseBlocks.find((block) => block.name === "final_result");
|
|
4700
|
-
if (finalResultCall) {
|
|
4701
|
-
// Extract structured output and convert to JSON string for finalText
|
|
4702
|
-
structuredOutput = finalResultCall.input;
|
|
4703
|
-
finalText = JSON.stringify(structuredOutput);
|
|
4704
|
-
modelFinished = true;
|
|
4705
|
-
logger.debug("[GoogleVertex] Extracted structured output from final_result tool (generate)", { keys: Object.keys(structuredOutput) });
|
|
4706
|
-
break; // We have the structured output, we're done
|
|
4707
|
-
}
|
|
4542
|
+
messages: cached.messages,
|
|
4543
|
+
};
|
|
4544
|
+
},
|
|
4545
|
+
planReclaim: (conversation) => {
|
|
4546
|
+
if (!contextGuard.shouldStop()) {
|
|
4547
|
+
return undefined;
|
|
4708
4548
|
}
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
// No tool calls, we're done
|
|
4714
|
-
finalText = responseText || accumulatedStepText;
|
|
4715
|
-
modelFinished = true;
|
|
4716
|
-
break;
|
|
4549
|
+
const working = [...conversation];
|
|
4550
|
+
if (reclaimVertexAnthropicContext(working, modelName, contextGuard.projectedNextPromptTokens)) {
|
|
4551
|
+
contextGuard.resetAfterReclaim();
|
|
4552
|
+
return { conversation: working };
|
|
4717
4553
|
}
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
//
|
|
4723
|
-
//
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
// while-top check finally breaks. Skipped tools get neither a call
|
|
4734
|
-
// row nor a result row, so persisted history stays paired.
|
|
4735
|
-
if (internalAbort.signal.aborted) {
|
|
4736
|
-
wasAborted = true;
|
|
4737
|
-
break;
|
|
4738
|
-
}
|
|
4739
|
-
allToolCalls.push({
|
|
4740
|
-
toolName: toolUse.name,
|
|
4741
|
-
args: toolUse.input,
|
|
4742
|
-
});
|
|
4743
|
-
stepStorageCalls.push({
|
|
4744
|
-
toolCallId: toolUse.id,
|
|
4745
|
-
toolName: toolUse.name,
|
|
4746
|
-
args: toolUse.input,
|
|
4747
|
-
});
|
|
4748
|
-
// Consecutive-failure breaker: stop re-executing a tool that has
|
|
4749
|
-
// already failed DEFAULT_TOOL_MAX_RETRIES times this turn (ports
|
|
4750
|
-
// the Gemini loops' failedTools map — the Anthropic loops let a
|
|
4751
|
-
// blocked tool be retried for the entire remaining step budget).
|
|
4752
|
-
const failedInfo = failedTools.get(toolUse.name);
|
|
4753
|
-
if (failedInfo && failedInfo.count >= DEFAULT_TOOL_MAX_RETRIES) {
|
|
4754
|
-
logger.warn(`[GoogleVertex] Tool "${toolUse.name}" has exceeded retry limit (${DEFAULT_TOOL_MAX_RETRIES}), skipping execution`);
|
|
4755
|
-
const errMsg = `TOOL_PERMANENTLY_FAILED: The tool "${toolUse.name}" has failed ${failedInfo.count} times and will not be retried. Last error: ${failedInfo.lastError}. Please proceed without using this tool or inform the user that this functionality is unavailable.`;
|
|
4756
|
-
const errorPayload = { error: errMsg };
|
|
4757
|
-
toolExecutions.push({
|
|
4758
|
-
name: toolUse.name,
|
|
4759
|
-
input: toolUse.input,
|
|
4760
|
-
output: errorPayload,
|
|
4761
|
-
});
|
|
4762
|
-
toolResults.push({
|
|
4763
|
-
type: "tool_result",
|
|
4764
|
-
tool_use_id: toolUse.id,
|
|
4765
|
-
content: errMsg,
|
|
4766
|
-
});
|
|
4767
|
-
stepStorageResults.push({
|
|
4768
|
-
toolCallId: toolUse.id,
|
|
4769
|
-
toolName: toolUse.name,
|
|
4770
|
-
output: errorPayload,
|
|
4771
|
-
});
|
|
4772
|
-
continue;
|
|
4773
|
-
}
|
|
4774
|
-
let execute = executeMap.get(toolUse.name);
|
|
4775
|
-
if (!execute) {
|
|
4776
|
-
// Snapshot miss — see the stream twin.
|
|
4777
|
-
execute = this.resolveAnthropicToolOnMiss(toolUse.name, options.tools, tools, executeMap, failedTools);
|
|
4778
|
-
}
|
|
4779
|
-
if (execute) {
|
|
4554
|
+
hitContextLimit = true;
|
|
4555
|
+
logger.warn(`[GoogleVertex] Anthropic generate turn stopped by the context guard: ` +
|
|
4556
|
+
`projected prompt ~${contextGuard.projectedNextPromptTokens} tokens ` +
|
|
4557
|
+
`>= threshold ${contextGuard.thresholdTokens} — forcing finalization.`);
|
|
4558
|
+
// STOP, not undefined: undefined means "nothing to reclaim, carry
|
|
4559
|
+
// on", which is the opposite of what the guard just decided.
|
|
4560
|
+
return { stop: true };
|
|
4561
|
+
},
|
|
4562
|
+
noteObservedPromptTokens: (tokens) => {
|
|
4563
|
+
contextGuard.noteUsage(tokens, 0);
|
|
4564
|
+
},
|
|
4565
|
+
...(useFinalResultTool
|
|
4566
|
+
? {
|
|
4567
|
+
finalResultToolName: "final_result",
|
|
4568
|
+
onTerminalResult: (text) => {
|
|
4780
4569
|
try {
|
|
4781
|
-
|
|
4782
|
-
toolCallId: toolUse.id,
|
|
4783
|
-
messages: [],
|
|
4784
|
-
abortSignal: internalAbort.signal,
|
|
4785
|
-
};
|
|
4786
|
-
turnClock.noteProgress();
|
|
4787
|
-
// Bound the execute() await — a wedged tool costs one step
|
|
4788
|
-
// (error tool_result), not the whole turn — and race it against
|
|
4789
|
-
// the turn's abort so a deadline/caller abort is observed
|
|
4790
|
-
// IMMEDIATELY instead of after the tool settles (live-verified:
|
|
4791
|
-
// an 8s deadline previously waited out a 60s tool).
|
|
4792
|
-
const result = await withTimeout(raceWithAbort(Promise.resolve(execute(toolUse.input, toolOptions)), internalAbort.signal), toolExecTimeoutMs, `Tool "${toolUse.name}" execution timed out after ${toolExecTimeoutMs}ms`);
|
|
4793
|
-
turnClock.noteProgress();
|
|
4794
|
-
// Error-shaped success (MCP isError / { error } payloads —
|
|
4795
|
-
// e.g. proxy-blocked tools) counts toward the breaker too:
|
|
4796
|
-
// these fail without throwing, and only counting throws lets
|
|
4797
|
-
// the model grind on a blocked tool for the whole budget.
|
|
4798
|
-
const resultErrorText = extractToolFailureText(result);
|
|
4799
|
-
if (resultErrorText) {
|
|
4800
|
-
const info = failedTools.get(toolUse.name) || {
|
|
4801
|
-
count: 0,
|
|
4802
|
-
lastError: "",
|
|
4803
|
-
};
|
|
4804
|
-
info.count++;
|
|
4805
|
-
info.lastError = resultErrorText;
|
|
4806
|
-
failedTools.set(toolUse.name, info);
|
|
4807
|
-
}
|
|
4808
|
-
else {
|
|
4809
|
-
// Genuinely consecutive: a success clears the strike count
|
|
4810
|
-
// (argument-dependent soft errors — file-not-found on
|
|
4811
|
-
// different paths — must not disable a working tool).
|
|
4812
|
-
failedTools.delete(toolUse.name);
|
|
4813
|
-
}
|
|
4814
|
-
toolExecutions.push({
|
|
4815
|
-
name: toolUse.name,
|
|
4816
|
-
input: toolUse.input,
|
|
4817
|
-
output: result,
|
|
4818
|
-
});
|
|
4819
|
-
// Anthropic requires tool_result.content to be a string.
|
|
4820
|
-
// JSON.stringify returns undefined for undefined/function/symbol,
|
|
4821
|
-
// so coerce defensively to keep the follow-up turn valid.
|
|
4822
|
-
const resultContent = typeof result === "string"
|
|
4823
|
-
? result
|
|
4824
|
-
: stringifyContentSafe(result ?? null);
|
|
4825
|
-
toolResults.push({
|
|
4826
|
-
type: "tool_result",
|
|
4827
|
-
tool_use_id: toolUse.id,
|
|
4828
|
-
content: resultContent,
|
|
4829
|
-
});
|
|
4830
|
-
stepStorageResults.push({
|
|
4831
|
-
toolCallId: toolUse.id,
|
|
4832
|
-
toolName: toolUse.name,
|
|
4833
|
-
output: result,
|
|
4834
|
-
});
|
|
4570
|
+
structuredOutput = JSON.parse(text);
|
|
4835
4571
|
}
|
|
4836
|
-
catch
|
|
4837
|
-
|
|
4838
|
-
// break the turn without recording an error execution/result.
|
|
4839
|
-
if (internalAbort.signal.aborted || isAbortError(err)) {
|
|
4840
|
-
// Keep persisted tool history paired: the call row was
|
|
4841
|
-
// already pushed above, so record a neutral cancellation
|
|
4842
|
-
// result (NOT an error) — an unpaired tool_use replayed on
|
|
4843
|
-
// the next turn would be rejected by the Anthropic API.
|
|
4844
|
-
stepStorageResults.push({
|
|
4845
|
-
toolCallId: toolUse.id,
|
|
4846
|
-
toolName: toolUse.name,
|
|
4847
|
-
output: { aborted: true },
|
|
4848
|
-
});
|
|
4849
|
-
wasAborted = true;
|
|
4850
|
-
break;
|
|
4851
|
-
}
|
|
4852
|
-
turnClock.noteProgress();
|
|
4853
|
-
if (err instanceof TimeoutError) {
|
|
4854
|
-
this.emitTurnEvent({
|
|
4855
|
-
phase: "tool-timeout",
|
|
4856
|
-
step,
|
|
4857
|
-
maxSteps,
|
|
4858
|
-
toolName: toolUse.name,
|
|
4859
|
-
});
|
|
4860
|
-
}
|
|
4861
|
-
// Count the failure toward the consecutive-failure breaker.
|
|
4862
|
-
const thrownErrorText = err instanceof Error ? err.message : String(err);
|
|
4863
|
-
const info = failedTools.get(toolUse.name) || {
|
|
4864
|
-
count: 0,
|
|
4865
|
-
lastError: "",
|
|
4866
|
-
};
|
|
4867
|
-
info.count++;
|
|
4868
|
-
info.lastError = thrownErrorText;
|
|
4869
|
-
failedTools.set(toolUse.name, info);
|
|
4870
|
-
logger.warn(`[GoogleVertex] Tool "${toolUse.name}" failed (attempt ${info.count}/${DEFAULT_TOOL_MAX_RETRIES}): ${thrownErrorText}`);
|
|
4871
|
-
const errMsg = `Error executing tool "${toolUse.name}": ${thrownErrorText}`;
|
|
4872
|
-
const errorPayload = { error: errMsg };
|
|
4873
|
-
toolExecutions.push({
|
|
4874
|
-
name: toolUse.name,
|
|
4875
|
-
input: toolUse.input,
|
|
4876
|
-
output: errorPayload,
|
|
4877
|
-
});
|
|
4878
|
-
toolResults.push({
|
|
4879
|
-
type: "tool_result",
|
|
4880
|
-
tool_use_id: toolUse.id,
|
|
4881
|
-
content: errMsg,
|
|
4882
|
-
});
|
|
4883
|
-
stepStorageResults.push({
|
|
4884
|
-
toolCallId: toolUse.id,
|
|
4885
|
-
toolName: toolUse.name,
|
|
4886
|
-
output: errorPayload,
|
|
4887
|
-
});
|
|
4572
|
+
catch {
|
|
4573
|
+
/* the caller's coercion layer repairs a partial payload */
|
|
4888
4574
|
}
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
const errMsg = `TOOL_NOT_FOUND: The tool "${toolUse.name}" does not exist.`;
|
|
4892
|
-
const errorPayload = { error: errMsg };
|
|
4893
|
-
// A missing tool counts toward the breaker too — a model
|
|
4894
|
-
// grinding on a hallucinated/stale tool name must not burn the
|
|
4895
|
-
// whole step budget on TOOL_NOT_FOUND round-trips.
|
|
4896
|
-
const notFoundInfo = failedTools.get(toolUse.name) || {
|
|
4897
|
-
count: 0,
|
|
4898
|
-
lastError: "",
|
|
4899
|
-
};
|
|
4900
|
-
notFoundInfo.count++;
|
|
4901
|
-
notFoundInfo.lastError = errMsg;
|
|
4902
|
-
failedTools.set(toolUse.name, notFoundInfo);
|
|
4903
|
-
toolExecutions.push({
|
|
4904
|
-
name: toolUse.name,
|
|
4905
|
-
input: toolUse.input,
|
|
4906
|
-
output: errorPayload,
|
|
4907
|
-
});
|
|
4908
|
-
toolResults.push({
|
|
4909
|
-
type: "tool_result",
|
|
4910
|
-
tool_use_id: toolUse.id,
|
|
4911
|
-
content: errMsg,
|
|
4912
|
-
});
|
|
4913
|
-
stepStorageResults.push({
|
|
4914
|
-
toolCallId: toolUse.id,
|
|
4915
|
-
toolName: toolUse.name,
|
|
4916
|
-
output: errorPayload,
|
|
4917
|
-
});
|
|
4918
|
-
}
|
|
4575
|
+
logger.debug("[GoogleVertex] Extracted structured output from final_result tool (generate)", { chars: text.length });
|
|
4576
|
+
},
|
|
4919
4577
|
}
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4578
|
+
: {}),
|
|
4579
|
+
});
|
|
4580
|
+
const adapter = {
|
|
4581
|
+
...baseAdapter,
|
|
4582
|
+
buildStepRequest: (conversation, engineStep) => {
|
|
4583
|
+
step = engineStep + 1;
|
|
4584
|
+
turnClock.noteProgress();
|
|
4585
|
+
return baseAdapter.buildStepRequest(conversation, engineStep);
|
|
4586
|
+
},
|
|
4587
|
+
// The provider's own miss handler, not the adapter's. The adapter
|
|
4588
|
+
// resolves a deferred tool and hands back its RAW executor; this one
|
|
4589
|
+
// also DECLARES the tool so Claude can call it on later steps, and
|
|
4590
|
+
// registers it in the turn's DedupExecuteMap so a repeat with
|
|
4591
|
+
// identical arguments is served from cache. Without the declaration a
|
|
4592
|
+
// hydrated tool works exactly once and is then invisible again.
|
|
4593
|
+
resolveToolOnMiss: (name) => {
|
|
4594
|
+
const hydrated = this.resolveAnthropicToolOnMiss(name, options.tools, tools, executeMap, failedTools);
|
|
4595
|
+
if (!hydrated) {
|
|
4596
|
+
return undefined;
|
|
4597
|
+
}
|
|
4598
|
+
return {
|
|
4599
|
+
execute: guardToolExecutor(name, hydrated, {
|
|
4600
|
+
toolTimeoutMs: toolExecTimeoutMs,
|
|
4601
|
+
abortSignal: internalAbort.signal,
|
|
4602
|
+
onProgress: () => turnClock.noteProgress(),
|
|
4603
|
+
}),
|
|
4604
|
+
};
|
|
4605
|
+
},
|
|
4606
|
+
buildToolResultMessages: (conversation, stepResult, toolResults, engineStep) => {
|
|
4607
|
+
for (const result of toolResults) {
|
|
4608
|
+
allToolCalls.push({ toolName: result.name, args: result.args });
|
|
4609
|
+
// Recorded here as well: `toolExecutions` feeds
|
|
4610
|
+
// resolveToolExecutionRecords and the result's own
|
|
4611
|
+
// toolExecutions, and pushing only to allToolCalls left it empty
|
|
4612
|
+
// for every generate turn.
|
|
4613
|
+
toolExecutions.push({
|
|
4614
|
+
name: result.name,
|
|
4615
|
+
input: result.args,
|
|
4616
|
+
output: result.output,
|
|
4931
4617
|
});
|
|
4932
4618
|
}
|
|
4933
|
-
//
|
|
4934
|
-
//
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4619
|
+
// Per STEP, with appendStepText's newline join — this hook runs once
|
|
4620
|
+
// per step and `stepResult.text` is that step's whole text.
|
|
4621
|
+
accumulatedStepText = appendStepText(accumulatedStepText, stepResult.text);
|
|
4622
|
+
const next = baseAdapter.buildToolResultMessages(conversation, stepResult, toolResults, engineStep);
|
|
4623
|
+
// Time-budget wrap-up nudge, as a trailing text block on the
|
|
4624
|
+
// tool_result user turn.
|
|
4625
|
+
if (turnClock.shouldNudgeWrapup()) {
|
|
4626
|
+
const last = next[next.length - 1];
|
|
4627
|
+
if (last && Array.isArray(last.content)) {
|
|
4628
|
+
last.content.push({
|
|
4629
|
+
type: "text",
|
|
4630
|
+
text: buildWrapupNudgeText(useFinalResultTool),
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4938
4633
|
}
|
|
4939
|
-
//
|
|
4940
|
-
//
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
: "provide your final answer."),
|
|
4634
|
+
// Per step, not batched at the end: tools that completed in a step
|
|
4635
|
+
// later aborted are real side effects and belong in the history.
|
|
4636
|
+
withTimeout(this.handleToolExecutionStorage(toolResults.map((result) => ({
|
|
4637
|
+
toolName: result.name,
|
|
4638
|
+
args: result.args,
|
|
4639
|
+
stepIndex: engineStep + 1,
|
|
4640
|
+
})), toolResults.map((result) => ({
|
|
4641
|
+
toolName: result.name,
|
|
4642
|
+
output: result.output,
|
|
4643
|
+
stepIndex: engineStep + 1,
|
|
4644
|
+
})), options, new Date()), TOOL_STORAGE_TIMEOUT_MS, "tool storage write timed out").catch((error) => {
|
|
4645
|
+
logger.warn("[GoogleVertex] Failed to store native Anthropic generate tool executions", {
|
|
4646
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4953
4647
|
});
|
|
4648
|
+
});
|
|
4649
|
+
try {
|
|
4650
|
+
const appended = next[next.length - 1];
|
|
4651
|
+
contextGuard.noteAppendedChars(JSON.stringify(appended?.content ?? []).length);
|
|
4954
4652
|
}
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
type: "text",
|
|
4958
|
-
text: buildWrapupNudgeText(useFinalResultTool),
|
|
4959
|
-
});
|
|
4653
|
+
catch {
|
|
4654
|
+
/* estimation is best-effort — never break the loop */
|
|
4960
4655
|
}
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
? block.content.length
|
|
4978
|
-
: block.text.length), 0));
|
|
4979
|
-
// Accumulate the step's prose so a capped turn can still surface it —
|
|
4980
|
-
// finalText is reserved for model-initiated finishes.
|
|
4981
|
-
accumulatedStepText = appendStepText(accumulatedStepText, responseText);
|
|
4656
|
+
return next;
|
|
4657
|
+
},
|
|
4658
|
+
};
|
|
4659
|
+
const { stream: engineStream, resultPromise } = runAgenticLoop(adapter, currentMessages.slice(), {
|
|
4660
|
+
tools: engineTools,
|
|
4661
|
+
abortSignal: internalAbort.signal,
|
|
4662
|
+
});
|
|
4663
|
+
// Drained and discarded: generate() returns one result rather than
|
|
4664
|
+
// streaming, and the per-step text is accumulated in
|
|
4665
|
+
// buildToolResultMessages instead — appendStepText joins steps with a
|
|
4666
|
+
// NEWLINE, which a chunk-by-chunk `+=` here would silently drop, running
|
|
4667
|
+
// consecutive steps' text together. The drain still has to happen:
|
|
4668
|
+
// leaving the channel unread stalls the engine once its buffer fills.
|
|
4669
|
+
const pump = (async () => {
|
|
4670
|
+
for await (const chunk of engineStream) {
|
|
4671
|
+
void chunk;
|
|
4982
4672
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4673
|
+
})();
|
|
4674
|
+
let engineResult;
|
|
4675
|
+
let turnFailure;
|
|
4676
|
+
try {
|
|
4677
|
+
engineResult = await resultPromise;
|
|
4678
|
+
}
|
|
4679
|
+
catch (error) {
|
|
4680
|
+
turnFailure = error;
|
|
4681
|
+
}
|
|
4682
|
+
await pump.catch(() => { });
|
|
4683
|
+
if (turnFailure !== undefined) {
|
|
4684
|
+
if (internalAbort.signal.aborted || isAbortError(turnFailure)) {
|
|
4685
|
+
wasAborted = true;
|
|
4686
|
+
}
|
|
4687
|
+
else {
|
|
4995
4688
|
logger.error("[GoogleVertex] Native Anthropic SDK generate error", {
|
|
4996
|
-
error,
|
|
4689
|
+
error: turnFailure,
|
|
4997
4690
|
model: modelName,
|
|
4998
|
-
step,
|
|
4999
|
-
status: error?.status,
|
|
5000
4691
|
});
|
|
5001
|
-
|
|
5002
|
-
// native Gemini catch for rationale.
|
|
5003
|
-
try {
|
|
5004
|
-
if (error && typeof error === "object") {
|
|
5005
|
-
error.requestModel = modelName;
|
|
5006
|
-
}
|
|
5007
|
-
}
|
|
5008
|
-
catch {
|
|
5009
|
-
/* frozen/sealed error — context stays best-effort */
|
|
5010
|
-
}
|
|
5011
|
-
releaseTurnResources();
|
|
5012
|
-
throw this.handleProviderError(error);
|
|
4692
|
+
throw this.handleProviderError(turnFailure);
|
|
5013
4693
|
}
|
|
5014
4694
|
}
|
|
4695
|
+
if (engineResult) {
|
|
4696
|
+
totalInputTokens += engineResult.usage.inputTokens;
|
|
4697
|
+
totalOutputTokens += engineResult.usage.outputTokens;
|
|
4698
|
+
totalCacheReadTokens += engineResult.usage.cacheReadTokens ?? 0;
|
|
4699
|
+
totalCacheCreationTokens += engineResult.usage.cacheWriteTokens ?? 0;
|
|
4700
|
+
lastStopReason = engineResult.rawStopReason ?? lastStopReason;
|
|
4701
|
+
finalText = engineResult.text || finalText;
|
|
4702
|
+
// Replace in place: the terminal block and the finalization call both
|
|
4703
|
+
// read `currentMessages`.
|
|
4704
|
+
currentMessages.length = 0;
|
|
4705
|
+
currentMessages.push(...engineResult.conversation);
|
|
4706
|
+
}
|
|
4707
|
+
if (internalAbort.signal.aborted) {
|
|
4708
|
+
wasAborted = true;
|
|
4709
|
+
}
|
|
5015
4710
|
// Terminal handling — the loop exited without a model-initiated finish
|
|
5016
4711
|
// (step budget exhausted, or the turn was aborted). Never return "":
|
|
5017
4712
|
// force the reserved final_result step on structured-output turns,
|