You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/13 19:58:07 UTC
(maka) branch fix/runtime-undispatched-tool-retry created (now cfafe805c2)
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a change to branch fix/runtime-undispatched-tool-retry
in repository https://gitbox.apache.org/repos/asf/maka.git
at cfafe805c2 fix(runtime): retry undispatched local tool intents safely
This branch includes the following new commits:
new cfafe805c2 fix(runtime): retry undispatched local tool intents safely
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/01: fix(runtime): retry undispatched local tool intents safely
Posted by as...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Astro-Han pushed a commit to branch fix/runtime-undispatched-tool-retry
in repository https://gitbox.apache.org/repos/asf/maka.git
commit cfafe805c26d99286b27e9a1ce82f1206a2da97f
Author: AstroHan <le...@outlook.com>
AuthorDate: Mon Sep 14 03:58:02 2026 +0800
fix(runtime): retry undispatched local tool intents safely
Generated-by: Codex
---
.../runtime/src/__tests__/ai-sdk-backend.test.ts | 119 ++++++++++++++++++++-
packages/runtime/src/ai-sdk-turn.ts | 5 +-
2 files changed, 121 insertions(+), 3 deletions(-)
diff --git a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
index 9cdf8b4afc..e30c8dc7a2 100644
--- a/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
+++ b/packages/runtime/src/__tests__/ai-sdk-backend.test.ts
@@ -8207,6 +8207,109 @@ describe('AiSdkBackend usage telemetry', () => {
assert.equal(events.find((event) => event.type === 'complete')?.stopReason, 'end_turn');
});
+ for (const visibleText of [false, true]) {
+ test(`isolates undispatched local calls after truncation (visible text: ${visibleText})`, async () => {
+ const durable = durableTurnHarness('turn-local-truncated', 'do the work');
+ const executed: string[] = [];
+ const assistants: AssistantMessage[] = [];
+ let calls = 0;
+ const model = new MockLanguageModelV4({
+ doStream: async () => {
+ calls += 1;
+ const chunks: LanguageModelV4StreamPart[] = [{ type: 'stream-start', warnings: [] }];
+ if (calls === 2 && visibleText) {
+ chunks.push(
+ { type: 'text-start', id: 'partial' },
+ { type: 'text-delta', id: 'partial', delta: 'Partial answer' },
+ );
+ }
+ if (calls < 4) {
+ chunks.push({
+ type: 'tool-call',
+ toolCallId: `call-${calls}`,
+ toolName: 'Write',
+ input: JSON.stringify({ value: ['prior', 'discarded', 'fresh'][calls - 1] }),
+ providerExecuted: false,
+ });
+ } else {
+ chunks.push(
+ { type: 'text-start', id: 'final' },
+ { type: 'text-delta', id: 'final', delta: 'Done' },
+ { type: 'text-end', id: 'final' },
+ );
+ }
+ if (calls !== 2) {
+ chunks.push({
+ type: 'finish',
+ finishReason: { unified: calls < 4 ? 'tool-calls' : 'stop', raw: 'stop' },
+ usage: {
+ inputTokens: { total: 7, noCache: 7, cacheRead: 0, cacheWrite: 0 },
+ outputTokens: { total: 3, text: 3, reasoning: 0 },
+ },
+ });
+ }
+ return {
+ stream: simulateReadableStream({
+ chunks,
+ initialDelayInMs: null,
+ chunkDelayInMs: null,
+ }),
+ };
+ },
+ });
+ const backend = createBackend({
+ connection: connection(),
+ modelId: 'mock-model-id',
+ modelFactory: () => model,
+ tools: [
+ {
+ ...testTool('Write', z.object({ value: z.string() })),
+ impl: async (input) => {
+ executed.push((input as { value: string }).value);
+ return { ok: true };
+ },
+ },
+ ],
+ appendMessage: async (message) => {
+ if (message.type === 'assistant') assistants.push(message);
+ },
+ loadTurnRuntimeEvents: durable.loadTurnRuntimeEvents,
+ providerRetrySleep: async () => {},
+ });
+ const events = await drainDurably(backend.send(durable.input()), durable);
+ assert.equal(calls, visibleText ? 2 : 4);
+ assert.deepEqual(executed, visibleText ? ['prior'] : ['prior', 'fresh']);
+ assert.equal(
+ events.some(
+ (event) =>
+ (event.type === 'tool_start' || event.type === 'tool_result') &&
+ event.toolUseId === 'call-2',
+ ),
+ false,
+ );
+ assert.equal(JSON.stringify(durable.ledger).includes('discarded'), false);
+ assert.deepEqual(
+ assistants.map((message) => message.text),
+ [visibleText ? 'Partial answer' : 'Done'],
+ );
+ const error = events.find((event) => event.type === 'error');
+ if (visibleText) {
+ assert.deepEqual(error?.retry, { decision: 'declined', because: 'observable_output' });
+ } else {
+ assert.equal(error, undefined);
+ assert.equal(
+ events.some((event) => event.type === 'token_usage'),
+ false,
+ );
+ assert.equal(JSON.stringify(model.doStreamCalls.slice(2)).includes('discarded'), false);
+ }
+ assert.equal(
+ events.find((event) => event.type === 'complete')?.stopReason,
+ visibleText ? 'error' : 'end_turn',
+ );
+ });
+ }
+
test('records exhaustion of output-free truncated stream recovery', async () => {
const durable = durableTurnHarness('turn-truncated-exhausted', 'analyse the image');
let calls = 0;
@@ -10414,7 +10517,7 @@ describe('AiSdkBackend RunTrace', () => {
assert.notEqual(assistants[0]?.id, assistants[1]?.id);
});
- test('retries a retryable network failure after partial thinking by sealing it', async () => {
+ test('seals partial thinking and discards local tool intents after a retryable network failure', async () => {
// Incident shape: the provider streamed thinking deltas, then the
// connection reset mid-step (ECONNRESET after ~120s). Recovery safety
// depends on what the attempt emitted, not on which side detected the
@@ -10452,6 +10555,13 @@ describe('AiSdkBackend RunTrace', () => {
{ type: 'stream-start', warnings: [] },
{ type: 'reasoning-start', id: 'reasoning-1' },
{ type: 'reasoning-delta', id: 'reasoning-1', delta: 'partial thought' },
+ {
+ type: 'tool-call',
+ toolCallId: 'discarded-call',
+ toolName: 'Write',
+ input: '{"value":"discarded"}',
+ providerExecuted: false,
+ },
],
connectionResetFailure(),
);
@@ -10466,7 +10576,7 @@ describe('AiSdkBackend RunTrace', () => {
connection: connection(),
modelId: 'mock-model-id',
modelFactory: () => model,
- tools: [],
+ tools: [testTool('Write', z.object({ value: z.string() }))],
loadTurnRuntimeEvents: durable.loadTurnRuntimeEvents,
providerRetrySleep: async () => {},
});
@@ -10511,6 +10621,11 @@ describe('AiSdkBackend RunTrace', () => {
const retryPrompt = JSON.stringify(model.doStreamCalls[1]?.prompt);
assert.equal(retryPrompt.includes('partial thought'), false);
assert.match(retryPrompt, /review the commits/);
+ assert.equal(
+ events.some((event) => event.type === 'tool_start' || event.type === 'tool_result'),
+ false,
+ );
+ assert.equal(JSON.stringify(durable.ledger).includes('discarded-call'), false);
});
test('retries a retryable network failure before any observable output', async () => {
diff --git a/packages/runtime/src/ai-sdk-turn.ts b/packages/runtime/src/ai-sdk-turn.ts
index 7e11505dad..dfb7a1c263 100644
--- a/packages/runtime/src/ai-sdk-turn.ts
+++ b/packages/runtime/src/ai-sdk-turn.ts
@@ -1542,6 +1542,9 @@ export class AiSdkTurn {
const providerToolInputs = new Map<string, unknown>();
let providerStepUsage: NormalizedUsage | undefined;
for (;;) {
+ // Local calls are only admitted after a successful provider outcome.
+ // A new physical request must not inherit the failed one's intents.
+ returnedToolCalls.length = 0;
providerRequestAbortController = new AbortController();
watchdogTimeoutState.current = null;
startWatchdog();
@@ -1945,9 +1948,9 @@ export class AiSdkTurn {
// work that the Runtime cannot observe or reconcile.
attemptSawToolActivity = true;
} else if (event.kind === 'tool-call') {
- attemptSawToolActivity = true;
recordStepContent('tools');
if (event.toolCall.providerExecuted) {
+ attemptSawToolActivity = true;
providerToolActivityCount += 1;
providerToolInputs.set(event.toolCall.toolCallId, event.toolCall.input);
queue.push({
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org