@yeaft/webchat-agent 1.0.351 → 1.0.352
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/connection/message-router.js +29 -1
- package/index.js +2 -0
- package/local-runtime/server/handlers/agent-sync.js +14 -0
- package/local-runtime/server/handlers/client-misc.js +21 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +52 -33
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/config-api.js +53 -1
- package/yeaft/config.js +54 -0
- package/yeaft/conversation/history-index-worker.js +13 -10
- package/yeaft/conversation/internal-control.js +1 -0
- package/yeaft/debug-trace.js +164 -47
- package/yeaft/engine.js +318 -28
- package/yeaft/llm/adapter.js +38 -0
- package/yeaft/llm/anthropic.js +11 -8
- package/yeaft/llm/openai-responses.js +11 -8
- package/yeaft/llm/router.js +1 -1
- package/yeaft/perf-trace.js +156 -24
- package/yeaft/session.js +7 -0
- package/yeaft/sessions/session-crud.js +19 -4
- package/yeaft/sub-agent/runner.js +4 -0
- package/yeaft/tools/agent.js +4 -0
- package/yeaft/tools/ask-user.js +1 -0
- package/yeaft/tools/bash.js +4 -0
- package/yeaft/tools/create-work-item.js +3 -0
- package/yeaft/tools/file-read.js +1 -0
- package/yeaft/tools/glob.js +1 -0
- package/yeaft/tools/grep.js +1 -0
- package/yeaft/tools/history-search.js +74 -20
- package/yeaft/tools/js-repl.js +1 -0
- package/yeaft/tools/list-agents.js +1 -0
- package/yeaft/tools/list-dir.js +1 -0
- package/yeaft/tools/list-tasks.js +1 -0
- package/yeaft/tools/read-task-log.js +1 -0
- package/yeaft/tools/route-forward.js +4 -0
- package/yeaft/tools/send-message.js +3 -0
- package/yeaft/tools/types.js +8 -0
- package/yeaft/tools/wait-agent.js +1 -0
- package/yeaft/utf8.js +44 -0
- package/yeaft/web-bridge.js +6 -0
- package/yeaft/work-center/runner.js +1 -0
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
redactRawRequest,
|
|
21
21
|
safeHeaders,
|
|
22
22
|
SseLineBuffer,
|
|
23
|
+
createBoundedTextAccumulator,
|
|
23
24
|
toWellFormedJson,
|
|
24
25
|
} from './adapter.js';
|
|
25
26
|
import {
|
|
@@ -242,7 +243,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
242
243
|
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', effortContext?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
243
244
|
* @returns {AsyncGenerator<import('./adapter.js').StreamEvent>}
|
|
244
245
|
*/
|
|
245
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange, onRequestStart }) {
|
|
246
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext, signal, onRawExchange, rawExchangeMaxBytes = 512 * 1024, onRequestStart }) {
|
|
246
247
|
if (signal?.aborted) throw new LLMAbortError();
|
|
247
248
|
|
|
248
249
|
const body = {
|
|
@@ -323,11 +324,10 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
323
324
|
// signature → next turn 400s identically).
|
|
324
325
|
/** @type {Map<number, { kind: string, [k: string]: any }>} */
|
|
325
326
|
const blockByIndex = new Map();
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
|
|
330
|
-
const rawSseBodyChunks = [];
|
|
327
|
+
// Keep raw SSE chunks only until the engine receives the bounded exchange
|
|
328
|
+
// callback. The engine owns the configured byte budget; the adapter avoids
|
|
329
|
+
// quadratic string concatenation by storing chunks separately.
|
|
330
|
+
const rawSseBody = createBoundedTextAccumulator(rawExchangeMaxBytes);
|
|
331
331
|
const responseHeaders = safeHeaders(response);
|
|
332
332
|
const responseStatus = response.status;
|
|
333
333
|
let sawStop = false;
|
|
@@ -344,7 +344,7 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
344
344
|
if (done) break;
|
|
345
345
|
|
|
346
346
|
const chunkText = decoder.decode(value, { stream: true });
|
|
347
|
-
|
|
347
|
+
rawSseBody.push(chunkText);
|
|
348
348
|
const lines = sseLines.push(chunkText);
|
|
349
349
|
|
|
350
350
|
for (const line of lines) {
|
|
@@ -515,8 +515,11 @@ export class AnthropicAdapter extends LLMAdapter {
|
|
|
515
515
|
rawResponse: {
|
|
516
516
|
status: responseStatus,
|
|
517
517
|
headers: responseHeaders,
|
|
518
|
-
body:
|
|
518
|
+
body: rawSseBody.text(),
|
|
519
519
|
format: 'sse',
|
|
520
|
+
truncated: rawSseBody.truncated,
|
|
521
|
+
originalBytes: rawSseBody.totalBytes,
|
|
522
|
+
maxBytes: rawSseBody.maxBytes,
|
|
520
523
|
},
|
|
521
524
|
});
|
|
522
525
|
} catch { /* ignore */ }
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
redactRawRequest,
|
|
40
40
|
safeHeaders,
|
|
41
41
|
SseLineBuffer,
|
|
42
|
+
createBoundedTextAccumulator,
|
|
42
43
|
toWellFormedJson,
|
|
43
44
|
} from './adapter.js';
|
|
44
45
|
import {
|
|
@@ -259,7 +260,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
259
260
|
* `api-key` headers are auto-redacted (see `redactRawRequest` in
|
|
260
261
|
* `adapter.js`); request-body fields are caller-controlled.
|
|
261
262
|
*/
|
|
262
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext = {}, extraBody, signal, onRawExchange, onRequestStart }) {
|
|
263
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext = {}, extraBody, signal, onRawExchange, rawExchangeMaxBytes = 512 * 1024, onRequestStart }) {
|
|
263
264
|
if (signal?.aborted) throw new LLMAbortError();
|
|
264
265
|
|
|
265
266
|
const body = {
|
|
@@ -346,11 +347,10 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
346
347
|
const emittedToolCallIds = new Set();
|
|
347
348
|
let sawToolCall = false;
|
|
348
349
|
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
const rawSseBodyChunks = [];
|
|
350
|
+
// Keep raw SSE chunks only until the engine receives the bounded exchange
|
|
351
|
+
// callback. The engine owns the configured byte budget; the adapter avoids
|
|
352
|
+
// quadratic string concatenation by storing chunks separately.
|
|
353
|
+
const rawSseBody = createBoundedTextAccumulator(rawExchangeMaxBytes);
|
|
354
354
|
const responseHeaders = safeHeaders(response);
|
|
355
355
|
const responseStatus = response.status;
|
|
356
356
|
let sawTerminalEvent = false;
|
|
@@ -364,7 +364,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
364
364
|
});
|
|
365
365
|
if (done) break;
|
|
366
366
|
const chunkText = decoder.decode(value, { stream: true });
|
|
367
|
-
|
|
367
|
+
rawSseBody.push(chunkText);
|
|
368
368
|
|
|
369
369
|
// SSE events are separated by blank lines; SseLineBuffer yields each
|
|
370
370
|
// completed line (newline stripped) in O(n) total.
|
|
@@ -503,8 +503,11 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
503
503
|
rawResponse: {
|
|
504
504
|
status: responseStatus,
|
|
505
505
|
headers: responseHeaders,
|
|
506
|
-
body:
|
|
506
|
+
body: rawSseBody.text(),
|
|
507
507
|
format: 'sse',
|
|
508
|
+
truncated: rawSseBody.truncated,
|
|
509
|
+
originalBytes: rawSseBody.totalBytes,
|
|
510
|
+
maxBytes: rawSseBody.maxBytes,
|
|
508
511
|
},
|
|
509
512
|
});
|
|
510
513
|
} catch { /* ignore */ }
|
package/yeaft/llm/router.js
CHANGED
|
@@ -610,7 +610,7 @@ export class AdapterRouter extends LLMAdapter {
|
|
|
610
610
|
const filtered = filterEffortForModel({ ...params, model: resolved.modelId }, resolved);
|
|
611
611
|
const sanitized = sanitizeMessagesForWire(filtered);
|
|
612
612
|
try {
|
|
613
|
-
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId, effortContext });
|
|
613
|
+
yield* resolved.adapter.stream({ ...sanitized, model: resolved.modelId, effortContext, rawExchangeMaxBytes: params.rawExchangeMaxBytes });
|
|
614
614
|
return;
|
|
615
615
|
} catch (err) {
|
|
616
616
|
this.#annotateAuthError(err, provider, params.model);
|
package/yeaft/perf-trace.js
CHANGED
|
@@ -1,20 +1,88 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync, readdirSync, rmSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { normalizeUtf8ByteBudget, toWellFormedText, utf8PrefixWithinBytes } from './utf8.js';
|
|
3
4
|
|
|
4
5
|
const MAX_DETAIL_STRING = 512;
|
|
5
6
|
const DEFAULT_RETENTION_DAYS = 3;
|
|
6
|
-
|
|
7
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 1_000;
|
|
8
|
+
const DEFAULT_MAX_QUEUE_SIZE = 5_000;
|
|
9
|
+
const DEFAULT_RAW_EXCHANGE_MAX_BYTES = 512 * 1024;
|
|
10
|
+
const lastCleanupDays = new Map();
|
|
11
|
+
const queues = new Map();
|
|
12
|
+
const flushTimers = new Map();
|
|
7
13
|
|
|
8
|
-
function
|
|
9
|
-
const
|
|
10
|
-
|
|
14
|
+
function telemetryConfig(config) {
|
|
15
|
+
const value = config?.telemetry && typeof config.telemetry === 'object' ? config.telemetry : {};
|
|
16
|
+
const number = (candidate, fallback, min, max) => {
|
|
17
|
+
const parsed = Number(candidate);
|
|
18
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
19
|
+
return Math.min(max, Math.max(min, Math.floor(parsed)));
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
enabled: value.enabled !== false,
|
|
23
|
+
retentionDays: number(value.retentionDays, DEFAULT_RETENTION_DAYS, 1, 3650),
|
|
24
|
+
flushIntervalMs: number(value.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS, 0, 60_000),
|
|
25
|
+
maxQueueSize: number(value.maxQueueSize, DEFAULT_MAX_QUEUE_SIZE, 100, 50_000),
|
|
26
|
+
rawExchangeMaxBytes: number(value.rawExchangeMaxBytes, DEFAULT_RAW_EXCHANGE_MAX_BYTES, 0, 4 * 1024 * 1024),
|
|
27
|
+
traceTextMaxBytes: number(value.traceTextMaxBytes, 256 * 1024, 0, 4 * 1024 * 1024),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveAgentLocalRoot(config) {
|
|
32
|
+
for (const candidate of [config?.yeaftDir, config?.dir]) {
|
|
33
|
+
if (typeof candidate === 'string' && candidate.trim()) return candidate.trim();
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function queueKey(config) {
|
|
39
|
+
return resolveAgentLocalRoot(config);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function truncateUtf8Text(value, maxBytes = DEFAULT_RAW_EXCHANGE_MAX_BYTES) {
|
|
43
|
+
const limit = normalizeUtf8ByteBudget(maxBytes, DEFAULT_RAW_EXCHANGE_MAX_BYTES);
|
|
44
|
+
const text = toWellFormedText(value);
|
|
45
|
+
const originalBytes = Buffer.byteLength(text, 'utf8');
|
|
46
|
+
if (originalBytes <= limit) return { value: text, truncated: false, originalBytes };
|
|
47
|
+
|
|
48
|
+
// One code-point pass avoids the old repeated slice + Buffer.byteLength()
|
|
49
|
+
// backtracking loop, which could stall the event loop on a large raw
|
|
50
|
+
// request or response. The shared helper also refuses to split surrogate
|
|
51
|
+
// pairs and normalizes lone surrogates before producing a preview.
|
|
52
|
+
const prefix = utf8PrefixWithinBytes(text, limit);
|
|
53
|
+
return { value: prefix.text, truncated: true, originalBytes };
|
|
11
54
|
}
|
|
12
55
|
|
|
13
|
-
function
|
|
56
|
+
export function boundRawExchange(value, maxBytes = DEFAULT_RAW_EXCHANGE_MAX_BYTES) {
|
|
57
|
+
const limit = normalizeUtf8ByteBudget(maxBytes, DEFAULT_RAW_EXCHANGE_MAX_BYTES);
|
|
58
|
+
if (value == null || limit <= 0) return limit <= 0 && value != null ? { __truncated: true, maxBytes: limit } : value;
|
|
59
|
+
if (typeof value === 'string') {
|
|
60
|
+
const result = truncateUtf8Text(value, limit);
|
|
61
|
+
return result.truncated ? { __truncated: true, maxBytes: limit, originalBytes: result.originalBytes, preview: result.value } : result.value;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const json = JSON.stringify(value);
|
|
65
|
+
const result = truncateUtf8Text(json, limit);
|
|
66
|
+
if (!result.truncated) return JSON.parse(json);
|
|
67
|
+
return { __truncated: true, maxBytes: limit, originalBytes: result.originalBytes, preview: result.value };
|
|
68
|
+
} catch {
|
|
69
|
+
return { __truncated: true, maxBytes: limit };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function retentionDays(config) {
|
|
74
|
+
const configured = Number(telemetryConfig(config).retentionDays);
|
|
75
|
+
const env = Number(process.env.PERF_TRACE_RETENTION_DAYS || process.env.YEAFT_PERF_TRACE_RETENTION_DAYS);
|
|
76
|
+
if (Number.isFinite(env) && env > 0) return Math.floor(env);
|
|
77
|
+
return Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_RETENTION_DAYS;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function cleanupOldTraceFiles(root, config = null) {
|
|
14
81
|
const day = new Date().toISOString().slice(0, 10);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
82
|
+
const key = root;
|
|
83
|
+
if (lastCleanupDays.get(key) === day) return;
|
|
84
|
+
lastCleanupDays.set(key, day);
|
|
85
|
+
const keepMs = retentionDays(config) * 24 * 60 * 60 * 1000;
|
|
18
86
|
const cutoff = Date.now() - keepMs;
|
|
19
87
|
try {
|
|
20
88
|
for (const file of readdirSync(root)) {
|
|
@@ -57,15 +125,74 @@ export function perfNowMs() {
|
|
|
57
125
|
return Number(process.hrtime.bigint()) / 1e6;
|
|
58
126
|
}
|
|
59
127
|
|
|
128
|
+
function flushQueue(key, config) {
|
|
129
|
+
const queue = queues.get(key);
|
|
130
|
+
if (!queue || queue.length === 0) return 0;
|
|
131
|
+
const batch = queue.splice(0, queue.length);
|
|
132
|
+
const root = join(key, 'perf-traces');
|
|
133
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
134
|
+
try {
|
|
135
|
+
mkdirSync(root, { recursive: true });
|
|
136
|
+
cleanupOldTraceFiles(root, config);
|
|
137
|
+
appendFileSync(join(root, `${day}.jsonl`), batch.map(row => JSON.stringify(row)).join('\n') + '\n');
|
|
138
|
+
return batch.length;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// Put the batch back only when the queue has room. Losing diagnostics is
|
|
141
|
+
// preferable to blocking the engine or growing memory without a bound.
|
|
142
|
+
const limit = telemetryConfig(config).maxQueueSize;
|
|
143
|
+
queues.set(key, [...batch.slice(-limit), ...(queues.get(key) || [])].slice(-limit));
|
|
144
|
+
if (process.env.YEAFT_PERF_TRACE_DEBUG === '1') {
|
|
145
|
+
console.warn('[Yeaft] perf trace write failed:', err?.message || err);
|
|
146
|
+
}
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function scheduleFlush(key, config) {
|
|
152
|
+
if (flushTimers.has(key)) return;
|
|
153
|
+
const delay = telemetryConfig(config).flushIntervalMs;
|
|
154
|
+
if (delay <= 0) {
|
|
155
|
+
queueMicrotask(() => flushQueue(key, config));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
flushTimers.delete(key);
|
|
160
|
+
flushQueue(key, config);
|
|
161
|
+
}, delay);
|
|
162
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
163
|
+
flushTimers.set(key, timer);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function flushAgentPerfTrace(config) {
|
|
167
|
+
const key = queueKey(config);
|
|
168
|
+
if (!key) return 0;
|
|
169
|
+
const timer = flushTimers.get(key);
|
|
170
|
+
if (timer) {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
flushTimers.delete(key);
|
|
173
|
+
}
|
|
174
|
+
return flushQueue(key, config);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function flushAllAgentPerfTraces() {
|
|
178
|
+
let count = 0;
|
|
179
|
+
for (const [key, queue] of queues) {
|
|
180
|
+
if (!queue.length) continue;
|
|
181
|
+
const config = { yeaftDir: key };
|
|
182
|
+
count += flushQueue(key, config);
|
|
183
|
+
}
|
|
184
|
+
for (const timer of flushTimers.values()) clearTimeout(timer);
|
|
185
|
+
flushTimers.clear();
|
|
186
|
+
return count;
|
|
187
|
+
}
|
|
188
|
+
|
|
60
189
|
export function recordAgentPerfTrace(config, event = {}) {
|
|
61
190
|
const traceId = typeof event.traceId === 'string' && event.traceId.trim()
|
|
62
191
|
? event.traceId.trim()
|
|
63
192
|
: (typeof event.perfTraceId === 'string' && event.perfTraceId.trim() ? event.perfTraceId.trim() : null);
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
const root = join(yeaftDir.trim(), 'perf-traces');
|
|
68
|
-
const day = new Date().toISOString().slice(0, 10);
|
|
193
|
+
const key = queueKey(config);
|
|
194
|
+
const settings = telemetryConfig(config);
|
|
195
|
+
if (!traceId || !key || !settings.enabled) return false;
|
|
69
196
|
const row = {
|
|
70
197
|
traceId,
|
|
71
198
|
source: 'agent',
|
|
@@ -82,20 +209,25 @@ export function recordAgentPerfTrace(config, event = {}) {
|
|
|
82
209
|
ok: typeof event.ok === 'boolean' ? event.ok : null,
|
|
83
210
|
detail: sanitizeValue(event.detail || null),
|
|
84
211
|
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
212
|
+
const queue = queues.get(key) || [];
|
|
213
|
+
if (queue.length >= settings.maxQueueSize) queue.shift();
|
|
214
|
+
queue.push(row);
|
|
215
|
+
queues.set(key, queue);
|
|
216
|
+
// Never write synchronously from the engine phase hook. The timer batches
|
|
217
|
+
// events across the turn; session shutdown calls flushAgentPerfTrace() as a
|
|
218
|
+
// durability boundary for short-lived CLI runs.
|
|
219
|
+
if (queue.length >= settings.maxQueueSize) flushQueue(key, config);
|
|
220
|
+
else scheduleFlush(key, config);
|
|
221
|
+
return true;
|
|
96
222
|
}
|
|
97
223
|
|
|
98
224
|
export const __perfTraceForTest = {
|
|
99
225
|
sanitizeValue,
|
|
100
226
|
cleanupOldTraceFiles,
|
|
227
|
+
telemetryConfig,
|
|
228
|
+
queues,
|
|
229
|
+
flushTimers,
|
|
230
|
+
boundRawExchange,
|
|
231
|
+
truncateUtf8Text,
|
|
232
|
+
flushAllAgentPerfTraces,
|
|
101
233
|
};
|
package/yeaft/session.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { initYeaftDir, DEFAULT_YEAFT_DIR, isWritable } from './init.js';
|
|
17
17
|
import { loadConfig, loadMCPConfig } from './config.js';
|
|
18
18
|
import { createTrace } from './debug-trace.js';
|
|
19
|
+
import { flushAgentPerfTrace } from './perf-trace.js';
|
|
19
20
|
import { createLLMAdapter } from './llm/adapter.js';
|
|
20
21
|
import { withUsageAccounting } from './llm/usage-accounting.js';
|
|
21
22
|
import { recordAgentTokenUsage } from '../metrics.js';
|
|
@@ -254,6 +255,7 @@ export async function loadSession(options = {}) {
|
|
|
254
255
|
const trace = createTrace({
|
|
255
256
|
enabled: config.debug === true,
|
|
256
257
|
dirPath: yeaftDir,
|
|
258
|
+
textMaxBytes: config.telemetry?.traceTextMaxBytes,
|
|
257
259
|
});
|
|
258
260
|
|
|
259
261
|
// ─── 4. Create LLM adapter ────────────────────────────
|
|
@@ -591,6 +593,11 @@ export async function loadSession(options = {}) {
|
|
|
591
593
|
} catch {
|
|
592
594
|
// Trace might not have close() (NullTrace)
|
|
593
595
|
}
|
|
596
|
+
try {
|
|
597
|
+
flushAgentPerfTrace(config);
|
|
598
|
+
} catch {
|
|
599
|
+
// Performance telemetry is best-effort and must not block shutdown.
|
|
600
|
+
}
|
|
594
601
|
try {
|
|
595
602
|
if (memoryIndex) memoryIndex.close();
|
|
596
603
|
} catch {
|
|
@@ -403,14 +403,17 @@ export function restoreSessionToRegistry(defaultYeaftDir, sessionId, workDir) {
|
|
|
403
403
|
return importedMeta;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
|
|
406
|
+
/**
|
|
407
|
+
* Resolve an already-existing Session owner without bootstrapping or migrating
|
|
408
|
+
* Session storage. Read-only callers must use this instead of
|
|
409
|
+
* resolveSessionYeaftDir(): the latter intentionally repairs the manifest for
|
|
410
|
+
* runtime/CRUD entry points.
|
|
411
|
+
*/
|
|
412
|
+
export function findExistingSessionYeaftDir(defaultYeaftDir, sessionId) {
|
|
407
413
|
if (!defaultYeaftDir || !sessionId) return defaultYeaftDir;
|
|
408
414
|
|
|
409
|
-
const manifestReady = hasSessionManifest(defaultYeaftDir);
|
|
410
|
-
ensureSessionManifestReady(defaultYeaftDir);
|
|
411
415
|
const manifestDir = resolveManifestSessionDir(defaultYeaftDir, sessionId);
|
|
412
416
|
if (manifestDir) return join(manifestDir, '..', '..');
|
|
413
|
-
if (manifestReady) return defaultYeaftDir;
|
|
414
417
|
|
|
415
418
|
const registry = readWorkDirRegistry(defaultYeaftDir);
|
|
416
419
|
const workDir = normalizeWorkDir(registry[sessionId]);
|
|
@@ -426,6 +429,18 @@ export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
|
|
|
426
429
|
return defaultYeaftDir;
|
|
427
430
|
}
|
|
428
431
|
|
|
432
|
+
export function resolveSessionYeaftDir(defaultYeaftDir, sessionId) {
|
|
433
|
+
if (!defaultYeaftDir || !sessionId) return defaultYeaftDir;
|
|
434
|
+
|
|
435
|
+
const manifestReady = hasSessionManifest(defaultYeaftDir);
|
|
436
|
+
ensureSessionManifestReady(defaultYeaftDir);
|
|
437
|
+
const manifestDir = resolveManifestSessionDir(defaultYeaftDir, sessionId);
|
|
438
|
+
if (manifestDir) return join(manifestDir, '..', '..');
|
|
439
|
+
if (manifestReady) return defaultYeaftDir;
|
|
440
|
+
|
|
441
|
+
return findExistingSessionYeaftDir(defaultYeaftDir, sessionId);
|
|
442
|
+
}
|
|
443
|
+
|
|
429
444
|
/** Build a safe group id from a display name (slug + ulid-lite suffix). */
|
|
430
445
|
export function makeSessionId(name) {
|
|
431
446
|
const slug = String(name || 'session')
|
|
@@ -422,6 +422,10 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
422
422
|
scenario: 'chat',
|
|
423
423
|
vpPersona,
|
|
424
424
|
sessionId: agent.parentSessionId || deps.parentSessionId || null,
|
|
425
|
+
// SpawnAgent records the caller-provided cwd on the agent. Thread it
|
|
426
|
+
// into the child Engine just like a parent query's workDir so child
|
|
427
|
+
// file tools resolve relative paths in the requested workspace.
|
|
428
|
+
workDir: agent.cwd,
|
|
425
429
|
projectSessionIds: queuedPrompt.projectSessionIds,
|
|
426
430
|
projectLabel: queuedPrompt.projectLabel,
|
|
427
431
|
projectInstruction: queuedPrompt.projectInstruction,
|
package/yeaft/tools/agent.js
CHANGED
|
@@ -341,6 +341,10 @@ use it as the default workflow or call it repeatedly in a loop.`,
|
|
|
341
341
|
},
|
|
342
342
|
isConcurrencySafe: () => false,
|
|
343
343
|
isReadOnly: () => false,
|
|
344
|
+
// The child driver is fire-and-forget and inherits writable workspace
|
|
345
|
+
// tools. A parent query therefore cannot safely reuse filesystem snapshots
|
|
346
|
+
// after a SpawnAgent call returns.
|
|
347
|
+
mayMutateWorkspaceAfterReturn: () => true,
|
|
344
348
|
async execute(input, ctx) {
|
|
345
349
|
// NB: every envelope below puts `next_steps` (or `error_next_steps`) at
|
|
346
350
|
// the FIRST position because `agent/yeaft/tools/registry.js` caps tool
|
package/yeaft/tools/ask-user.js
CHANGED
package/yeaft/tools/bash.js
CHANGED
|
@@ -171,6 +171,10 @@ Guidelines:
|
|
|
171
171
|
timeoutMs: 0,
|
|
172
172
|
isConcurrencySafe: () => false,
|
|
173
173
|
isReadOnly: () => false,
|
|
174
|
+
// A detached shell task can write after this tool has returned. Once one is
|
|
175
|
+
// launched, same-query filesystem reads must execute instead of reusing a
|
|
176
|
+
// snapshot captured before the task's eventual mutation.
|
|
177
|
+
mayMutateWorkspaceAfterReturn: input => input?.background === true,
|
|
174
178
|
isDestructive: (input) => {
|
|
175
179
|
if (!input?.command) return false;
|
|
176
180
|
const cmd = input.command.toLowerCase();
|
|
@@ -49,6 +49,9 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
49
49
|
},
|
|
50
50
|
isConcurrencySafe: () => false,
|
|
51
51
|
isReadOnly: () => false,
|
|
52
|
+
// The Work Center watcher can advance from triage into a writable Action
|
|
53
|
+
// after creation returns. A paused item has no watcher-owned execution.
|
|
54
|
+
mayMutateWorkspaceAfterReturn: input => input?.start !== false,
|
|
52
55
|
async execute(input, ctx = {}) {
|
|
53
56
|
const sessionId = typeof ctx.sessionId === 'string' ? ctx.sessionId.trim() : '';
|
|
54
57
|
if (!sessionId) throw new Error('CreateWorkItem requires an active Session');
|
package/yeaft/tools/file-read.js
CHANGED
|
@@ -139,6 +139,7 @@ Guidelines:
|
|
|
139
139
|
},
|
|
140
140
|
isConcurrencySafe: () => true,
|
|
141
141
|
isReadOnly: () => true,
|
|
142
|
+
cacheWithinQuery: true,
|
|
142
143
|
async execute(input, ctx) {
|
|
143
144
|
const { file_path, offset = 0, column_offset = 0, limit = DEFAULT_LIMIT } = input;
|
|
144
145
|
if (!file_path) return JSON.stringify({ error: 'file_path is required' });
|
package/yeaft/tools/glob.js
CHANGED
|
@@ -164,6 +164,7 @@ Guidelines:
|
|
|
164
164
|
},
|
|
165
165
|
isConcurrencySafe: () => true,
|
|
166
166
|
isReadOnly: () => true,
|
|
167
|
+
cacheWithinQuery: true,
|
|
167
168
|
async execute(input, ctx) {
|
|
168
169
|
const { pattern, path: searchPath, limit = 500 } = input;
|
|
169
170
|
if (!pattern) return JSON.stringify({ error: 'pattern is required' });
|
package/yeaft/tools/grep.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { defineTool } from './types.js';
|
|
9
9
|
import { searchMessages } from '../conversation/search.js';
|
|
10
|
+
import { findExistingSessionYeaftDir } from '../sessions/session-crud.js';
|
|
10
11
|
|
|
11
12
|
const DEFAULT_RESULT_LIMIT = 10;
|
|
12
13
|
const MAX_SNIPPET_CHARS = 1000;
|
|
@@ -137,7 +138,7 @@ export default defineTool({
|
|
|
137
138
|
description: {
|
|
138
139
|
en: `Search through past conversation history.
|
|
139
140
|
|
|
140
|
-
Searches message content for all whitespace-separated terms (case-insensitive).
|
|
141
|
+
Searches message content for all whitespace-separated terms (case-insensitive) from persisted Session history.
|
|
141
142
|
Inside a Session, search is limited to that Session plus sibling Sessions in the same Project on this Agent. Tool-result messages are excluded. Useful for finding previous discussions, decisions, or code snippets.
|
|
142
143
|
|
|
143
144
|
Results are returned newest-first with a bounded matching snippet and source metadata.`,
|
|
@@ -171,7 +172,8 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
171
172
|
isReadOnly: () => true,
|
|
172
173
|
async execute(input, ctx) {
|
|
173
174
|
const { keyword, limit = DEFAULT_RESULT_LIMIT } = input;
|
|
174
|
-
|
|
175
|
+
const normalizedKeyword = typeof keyword === 'string' ? keyword.trim() : '';
|
|
176
|
+
if (!normalizedKeyword) return JSON.stringify({ error: 'keyword is required' });
|
|
175
177
|
|
|
176
178
|
const yeaftDir = ctx?.yeaftDir;
|
|
177
179
|
if (!yeaftDir) {
|
|
@@ -179,44 +181,96 @@ Results are returned newest-first with a bounded matching snippet and source met
|
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
try {
|
|
182
|
-
const telemetry = {};
|
|
183
184
|
const projectSessionIds = Array.isArray(ctx?.projectSessionIds)
|
|
184
185
|
? ctx.projectSessionIds.filter(id => typeof id === 'string' && id.trim()).map(id => id.trim())
|
|
185
186
|
: [];
|
|
186
187
|
const scopedSessionIds = ctx?.sessionId
|
|
187
188
|
? Array.from(new Set([ctx.sessionId, ...projectSessionIds]))
|
|
188
189
|
: null;
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
const telemetry = {};
|
|
191
|
+
let indexedResults = [];
|
|
192
|
+
|
|
193
|
+
if (scopedSessionIds) {
|
|
194
|
+
// The index manager builds/rebuilds SQLite state and Session location
|
|
195
|
+
// repair may migrate the manifest. Both are writes, so they cannot run
|
|
196
|
+
// behind a read-only/cachable tool declaration. Scan only existing
|
|
197
|
+
// transcript files; normal Session boot and maintenance own indexing.
|
|
198
|
+
for (const sessionId of scopedSessionIds) {
|
|
199
|
+
const storeDir = findExistingSessionYeaftDir(yeaftDir, sessionId);
|
|
200
|
+
const scanTelemetry = {};
|
|
201
|
+
const messages = searchMessages(storeDir, normalizedKeyword, limit, {
|
|
202
|
+
telemetry: scanTelemetry,
|
|
203
|
+
sessionIds: [sessionId],
|
|
204
|
+
});
|
|
205
|
+
telemetry.scannedSessions = (telemetry.scannedSessions || 0) + 1;
|
|
206
|
+
telemetry.scannedFiles = (telemetry.scannedFiles || 0) + (scanTelemetry.scannedFiles || 0);
|
|
207
|
+
telemetry.scannedMessages = (telemetry.scannedMessages || 0) + (scanTelemetry.scannedMessages || 0);
|
|
208
|
+
telemetry.scannedBytes = (telemetry.scannedBytes || 0) + (scanTelemetry.scannedBytes || 0);
|
|
209
|
+
for (const message of messages) {
|
|
210
|
+
indexedResults.push({
|
|
211
|
+
messageId: message.id || null,
|
|
212
|
+
sessionId: message.sessionId || sessionId,
|
|
213
|
+
role: message.role,
|
|
214
|
+
content: buildSnippet(message.content || '', normalizedKeyword),
|
|
215
|
+
mode: message.mode || null,
|
|
216
|
+
time: message.time || message.timestamp || null,
|
|
217
|
+
source: message.historySource || 'session-scan',
|
|
218
|
+
turnId: message.turnId || null,
|
|
219
|
+
_seq: 0,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// CLI and legacy callers without a Session context retain the old
|
|
225
|
+
// global search path. The indexed path requires an explicit session
|
|
226
|
+
// because it is intentionally scoped and owner-safe.
|
|
227
|
+
const legacyResults = searchMessages(yeaftDir, normalizedKeyword, limit, { telemetry });
|
|
228
|
+
indexedResults = legacyResults.map(msg => ({
|
|
229
|
+
messageId: msg.id || null,
|
|
230
|
+
sessionId: msg.sessionId || null,
|
|
231
|
+
role: msg.role,
|
|
232
|
+
content: buildSnippet(msg.content, normalizedKeyword),
|
|
233
|
+
mode: msg.mode,
|
|
234
|
+
time: msg.time || msg.timestamp || null,
|
|
235
|
+
source: msg.historySource || null,
|
|
236
|
+
_seq: 0,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
indexedResults.sort((a, b) => {
|
|
241
|
+
const time = String(b.time || '').localeCompare(String(a.time || ''));
|
|
242
|
+
if (time !== 0) return time;
|
|
243
|
+
return (b._seq || 0) - (a._seq || 0);
|
|
192
244
|
});
|
|
245
|
+
const results = indexedResults.slice(0, Math.max(1, Math.min(100, Number(limit) || DEFAULT_RESULT_LIMIT)));
|
|
193
246
|
const searchTelemetry = {
|
|
194
247
|
resultCount: results.length,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
248
|
+
...(scopedSessionIds
|
|
249
|
+
? {
|
|
250
|
+
scannedSessions: telemetry.scannedSessions || 0,
|
|
251
|
+
scannedFiles: telemetry.scannedFiles || 0,
|
|
252
|
+
scannedMessages: telemetry.scannedMessages || 0,
|
|
253
|
+
scannedBytes: telemetry.scannedBytes || 0,
|
|
254
|
+
}
|
|
255
|
+
: {
|
|
256
|
+
scannedFiles: telemetry.scannedFiles || 0,
|
|
257
|
+
scannedMessages: telemetry.scannedMessages || 0,
|
|
258
|
+
scannedBytes: telemetry.scannedBytes || 0,
|
|
259
|
+
}),
|
|
198
260
|
};
|
|
199
261
|
|
|
200
262
|
if (results.length === 0) {
|
|
201
263
|
return serializeHistorySearchOutput({
|
|
202
264
|
results: [],
|
|
203
|
-
message: `No matches found for "${
|
|
265
|
+
message: `No matches found for "${normalizedKeyword}"`,
|
|
204
266
|
telemetry: searchTelemetry,
|
|
205
267
|
});
|
|
206
268
|
}
|
|
207
269
|
|
|
208
270
|
return serializeHistorySearchOutput({
|
|
209
|
-
results: results.map(
|
|
210
|
-
messageId: msg.id || null,
|
|
211
|
-
sessionId: msg.sessionId || null,
|
|
212
|
-
role: msg.role,
|
|
213
|
-
content: buildSnippet(msg.content, keyword),
|
|
214
|
-
mode: msg.mode,
|
|
215
|
-
time: msg.time || msg.timestamp || null,
|
|
216
|
-
source: msg.historySource || null,
|
|
217
|
-
})),
|
|
271
|
+
results: results.map(({ _seq, ...result }) => result),
|
|
218
272
|
totalResults: results.length,
|
|
219
|
-
keyword,
|
|
273
|
+
keyword: normalizedKeyword,
|
|
220
274
|
telemetry: searchTelemetry,
|
|
221
275
|
});
|
|
222
276
|
} catch (err) {
|
package/yeaft/tools/js-repl.js
CHANGED
|
@@ -57,6 +57,7 @@ stale/stalled 诊断、result 尾部和消息数量。将此作为异步子 Agen
|
|
|
57
57
|
},
|
|
58
58
|
isConcurrencySafe: () => true,
|
|
59
59
|
isReadOnly: () => true,
|
|
60
|
+
cacheWithinQuery: false,
|
|
60
61
|
async execute(input, ctx) {
|
|
61
62
|
const includeTerminal = Boolean(input?.include_closed || input?.include_terminal);
|
|
62
63
|
const agents = getAgentRegistry();
|
package/yeaft/tools/list-dir.js
CHANGED
|
@@ -52,6 +52,7 @@ This is better than using Bash with 'ls' because it provides structured output.`
|
|
|
52
52
|
},
|
|
53
53
|
isConcurrencySafe: () => true,
|
|
54
54
|
isReadOnly: () => true,
|
|
55
|
+
cacheWithinQuery: true,
|
|
55
56
|
async execute(input, ctx) {
|
|
56
57
|
const { path: dirPath, show_hidden = true } = input;
|
|
57
58
|
|