mocode-ai 1.6.8 → 1.6.10
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/model-turn.js +6 -1
- package/dist/agent/run-coordinator.js +215 -149
- package/dist/agent/stages/tool-dispatcher.js +150 -126
- package/dist/headless.js +27 -1
- package/dist/jobs/launch.js +14 -7
- package/dist/jobs/worktree.js +20 -0
- package/dist/llm/index.js +33 -1
- package/dist/llm/providers/vertex.js +15 -1
- package/dist/llm/proxy.js +24 -1
- package/dist/repl/commands/diff.js +189 -0
- package/dist/repl/commands/jobs.js +2 -2
- package/dist/repl/commands/model-actions.js +8 -0
- package/dist/repl/commands/registry.js +2 -0
- package/dist/repl/commands.js +1 -0
- package/dist/repl/runtime.js +29 -10
- package/dist/rollback/store.js +7 -0
- package/dist/runtime/dev-server-manager.js +27 -5
- package/dist/schedule/cli.js +1 -1
- package/dist/schedule/daemon.js +3 -3
- package/dist/skills/builtin-skills.js +99 -59
- package/dist/tools/builtins/index.js +6 -2
- package/dist/tools/builtins/run-command.js +32 -10
- package/dist/tools/policy.js +2 -2
- package/dist/tools/resource-lock.js +5 -1
- package/dist/tools/ripgrep-client.js +9 -1
- package/dist/ui/batch.js +5 -4
- package/package.json +1 -1
package/dist/agent/model-turn.js
CHANGED
|
@@ -21,7 +21,7 @@ function traceCauseCode(err) {
|
|
|
21
21
|
}
|
|
22
22
|
/** Executes context preparation plus exactly one model step, including the single overflow retry path. */
|
|
23
23
|
export async function runModelTurn(input) {
|
|
24
|
-
const { opts, ctx, history, historyManager, runtimeContextState, scheduler, contextTrimmer, modelRunner, activeTools, runPolicy, step, readDedup, cacheState, turnLifecycle, cancellationLifecycle, rebuildHistoryIndexes, } = input;
|
|
24
|
+
const { opts, ctx, history, historyManager, runtimeContextState, scheduler, contextTrimmer, modelRunner, activeTools, runPolicy, step, maxSteps, readDedup, cacheState, turnLifecycle, cancellationLifecycle, rebuildHistoryIndexes, } = input;
|
|
25
25
|
const { signal, onContextUpdate, hooks } = opts;
|
|
26
26
|
const { usageMeter, emitTrace } = turnLifecycle;
|
|
27
27
|
const requestBaseURL = ctx.config.baseURL;
|
|
@@ -105,11 +105,16 @@ export async function runModelTurn(input) {
|
|
|
105
105
|
const provider = ctx.safeProviderId(requestBaseURL);
|
|
106
106
|
emitTrace('model_start', { model: requestModel, provider });
|
|
107
107
|
const buildRequestHistory = () => {
|
|
108
|
+
const remainingSteps = maxSteps - step;
|
|
108
109
|
const ephemeralReminder = [
|
|
109
110
|
runPolicy.reminder,
|
|
110
111
|
!opts.suppressOpeningAnalysis && step === 0
|
|
111
112
|
? '## Opening analysis\nStart your first response of this turn with a brief analysis of the request and approach (1-3 sentences, no filler), then start tool calls. This is the only expected pre-tool prose; afterwards work quietly, no narration between calls.'
|
|
112
113
|
: '',
|
|
114
|
+
remainingSteps <= 3
|
|
115
|
+
? `## Step budget warning\nOnly ${remainingSteps} step(s) remain before the hard step limit (${maxSteps}). ` +
|
|
116
|
+
'Stop opening new lines of exploration NOW. Wind down: finish the current tool call if any, then use the remaining step(s) to produce your final summary — what you found, key files/evidence, and what remains undone. A terse partial report is far better than being cut off mid-work.'
|
|
117
|
+
: '',
|
|
113
118
|
historyRebuilt
|
|
114
119
|
? '## Post-compaction recovery\nContext was compacted before this request. Recover first, in this order:\n' +
|
|
115
120
|
'1. Read the session summary at the top of the history: `## Completed` is done — do not redo or re-verify it; `## In Progress` / `## Next Steps` say where work stopped and what is next.\n' +
|
|
@@ -177,6 +177,7 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
177
177
|
activeTools,
|
|
178
178
|
runPolicy,
|
|
179
179
|
step,
|
|
180
|
+
maxSteps,
|
|
180
181
|
readDedup,
|
|
181
182
|
cacheState: modelCacheState,
|
|
182
183
|
turnLifecycle,
|
|
@@ -378,127 +379,55 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
378
379
|
calls.forEach((tc, index) => (tc.name === ADD_TOOL_GROUPS_TOOL_NAME ? controlIndexes : otherIndexes).push(index));
|
|
379
380
|
const hasToolRouteBarrier = controlIndexes.length > 0;
|
|
380
381
|
if (hasToolRouteBarrier) {
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
382
|
+
// add_tool_groups 混批语义 = 部分执行:同批所有「未被禁用的并行安全(只读)」调用
|
|
383
|
+
// 本 step 照常并发执行,控制调用照常应用扩容(只改下一 step 的 schema);仅写/执行
|
|
384
|
+
// 等非并行调用被配对拒绝并提示下一 step 重试——单个违规调用不再让整批陪葬。
|
|
385
|
+
for (let index = 0; index < calls.length; index++) {
|
|
386
|
+
hooks.onToolHeader?.(calls[index]);
|
|
387
|
+
}
|
|
388
|
+
// 只读调用先并发启动(执行序不变);发布(history 回灌/事件)按 provider
|
|
389
|
+
// 声明序交错进行:批校验按 assistant tool_calls 逐位配对 id,若按执行序
|
|
390
|
+
// 发布(只读先、扩容后),add_tool_groups 排在只读之前时必然 id 错位,整轮 rollback。
|
|
391
|
+
// 非并行(写/执行/串行)调用不启动:它们与扩容同批时被拒绝,下一 step 重试。
|
|
392
|
+
const startedReadonly = new Array(calls.length);
|
|
393
|
+
const eligibleReadonly = otherIndexes.filter((index) => isParallelTool(calls[index].name, ctx.toolRuntime) && !isToolDeniedForStep(calls[index].name));
|
|
394
|
+
if (eligibleReadonly.length > 0) {
|
|
395
|
+
hooks.onToolStart?.(calls[eligibleReadonly[0]].name);
|
|
396
|
+
for (const index of eligibleReadonly) {
|
|
397
|
+
startedReadonly[index] = ctx.toolRuntime.executeToolOutcome(calls[index].name, calls[index].arguments, signal, {
|
|
391
398
|
callId: calls[index].id,
|
|
392
399
|
allowedToolNames: currentAllowedToolNames(),
|
|
393
400
|
delegation: delegationForOrchestrator(),
|
|
394
|
-
})
|
|
395
|
-
for (let k = 0; k < otherIndexes.length; k++) {
|
|
396
|
-
const index = otherIndexes[k];
|
|
397
|
-
const tc = calls[index];
|
|
398
|
-
const outcome = await startedReadonly[k];
|
|
399
|
-
usageMeter.add(outcome.usage);
|
|
400
|
-
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
401
|
-
traceToolEnd(tc, index, outcome);
|
|
402
|
-
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
403
|
-
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
404
|
-
}
|
|
405
|
-
hooks.onToolDone?.();
|
|
401
|
+
});
|
|
406
402
|
}
|
|
407
|
-
|
|
403
|
+
}
|
|
404
|
+
const lastReadonlyIndex = eligibleReadonly.length > 0 ? eligibleReadonly[eligibleReadonly.length - 1] : -1;
|
|
405
|
+
for (let index = 0; index < calls.length; index++) {
|
|
406
|
+
const readonlyPromise = startedReadonly[index];
|
|
407
|
+
if (readonlyPromise) {
|
|
408
408
|
const tc = calls[index];
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
status: 'denied',
|
|
414
|
-
code: 'TOOL_DISABLED',
|
|
415
|
-
retryable: false,
|
|
416
|
-
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
417
|
-
changedFiles: [],
|
|
418
|
-
durationMs: 0,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
else if (!opts.toolPolicy) {
|
|
422
|
-
outcome = {
|
|
423
|
-
status: 'denied',
|
|
424
|
-
code: 'TOOL_DISABLED',
|
|
425
|
-
retryable: false,
|
|
426
|
-
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
427
|
-
changedFiles: [],
|
|
428
|
-
durationMs: 0,
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
else if (!parsed ||
|
|
432
|
-
!Array.isArray(parsed.groups) ||
|
|
433
|
-
parsed.groups.length === 0 ||
|
|
434
|
-
typeof parsed.reason !== 'string' ||
|
|
435
|
-
!parsed.reason.trim()) {
|
|
436
|
-
outcome = {
|
|
437
|
-
status: 'error',
|
|
438
|
-
code: 'INVALID_ARGUMENTS',
|
|
439
|
-
retryable: false,
|
|
440
|
-
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
441
|
-
changedFiles: [],
|
|
442
|
-
durationMs: 0,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
447
|
-
const succeeded = expansion.added.length > 0;
|
|
448
|
-
const details = [
|
|
449
|
-
succeeded
|
|
450
|
-
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
451
|
-
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
452
|
-
expansion.implied.length > 0
|
|
453
|
-
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
454
|
-
: '',
|
|
455
|
-
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
456
|
-
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
457
|
-
]
|
|
458
|
-
.filter(Boolean)
|
|
459
|
-
.join('\n');
|
|
460
|
-
outcome = {
|
|
461
|
-
status: succeeded ? 'success' : 'error',
|
|
462
|
-
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
463
|
-
retryable: false,
|
|
464
|
-
output: details,
|
|
465
|
-
changedFiles: [],
|
|
466
|
-
durationMs: 0,
|
|
467
|
-
};
|
|
468
|
-
emitTrace('tool_route_expand', {
|
|
469
|
-
policyId: expansion.snapshot.id,
|
|
470
|
-
fromVersion: policySnapshot?.version,
|
|
471
|
-
toVersion: expansion.snapshot.version,
|
|
472
|
-
requestedGroups: parsed.groups.map(String),
|
|
473
|
-
addedGroups: expansion.added,
|
|
474
|
-
impliedGroups: expansion.implied,
|
|
475
|
-
rejected: expansion.rejected,
|
|
476
|
-
reason: parsed.reason,
|
|
477
|
-
status: outcome.status,
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
409
|
+
const outcome = await readonlyPromise;
|
|
410
|
+
usageMeter.add(outcome.usage);
|
|
411
|
+
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
412
|
+
traceToolEnd(tc, index, outcome);
|
|
481
413
|
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
482
414
|
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
483
|
-
|
|
415
|
+
// done 紧跟最后一个只读结果(控制调用在尾部时与旧事件序完全一致)。
|
|
416
|
+
if (index === lastReadonlyIndex)
|
|
417
|
+
hooks.onToolDone?.();
|
|
418
|
+
continue;
|
|
484
419
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
for (let index = 0; index < calls.length; index++) {
|
|
420
|
+
if (calls[index].name !== ADD_TOOL_GROUPS_TOOL_NAME) {
|
|
421
|
+
// 非并行(写/执行/串行)或被禁用的普通调用:与扩容同批时不执行,
|
|
422
|
+
// 配对拒绝并提示下一 step 重试;控制调用与只读不受其影响照常处理。
|
|
489
423
|
const tc = calls[index];
|
|
490
|
-
hooks.onToolHeader?.(tc);
|
|
491
|
-
const isControl = tc.name === ADD_TOOL_GROUPS_TOOL_NAME;
|
|
492
|
-
const isReadonly = isParallelTool(tc.name, ctx.toolRuntime);
|
|
493
424
|
const outcome = {
|
|
494
425
|
status: 'denied',
|
|
495
|
-
code:
|
|
426
|
+
code: 'TOOL_DISABLED',
|
|
496
427
|
retryable: false,
|
|
497
|
-
output:
|
|
498
|
-
?
|
|
499
|
-
:
|
|
500
|
-
? `错误:同一响应包含 add_tool_groups,工具 ${tc.name} 未执行。请在下一 step 重试。`
|
|
501
|
-
: `错误:add_tool_groups 不能与写/执行工具 ${tc.name} 同批;请先完成扩容,再在下一 step 调用 ${tc.name}。`,
|
|
428
|
+
output: isToolDeniedForStep(tc.name)
|
|
429
|
+
? `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`
|
|
430
|
+
: `错误:add_tool_groups 不能与写/执行工具 ${tc.name} 同批执行,本 step 已跳过该调用(其余只读调用与扩容已正常处理);请在下一 step 重试 ${tc.name}。`,
|
|
502
431
|
changedFiles: [],
|
|
503
432
|
durationMs: 0,
|
|
504
433
|
};
|
|
@@ -506,11 +435,91 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
506
435
|
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
507
436
|
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, false);
|
|
508
437
|
traceToolEnd(tc, index, outcome);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// 控制调用(header 已发):校验并应用扩容。扩容只改下一 step 的 schema,
|
|
441
|
+
// 本批只读执行都走 step 快照,与执行的先后无行为差。
|
|
442
|
+
const tc = calls[index];
|
|
443
|
+
const parsed = parseArgs(tc.arguments);
|
|
444
|
+
let outcome;
|
|
445
|
+
if (isToolDeniedForStep(tc.name)) {
|
|
446
|
+
outcome = {
|
|
447
|
+
status: 'denied',
|
|
448
|
+
code: 'TOOL_DISABLED',
|
|
449
|
+
retryable: false,
|
|
450
|
+
output: `错误:当前 tool policy snapshot 不允许调用 ${tc.name}。`,
|
|
451
|
+
changedFiles: [],
|
|
452
|
+
durationMs: 0,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
else if (!opts.toolPolicy) {
|
|
456
|
+
outcome = {
|
|
457
|
+
status: 'denied',
|
|
458
|
+
code: 'TOOL_DISABLED',
|
|
459
|
+
retryable: false,
|
|
460
|
+
output: '错误:当前 Agent 未启用动态工具策略,无法调用 add_tool_groups。',
|
|
461
|
+
changedFiles: [],
|
|
462
|
+
durationMs: 0,
|
|
463
|
+
};
|
|
509
464
|
}
|
|
465
|
+
else if (!parsed ||
|
|
466
|
+
!Array.isArray(parsed.groups) ||
|
|
467
|
+
parsed.groups.length === 0 ||
|
|
468
|
+
typeof parsed.reason !== 'string' ||
|
|
469
|
+
!parsed.reason.trim()) {
|
|
470
|
+
outcome = {
|
|
471
|
+
status: 'error',
|
|
472
|
+
code: 'INVALID_ARGUMENTS',
|
|
473
|
+
retryable: false,
|
|
474
|
+
output: '错误:add_tool_groups 需要非空 groups 数组和非空 reason。',
|
|
475
|
+
changedFiles: [],
|
|
476
|
+
durationMs: 0,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
const expansion = opts.toolPolicy.expand(parsed.groups, parsed.reason);
|
|
481
|
+
const succeeded = expansion.added.length > 0;
|
|
482
|
+
const details = [
|
|
483
|
+
succeeded
|
|
484
|
+
? `Tool policy expanded to v${expansion.snapshot.version}; added groups: ${expansion.added.join(', ')}.`
|
|
485
|
+
: `Tool policy was not expanded (still v${expansion.snapshot.version}).`,
|
|
486
|
+
expansion.implied.length > 0
|
|
487
|
+
? `Implied groups also activated: ${expansion.implied.join(', ')}.`
|
|
488
|
+
: '',
|
|
489
|
+
expansion.rejected.length > 0 ? `Rejected: ${expansion.rejected.join('; ')}.` : '',
|
|
490
|
+
succeeded ? 'The added tool schemas become available on the next model step.' : '',
|
|
491
|
+
]
|
|
492
|
+
.filter(Boolean)
|
|
493
|
+
.join('\n');
|
|
494
|
+
outcome = {
|
|
495
|
+
status: succeeded ? 'success' : 'error',
|
|
496
|
+
code: succeeded ? 'OK' : 'INVALID_ARGUMENTS',
|
|
497
|
+
retryable: false,
|
|
498
|
+
output: details,
|
|
499
|
+
changedFiles: [],
|
|
500
|
+
durationMs: 0,
|
|
501
|
+
};
|
|
502
|
+
emitTrace('tool_route_expand', {
|
|
503
|
+
policyId: expansion.snapshot.id,
|
|
504
|
+
fromVersion: policySnapshot?.version,
|
|
505
|
+
toVersion: expansion.snapshot.version,
|
|
506
|
+
requestedGroups: parsed.groups.map(String),
|
|
507
|
+
addedGroups: expansion.added,
|
|
508
|
+
impliedGroups: expansion.implied,
|
|
509
|
+
rejected: expansion.rejected,
|
|
510
|
+
reason: parsed.reason,
|
|
511
|
+
status: outcome.status,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
opts.onToolOutcome?.(tc.name, parsed ?? {}, outcome);
|
|
515
|
+
hooks.onToolResult?.(tc, outcome.output, null, null, 1);
|
|
516
|
+
pushToolResult(history, tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
517
|
+
traceToolEnd(tc, index, outcome);
|
|
510
518
|
}
|
|
511
519
|
}
|
|
512
520
|
// add_tool_groups 是 step 屏障:只要本响应出现该控制调用,本批所有普通工具都不执行。
|
|
513
521
|
// 但上面仍为每个 provider tool_call 写入了配对 tool_result,保持 OpenAI 协议完整。
|
|
522
|
+
// (部分执行语义:并行安全只读与扩容照常,写/执行调用跳过待下一 step 重试。)
|
|
514
523
|
let i = hasToolRouteBarrier ? calls.length : 0;
|
|
515
524
|
while (i < calls.length) {
|
|
516
525
|
const currentCall = calls[i];
|
|
@@ -551,12 +560,17 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
551
560
|
allowedToolNames: currentAllowedToolNames(),
|
|
552
561
|
delegation: delegationForOrchestrator(),
|
|
553
562
|
}));
|
|
563
|
+
// 完成即发:usage/onToolOutcome/trace 在各 outcome 落地瞬间发出(1s 的
|
|
564
|
+
// grep 不再被 60s 的 web_fetch 卡住可见性);history 回灌严格按原序。
|
|
565
|
+
await Promise.all(started.map(async (pending, k) => {
|
|
566
|
+
const outcome = await pending;
|
|
567
|
+
usageMeter.add(outcome.usage);
|
|
568
|
+
opts.onToolOutcome?.(batch[k].name, parseArgs(batch[k].arguments) ?? {}, outcome);
|
|
569
|
+
traceToolEnd(batch[k], i + k, outcome);
|
|
570
|
+
}));
|
|
554
571
|
for (let k = 0; k < batch.length; k++) {
|
|
555
572
|
const tc = batch[k];
|
|
556
573
|
const outcome = await started[k];
|
|
557
|
-
usageMeter.add(outcome.usage);
|
|
558
|
-
opts.onToolOutcome?.(tc.name, parseArgs(tc.arguments) ?? {}, outcome);
|
|
559
|
-
traceToolEnd(tc, i + k, outcome);
|
|
560
574
|
const output = outcome.output;
|
|
561
575
|
hooks.onToolResult?.(tc, output, null, null, 1); // 并行工具无 diff
|
|
562
576
|
if (tc.name === 'ask_human' && outcome.status === 'success') {
|
|
@@ -620,44 +634,54 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
620
634
|
const firstAllowed = entries.find((entry) => !entry.denied);
|
|
621
635
|
if (firstAllowed)
|
|
622
636
|
hooks.onToolStart?.(firstAllowed.tc.name);
|
|
637
|
+
// 动态补位池:启动上限 concurrency,任意一个完成立即补位——静态分块时
|
|
638
|
+
// 「7 个任务、1 个跑 10 分钟」会让第 5 分钟起槽位空转而 6/7 号干等,
|
|
639
|
+
// 总耗时≈最慢链;补位后≈最满调度。与 staged dispatcher 同构。
|
|
640
|
+
// 完成即发:usage/onToolOutcome/trace 在各 outcome 落地瞬间发出(快调用
|
|
641
|
+
// 不再被批内慢调用卡住可见性),history 回灌(pushToolResult)仍严格原序。
|
|
642
|
+
// 实现用 worker 循环而非「补位回调 + outcomes.map」:outcomes 是稀疏数组,
|
|
643
|
+
// Array.prototype.map 会跳过未启动槽位的洞,补位链条会在第 N+1 个断掉。
|
|
623
644
|
const concurrency = Math.max(1, ctx.config.subAgentConcurrency);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
return Promise.resolve(entry.denied);
|
|
645
|
+
const outcomes = new Array(entries.length);
|
|
646
|
+
let cursor = 0;
|
|
647
|
+
const runWorker = async () => {
|
|
648
|
+
while (cursor < entries.length) {
|
|
649
|
+
const position = cursor++;
|
|
650
|
+
const entry = entries[position];
|
|
631
651
|
const hint = argumentErrorHint(entry.tc.name, runtimeContextState);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const outcome = await started[k];
|
|
652
|
+
const outcome = entry.denied
|
|
653
|
+
? entry.denied
|
|
654
|
+
: await ctx.toolRuntime.executeToolOutcome(entry.tc.name, entry.tc.arguments, signal, {
|
|
655
|
+
callId: entry.tc.id,
|
|
656
|
+
allowedToolNames: currentAllowedToolNames(),
|
|
657
|
+
delegation: delegationForOrchestrator(),
|
|
658
|
+
...(hint ? { argumentErrorHint: hint } : {}),
|
|
659
|
+
onLockAcquired: (lockedArgs) => {
|
|
660
|
+
entry.diff = readDiffContext(entry.tc, lockedArgs, ctx.jailResolve);
|
|
661
|
+
},
|
|
662
|
+
});
|
|
663
|
+
outcomes[position] = Promise.resolve(outcome);
|
|
645
664
|
usageMeter.add(outcome.usage);
|
|
646
665
|
opts.onToolOutcome?.(entry.tc.name, entry.parsed ?? {}, outcome);
|
|
647
|
-
traceToolEnd(entry.tc, i +
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
666
|
+
traceToolEnd(entry.tc, i + position, outcome);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, entries.length) }, () => runWorker()));
|
|
670
|
+
for (let offset = 0; offset < entries.length; offset++) {
|
|
671
|
+
const entry = entries[offset];
|
|
672
|
+
const outcome = await outcomes[offset];
|
|
673
|
+
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
674
|
+
pushToolResult(history, entry.tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
675
|
+
const invalidatedFiles = [
|
|
676
|
+
...new Set([...(outcome.changedFiles ?? []), ...(outcome.staleFiles ?? [])]),
|
|
677
|
+
];
|
|
678
|
+
if (invalidatedFiles.length > 0) {
|
|
679
|
+
for (const changedFile of invalidatedFiles) {
|
|
680
|
+
relprune?.observeMutation(history, changedFile);
|
|
681
|
+
lifecycle?.pushMutation(history, history.length - 1, changedFile);
|
|
660
682
|
}
|
|
683
|
+
invalidateArtifacts(runtimeContextState, history, invalidatedFiles);
|
|
684
|
+
runtimeContextState.lifecycleStats = lifecycle?.stats();
|
|
661
685
|
}
|
|
662
686
|
}
|
|
663
687
|
if (firstAllowed)
|
|
@@ -724,12 +748,17 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
724
748
|
},
|
|
725
749
|
});
|
|
726
750
|
});
|
|
751
|
+
// 完成即发:usage/onToolOutcome/trace 落地瞬间发出;history 回灌严格原序——
|
|
752
|
+
// 同文件排队由 canonical file 锁保证,异文件并发不受影响。
|
|
753
|
+
await Promise.all(started.map(async (pending, k) => {
|
|
754
|
+
const outcome = await pending;
|
|
755
|
+
usageMeter.add(outcome.usage);
|
|
756
|
+
opts.onToolOutcome?.(entries[k].tc.name, entries[k].parsed ?? {}, outcome);
|
|
757
|
+
traceToolEnd(entries[k].tc, i + k, outcome);
|
|
758
|
+
}));
|
|
727
759
|
for (let k = 0; k < entries.length; k++) {
|
|
728
760
|
const entry = entries[k];
|
|
729
761
|
const outcome = await started[k];
|
|
730
|
-
usageMeter.add(outcome.usage);
|
|
731
|
-
opts.onToolOutcome?.(entry.tc.name, entry.parsed ?? {}, outcome);
|
|
732
|
-
traceToolEnd(entry.tc, i + k, outcome);
|
|
733
762
|
hooks.onToolResult?.(entry.tc, outcome.output, entry.denied ? null : entry.parsed, entry.diff.preWriteOld, entry.diff.editStartLine);
|
|
734
763
|
pushToolResult(history, entry.tc, outcome.output, relprune, lifecycle, scheduler, runtimeContextState, outcome.status === 'success');
|
|
735
764
|
const invalidatedFiles = [
|
|
@@ -886,10 +915,47 @@ export async function runAgentCoreLegacy(opts, historyManager, stages) {
|
|
|
886
915
|
hooks.onMaxSteps?.();
|
|
887
916
|
turnLifecycle.markMaxSteps();
|
|
888
917
|
const finalMutation = ctx.getCurrentTurnMutationState();
|
|
918
|
+
// ── 撞限 salvage:最后一次不带工具的收尾请求 ──
|
|
919
|
+
// 撞步数上限时 history 里往往已有几十步探索/实现成果,直接返回会把它们整体丢掉
|
|
920
|
+
// (调用方只拿到 finalText=null →「子 agent 被中断」,token 白花还得重新探索)。
|
|
921
|
+
// 这里多发一步无工具(tools=[])的请求,ephemeral system 指示立即输出最终摘要;
|
|
922
|
+
// 成功则把它作为 finalText 带回(terminationReason/completed 语义不变,诚实反映撞限),
|
|
923
|
+
// 请求失败或中止则静默回退旧行为。跳过条件:本来就无工具调用(纯闲聊撞限,无成果可捞)
|
|
924
|
+
// 或已 abort(用户主动取消,不应再花钱)。
|
|
925
|
+
let salvageText = null;
|
|
926
|
+
const hasToolHistory = history.some((m) => m.role === 'tool');
|
|
927
|
+
if (hasToolHistory && !signal?.aborted) {
|
|
928
|
+
const salvageInstruction = '## Step limit reached — final summary required\n' +
|
|
929
|
+
`The hard step limit (${maxSteps}) has been reached and no further tool calls are possible. ` +
|
|
930
|
+
'Write your final report NOW as plain text (no tool calls): what you accomplished, key findings with file paths / evidence, decisions made, and what remains undone. Be concise but complete — this text is the only thing the caller will receive.';
|
|
931
|
+
try {
|
|
932
|
+
const salvage = await modelRunner.run({
|
|
933
|
+
history: [...history, { role: 'system', content: salvageInstruction }],
|
|
934
|
+
handlers: {},
|
|
935
|
+
tools: [],
|
|
936
|
+
}, signal);
|
|
937
|
+
if (salvage.content?.trim())
|
|
938
|
+
salvageText = salvage.content.trim();
|
|
939
|
+
if (salvage.usage)
|
|
940
|
+
usageMeter.add(salvage.usage);
|
|
941
|
+
emitTrace('max_steps_salvage', {
|
|
942
|
+
recovered: !!salvageText,
|
|
943
|
+
tokens: salvage.usage?.totalTokens,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
// salvage 是 best-effort:失败不改变撞限终止语义,静默回退;但留一条 trace 取证
|
|
948
|
+
// (否则事后只看到 finalText=null,无法区分「无事可捞」与「收尾请求本身失败」)。
|
|
949
|
+
emitTrace('max_steps_salvage', {
|
|
950
|
+
recovered: false,
|
|
951
|
+
error: error instanceof Error ? error.message.slice(0, 300) : String(error),
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
889
955
|
return {
|
|
890
956
|
completed: false,
|
|
891
957
|
terminationReason: 'max_steps',
|
|
892
|
-
finalText:
|
|
958
|
+
finalText: salvageText,
|
|
893
959
|
usage: usageMeter.snapshot(),
|
|
894
960
|
changedFiles: finalMutation.changedFiles.map((item) => item.path),
|
|
895
961
|
};
|