@yeaft/webchat-agent 0.1.975 → 0.1.977
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/package.json +1 -1
- package/yeaft/debug-trace.js +15 -9
- package/yeaft/engine.js +15 -13
- package/yeaft/history-compact.js +23 -2
- package/yeaft/tools/registry.js +18 -18
- package/yeaft/web-bridge.js +2 -0
package/package.json
CHANGED
package/yeaft/debug-trace.js
CHANGED
|
@@ -48,6 +48,7 @@ const SCHEMA = `
|
|
|
48
48
|
tool_name TEXT NOT NULL,
|
|
49
49
|
tool_input TEXT,
|
|
50
50
|
tool_output TEXT,
|
|
51
|
+
tool_call_id TEXT,
|
|
51
52
|
duration_ms INTEGER,
|
|
52
53
|
is_error INTEGER DEFAULT 0,
|
|
53
54
|
created_at INTEGER NOT NULL,
|
|
@@ -101,12 +102,12 @@ function migrateAddColumn(db, table, column, type) {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
/** Max
|
|
105
|
-
const
|
|
105
|
+
/** Max tool input size stored inline. Tool output is persisted raw. */
|
|
106
|
+
const MAX_TOOL_INPUT = 10240;
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
109
|
* Max per-loop payload (system prompt, messages JSON, raw request /
|
|
109
|
-
* response, response text) stored per row. Larger than
|
|
110
|
+
* response, response text) stored per row. Larger than MAX_TOOL_INPUT
|
|
110
111
|
* because real-world LLM exchanges (system prompt + 30K-token message
|
|
111
112
|
* trail + raw response) routinely cross 10KB. 256KB lets us replay the
|
|
112
113
|
* panel verbatim for the most recent traces without bloating the DB.
|
|
@@ -207,6 +208,7 @@ export class DebugTrace {
|
|
|
207
208
|
// snapshot — `messages.find(role==='user')` would return turn 1's
|
|
208
209
|
// text for every subsequent turn, mislabeling every Turn header.
|
|
209
210
|
migrateAddColumn(this.#db, 'trace_turns', 'user_prompt', 'TEXT');
|
|
211
|
+
migrateAddColumn(this.#db, 'trace_tools', 'tool_call_id', 'TEXT');
|
|
210
212
|
// Indexes on the just-added columns. Must run AFTER the ALTER TABLEs
|
|
211
213
|
// — running them inside SCHEMA's CREATE INDEX IF NOT EXISTS block
|
|
212
214
|
// would fail with "no such column: group_id" on a pre-bugfix DB.
|
|
@@ -308,11 +310,12 @@ export class DebugTrace {
|
|
|
308
310
|
/**
|
|
309
311
|
* Log a tool call within a turn.
|
|
310
312
|
* @param {string} turnId
|
|
311
|
-
* @param {{ toolName: string, toolInput?: string, toolOutput?: string, durationMs?: number, isError?: boolean }} info
|
|
313
|
+
* @param {{ toolName: string, toolCallId?: string|null, toolInput?: string, toolOutput?: string, durationMs?: number, isError?: boolean }} info
|
|
312
314
|
* @returns {string} — tool record id
|
|
313
315
|
*/
|
|
314
316
|
logTool(turnId, {
|
|
315
317
|
toolName,
|
|
318
|
+
toolCallId = null,
|
|
316
319
|
toolInput = null,
|
|
317
320
|
toolOutput = null,
|
|
318
321
|
durationMs = null,
|
|
@@ -321,12 +324,13 @@ export class DebugTrace {
|
|
|
321
324
|
const id = randomUUID();
|
|
322
325
|
const now = Date.now();
|
|
323
326
|
this.#prepare('insertTool', `
|
|
324
|
-
INSERT INTO trace_tools (id, turn_id, tool_name, tool_input, tool_output, duration_ms, is_error, created_at)
|
|
325
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
327
|
+
INSERT INTO trace_tools (id, turn_id, tool_name, tool_input, tool_output, tool_call_id, duration_ms, is_error, created_at)
|
|
328
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
326
329
|
`).run(
|
|
327
330
|
id, turnId, toolName,
|
|
328
|
-
truncate(toolInput,
|
|
329
|
-
|
|
331
|
+
truncate(toolInput, MAX_TOOL_INPUT),
|
|
332
|
+
toolOutput == null ? null : String(toolOutput),
|
|
333
|
+
toolCallId,
|
|
330
334
|
durationMs, isError ? 1 : 0, now,
|
|
331
335
|
);
|
|
332
336
|
return id;
|
|
@@ -518,8 +522,10 @@ export class DebugTrace {
|
|
|
518
522
|
if (!t) continue;
|
|
519
523
|
t.tools.push({
|
|
520
524
|
loopNumber: owner.turn_number || 0,
|
|
521
|
-
callId: tool.id,
|
|
525
|
+
callId: tool.tool_call_id || tool.id,
|
|
526
|
+
traceToolId: tool.id,
|
|
522
527
|
name: tool.tool_name,
|
|
528
|
+
toolOutput: tool.tool_output == null ? null : String(tool.tool_output),
|
|
523
529
|
durationMs: tool.duration_ms || 0,
|
|
524
530
|
isError: !!tool.is_error,
|
|
525
531
|
});
|
package/yeaft/engine.js
CHANGED
|
@@ -43,7 +43,7 @@ import { countTurns } from './turn-utils.js';
|
|
|
43
43
|
import { attachRouterPlan, extractPriorPlan, stripMetaForWire } from './router/continuity.js';
|
|
44
44
|
import { resolveThinking } from './router/thinking.js';
|
|
45
45
|
import { approxTokens } from './memory/budget.js';
|
|
46
|
-
import { COLLAB_TOOL_POLICY, truncateToolResultIfNeeded } from './tools/registry.js';
|
|
46
|
+
import { COLLAB_TOOL_POLICY, normalizeToolOutput, truncateToolResultIfNeeded } from './tools/registry.js';
|
|
47
47
|
import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
|
|
48
48
|
import {
|
|
49
49
|
TOOL_BATCH_SIZE,
|
|
@@ -2489,13 +2489,7 @@ export class Engine {
|
|
|
2489
2489
|
// is exercised by tests and a few standalone tools. Aligning
|
|
2490
2490
|
// both paths keeps `ctx.cwd` semantics consistent.
|
|
2491
2491
|
const rawOutput = await tool.execute(tc.input, toolCtx);
|
|
2492
|
-
|
|
2493
|
-
// ToolRegistry.execute. Otherwise a deployment using the legacy
|
|
2494
|
-
// registration path bypasses the defense entirely.
|
|
2495
|
-
output = truncateToolResultIfNeeded(rawOutput, {
|
|
2496
|
-
toolName: tc.name,
|
|
2497
|
-
language: this.#config?.language,
|
|
2498
|
-
});
|
|
2492
|
+
output = normalizeToolOutput(rawOutput);
|
|
2499
2493
|
}
|
|
2500
2494
|
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: false, threadId: this.currentThreadId };
|
|
2501
2495
|
} catch (err) {
|
|
@@ -2508,9 +2502,8 @@ export class Engine {
|
|
|
2508
2502
|
const toolDurationMs = Date.now() - toolStartTime;
|
|
2509
2503
|
|
|
2510
2504
|
// feat-6af5f9f1 PR B: emit a structured `tool_exec` event for the
|
|
2511
|
-
// debug panel.
|
|
2512
|
-
//
|
|
2513
|
-
// duplicate them here — only the per-tool timing + status.
|
|
2505
|
+
// debug panel. Keep raw output here; the model-facing tool
|
|
2506
|
+
// message below is deliberately truncated for context budget.
|
|
2514
2507
|
yield {
|
|
2515
2508
|
type: 'tool_exec',
|
|
2516
2509
|
turnId: queryTurnId,
|
|
@@ -2520,6 +2513,7 @@ export class Engine {
|
|
|
2520
2513
|
name: tc.name,
|
|
2521
2514
|
durationMs: toolDurationMs,
|
|
2522
2515
|
isError,
|
|
2516
|
+
toolOutput: output,
|
|
2523
2517
|
};
|
|
2524
2518
|
|
|
2525
2519
|
// 2026-05-13: feed the per-tool counters. Stays best-effort — a
|
|
@@ -2539,17 +2533,25 @@ export class Engine {
|
|
|
2539
2533
|
// Log tool to debug trace
|
|
2540
2534
|
this.#trace.logTool(turnId, {
|
|
2541
2535
|
toolName: tc.name,
|
|
2536
|
+
toolCallId: tc.id,
|
|
2542
2537
|
toolInput: JSON.stringify(tc.input),
|
|
2543
2538
|
toolOutput: output,
|
|
2544
2539
|
durationMs: toolDurationMs,
|
|
2545
2540
|
isError,
|
|
2541
|
+
toolOutput: output,
|
|
2546
2542
|
});
|
|
2547
2543
|
|
|
2548
|
-
// Append
|
|
2544
|
+
// Append only the bounded copy to the model message history. Raw
|
|
2545
|
+
// `output` is still used for debug traces, UI events, exec-log, and
|
|
2546
|
+
// persistence so large tool results are not lost outside context.
|
|
2547
|
+
const contextOutput = truncateToolResultIfNeeded(output, {
|
|
2548
|
+
toolName: tc.name,
|
|
2549
|
+
language: this.#config?.language,
|
|
2550
|
+
});
|
|
2549
2551
|
conversationMessages.push({
|
|
2550
2552
|
role: 'tool',
|
|
2551
2553
|
toolCallId: tc.id,
|
|
2552
|
-
content:
|
|
2554
|
+
content: contextOutput,
|
|
2553
2555
|
isError,
|
|
2554
2556
|
});
|
|
2555
2557
|
|
package/yeaft/history-compact.js
CHANGED
|
@@ -54,12 +54,28 @@
|
|
|
54
54
|
|
|
55
55
|
import { estimateTokens } from './conversation/persist.js';
|
|
56
56
|
import { pairSanitize } from './pair-sanitize.js';
|
|
57
|
+
import { truncateToolResultIfNeeded } from './tools/registry.js';
|
|
57
58
|
import {
|
|
58
59
|
countTurns as countTurnsImpl,
|
|
59
60
|
indexOfNthTurnFromEnd,
|
|
60
61
|
sliceLastNTurns,
|
|
61
62
|
} from './turn-utils.js';
|
|
62
63
|
|
|
64
|
+
|
|
65
|
+
function truncateToolResultsForModel(messages, opts = {}) {
|
|
66
|
+
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
67
|
+
return messages.map((m) => {
|
|
68
|
+
if (!m || m.role !== 'tool' || typeof m.content !== 'string') return { ...m };
|
|
69
|
+
return {
|
|
70
|
+
...m,
|
|
71
|
+
content: truncateToolResultIfNeeded(m.content, {
|
|
72
|
+
toolName: m.name || m.toolName || 'tool_result',
|
|
73
|
+
language: opts.language,
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
63
79
|
/**
|
|
64
80
|
* Re-export `countTurns` so existing callers / tests that import it
|
|
65
81
|
* from this module continue to work. Implementation now lives in
|
|
@@ -706,7 +722,7 @@ export async function compactHistory(messages, options) {
|
|
|
706
722
|
* between trim (per-call) and compact (global) explicit.
|
|
707
723
|
*
|
|
708
724
|
* @param {Array<object>} snapshot
|
|
709
|
-
* @param {{ messageTokenBudget?: number, recentTurnCap?: number, keepToolTurns?: number }} [opts]
|
|
725
|
+
* @param {{ messageTokenBudget?: number, recentTurnCap?: number, keepToolTurns?: number, language?: string }} [opts]
|
|
710
726
|
* @returns {Array<object>}
|
|
711
727
|
*/
|
|
712
728
|
export function trimSnapshotForBudget(snapshot, opts = {}) {
|
|
@@ -738,6 +754,11 @@ export function trimSnapshotForBudget(snapshot, opts = {}) {
|
|
|
738
754
|
keepToolTurns: opts.keepToolTurns,
|
|
739
755
|
});
|
|
740
756
|
|
|
741
|
-
// Stage 4:
|
|
757
|
+
// Stage 4: bound the raw tool result copy that is fed back into the model.
|
|
758
|
+
// The in-memory/persisted transcript keeps the full content; this transform
|
|
759
|
+
// only affects the per-query snapshot passed to engine.query().
|
|
760
|
+
trimmed = truncateToolResultsForModel(trimmed, { language: opts.language });
|
|
761
|
+
|
|
762
|
+
// Stage 5: pair-sanitize to drop orphan tool_use/tool_result.
|
|
742
763
|
return pairSanitize(trimmed);
|
|
743
764
|
}
|
package/yeaft/tools/registry.js
CHANGED
|
@@ -33,19 +33,17 @@ export const SUB_AGENT_TOOL_NAMES = Object.freeze([
|
|
|
33
33
|
export const FORWARD_TOOL_NAMES = Object.freeze(['RouteForward']);
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Per-tool-result
|
|
36
|
+
* Per-tool-result model-context cap.
|
|
37
37
|
*
|
|
38
38
|
* A single tool can return megabytes (a grep over a large repo, a large file
|
|
39
39
|
* read, a paginated web fetch). If we forward that verbatim into the next LLM
|
|
40
|
-
* request,
|
|
41
|
-
*
|
|
40
|
+
* request, it bloats context and makes every replay expensive. Keep the
|
|
41
|
+
* boundary small and deterministic for LLM message history: one tool result
|
|
42
42
|
* gets at most 1 KiB before a visible truncation marker is appended.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* the user sees the full 2 MB output but the model gets a stub —
|
|
48
|
-
* confusing.
|
|
44
|
+
* Do NOT apply this at tool execution time. Debug events, exec logs, and
|
|
45
|
+
* persisted transcripts need the raw result. The engine/history replay path
|
|
46
|
+
* applies this only when building messages for the model.
|
|
49
47
|
*/
|
|
50
48
|
export const TOOL_RESULT_MAX_BYTES = 1024;
|
|
51
49
|
|
|
@@ -170,7 +168,7 @@ export class ToolExecutionTimeoutError extends Error {
|
|
|
170
168
|
* @param {{ toolName: string, language?: string }} opts
|
|
171
169
|
* @returns {string}
|
|
172
170
|
*/
|
|
173
|
-
export function
|
|
171
|
+
export function normalizeToolOutput(output) {
|
|
174
172
|
let text;
|
|
175
173
|
if (typeof output === 'string') {
|
|
176
174
|
text = output;
|
|
@@ -182,6 +180,11 @@ export function truncateToolResultIfNeeded(output, { toolName, language } = {})
|
|
|
182
180
|
text = String(output);
|
|
183
181
|
}
|
|
184
182
|
}
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function truncateToolResultIfNeeded(output, { toolName, language } = {}) {
|
|
187
|
+
const text = normalizeToolOutput(output);
|
|
185
188
|
const originalBytes = Buffer.byteLength(text, 'utf8');
|
|
186
189
|
if (originalBytes <= TOOL_RESULT_MAX_BYTES) return text;
|
|
187
190
|
|
|
@@ -195,8 +198,8 @@ export function truncateToolResultIfNeeded(output, { toolName, language } = {})
|
|
|
195
198
|
}
|
|
196
199
|
const head = chunks.join('');
|
|
197
200
|
const marker = normalizeLanguage(language) === 'zh'
|
|
198
|
-
? `\n\n[已截断:${toolName} 返回 ${formatSize(originalBytes)},上限为 ${formatSize(TOOL_RESULT_MAX_BYTES)};原因:单个 tool result 超过 1KB
|
|
199
|
-
: `\n\n[truncated: ${toolName} returned ${formatSize(originalBytes)}, capped at ${formatSize(TOOL_RESULT_MAX_BYTES)}; reason: single tool result exceeded 1KB, the model
|
|
201
|
+
? `\n\n[已截断:${toolName} 返回 ${formatSize(originalBytes)},上限为 ${formatSize(TOOL_RESULT_MAX_BYTES)};原因:单个 tool result 超过 1KB,模型消息历史不会看到剩余内容]`
|
|
202
|
+
: `\n\n[truncated: ${toolName} returned ${formatSize(originalBytes)}, capped at ${formatSize(TOOL_RESULT_MAX_BYTES)}; reason: single tool result exceeded 1KB, the model message history will not see the rest]`;
|
|
200
203
|
return head + marker;
|
|
201
204
|
}
|
|
202
205
|
|
|
@@ -372,9 +375,9 @@ export class ToolRegistry {
|
|
|
372
375
|
/**
|
|
373
376
|
* Execute a tool by name.
|
|
374
377
|
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
+
* Returns the raw tool result as text. Do not truncate here: debug display,
|
|
379
|
+
* exec logs, and persistence must retain the full result. The engine/history
|
|
380
|
+
* replay path truncates only the copy inserted into model message history.
|
|
378
381
|
*
|
|
379
382
|
* @param {string} name
|
|
380
383
|
* @param {object} input
|
|
@@ -398,10 +401,7 @@ export class ToolRegistry {
|
|
|
398
401
|
? await runWithTimeout(tool.execute(input, ctx), rawTimeout, name)
|
|
399
402
|
: await tool.execute(input, ctx);
|
|
400
403
|
|
|
401
|
-
return
|
|
402
|
-
toolName: name,
|
|
403
|
-
language: ctx.config?.language,
|
|
404
|
-
});
|
|
404
|
+
return normalizeToolOutput(output);
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
/** Number of registered tools. */
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -2276,6 +2276,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
2276
2276
|
name: event.name,
|
|
2277
2277
|
durationMs: event.durationMs,
|
|
2278
2278
|
isError: event.isError,
|
|
2279
|
+
toolOutput: event.toolOutput,
|
|
2279
2280
|
}, envelope);
|
|
2280
2281
|
break;
|
|
2281
2282
|
|
|
@@ -3035,6 +3036,7 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3035
3036
|
// array). See `trimSnapshotForBudget` doc-block for policy.
|
|
3036
3037
|
const trimmedMessages = trimSnapshotForBudget(baseSnapshot, {
|
|
3037
3038
|
messageTokenBudget: session?.config?.messageTokenBudget,
|
|
3039
|
+
language: session?.config?.language,
|
|
3038
3040
|
});
|
|
3039
3041
|
for await (const event of vpEngine.query({
|
|
3040
3042
|
prompt,
|