@xiaohhhh1/canvas-agent 0.4.54 → 0.4.56
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/dist/agent/codex-client.js +25 -3
- package/dist/agent/codex.d.ts +10 -0
- package/dist/agent/codex.js +42 -18
- package/dist/workflow/manager.d.ts +12 -1
- package/dist/workflow/manager.js +62 -36
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@ const canvasAgentMcp = canvasAgentMcpCommand();
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
10
|
const STREAM_UPDATE_INTERVAL_MS = 40;
|
|
11
11
|
const TURN_RECONCILE_INTERVAL_MS = 5_000;
|
|
12
|
+
const APP_SERVER_INITIALIZE_TIMEOUT_MS = 30_000;
|
|
12
13
|
/** 封装 Codex app-server 的 JSON-RPC 通信与事件转换。 */
|
|
13
14
|
export class CodexAppClient {
|
|
14
15
|
child;
|
|
@@ -52,9 +53,15 @@ export class CodexAppClient {
|
|
|
52
53
|
onExit();
|
|
53
54
|
emit("agent_log", { text: `Codex app-server exited: ${code ?? 0}` });
|
|
54
55
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
try {
|
|
57
|
+
await withTimeout(client.request("initialize", { clientInfo: { name: "canvas-agent", title: "Infinite Canvas Agent", version: VERSION }, capabilities: { experimentalApi: true, requestAttestation: false } }), APP_SERVER_INITIALIZE_TIMEOUT_MS, "Codex app-server initialize timed out");
|
|
58
|
+
client.notify("initialized");
|
|
59
|
+
return client;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
await client.terminate("Codex app-server 初始化超时,已回收失去响应的进程");
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
/** 创建新的 Codex 线程。 */
|
|
60
67
|
async startThread(cwd, permissionMode = "request", modelSettings = {}) {
|
|
@@ -408,6 +415,21 @@ export class CodexAppClient {
|
|
|
408
415
|
this.currentTurnId = "";
|
|
409
416
|
}
|
|
410
417
|
}
|
|
418
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
419
|
+
let timer;
|
|
420
|
+
try {
|
|
421
|
+
return await Promise.race([
|
|
422
|
+
promise,
|
|
423
|
+
new Promise((_resolve, reject) => {
|
|
424
|
+
timer = setTimeout(() => reject(new Error(message)), Math.max(1, timeoutMs));
|
|
425
|
+
}),
|
|
426
|
+
]);
|
|
427
|
+
}
|
|
428
|
+
finally {
|
|
429
|
+
if (timer)
|
|
430
|
+
clearTimeout(timer);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
411
433
|
/** 只有正常 completed 才能被调用方视为可用脚本。 */
|
|
412
434
|
export function isTerminalTurnStatus(status) {
|
|
413
435
|
return ["completed", "failed", "interrupted", "cancelled", "canceled"].includes(String(status || "").toLowerCase());
|
package/dist/agent/codex.d.ts
CHANGED
|
@@ -56,6 +56,16 @@ export declare function runCodexWorkflowTurn(prompt: string, emit: AgentEmit, op
|
|
|
56
56
|
onWorkerStart?: () => void;
|
|
57
57
|
onWorkerFinish?: () => void;
|
|
58
58
|
}): Promise<CodexWorkflowRunResult>;
|
|
59
|
+
/**
|
|
60
|
+
* 给 Flow C 的完整 app-server 链路设置硬截止时间。超时后不再等待原 Promise
|
|
61
|
+
* 自行结束;否则初始化或 thread/start 永不返回时会永久占住 worker lane。
|
|
62
|
+
*/
|
|
63
|
+
export declare function runBoundedWorkflowOperation<T>(operation: Promise<T>, timeoutMs: number, onTimeout: () => Promise<void> | void): Promise<{
|
|
64
|
+
timedOut: false;
|
|
65
|
+
value: T;
|
|
66
|
+
} | {
|
|
67
|
+
timedOut: true;
|
|
68
|
+
}>;
|
|
59
69
|
/** Contract/preflight 4xx errors are deterministic and must never burn every fallback chunk size. */
|
|
60
70
|
export declare function isDeterministicWorkflowContractError(error: unknown): boolean;
|
|
61
71
|
/** 强制回收失去响应的 app-server;只用于有界超时恢复。 */
|
package/dist/agent/codex.js
CHANGED
|
@@ -44,26 +44,26 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
44
44
|
const threadStartedAt = Date.now();
|
|
45
45
|
let threadStartMs = 0;
|
|
46
46
|
let modelStartedAt = 0;
|
|
47
|
-
let timer;
|
|
48
47
|
let app = workflowCodexApps.get(workerIndex);
|
|
49
48
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
const operation = (async () => {
|
|
50
|
+
if (!app)
|
|
51
|
+
app = await startWorkflowCodexApp(workerIndex, options.appEmit || emit);
|
|
52
|
+
const thread = await app.startThread(options.cwd, options.permissionMode || "request", modelSettings);
|
|
53
|
+
const threadId = String(field(thread, "id") || "");
|
|
54
|
+
options.onThread?.(threadId);
|
|
55
|
+
threadStartMs = Date.now() - threadStartedAt;
|
|
56
|
+
modelStartedAt = Date.now();
|
|
57
|
+
return await app.startTurn(threadId, prompt, [], options.permissionMode || "request", options.onTurn, options.outputSchema, modelSettings);
|
|
58
|
+
})();
|
|
59
|
+
const result = await runBoundedWorkflowOperation(operation, options.timeoutMs, async () => {
|
|
61
60
|
workflowCodexApps.delete(workerIndex);
|
|
62
|
-
await app
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
await app?.terminate("Flow C 脚本执行链路超时,已仅回收当前 worker");
|
|
62
|
+
});
|
|
63
|
+
if (result.timedOut) {
|
|
64
|
+
return { ok: false, error: "Flow C 脚本执行链路超过 8 分钟,已自动终止当前 worker", retryable: true, timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
65
65
|
}
|
|
66
|
-
return { ok: true, text: result, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
66
|
+
return { ok: true, text: result.value, timings: { queueWaitMs, threadStartMs, modelMs: Date.now() - modelStartedAt } };
|
|
67
67
|
}
|
|
68
68
|
catch (error) {
|
|
69
69
|
logger.error("Flow C Codex worker failed", { workerIndex, error });
|
|
@@ -72,12 +72,36 @@ export async function runCodexWorkflowTurn(prompt, emit, options) {
|
|
|
72
72
|
return { ok: false, error: message, retryable: !isDeterministicWorkflowContractError(message), timings: { queueWaitMs, threadStartMs: threadStartMs || Date.now() - threadStartedAt, modelMs: modelStartedAt ? Date.now() - modelStartedAt : 0 } };
|
|
73
73
|
}
|
|
74
74
|
finally {
|
|
75
|
-
if (timer)
|
|
76
|
-
clearTimeout(timer);
|
|
77
75
|
options.onWorkerFinish?.();
|
|
78
76
|
}
|
|
79
77
|
});
|
|
80
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 给 Flow C 的完整 app-server 链路设置硬截止时间。超时后不再等待原 Promise
|
|
81
|
+
* 自行结束;否则初始化或 thread/start 永不返回时会永久占住 worker lane。
|
|
82
|
+
*/
|
|
83
|
+
export async function runBoundedWorkflowOperation(operation, timeoutMs, onTimeout) {
|
|
84
|
+
let timer;
|
|
85
|
+
const timeout = new Promise((resolve) => {
|
|
86
|
+
timer = setTimeout(() => resolve({ timedOut: true }), Math.max(1, timeoutMs));
|
|
87
|
+
});
|
|
88
|
+
try {
|
|
89
|
+
const result = await Promise.race([
|
|
90
|
+
operation.then((value) => ({ timedOut: false, value })),
|
|
91
|
+
timeout,
|
|
92
|
+
]);
|
|
93
|
+
if (!result.timedOut)
|
|
94
|
+
return result;
|
|
95
|
+
// Avoid an unhandled rejection if process termination settles the abandoned operation later.
|
|
96
|
+
void operation.catch(() => undefined);
|
|
97
|
+
await onTimeout();
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
if (timer)
|
|
102
|
+
clearTimeout(timer);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
81
105
|
/** Contract/preflight 4xx errors are deterministic and must never burn every fallback chunk size. */
|
|
82
106
|
export function isDeterministicWorkflowContractError(error) {
|
|
83
107
|
const message = errorMessage(error);
|
|
@@ -270,7 +270,6 @@ export declare class WorkflowManager {
|
|
|
270
270
|
private pumpScriptQueue;
|
|
271
271
|
private finishDownloadDirectorySelection;
|
|
272
272
|
private runScript;
|
|
273
|
-
private ensureCreativeCandidateSelections;
|
|
274
273
|
private runCandidateChunk;
|
|
275
274
|
/** 一个 chunk 使用独立线程;解析与中心持久化失败不会影响并行 lane。 */
|
|
276
275
|
private runScriptChunk;
|
|
@@ -296,5 +295,17 @@ export declare function terminalScriptChunkError(results: Array<{
|
|
|
296
295
|
}>): string;
|
|
297
296
|
export declare function scriptChunkPrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
298
297
|
export declare function creativeCandidatePrompt(id: string, task: ScriptTask, ordinals: number[]): string;
|
|
298
|
+
/**
|
|
299
|
+
* 已选创意永远优先进入脚本扩写;只有当前没有可写脚本时才生成下一小波候选。
|
|
300
|
+
* 每次最多占用受控 worker 数量的子批,避免把整批候选提前塞满 worker 队列。
|
|
301
|
+
*/
|
|
302
|
+
export declare function nextScriptPipelineWave(task: Pick<ScriptTask, "requested_count" | "selected_candidates">, receivedOrdinals: number[], durationSeconds: 10 | 20 | 30, scriptChunkSize: number, candidateChunkSize?: number, concurrency?: number): {
|
|
303
|
+
stage: "script";
|
|
304
|
+
chunks: number[][];
|
|
305
|
+
} | {
|
|
306
|
+
stage: "candidate";
|
|
307
|
+
chunks: number[][];
|
|
308
|
+
};
|
|
309
|
+
export declare function immediateScriptOrdinals(task: Pick<ScriptTask, "selected_candidates">, receivedOrdinals: number[], candidateOrdinals: number[]): number[];
|
|
299
310
|
export declare function missingOrdinals(total: number, received: number[]): number[];
|
|
300
311
|
export {};
|
package/dist/workflow/manager.js
CHANGED
|
@@ -244,7 +244,6 @@ export class WorkflowManager {
|
|
|
244
244
|
if (Date.parse(task.expires_at) <= Date.now())
|
|
245
245
|
throw new ExpiredCapabilityError("脚本交接已过期,请在网页重新点击交给本机 Codex");
|
|
246
246
|
const workspace = ensureSiteWorkspace(this.config);
|
|
247
|
-
task = await this.ensureCreativeCandidateSelections(id, task, workspace.workspacePath);
|
|
248
247
|
const durationSeconds = Number(task.duration_seconds || 10);
|
|
249
248
|
const chunkSizes = flowCScriptChunkSizes(durationSeconds);
|
|
250
249
|
// Fail locally before starting any worker if a future schema edit
|
|
@@ -253,17 +252,55 @@ export class WorkflowManager {
|
|
|
253
252
|
flowCScriptOutputSchema(durationSeconds, chunkSize);
|
|
254
253
|
record.activeChunks = 0;
|
|
255
254
|
let chunkSizeIndex = 0;
|
|
255
|
+
let candidateChunkSize = 2;
|
|
256
256
|
while (record.receivedOrdinals.length < task.requested_count) {
|
|
257
|
+
task = await this.scriptTask(id);
|
|
258
|
+
const selectedBefore = selectedCandidateOrdinals(task);
|
|
259
|
+
const wave = nextScriptPipelineWave(task, record.receivedOrdinals, durationSeconds, chunkSizes[chunkSizeIndex], candidateChunkSize);
|
|
260
|
+
if (wave.stage === "candidate") {
|
|
261
|
+
const receivedBefore = record.receivedOrdinals.length;
|
|
262
|
+
record.message = `本机 Codex 正在选择下一小批创意(${selectedBefore.length}/${task.requested_count});选好后立即写对应脚本`;
|
|
263
|
+
record.activeChunks = 0;
|
|
264
|
+
record.updatedAt = now();
|
|
265
|
+
this.save();
|
|
266
|
+
const pipelineResults = await Promise.all(wave.chunks.map(async (ordinals) => {
|
|
267
|
+
const candidateResult = await this.runCandidateChunk(id, task, ordinals, workspace.workspacePath);
|
|
268
|
+
if (candidateResult.error)
|
|
269
|
+
return [candidateResult];
|
|
270
|
+
const selectedTask = await this.scriptTask(id);
|
|
271
|
+
const scriptOrdinals = immediateScriptOrdinals(selectedTask, this.scriptRecord(id).receivedOrdinals, ordinals);
|
|
272
|
+
if (!scriptOrdinals.length)
|
|
273
|
+
return [candidateResult];
|
|
274
|
+
this.scriptRecord(id).attempts += 1;
|
|
275
|
+
const scriptResult = await this.runScriptChunk(id, selectedTask, scriptOrdinals, workspace.workspacePath);
|
|
276
|
+
return [candidateResult, scriptResult];
|
|
277
|
+
}));
|
|
278
|
+
const results = pipelineResults.flat();
|
|
279
|
+
task = await this.scriptTask(id);
|
|
280
|
+
const terminalError = terminalScriptChunkError(results);
|
|
281
|
+
if (terminalError)
|
|
282
|
+
throw new Error(`创意或脚本结构化契约被 Codex 拒绝,已停止自动重试(${terminalError})`);
|
|
283
|
+
if (record.receivedOrdinals.length > receivedBefore)
|
|
284
|
+
chunkSizeIndex = 0;
|
|
285
|
+
if (selectedCandidateOrdinals(task).length > selectedBefore.length) {
|
|
286
|
+
candidateChunkSize = 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (candidateChunkSize > 1) {
|
|
290
|
+
candidateChunkSize = 1;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const lastError = results.map((result) => result.error).filter(Boolean).at(-1) || "候选生成或中心选题未返回结果";
|
|
294
|
+
throw new Error(`创意候选阶段已隔离到单条仍失败(${lastError}),请点击重试`);
|
|
295
|
+
}
|
|
257
296
|
const before = record.receivedOrdinals.length;
|
|
258
|
-
const missing = missingOrdinals(task.requested_count, record.receivedOrdinals);
|
|
259
297
|
const chunkSize = chunkSizes[chunkSizeIndex];
|
|
260
|
-
const chunks = flowCScriptChunks(durationSeconds, missing, chunkSize);
|
|
261
298
|
record.chunkSize = chunkSize;
|
|
262
|
-
record.attempts += chunks.length;
|
|
263
|
-
record.message =
|
|
299
|
+
record.attempts += wave.chunks.length;
|
|
300
|
+
record.message = `创意已选 ${selectedBefore.length}/${task.requested_count};正在立即写 ${wave.chunks.length} 个对应脚本子批(已回传 ${before}/${task.requested_count})`;
|
|
264
301
|
record.updatedAt = now();
|
|
265
302
|
this.save();
|
|
266
|
-
const results = await Promise.all(chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
|
|
303
|
+
const results = await Promise.all(wave.chunks.map((ordinals) => this.runScriptChunk(id, task, ordinals, workspace.workspacePath)));
|
|
267
304
|
task = await this.scriptTask(id);
|
|
268
305
|
const terminalError = terminalScriptChunkError(results);
|
|
269
306
|
if (terminalError) {
|
|
@@ -304,36 +341,6 @@ export class WorkflowManager {
|
|
|
304
341
|
this.runningScripts.delete(id);
|
|
305
342
|
}
|
|
306
343
|
}
|
|
307
|
-
async ensureCreativeCandidateSelections(id, initialTask, cwd) {
|
|
308
|
-
let task = initialTask;
|
|
309
|
-
let chunkSize = 2;
|
|
310
|
-
while (selectedCandidateOrdinals(task).length < task.requested_count) {
|
|
311
|
-
const before = selectedCandidateOrdinals(task).length;
|
|
312
|
-
const missing = missingOrdinals(task.requested_count, selectedCandidateOrdinals(task));
|
|
313
|
-
const chunks = chunkNumbers(missing, chunkSize);
|
|
314
|
-
const record = this.scriptRecord(id);
|
|
315
|
-
record.message = `本机 Codex 正在生成可审核创意候选,中心将自动选题(${before}/${task.requested_count})`;
|
|
316
|
-
record.activeChunks = 0;
|
|
317
|
-
record.updatedAt = now();
|
|
318
|
-
this.save();
|
|
319
|
-
const results = await Promise.all(chunks.map((ordinals) => this.runCandidateChunk(id, task, ordinals, cwd)));
|
|
320
|
-
task = await this.scriptTask(id);
|
|
321
|
-
const terminalError = terminalScriptChunkError(results);
|
|
322
|
-
if (terminalError)
|
|
323
|
-
throw new Error(`创意候选结构化契约被 Codex 拒绝,已停止自动重试(${terminalError})`);
|
|
324
|
-
if (selectedCandidateOrdinals(task).length > before) {
|
|
325
|
-
chunkSize = 2;
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (chunkSize > 1) {
|
|
329
|
-
chunkSize = 1;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const lastError = results.map((result) => result.error).filter(Boolean).at(-1) || "候选生成或中心选题未返回结果";
|
|
333
|
-
throw new Error(`创意候选阶段已隔离到单条仍失败(${lastError}),请点击重试`);
|
|
334
|
-
}
|
|
335
|
-
return task;
|
|
336
|
-
}
|
|
337
344
|
async runCandidateChunk(id, task, ordinals, cwd) {
|
|
338
345
|
const result = await runCodexWorkflowTurn(creativeCandidatePrompt(id, task, ordinals), this.emit, {
|
|
339
346
|
cwd,
|
|
@@ -676,6 +683,25 @@ function chunkNumbers(values, size) {
|
|
|
676
683
|
chunks.push(values.slice(index, index + size));
|
|
677
684
|
return chunks;
|
|
678
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* 已选创意永远优先进入脚本扩写;只有当前没有可写脚本时才生成下一小波候选。
|
|
688
|
+
* 每次最多占用受控 worker 数量的子批,避免把整批候选提前塞满 worker 队列。
|
|
689
|
+
*/
|
|
690
|
+
export function nextScriptPipelineWave(task, receivedOrdinals, durationSeconds, scriptChunkSize, candidateChunkSize = 2, concurrency = FLOW_C_CODEX_WORKER_CONCURRENCY) {
|
|
691
|
+
const selected = selectedCandidateOrdinals(task);
|
|
692
|
+
const selectedSet = new Set(selected);
|
|
693
|
+
const scriptReady = missingOrdinals(task.requested_count, receivedOrdinals).filter((ordinal) => selectedSet.has(ordinal));
|
|
694
|
+
if (scriptReady.length) {
|
|
695
|
+
return { stage: "script", chunks: flowCScriptChunks(durationSeconds, scriptReady, scriptChunkSize).slice(0, concurrency) };
|
|
696
|
+
}
|
|
697
|
+
const candidateMissing = missingOrdinals(task.requested_count, selected);
|
|
698
|
+
return { stage: "candidate", chunks: chunkNumbers(candidateMissing, candidateChunkSize).slice(0, concurrency) };
|
|
699
|
+
}
|
|
700
|
+
export function immediateScriptOrdinals(task, receivedOrdinals, candidateOrdinals) {
|
|
701
|
+
const selected = new Set(selectedCandidateOrdinals(task));
|
|
702
|
+
const received = new Set(receivedOrdinals);
|
|
703
|
+
return candidateOrdinals.filter((ordinal) => selected.has(ordinal) && !received.has(ordinal));
|
|
704
|
+
}
|
|
679
705
|
function compactJson(value, limit) {
|
|
680
706
|
return JSON.stringify(value).slice(0, limit);
|
|
681
707
|
}
|