fix(agent): recover read timeouts and align model waits with safe UTC deployment checks
This commit is contained in:
parent
59994062c9
commit
7925545980
@ -188,7 +188,9 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
|||||||
await ports.consumeInputs(snapshot);
|
await ports.consumeInputs(snapshot);
|
||||||
});
|
});
|
||||||
if (await ports.isCancelled()) return await save("已停止后续处理,完成的结果保留在画布。", "cancelled");
|
if (await ports.isCancelled()) return await save("已停止后续处理,完成的结果保留在画布。", "cancelled");
|
||||||
if (snapshot.calls.some(call => (call.status === "failed" && !call.rejected) || call.uncertain))
|
// A failed read is not an uncertain paid operation. Independently verify
|
||||||
|
// actual outputs below instead of failing a task because an earlier read timed out.
|
||||||
|
if (snapshot.calls.some(call => (call.status === "failed" && !call.rejected && !ports.tools[call.name]?.readOnly) || call.uncertain))
|
||||||
return await save("部分工具结果待确认,已保留执行记录,请核对后继续。", "needs_review");
|
return await save("部分工具结果待确认,已保留执行记录,请核对后继续。", "needs_review");
|
||||||
if (question || snapshot.calls.some(call => call.output?.mode === "interactive"))
|
if (question || snapshot.calls.some(call => call.output?.mode === "interactive"))
|
||||||
return await save(question || "请在已打开的工具面板中完成操作,然后继续任务。", "needs_review");
|
return await save(question || "请在已打开的工具面板中完成操作,然后继续任务。", "needs_review");
|
||||||
@ -204,12 +206,17 @@ export async function runZcodeCanvasTask(ports: Ports): Promise<CanvasAgentRunSt
|
|||||||
// still use the existing independent checks against actual saved nodes.
|
// still use the existing independent checks against actual saved nodes.
|
||||||
const mutated = snapshot.calls.some(call => !call.rejected && !ports.tools[call.name]?.readOnly);
|
const mutated = snapshot.calls.some(call => !call.rejected && !ports.tools[call.name]?.readOnly);
|
||||||
if (!mutated && !snapshot.requirements?.expectedImageCount) {
|
if (!mutated && !snapshot.requirements?.expectedImageCount) {
|
||||||
|
const unresolvedRead = snapshot.calls.some((call, index) => call.status === "failed" && !call.rejected &&
|
||||||
|
!snapshot.calls.slice(index + 1).some(later => later.name === call.name && later.status === "succeeded" &&
|
||||||
|
JSON.stringify(later.input) === JSON.stringify(call.input)));
|
||||||
|
if (unresolvedRead) return await save("图片或资料暂未读取成功,已保留记录,请稍后继续核对。", "needs_review");
|
||||||
const expectsChange = /生成|抠图|去背景|换背景|删除|移除|替换|改名|移动|放大|裁剪|旋转|导出|下载|制作|添加|上传/.test(snapshot.objective)
|
const expectsChange = /生成|抠图|去背景|换背景|删除|移除|替换|改名|移动|放大|裁剪|旋转|导出|下载|制作|添加|上传/.test(snapshot.objective)
|
||||||
&& !/只(?:要|需)?(?:分析|讨论|建议|提示词)|不要(?:执行|生成|修改)/.test(snapshot.objective);
|
&& !/只(?:要|需)?(?:分析|讨论|建议|提示词)|不要(?:执行|生成|修改)/.test(snapshot.objective);
|
||||||
return await save(expectsChange ? "尚未执行画布操作,请核对任务计划后继续。" : result.response || "已完成画布分析。", expectsChange ? "needs_review" : "completed");
|
return await save(expectsChange ? "尚未执行画布操作,请核对任务计划后继续。" : result.response || "已完成画布分析。", expectsChange ? "needs_review" : "completed");
|
||||||
}
|
}
|
||||||
await save("正在核对画布中的实际结果");
|
await save("正在核对画布中的实际结果");
|
||||||
const reviewSnapshot = { ...snapshot, calls: snapshot.calls.filter(call => !call.rejected) };
|
const reviewSnapshot = { ...snapshot, calls: snapshot.calls.filter(call => !call.rejected &&
|
||||||
|
!(call.status === "failed" && ports.tools[call.name]?.readOnly)) };
|
||||||
const verification = await ports.verify(reviewSnapshot); snapshot.verification = verification;
|
const verification = await ports.verify(reviewSnapshot); snapshot.verification = verification;
|
||||||
snapshot.inputTokens = reviewSnapshot.inputTokens; snapshot.outputTokens = reviewSnapshot.outputTokens;
|
snapshot.inputTokens = reviewSnapshot.inputTokens; snapshot.outputTokens = reviewSnapshot.outputTokens;
|
||||||
if (verification.passed) return await save(result.response || "任务完成,结果已写入画布。", "completed");
|
if (verification.passed) return await save(result.response || "任务完成,结果已写入画布。", "completed");
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { tool } from 'ai';
|
import { tool } from 'ai';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { runZcodeCanvasTask } from '../../lib/ai/zcode-agent/task';
|
import { runZcodeCanvasTask } from '../../lib/ai/zcode-agent/task';
|
||||||
import { AgentToolRejectedError, newAgentRunSnapshot } from '../../lib/ai/canvas-agent-runtime/protocol';
|
import { AgentToolRejectedError, newAgentRunSnapshot, type AgentRunSnapshot } from '../../lib/ai/canvas-agent-runtime/protocol';
|
||||||
import type { runNativeZcode } from '../../services/zcode-agent/runner.mjs';
|
import type { runNativeZcode } from '../../services/zcode-agent/runner.mjs';
|
||||||
type NativeOptions = Parameters<typeof runNativeZcode>[0];
|
type NativeOptions = Parameters<typeof runNativeZcode>[0];
|
||||||
function fixture(objective = '修改图片') {
|
function fixture(objective = '修改图片') {
|
||||||
@ -12,7 +12,7 @@ function fixture(objective = '修改图片') {
|
|||||||
const ports = { runId: crypto.randomUUID(), snapshot, definitions, instructions: '', context: {},
|
const ports = { runId: crypto.randomUUID(), snapshot, definitions, instructions: '', context: {},
|
||||||
gateway: { modelId: 'fixture', apiType: 'openai-chat-completions', forward: async () => Response.json({}) }, signal: new AbortController().signal,
|
gateway: { modelId: 'fixture', apiType: 'openai-chat-completions', forward: async () => Response.json({}) }, signal: new AbortController().signal,
|
||||||
checkpoint: async (_snapshot: unknown, status: string) => { saved.push(status); }, consumeInputs: async () => ({ cancelled: false }), isCancelled: async () => false,
|
checkpoint: async (_snapshot: unknown, status: string) => { saved.push(status); }, consumeInputs: async () => ({ cancelled: false }), isCancelled: async () => false,
|
||||||
verify: async () => ({ passed: true, issues: [] as string[] }),
|
verify: async (_snapshot: AgentRunSnapshot) => ({ passed: true, issues: [] as string[] }),
|
||||||
tools: { planCanvasTask: { title: '计划', readOnly: true, concurrentSafe: true, execute: async () => ({ steps: ['修改图片'] }) },
|
tools: { planCanvasTask: { title: '计划', readOnly: true, concurrentSafe: true, execute: async () => ({ steps: ['修改图片'] }) },
|
||||||
readCanvas: { title: '读取', readOnly: true, concurrentSafe: true, execute: async () => ({ nodes: [] }) },
|
readCanvas: { title: '读取', readOnly: true, concurrentSafe: true, execute: async () => ({ nodes: [] }) },
|
||||||
edit: { title: '修改', readOnly: false, concurrentSafe: false, execute: async () => { writes++; return { updatedNodeIds: ['a'] }; } } },
|
edit: { title: '修改', readOnly: false, concurrentSafe: false, execute: async () => { writes++; return { updatedNodeIds: ['a'] }; } } },
|
||||||
@ -118,6 +118,56 @@ let checks = 0;
|
|||||||
};
|
};
|
||||||
assert.equal(await runZcodeCanvasTask(ports), 'completed');assert.equal(aborted,false);checks++;
|
assert.equal(await runZcodeCanvasTask(ports), 'completed');assert.equal(aborted,false);checks++;
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
const h = fixture(); let reads = 0, reviews = 0;
|
||||||
|
h.ports.tools.readCanvas.execute = async () => { if (++reads === 1) throw new Error('The operation was aborted due to timeout'); return { nodes: [] }; };
|
||||||
|
h.ports.verify = async snapshot => { reviews++; assert(!snapshot.calls.some(call => call.status === 'failed')); return { passed: true, issues: [] }; };
|
||||||
|
h.ports.native = async options => {
|
||||||
|
await options.execute('planCanvasTask', {}, 'plan', options.signal!);
|
||||||
|
await options.execute('edit', { nodeIds: ['a'] }, 'edit', options.signal!);
|
||||||
|
await assert.rejects(options.execute('readCanvas', {}, 'read-timeout', options.signal!));
|
||||||
|
await options.execute('readCanvas', {}, 'read-recovered', options.signal!); return result;
|
||||||
|
};
|
||||||
|
assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.writes(), 1); assert.equal(reviews, 1);
|
||||||
|
assert.equal(h.snapshot.calls.find(call => call.id === 'read-timeout')?.status, 'failed'); checks++;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const h = fixture('画布里有什么');
|
||||||
|
h.ports.tools.readCanvas.execute = async () => { throw new Error('read timeout'); };
|
||||||
|
h.ports.native = async options => { await assert.rejects(options.execute('readCanvas', {}, 'read', options.signal!)); return result; };
|
||||||
|
assert.equal(await runZcodeCanvasTask(h.ports), 'needs_review'); assert.equal(h.writes(), 0); checks++;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const h = fixture('画布里有什么'); let reads = 0;
|
||||||
|
h.ports.tools.readCanvas.execute = async () => { if (++reads === 1) throw new Error('read timeout'); return { nodes: [] }; };
|
||||||
|
h.ports.native = async options => {
|
||||||
|
await assert.rejects(options.execute('readCanvas', {}, 'failed-read', options.signal!));
|
||||||
|
await options.execute('readCanvas', {}, 'recovered-read', options.signal!); return result;
|
||||||
|
};
|
||||||
|
assert.equal(await runZcodeCanvasTask(h.ports), 'completed'); assert.equal(h.writes(), 0); checks++;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const h = fixture();
|
||||||
|
h.ports.tools.readCanvas.execute = async () => { throw new Error('read timeout'); };
|
||||||
|
h.ports.verify = async () => ({ passed: false, issues: ['实际结果不符合要求'] });
|
||||||
|
h.ports.native = async options => {
|
||||||
|
await options.execute('planCanvasTask', {}, 'plan', options.signal!);
|
||||||
|
await options.execute('edit', { nodeIds: ['a'] }, 'edit', options.signal!);
|
||||||
|
await assert.rejects(options.execute('readCanvas', {}, 'failed-read', options.signal!)); return result;
|
||||||
|
};
|
||||||
|
assert.equal(await runZcodeCanvasTask(h.ports), 'needs_review'); assert.match(h.snapshot.message, /实际结果不符合要求/); checks++;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const h = fixture();
|
||||||
|
h.ports.tools.edit.execute = async () => { throw new Error('paid request outcome unknown'); };
|
||||||
|
h.ports.native = async options => {
|
||||||
|
await options.execute('planCanvasTask', {}, 'plan', options.signal!);
|
||||||
|
await assert.rejects(options.execute('edit', { nodeIds: ['a'] }, 'uncertain-paid', options.signal!));
|
||||||
|
await options.execute('readCanvas', {}, 'read', options.signal!); return result;
|
||||||
|
};
|
||||||
|
assert.equal(await runZcodeCanvasTask(h.ports), 'needs_review');
|
||||||
|
assert.equal(h.snapshot.calls.find(call => call.id === 'uncertain-paid')?.uncertain, true); checks++;
|
||||||
|
}
|
||||||
console.log('NATIVE_TASK_ADAPTER_PASSED', { checks });
|
console.log('NATIVE_TASK_ADAPTER_PASSED', { checks });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,7 +112,9 @@ export async function runNativeZcode(options) {
|
|||||||
const nativeNode = linux ? '/runtime/bin/node' : options.nodePath;
|
const nativeNode = linux ? '/runtime/bin/node' : options.nodePath;
|
||||||
const configuration = { features: { mcp: true, subagent: true, memory: true, skill: true },
|
const configuration = { features: { mcp: true, subagent: true, memory: true, skill: true },
|
||||||
plugins: { enabled: true }, skills: { enabled: true }, hooks: { enabled: false },
|
plugins: { enabled: true }, skills: { enabled: true }, hooks: { enabled: false },
|
||||||
memory: { use: true }, ui: { locale: 'zh-CN' }, modelStream: { idleTimeoutMs: 120000 },
|
// The host gateway can spend 150 seconds trying alternate models before
|
||||||
|
// returning a buffered answer. The native client must outlast that budget.
|
||||||
|
memory: { use: true }, ui: { locale: 'zh-CN' }, modelStream: { idleTimeoutMs: 180000 },
|
||||||
mcp: { servers: { canvas: { type: 'stdio', command: nativeNode, args: [bridge, 'mcp'],
|
mcp: { servers: { canvas: { type: 'stdio', command: nativeNode, args: [bridge, 'mcp'],
|
||||||
enabled: true, timeoutMs: 20 * 60_000 } } } };
|
enabled: true, timeoutMs: 20 * 60_000 } } } };
|
||||||
await writeFile(join(home, '.zcode/cli/config.json'), JSON.stringify(configuration), { mode: 0o600 });
|
await writeFile(join(home, '.zcode/cli/config.json'), JSON.stringify(configuration), { mode: 0o600 });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user