@yeaft/webchat-agent 1.0.431 → 1.0.432
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 +178 -28
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.432"}
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -89,6 +89,13 @@ import {
|
|
|
89
89
|
/** Maximum auto-continue turns when stopReason is 'max_tokens'. */
|
|
90
90
|
const MAX_CONTINUE_TURNS = 3;
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Keep one provider tool batch from opening an unbounded number of filesystem,
|
|
94
|
+
* network, or subprocess reads. Only tools whose metadata explicitly declares
|
|
95
|
+
* both read-only and concurrency-safe execution enter this lane.
|
|
96
|
+
*/
|
|
97
|
+
const MAX_CONCURRENT_READ_ONLY_TOOLS = 4;
|
|
98
|
+
|
|
92
99
|
/** Bound the best-effort post-turn AMS LLM call independently of the user turn. */
|
|
93
100
|
const MAINTENANCE_CALL_TIMEOUT_MS = 30_000;
|
|
94
101
|
|
|
@@ -112,6 +119,16 @@ function isReadOnlyTool(engine, name, input) {
|
|
|
112
119
|
}
|
|
113
120
|
}
|
|
114
121
|
|
|
122
|
+
function isConcurrencySafeTool(engine, name, input) {
|
|
123
|
+
try {
|
|
124
|
+
const tool = toolDefinitionFor(engine, name);
|
|
125
|
+
return tool?.isReadOnly?.(input) === true
|
|
126
|
+
&& tool?.isConcurrencySafe?.(input) === true;
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
115
132
|
function isCacheableTool(engine, name, input) {
|
|
116
133
|
try {
|
|
117
134
|
const tool = toolDefinitionFor(engine, name);
|
|
@@ -4218,21 +4235,131 @@ export class Engine {
|
|
|
4218
4235
|
let abortedDuringTools = false;
|
|
4219
4236
|
/** @type {string[]} */
|
|
4220
4237
|
const pendingDupReminders = [];
|
|
4238
|
+
/**
|
|
4239
|
+
* Completed executions waiting for their original-order commit. Starting
|
|
4240
|
+
* a bounded read-only segment together removes wall-clock latency without
|
|
4241
|
+
* changing tool result, persistence, trace, or provider message order.
|
|
4242
|
+
* @type {Map<string, { call: object, startedAt: number, durationMs: number, output?: string, error?: Error, toolErrorOutput?: string|null }>}
|
|
4243
|
+
*/
|
|
4244
|
+
const parallelToolExecutions = new Map();
|
|
4245
|
+
const announcedParallelToolCalls = new Set();
|
|
4246
|
+
const toolAllowedForRequest = (toolCall) => (this.#toolRegistry
|
|
4247
|
+
? this.#toolRegistry.isAllowed(toolCall.name, {
|
|
4248
|
+
collabToolPolicy: effectiveCollabToolPolicy,
|
|
4249
|
+
plugins: this.#config?.plugins,
|
|
4250
|
+
activeToolNames,
|
|
4251
|
+
})
|
|
4252
|
+
: this.#tools.has(toolCall.name));
|
|
4253
|
+
const toolContextForCall = (toolCall) => {
|
|
4254
|
+
const stableToolCall = {
|
|
4255
|
+
id: toolCall.id,
|
|
4256
|
+
name: toolCall.name,
|
|
4257
|
+
threadId: runtimeThreadId,
|
|
4258
|
+
};
|
|
4259
|
+
return {
|
|
4260
|
+
...toolCtx,
|
|
4261
|
+
currentToolCall: () => ({ ...stableToolCall }),
|
|
4262
|
+
askUser: typeof askUser === 'function'
|
|
4263
|
+
? input => askUser(input, { ...stableToolCall })
|
|
4264
|
+
: toolCtx.askUser,
|
|
4265
|
+
registerAsyncTask: (taskId, meta = {}) => {
|
|
4266
|
+
this.#registerAsyncTask(taskId, { ...stableToolCall, ...(meta || {}) });
|
|
4267
|
+
},
|
|
4268
|
+
requestToolBatchBarrier: (reason) => {
|
|
4269
|
+
if (toolBatchBarrier != null) return;
|
|
4270
|
+
const detail = reason && typeof reason === 'object'
|
|
4271
|
+
? { ...reason }
|
|
4272
|
+
: { message: String(reason || 'A preceding tool result invalidated the remaining batch.') };
|
|
4273
|
+
toolBatchBarrier = {
|
|
4274
|
+
...detail,
|
|
4275
|
+
sourceToolCallId: stableToolCall.id,
|
|
4276
|
+
sourceToolName: stableToolCall.name,
|
|
4277
|
+
};
|
|
4278
|
+
},
|
|
4279
|
+
};
|
|
4280
|
+
};
|
|
4221
4281
|
|
|
4222
|
-
for (
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
//
|
|
4226
|
-
//
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
break;
|
|
4230
|
-
}
|
|
4282
|
+
for (let toolCallIndex = 0; toolCallIndex < toolCalls.length; toolCallIndex += 1) {
|
|
4283
|
+
const tc = toolCalls[toolCallIndex];
|
|
4284
|
+
const preparedParallelExecution = parallelToolExecutions.get(tc.id) || null;
|
|
4285
|
+
// task-325a: honour abort between tools. We don't cancel tools already
|
|
4286
|
+
// running in a parallel segment; every provider call still commits a
|
|
4287
|
+
// paired result in provider order, but no not-yet-started call is
|
|
4288
|
+
// dispatched after the abort boundary.
|
|
4231
4289
|
if (signal?.aborted) abortedDuringTools = true;
|
|
4232
4290
|
|
|
4291
|
+
if (!preparedParallelExecution && !toolBatchBarrier && !signal?.aborted
|
|
4292
|
+
&& toolAllowedForRequest(tc) && isConcurrencySafeTool(this, tc.name, tc.input)
|
|
4293
|
+
&& !mayMutateWorkspaceAfterReturn(this, tc.name, tc.input)) {
|
|
4294
|
+
const parallelCalls = [];
|
|
4295
|
+
const segmentCacheKeys = new Set();
|
|
4296
|
+
for (let candidateIndex = toolCallIndex;
|
|
4297
|
+
candidateIndex < toolCalls.length && parallelCalls.length < MAX_CONCURRENT_READ_ONLY_TOOLS;
|
|
4298
|
+
candidateIndex += 1) {
|
|
4299
|
+
const candidate = toolCalls[candidateIndex];
|
|
4300
|
+
if (!toolAllowedForRequest(candidate)
|
|
4301
|
+
|| !isConcurrencySafeTool(this, candidate.name, candidate.input)
|
|
4302
|
+
|| mayMutateWorkspaceAfterReturn(this, candidate.name, candidate.input)) break;
|
|
4303
|
+
const candidateKey = `${candidate.name}\u001f${argsHashOf(candidate.input)}`;
|
|
4304
|
+
const candidateCacheable = isCacheableTool(this, candidate.name, candidate.input);
|
|
4305
|
+
// Keep identical cacheable reads on the serial commit path so the
|
|
4306
|
+
// second call reuses the first result instead of duplicating I/O.
|
|
4307
|
+
if (candidateCacheable
|
|
4308
|
+
&& (readOnlyToolResults.has(candidateKey) || segmentCacheKeys.has(candidateKey))) break;
|
|
4309
|
+
parallelCalls.push(candidate);
|
|
4310
|
+
if (candidateCacheable) segmentCacheKeys.add(candidateKey);
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
if (parallelCalls.length > 1) {
|
|
4314
|
+
const executions = [];
|
|
4315
|
+
for (const call of parallelCalls) {
|
|
4316
|
+
if (signal?.aborted) {
|
|
4317
|
+
abortedDuringTools = true;
|
|
4318
|
+
break;
|
|
4319
|
+
}
|
|
4320
|
+
announcedParallelToolCalls.add(call.id);
|
|
4321
|
+
yield {
|
|
4322
|
+
type: 'tool_start',
|
|
4323
|
+
id: call.id,
|
|
4324
|
+
name: call.name,
|
|
4325
|
+
input: call.input,
|
|
4326
|
+
threadId: this.currentThreadId,
|
|
4327
|
+
};
|
|
4328
|
+
if (signal?.aborted) {
|
|
4329
|
+
abortedDuringTools = true;
|
|
4330
|
+
break;
|
|
4331
|
+
}
|
|
4332
|
+
executions.push((async () => {
|
|
4333
|
+
const startedAt = Date.now();
|
|
4334
|
+
const callContext = toolContextForCall(call);
|
|
4335
|
+
const toolErrorOutput = this.#toolRegistry
|
|
4336
|
+
? this.#toolRegistry.get(call.name)?.errorOutput || null
|
|
4337
|
+
: this.#tools.get(call.name)?.errorOutput || null;
|
|
4338
|
+
try {
|
|
4339
|
+
const output = this.#toolRegistry
|
|
4340
|
+
? await this.#toolRegistry.execute(call.name, call.input, callContext)
|
|
4341
|
+
: normalizeToolOutput(await this.#tools.get(call.name).execute(call.input, callContext));
|
|
4342
|
+
return { call, startedAt, durationMs: Date.now() - startedAt, output, toolErrorOutput };
|
|
4343
|
+
} catch (error) {
|
|
4344
|
+
return { call, startedAt, durationMs: Date.now() - startedAt, error, toolErrorOutput };
|
|
4345
|
+
}
|
|
4346
|
+
})());
|
|
4347
|
+
}
|
|
4348
|
+
const completed = await Promise.all(executions);
|
|
4349
|
+
for (const execution of completed) {
|
|
4350
|
+
parallelToolExecutions.set(execution.call.id, execution);
|
|
4351
|
+
}
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
const readyParallelExecution = parallelToolExecutions.get(tc.id) || null;
|
|
4356
|
+
if (readyParallelExecution) announcedParallelToolCalls.delete(tc.id);
|
|
4233
4357
|
const activeToolBatchBarrier = toolBatchBarrier;
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4358
|
+
const abortSkipped = Boolean(signal?.aborted && !readyParallelExecution);
|
|
4359
|
+
if (abortSkipped) announcedParallelToolCalls.delete(tc.id);
|
|
4360
|
+
const skipped = abortSkipped
|
|
4361
|
+
|| (activeToolBatchBarrier != null && !readyParallelExecution);
|
|
4362
|
+
const toolStartTime = readyParallelExecution?.startedAt || Date.now();
|
|
4236
4363
|
|
|
4237
4364
|
// PR-L: duplicate-call detection. If this exact (toolName,
|
|
4238
4365
|
// argsHash) pair has already been executed DUP_TOOL_THRESHOLD
|
|
@@ -4297,7 +4424,20 @@ export class Engine {
|
|
|
4297
4424
|
: new Set();
|
|
4298
4425
|
const needsProjectDocReload = missingProjectDocScopes.size > 0;
|
|
4299
4426
|
|
|
4300
|
-
if (
|
|
4427
|
+
if (abortSkipped) {
|
|
4428
|
+
output = `Skipped ${tc.name} because the turn was aborted before this tool started.`;
|
|
4429
|
+
isError = true;
|
|
4430
|
+
yield {
|
|
4431
|
+
type: 'tool_end',
|
|
4432
|
+
id: tc.id,
|
|
4433
|
+
name: tc.name,
|
|
4434
|
+
output,
|
|
4435
|
+
isError: true,
|
|
4436
|
+
skipped: true,
|
|
4437
|
+
aborted: true,
|
|
4438
|
+
threadId: this.currentThreadId,
|
|
4439
|
+
};
|
|
4440
|
+
} else if (skipped) {
|
|
4301
4441
|
const source = activeToolBatchBarrier.sourceToolName || 'a preceding tool';
|
|
4302
4442
|
const sourceId = activeToolBatchBarrier.sourceToolCallId
|
|
4303
4443
|
? ` (${activeToolBatchBarrier.sourceToolCallId})`
|
|
@@ -4381,22 +4521,32 @@ export class Engine {
|
|
|
4381
4521
|
threadId: this.currentThreadId,
|
|
4382
4522
|
};
|
|
4383
4523
|
} else try {
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
toolErrorOutput =
|
|
4387
|
-
|
|
4524
|
+
if (readyParallelExecution) {
|
|
4525
|
+
parallelToolExecutions.delete(tc.id);
|
|
4526
|
+
toolErrorOutput = readyParallelExecution.toolErrorOutput || null;
|
|
4527
|
+
if (readyParallelExecution.error) throw readyParallelExecution.error;
|
|
4528
|
+
output = readyParallelExecution.output;
|
|
4388
4529
|
} else {
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4530
|
+
if (!announcedParallelToolCalls.delete(tc.id)) {
|
|
4531
|
+
yield { type: 'tool_start', id: tc.id, name: tc.name, input: tc.input, threadId: this.currentThreadId };
|
|
4532
|
+
}
|
|
4533
|
+
const callToolCtx = toolContextForCall(tc);
|
|
4534
|
+
if (this.#toolRegistry) {
|
|
4535
|
+
toolErrorOutput = this.#toolRegistry.get(tc.name)?.errorOutput || null;
|
|
4536
|
+
output = await this.#toolRegistry.execute(tc.name, tc.input, callToolCtx);
|
|
4537
|
+
} else {
|
|
4538
|
+
const tool = this.#tools.get(tc.name);
|
|
4539
|
+
toolErrorOutput = tool.errorOutput || null;
|
|
4540
|
+
// Pass the full toolCtx (cwd, workDir, signal, …) — not just
|
|
4541
|
+
// `{ signal }`. Legacy registerTool() callers historically got
|
|
4542
|
+
// a 1-field ctx, but that means tools like bash/file-read run
|
|
4543
|
+
// in the agent process cwd instead of the group's workDir.
|
|
4544
|
+
// Real production goes through #toolRegistry; the legacy path
|
|
4545
|
+
// is exercised by tests and a few standalone tools. Aligning
|
|
4546
|
+
// both paths keeps `ctx.cwd` semantics consistent.
|
|
4547
|
+
const rawOutput = await tool.execute(tc.input, callToolCtx);
|
|
4548
|
+
output = normalizeToolOutput(rawOutput);
|
|
4549
|
+
}
|
|
4400
4550
|
}
|
|
4401
4551
|
displayImages = extractDisplayImages(tc.name, output);
|
|
4402
4552
|
if (displayImages.length > 0) {
|
|
@@ -4443,7 +4593,7 @@ export class Engine {
|
|
|
4443
4593
|
|
|
4444
4594
|
currentToolCallForAsyncTask = null;
|
|
4445
4595
|
|
|
4446
|
-
const toolDurationMs = Date.now() - toolStartTime;
|
|
4596
|
+
const toolDurationMs = readyParallelExecution?.durationMs ?? (Date.now() - toolStartTime);
|
|
4447
4597
|
|
|
4448
4598
|
// feat-6af5f9f1 PR B: emit a structured `tool_exec` event for the
|
|
4449
4599
|
// debug panel. Keep raw output here; the model-facing tool
|