@yeaft/webchat-agent 0.1.829 → 0.1.831

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.829",
3
+ "version": "0.1.831",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/config.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * "language": "en",
14
14
  * "debug": false,
15
15
  * "maxContextTokens": 200000,
16
- * "messageTokenBudget": 8192
16
+ * "messageTokenBudget": 32768
17
17
  * }
18
18
  *
19
19
  * Legacy: if config.json doesn't exist, falls back to old config.md + .env for migration.
@@ -31,7 +31,7 @@ const DEFAULTS = {
31
31
  dir: DEFAULT_YEAFT_DIR,
32
32
  maxContextTokens: 200000,
33
33
  maxOutputTokens: 16384,
34
- messageTokenBudget: 8192,
34
+ messageTokenBudget: 32768,
35
35
  maxContinueTurns: 3,
36
36
  // task-318: Unify-specific runtime caps. `maxConcurrentThreads` gates
37
37
  // how many ThreadEngineRegistry instances may be live at once; dispatch
package/unify/engine.js CHANGED
@@ -1041,7 +1041,7 @@ export class Engine {
1041
1041
  if (!this.#conversationStore) return null;
1042
1042
  if (this.#config._readOnly) return null;
1043
1043
 
1044
- const budget = this.#config.messageTokenBudget || 8192;
1044
+ const budget = this.#config.messageTokenBudget || 32768;
1045
1045
  const compactCfg = (this.#config && this.#config.compact) || {};
1046
1046
  return this.#runOrchestratorCompact(budget, compactCfg);
1047
1047
  }
@@ -2255,12 +2255,12 @@ export class Engine {
2255
2255
  } else {
2256
2256
  const tool = this.#tools.get(tc.name);
2257
2257
  const rawOutput = await tool.execute(tc.input, { signal });
2258
- // task-704b: legacy #tools branch must apply the same per-tool
2259
- // cap as ToolRegistry.execute. Otherwise a deployment using
2260
- // the legacy registration path bypasses the defense entirely.
2258
+ // Legacy #tools branch must apply the same per-tool cap as
2259
+ // ToolRegistry.execute. Otherwise a deployment using the legacy
2260
+ // registration path bypasses the defense entirely.
2261
2261
  output = truncateToolResultIfNeeded(rawOutput, {
2262
- contextWindow: currentContextWindow,
2263
2262
  toolName: tc.name,
2263
+ language: this.#config?.language,
2264
2264
  });
2265
2265
  }
2266
2266
  yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: false, threadId: this.currentThreadId };
@@ -142,10 +142,10 @@ export const DEFAULT_RECENT_TURN_CAP = 25;
142
142
 
143
143
  /**
144
144
  * Default per-query token budget for the snapshot (separate from the
145
- * `tokenLimit` used by compact triggers). Mirrors the historical default
145
+ * `tokenLimit` used by compact triggers). Mirrors the default
146
146
  * carried in `~/.yeaft/config.json`'s `messageTokenBudget` field.
147
147
  */
148
- export const DEFAULT_MESSAGE_TOKEN_BUDGET = 8192;
148
+ export const DEFAULT_MESSAGE_TOKEN_BUDGET = 32768;
149
149
 
150
150
  /**
151
151
  * Estimate the token weight of a single message including role overhead
@@ -628,7 +628,7 @@ export async function compactHistory(messages, options) {
628
628
  * boundary and walks forward through `@vp-X` variants of the
629
629
  * cut turn so the slice is pair-safe.
630
630
  * 2. **Token budget** — if the trimmed slice still exceeds
631
- * `messageTokenBudget` tokens (default 8192 from
631
+ * `messageTokenBudget` tokens (default 32768 from
632
632
  * `~/.yeaft/config.json`), iteratively drop the oldest turn until
633
633
  * we're under budget. We never drop below 1 turn — even a single
634
634
  * huge turn is preferable to no context.
package/unify/init.js CHANGED
@@ -102,7 +102,7 @@ const DEFAULT_CONFIG_JSON = `{
102
102
  "language": "en",
103
103
  "debug": false,
104
104
  "maxContextTokens": 200000,
105
- "messageTokenBudget": 8192
105
+ "messageTokenBudget": 32768
106
106
  }
107
107
  `;
108
108
 
@@ -16,8 +16,8 @@
16
16
 
17
17
  // ─── Constants ──────────────────────────────────────────────────
18
18
 
19
- /** Default MESSAGE_TOKEN_BUDGET (context * 4%, default ~8192). */
20
- export const DEFAULT_MESSAGE_TOKEN_BUDGET = 8192;
19
+ /** Default MESSAGE_TOKEN_BUDGET for hot message compaction. */
20
+ export const DEFAULT_MESSAGE_TOKEN_BUDGET = 32768;
21
21
 
22
22
  /** After compact, keep this fraction of the budget. */
23
23
  export const COMPACT_KEEP_RATIO = 0.4;
@@ -10,27 +10,15 @@
10
10
  */
11
11
 
12
12
  import { formatSize } from '../archive/tool-results.js';
13
- import { DEFAULT_CONTEXT_WINDOW } from '../models.js';
14
13
 
15
14
  /**
16
15
  * Per-tool-result hard cap.
17
16
  *
18
- * A single tool can return megabytes (a grep over a large repo, a large
19
- * file read, a paginated web fetch). If we forward that verbatim into the
20
- * next LLM request the model returns LLMContextError ("context_length_exceeded")
21
- * and the user sees a hard failure mid-turn. Cap at a fraction of the
22
- * model's context window so even a runaway tool can't kill the request.
23
- *
24
- * Cap = max(MIN_CAP, floor(contextWindow * RATIO))
25
- * - RATIO = 10%: leaves 90% of context for system prompt, history, the
26
- * model's own output, and other tools called this turn. Generous in
27
- * absolute terms (25.6 KB on a 256 K window, ~20 K on a 200 K window),
28
- * plenty for a real tool result; small enough that 5 such results
29
- * still fit alongside everything else.
30
- * - MIN_CAP = 8 KB: sanity floor for tiny test fixtures (4 K context
31
- * models in tests would otherwise cap at 409 chars, defeating the
32
- * test's purpose). Real production models all have ≥ 32 K context, so
33
- * the floor never bites in practice.
17
+ * A single tool can return megabytes (a grep over a large repo, a large file
18
+ * read, a paginated web fetch). If we forward that verbatim into the next LLM
19
+ * request, persistence, or UI event, it bloats context and makes every replay
20
+ * expensive. Keep the hard boundary small and deterministic: one tool result
21
+ * gets at most 1 KiB before a visible truncation marker is appended.
34
22
  *
35
23
  * The truncation lands HERE (not in engine.js when pushing tool results
36
24
  * into messages) so the UI's `tool_end` event, the exec log, AND the
@@ -38,8 +26,7 @@ import { DEFAULT_CONTEXT_WINDOW } from '../models.js';
38
26
  * the user sees the full 2 MB output but the model gets a stub —
39
27
  * confusing.
40
28
  */
41
- const TOOL_RESULT_CAP_RATIO = 0.10;
42
- const TOOL_RESULT_MIN_CAP = 8 * 1024;
29
+ export const TOOL_RESULT_MAX_BYTES = 1024;
43
30
 
44
31
  function normalizeLanguage(language) {
45
32
  return String(language || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
@@ -151,7 +138,7 @@ export class ToolExecutionTimeoutError extends Error {
151
138
  /**
152
139
  * Truncate a tool result if it exceeds the per-result cap. Non-string
153
140
  * outputs are JSON-stringified first (matching what engine.js eventually
154
- * pushes into `content`), then capped.
141
+ * pushes into `content`), then capped by UTF-8 byte length.
155
142
  *
156
143
  * Edge cases handled:
157
144
  * - `undefined` → `JSON.stringify(undefined)` returns `undefined`, not
@@ -159,10 +146,10 @@ export class ToolExecutionTimeoutError extends Error {
159
146
  * - circular refs / `JSON.stringify` throws → fall back to `String(...)`.
160
147
  *
161
148
  * @param {unknown} output
162
- * @param {{ contextWindow?: number, toolName: string }} opts
149
+ * @param {{ toolName: string, language?: string }} opts
163
150
  * @returns {string}
164
151
  */
165
- export function truncateToolResultIfNeeded(output, { contextWindow, toolName, language } = {}) {
152
+ export function truncateToolResultIfNeeded(output, { toolName, language } = {}) {
166
153
  let text;
167
154
  if (typeof output === 'string') {
168
155
  text = output;
@@ -174,14 +161,21 @@ export function truncateToolResultIfNeeded(output, { contextWindow, toolName, la
174
161
  text = String(output);
175
162
  }
176
163
  }
177
- const ctx = Number.isFinite(contextWindow) && contextWindow > 0
178
- ? contextWindow : DEFAULT_CONTEXT_WINDOW;
179
- const cap = Math.max(TOOL_RESULT_MIN_CAP, Math.floor(ctx * TOOL_RESULT_CAP_RATIO));
180
- if (text.length <= cap) return text;
181
- const head = text.slice(0, cap);
164
+ const originalBytes = Buffer.byteLength(text, 'utf8');
165
+ if (originalBytes <= TOOL_RESULT_MAX_BYTES) return text;
166
+
167
+ const chunks = [];
168
+ let used = 0;
169
+ for (const ch of text) {
170
+ const n = Buffer.byteLength(ch, 'utf8');
171
+ if (used + n > TOOL_RESULT_MAX_BYTES) break;
172
+ chunks.push(ch);
173
+ used += n;
174
+ }
175
+ const head = chunks.join('');
182
176
  const marker = normalizeLanguage(language) === 'zh'
183
- ? `\n\n[已截断:${toolName} 返回 ${formatSize(text.length)},上限为 ${formatSize(cap)};模型不会看到剩余内容]`
184
- : `\n\n[truncated: ${toolName} returned ${formatSize(text.length)}, capped at ${formatSize(cap)}; the model will not see the rest of this output]`;
177
+ ? `\n\n[已截断:${toolName} 返回 ${formatSize(originalBytes)},上限为 ${formatSize(TOOL_RESULT_MAX_BYTES)};原因:单个 tool result 超过 1KB,模型和持久化展示不会看到剩余内容]`
178
+ : `\n\n[truncated: ${toolName} returned ${formatSize(originalBytes)}, capped at ${formatSize(TOOL_RESULT_MAX_BYTES)}; reason: single tool result exceeded 1KB, the model and persisted display will not see the rest]`;
185
179
  return head + marker;
186
180
  }
187
181
 
@@ -325,9 +319,8 @@ export class ToolRegistry {
325
319
  * Execute a tool by name.
326
320
  *
327
321
  * The result is passed through {@link truncateToolResultIfNeeded} so that
328
- * a single tool can never blow the context window. The cap derives from
329
- * `ctx.contextWindow` (the live model's window, threaded by engine.js);
330
- * fall back to a 200K default for callers that don't supply it.
322
+ * a single tool result never injects more than 1KB before the visible
323
+ * truncation marker.
331
324
  *
332
325
  * @param {string} name
333
326
  * @param {object} input
@@ -352,7 +345,6 @@ export class ToolRegistry {
352
345
  : await tool.execute(input, ctx);
353
346
 
354
347
  return truncateToolResultIfNeeded(output, {
355
- contextWindow: ctx.contextWindow,
356
348
  toolName: name,
357
349
  language: ctx.config?.language,
358
350
  });