@yeaft/webchat-agent 1.0.325 → 1.0.326
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/engine.js +88 -37
- package/yeaft/tools/bash.js +42 -11
- package/yeaft/tools/process-runner.js +24 -10
- package/yeaft/tools/types.js +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.326"}
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -365,7 +365,7 @@ export function shouldAllowGroupReflection({
|
|
|
365
365
|
* @typedef {{ type: 'turn_start', turnNumber: number }} TurnStartEvent
|
|
366
366
|
* @typedef {{ type: 'turn_end', turnNumber: number, stopReason: string, terminal?: boolean }} TurnEndEvent
|
|
367
367
|
* @typedef {{ type: 'tool_start', id: string, name: string, input: object }} ToolStartEvent
|
|
368
|
-
* @typedef {{ type: 'tool_end', id: string, name: string, output: string, isError: boolean }} ToolEndEvent
|
|
368
|
+
* @typedef {{ type: 'tool_end', id: string, name: string, output: string, isError: boolean, skipped?: boolean }} ToolEndEvent
|
|
369
369
|
* @typedef {{ type: 'consolidate', archivedCount: number, extractedCount: number }} ConsolidateEvent
|
|
370
370
|
* @typedef {{ type: 'recall', entryCount: number, cached: boolean }} RecallEvent
|
|
371
371
|
* @typedef {{ type: 'fallback', from: string, to: string, reason: string }} FallbackEvent
|
|
@@ -1339,6 +1339,7 @@ export class Engine {
|
|
|
1339
1339
|
// can mark "after this batch, end the turn — do NOT call adapter
|
|
1340
1340
|
// again". Honored at the top of the tool-loop continuation.
|
|
1341
1341
|
requestEndTurn: vpCtx?.requestEndTurn,
|
|
1342
|
+
requestToolBatchBarrier: vpCtx?.requestToolBatchBarrier,
|
|
1342
1343
|
// Result-producing async-task ownership hook. Tools such as SpawnAgent
|
|
1343
1344
|
// call this with the new `task.id` so the engine keeps the current query
|
|
1344
1345
|
// parked at end_turn until the result arrives. Persistent background
|
|
@@ -3636,6 +3637,8 @@ export class Engine {
|
|
|
3636
3637
|
}
|
|
3637
3638
|
|
|
3638
3639
|
// Execute tool calls and feed results back
|
|
3640
|
+
/** @type {{ kind?: string, message?: string, sourceToolCallId?: string, sourceToolName?: string } | null} */
|
|
3641
|
+
let toolBatchBarrier = null;
|
|
3639
3642
|
let currentToolCallForAsyncTask = null;
|
|
3640
3643
|
// task-707: requestEndTurn is a per-batch closure that lets a tool
|
|
3641
3644
|
// signal "end this turn after the current batch — no adapter retry".
|
|
@@ -3667,6 +3670,17 @@ export class Engine {
|
|
|
3667
3670
|
endTurnRequested = reason || { kind: 'tool_handoff' };
|
|
3668
3671
|
}
|
|
3669
3672
|
},
|
|
3673
|
+
requestToolBatchBarrier: (reason) => {
|
|
3674
|
+
if (toolBatchBarrier != null) return;
|
|
3675
|
+
const detail = reason && typeof reason === 'object'
|
|
3676
|
+
? { ...reason }
|
|
3677
|
+
: { message: String(reason || 'A preceding tool result invalidated the remaining batch.') };
|
|
3678
|
+
toolBatchBarrier = {
|
|
3679
|
+
...detail,
|
|
3680
|
+
sourceToolCallId: currentToolCallForAsyncTask?.id || null,
|
|
3681
|
+
sourceToolName: currentToolCallForAsyncTask?.name || null,
|
|
3682
|
+
};
|
|
3683
|
+
},
|
|
3670
3684
|
});
|
|
3671
3685
|
|
|
3672
3686
|
// task-325a: track whether we aborted mid tool-loop so we can
|
|
@@ -3681,11 +3695,14 @@ export class Engine {
|
|
|
3681
3695
|
// that's already running (the signal is passed in, tools decide
|
|
3682
3696
|
// themselves whether to bail early), but we stop dispatching
|
|
3683
3697
|
// any remaining tools the moment abort fires.
|
|
3684
|
-
if (signal?.aborted) {
|
|
3698
|
+
if (signal?.aborted && !toolBatchBarrier) {
|
|
3685
3699
|
abortedDuringTools = true;
|
|
3686
3700
|
break;
|
|
3687
3701
|
}
|
|
3702
|
+
if (signal?.aborted) abortedDuringTools = true;
|
|
3688
3703
|
|
|
3704
|
+
const activeToolBatchBarrier = toolBatchBarrier;
|
|
3705
|
+
const skipped = activeToolBatchBarrier != null;
|
|
3689
3706
|
const toolStartTime = Date.now();
|
|
3690
3707
|
|
|
3691
3708
|
// PR-L: duplicate-call detection. If this exact (toolName,
|
|
@@ -3696,25 +3713,27 @@ export class Engine {
|
|
|
3696
3713
|
// assistant(tool_use) → user(tool_result, …) pairing demanded
|
|
3697
3714
|
// by the Anthropic / OpenAI Responses APIs stays intact. We
|
|
3698
3715
|
// don't block the call — the LLM still decides.
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
argsHash: dupHash,
|
|
3709
|
-
currentTurn: queryNumber,
|
|
3710
|
-
lookbackTurns: 2,
|
|
3711
|
-
});
|
|
3712
|
-
if (dupInfo.count + 1 >= DUP_TOOL_THRESHOLD) {
|
|
3713
|
-
pendingDupReminders.push(buildDuplicateReminder({
|
|
3716
|
+
if (!skipped) {
|
|
3717
|
+
const dupHash = argsHashOf(tc.input);
|
|
3718
|
+
// PR-L follow-up: lookback is by user-conversation turn
|
|
3719
|
+
// (`queryNumber`), NOT by inner adapter loop iteration. Each call
|
|
3720
|
+
// to query() bumps queryNumber once, so "last 2 turns" means the
|
|
3721
|
+
// current user turn + the previous two user turns — the natural
|
|
3722
|
+
// semantic for "the model is stuck in a loop across the
|
|
3723
|
+
// conversation."
|
|
3724
|
+
const dupInfo = this.#execLog.dupInfo({
|
|
3714
3725
|
toolName: tc.name,
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3726
|
+
argsHash: dupHash,
|
|
3727
|
+
currentTurn: queryNumber,
|
|
3728
|
+
lookbackTurns: 2,
|
|
3729
|
+
});
|
|
3730
|
+
if (dupInfo.count + 1 >= DUP_TOOL_THRESHOLD) {
|
|
3731
|
+
pendingDupReminders.push(buildDuplicateReminder({
|
|
3732
|
+
toolName: tc.name,
|
|
3733
|
+
count: dupInfo.count + 1,
|
|
3734
|
+
lastResultBrief: dupInfo.lastResultBrief,
|
|
3735
|
+
}));
|
|
3736
|
+
}
|
|
3718
3737
|
}
|
|
3719
3738
|
|
|
3720
3739
|
let output;
|
|
@@ -3722,18 +3741,40 @@ export class Engine {
|
|
|
3722
3741
|
let isError = false;
|
|
3723
3742
|
let toolErrorOutput = null;
|
|
3724
3743
|
let fatalToolError = null;
|
|
3725
|
-
currentToolCallForAsyncTask =
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3744
|
+
currentToolCallForAsyncTask = skipped
|
|
3745
|
+
? null
|
|
3746
|
+
: {
|
|
3747
|
+
id: tc.id,
|
|
3748
|
+
name: tc.name,
|
|
3749
|
+
threadId: runtimeThreadId,
|
|
3750
|
+
};
|
|
3730
3751
|
|
|
3731
3752
|
// Resolve tool: prefer ToolRegistry, fallback to legacy #tools Map
|
|
3732
3753
|
const hasTool = this.#toolRegistry
|
|
3733
3754
|
? this.#toolRegistry.isAllowed(tc.name, { collabToolPolicy: effectiveCollabToolPolicy })
|
|
3734
3755
|
: this.#tools.has(tc.name);
|
|
3735
3756
|
|
|
3736
|
-
if (
|
|
3757
|
+
if (skipped) {
|
|
3758
|
+
const source = activeToolBatchBarrier.sourceToolName || 'a preceding tool';
|
|
3759
|
+
const sourceId = activeToolBatchBarrier.sourceToolCallId
|
|
3760
|
+
? ` (${activeToolBatchBarrier.sourceToolCallId})`
|
|
3761
|
+
: '';
|
|
3762
|
+
output = [
|
|
3763
|
+
`Skipped ${tc.name} because ${source}${sourceId} invalidated the remaining tool batch.`,
|
|
3764
|
+
activeToolBatchBarrier.message || 'Review the preceding tool result before deciding whether to retry this call.',
|
|
3765
|
+
'This tool was not executed. Submit it again only after reviewing the preceding result.',
|
|
3766
|
+
].join('\n');
|
|
3767
|
+
isError = true;
|
|
3768
|
+
yield {
|
|
3769
|
+
type: 'tool_end',
|
|
3770
|
+
id: tc.id,
|
|
3771
|
+
name: tc.name,
|
|
3772
|
+
output,
|
|
3773
|
+
isError: true,
|
|
3774
|
+
skipped: true,
|
|
3775
|
+
threadId: this.currentThreadId,
|
|
3776
|
+
};
|
|
3777
|
+
} else if (!hasTool) {
|
|
3737
3778
|
output = `Error: unknown tool "${tc.name}"`;
|
|
3738
3779
|
isError = true;
|
|
3739
3780
|
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true, threadId: this.currentThreadId };
|
|
@@ -3816,13 +3857,14 @@ export class Engine {
|
|
|
3816
3857
|
durationMs: toolDurationMs,
|
|
3817
3858
|
isError,
|
|
3818
3859
|
toolOutput: output,
|
|
3860
|
+
...(skipped ? { skipped: true } : {}),
|
|
3819
3861
|
...(displayImages.length > 0 ? { displayImageCount: displayImages.length } : {}),
|
|
3820
3862
|
};
|
|
3821
3863
|
|
|
3822
3864
|
// 2026-05-13: feed the per-tool counters. Stays best-effort — a
|
|
3823
3865
|
// stats sink that throws shouldn't crash the engine. `record`
|
|
3824
3866
|
// already swallows internal write errors.
|
|
3825
|
-
if (this.#toolStats && typeof this.#toolStats.record === 'function') {
|
|
3867
|
+
if (!skipped && this.#toolStats && typeof this.#toolStats.record === 'function') {
|
|
3826
3868
|
try {
|
|
3827
3869
|
this.#toolStats.record({
|
|
3828
3870
|
name: tc.name,
|
|
@@ -3841,6 +3883,7 @@ export class Engine {
|
|
|
3841
3883
|
toolOutput: output,
|
|
3842
3884
|
durationMs: toolDurationMs,
|
|
3843
3885
|
isError,
|
|
3886
|
+
skipped,
|
|
3844
3887
|
});
|
|
3845
3888
|
|
|
3846
3889
|
// Append only the bounded copy to the model message history. Raw
|
|
@@ -3876,14 +3919,16 @@ export class Engine {
|
|
|
3876
3919
|
// user-conversation turn), not the inner loop's turnNumber.
|
|
3877
3920
|
// Aligns exec-log layout with dup detection lookback and the
|
|
3878
3921
|
// T2 fallback-stub readTurn() call below.
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3922
|
+
if (!skipped) {
|
|
3923
|
+
this.#execLog.append(queryNumber, buildExecLogEntry({
|
|
3924
|
+
loopIdx: queryToolCount,
|
|
3925
|
+
toolName: tc.name,
|
|
3926
|
+
args: tc.input,
|
|
3927
|
+
output,
|
|
3928
|
+
isError,
|
|
3929
|
+
}));
|
|
3930
|
+
queryToolCount += 1;
|
|
3931
|
+
}
|
|
3887
3932
|
if (fatalToolError) throw fatalToolError;
|
|
3888
3933
|
}
|
|
3889
3934
|
|
|
@@ -3895,6 +3940,11 @@ export class Engine {
|
|
|
3895
3940
|
conversationMessages.push({ role: 'user', content: reminder });
|
|
3896
3941
|
}
|
|
3897
3942
|
|
|
3943
|
+
// A batch barrier deliberately returns control to the provider. Any
|
|
3944
|
+
// handoff requested by an earlier call belongs to the invalidated plan
|
|
3945
|
+
// and must not leak into a later provider-generated batch.
|
|
3946
|
+
if (toolBatchBarrier) endTurnRequested = null;
|
|
3947
|
+
|
|
3898
3948
|
// task-707: tool-callable end-turn signal. If a tool in this batch
|
|
3899
3949
|
// called toolCtx.requestEndTurn(reason), break out of the outer
|
|
3900
3950
|
// while-loop now — DON'T call adapter.stream() again. The
|
|
@@ -3907,7 +3957,7 @@ export class Engine {
|
|
|
3907
3957
|
// collapse the arc into a summary that's only valuable across
|
|
3908
3958
|
// multi-iteration tool loops) and BEFORE the abortedDuringTools
|
|
3909
3959
|
// check (so a clean handoff doesn't get reported as 'aborted').
|
|
3910
|
-
if (endTurnRequested) {
|
|
3960
|
+
if (endTurnRequested && !toolBatchBarrier) {
|
|
3911
3961
|
if (pendingSubAgentNotifs.length > 0) {
|
|
3912
3962
|
acknowledgePendingNotifications(notifScope, pendingSubAgentNotifs.map(n => n.id));
|
|
3913
3963
|
}
|
|
@@ -3945,7 +3995,8 @@ export class Engine {
|
|
|
3945
3995
|
// batch within the same query gets a distinct entry — without
|
|
3946
3996
|
// this the second batch would be silently skipped.
|
|
3947
3997
|
const t1BatchDue = queryToolCount - lastT1AtToolCount >= TOOL_BATCH_SIZE;
|
|
3948
|
-
if (groupReflectionAllowed && t1BatchDue && !
|
|
3998
|
+
if (groupReflectionAllowed && t1BatchDue && !toolBatchBarrier
|
|
3999
|
+
&& !abortedDuringTools && !signal?.aborted) {
|
|
3949
4000
|
const t1DedupKey = `${queryNumber}:t1:${queryToolCount}`;
|
|
3950
4001
|
if (this.#reflectedTurns.has(t1DedupKey)) {
|
|
3951
4002
|
// Defensive: should never hit since t1BatchDue gates re-entry
|
package/yeaft/tools/bash.js
CHANGED
|
@@ -29,23 +29,18 @@ const DEFAULT_TIMEOUT_MS = 120_000;
|
|
|
29
29
|
|
|
30
30
|
/** Max timeout in ms (10 minutes). */
|
|
31
31
|
const MAX_TIMEOUT_MS = 600_000;
|
|
32
|
-
/**
|
|
33
|
-
* ToolRegistry must not preempt Bash's own process timeout. Bash terminates
|
|
34
|
-
* the process tree and waits for close before returning exit 124; the grace
|
|
35
|
-
* covers TERM/KILL escalation and the bounded close-confirmation window.
|
|
36
|
-
*/
|
|
37
|
-
const REGISTRY_TIMEOUT_MS = MAX_TIMEOUT_MS + 15_000;
|
|
38
32
|
const LINUX_NAMESPACE_HELPER = resolve(
|
|
39
33
|
dirname(fileURLToPath(import.meta.url)),
|
|
40
34
|
'..',
|
|
41
35
|
'linux-process-namespace.js',
|
|
42
36
|
);
|
|
37
|
+
const RUN_PROCESS_OVERRIDE = Symbol('runProcessOverride');
|
|
43
38
|
|
|
44
39
|
/**
|
|
45
40
|
* Run a command in a child process.
|
|
46
|
-
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number, timedOut: boolean }>}
|
|
41
|
+
* @returns {Promise<{ stdout: string, stderr: string, exitCode: number, timedOut: boolean, terminationError: string | null }>}
|
|
47
42
|
*/
|
|
48
|
-
function runCommand(command, { cwd, timeout, signal, runtimePlatform }) {
|
|
43
|
+
function runCommand(command, { cwd, timeout, signal, runtimePlatform, runProcessImpl = runProcess }) {
|
|
49
44
|
const platform = runtimePlatform || getRuntimePlatformInfo();
|
|
50
45
|
const env = { ...process.env, TERM: 'dumb', FORCE_COLOR: '0' };
|
|
51
46
|
const baseInvocation = buildShellInvocation(command, { runtimePlatform: platform });
|
|
@@ -76,7 +71,7 @@ function runCommand(command, { cwd, timeout, signal, runtimePlatform }) {
|
|
|
76
71
|
systemdControl: null,
|
|
77
72
|
};
|
|
78
73
|
}
|
|
79
|
-
return
|
|
74
|
+
return runProcessImpl(invocation.command, invocation.args, {
|
|
80
75
|
cwd,
|
|
81
76
|
env,
|
|
82
77
|
signal,
|
|
@@ -91,10 +86,11 @@ function runCommand(command, { cwd, timeout, signal, runtimePlatform }) {
|
|
|
91
86
|
stderr: result.stderr,
|
|
92
87
|
exitCode: result.code,
|
|
93
88
|
timedOut: result.timedOut,
|
|
89
|
+
terminationError: result.terminationError || null,
|
|
94
90
|
}));
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
|
|
93
|
+
const bashTool = defineTool({
|
|
98
94
|
name: 'Bash',
|
|
99
95
|
description: {
|
|
100
96
|
en: `Execute a shell command and return its output.
|
|
@@ -111,6 +107,7 @@ Guidelines:
|
|
|
111
107
|
- Use absolute paths when possible
|
|
112
108
|
- Avoid interactive commands (no stdin support)
|
|
113
109
|
- Use background=true for long-running or persistent tasks that should survive across turns
|
|
110
|
+
- Foreground timeout and cleanup status is returned to you; decide whether to inspect, retry, stop, or use a background task
|
|
114
111
|
- stderr is captured separately and included in the result`,
|
|
115
112
|
zh: `执行 Shell 命令并返回输出。
|
|
116
113
|
|
|
@@ -123,6 +120,7 @@ Guidelines:
|
|
|
123
120
|
- 尽量使用绝对路径
|
|
124
121
|
- 避免交互式命令(不支持 stdin)
|
|
125
122
|
- 长时间或需要跨 turn 持续存在的任务使用 background=true
|
|
123
|
+
- 前台命令的超时和清理状态会返回给你;由你决定检查、重试、停止或改用后台任务
|
|
126
124
|
- stderr 单独捕获并包含在结果中`
|
|
127
125
|
},
|
|
128
126
|
parameters: {
|
|
@@ -167,7 +165,10 @@ Guidelines:
|
|
|
167
165
|
required: ['command'],
|
|
168
166
|
},
|
|
169
167
|
errorOutput: null,
|
|
170
|
-
|
|
168
|
+
// Foreground Bash owns a bounded timeout and process-tree cleanup state
|
|
169
|
+
// machine. A second ToolRegistry timer can preempt that cleanup and turn an
|
|
170
|
+
// owned exit 124 into a fatal orphan, so it must stay disabled for this tool.
|
|
171
|
+
timeoutMs: 0,
|
|
171
172
|
isConcurrencySafe: () => false,
|
|
172
173
|
isReadOnly: () => false,
|
|
173
174
|
isDestructive: (input) => {
|
|
@@ -224,6 +225,7 @@ Guidelines:
|
|
|
224
225
|
timeout,
|
|
225
226
|
signal: ctx?.signal,
|
|
226
227
|
runtimePlatform,
|
|
228
|
+
runProcessImpl: ctx?.[RUN_PROCESS_OVERRIDE] || runProcess,
|
|
227
229
|
});
|
|
228
230
|
|
|
229
231
|
// Format output similar to Claude Code
|
|
@@ -231,6 +233,24 @@ Guidelines:
|
|
|
231
233
|
if (result.stdout) parts.push(result.stdout);
|
|
232
234
|
if (result.stderr) parts.push(`STDERR:\n${result.stderr}`);
|
|
233
235
|
if (result.timedOut) parts.push(`\n(Command timed out after ${timeout}ms)`);
|
|
236
|
+
if (result.terminationError) {
|
|
237
|
+
parts.push([
|
|
238
|
+
`WARNING: ${result.terminationError}`,
|
|
239
|
+
'The command timed out, but process-tree termination could not be confirmed. The command may still be running.',
|
|
240
|
+
'Decide whether to inspect or stop it, retry, or use background=true.',
|
|
241
|
+
].join('\n'));
|
|
242
|
+
}
|
|
243
|
+
if (result.timedOut) {
|
|
244
|
+
ctx?.requestToolBatchBarrier?.({
|
|
245
|
+
kind: 'owned_timeout',
|
|
246
|
+
message: [
|
|
247
|
+
result.terminationError
|
|
248
|
+
? 'The foreground Bash command timed out and process-tree termination could not be confirmed.'
|
|
249
|
+
: 'The foreground Bash command timed out.',
|
|
250
|
+
'Return this result to the model before executing another tool call from the same batch.',
|
|
251
|
+
].join(' '),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
234
254
|
|
|
235
255
|
const output = parts.join('\n');
|
|
236
256
|
if (result.exitCode !== 0) {
|
|
@@ -243,3 +263,14 @@ Guidelines:
|
|
|
243
263
|
}
|
|
244
264
|
},
|
|
245
265
|
});
|
|
266
|
+
|
|
267
|
+
export function createBashTool({ runProcessImpl = runProcess } = {}) {
|
|
268
|
+
return {
|
|
269
|
+
...bashTool,
|
|
270
|
+
execute(input, ctx) {
|
|
271
|
+
return bashTool.execute(input, { ...ctx, [RUN_PROCESS_OVERRIDE]: runProcessImpl });
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default bashTool;
|
|
@@ -102,6 +102,7 @@ function killProcessTree(proc, signalName, platform, spawnProcessSync, systemdSc
|
|
|
102
102
|
* @param {string} command
|
|
103
103
|
* @param {string[]} args
|
|
104
104
|
* @param {{ cwd?: string, signal?: AbortSignal, timeoutMs?: number, maxBytes?: number, env?: NodeJS.ProcessEnv, preserveCarriageReturns?: boolean, killGraceMs?: number, forceSettleMs?: number, requireExitConfirmation?: boolean, systemdScope?: { unit: string, systemctlPath: string, env?: NodeJS.ProcessEnv } | null, onSettled?: (() => void) | null, platform?: NodeJS.Platform, spawnProcess?: typeof spawn, spawnProcessSync?: typeof spawnSync }} [options]
|
|
105
|
+
* @returns {Promise<{ code: number, stdout: string, stderr: string, truncated: boolean, timedOut: boolean, terminationError?: string }>}
|
|
105
106
|
*/
|
|
106
107
|
export function runProcess(command, args, options = {}) {
|
|
107
108
|
if (options.signal?.aborted) {
|
|
@@ -188,21 +189,30 @@ export function runProcess(command, args, options = {}) {
|
|
|
188
189
|
if (settled) return;
|
|
189
190
|
settled = true;
|
|
190
191
|
cleanup();
|
|
191
|
-
|
|
192
|
-
reject(error);
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (aborted) {
|
|
196
|
-
reject(abortError(options.signal));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
resolve({
|
|
192
|
+
const result = {
|
|
200
193
|
code: timedOut ? 124 : (code ?? 1),
|
|
201
194
|
stdout: decode(stdout, stdoutTruncated, options.preserveCarriageReturns),
|
|
202
195
|
stderr: decode(stderr, stderrTruncated),
|
|
203
196
|
truncated,
|
|
204
197
|
timedOut,
|
|
205
|
-
}
|
|
198
|
+
};
|
|
199
|
+
if (aborted) {
|
|
200
|
+
reject(abortError(options.signal));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (error) {
|
|
204
|
+
// A command timeout is an owned, bounded outcome. Failure to observe
|
|
205
|
+
// the final child close is important context for the caller, but it
|
|
206
|
+
// must not turn the timeout into an infrastructure exception that
|
|
207
|
+
// prevents the model from deciding what to do next.
|
|
208
|
+
if (timedOut && error instanceof ProcessTerminationError) {
|
|
209
|
+
resolve({ ...result, terminationError: error.message });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
reject(error);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
resolve(result);
|
|
206
216
|
};
|
|
207
217
|
const terminationConfirmed = () => {
|
|
208
218
|
if (!options.requireExitConfirmation) return directClosed;
|
|
@@ -334,6 +344,10 @@ export function runProcess(command, args, options = {}) {
|
|
|
334
344
|
|
|
335
345
|
if (Number.isFinite(options.timeoutMs) && options.timeoutMs > 0) {
|
|
336
346
|
timer = setTimeout(() => {
|
|
347
|
+
// Preserve the first stop reason. Output overflow and Abort may have
|
|
348
|
+
// already started termination; a later deadline must not relabel that
|
|
349
|
+
// cleanup failure as a recoverable command timeout.
|
|
350
|
+
if (settled || stopRequested) return;
|
|
337
351
|
timedOut = true;
|
|
338
352
|
stop();
|
|
339
353
|
}, options.timeoutMs);
|
package/yeaft/tools/types.js
CHANGED
|
@@ -45,6 +45,11 @@
|
|
|
45
45
|
* the supplied reason as `detail`. `reason` may be a structured object
|
|
46
46
|
* `{kind, ...}` so downstream observers (web-bridge) can render UI hints
|
|
47
47
|
* (e.g. "↪ 已转交给 @vp-b") without re-parsing strings.
|
|
48
|
+
* @property {(reason?: string|object) => void} [requestToolBatchBarrier]
|
|
49
|
+
* — stop executing the remaining calls in the current assistant tool batch
|
|
50
|
+
* while still pairing each call with an explicit skipped tool result. The
|
|
51
|
+
* engine then returns those results to the provider before accepting another
|
|
52
|
+
* plan. Used when a completed tool result invalidates the rest of the batch.
|
|
48
53
|
* @property {string} [senderVpId] — id of the VP whose turn is currently
|
|
49
54
|
* running. Used by `route_forward` to stamp the forwarded message and
|
|
50
55
|
* by the loop guard to key per-sender throttling.
|