@yeaft/webchat-agent 0.1.823 → 0.1.826
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/unify/debug-trace.js +58 -8
- package/unify/dream-v2/session-wiring.js +158 -34
- package/unify/sub-agent/runner.js +4 -2
- package/unify/sub-agent/spawned-prompt.js +2 -2
- package/unify/tools/agent.js +4 -3
- package/unify/tools/index.js +0 -4
- package/unify/tools/registry.js +29 -6
- package/unify/tools/send-message.js +9 -5
- package/unify/tools/types.js +7 -0
- package/unify/tools/wait-agent.js +1 -1
- package/unify/web-bridge.js +18 -7
- package/unify/tools/open-source-message.js +0 -49
- package/unify/tools/request-permissions.js +0 -59
package/package.json
CHANGED
package/unify/debug-trace.js
CHANGED
|
@@ -125,6 +125,38 @@ function truncate(str, max) {
|
|
|
125
125
|
return str.slice(0, max) + '... [truncated]';
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
function truncatedJsonSentinel(originalBytes) {
|
|
129
|
+
return {
|
|
130
|
+
__truncated: true,
|
|
131
|
+
originalBytes,
|
|
132
|
+
maxBytes: MAX_LOOP_PAYLOAD,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function truncateJsonValue(value) {
|
|
137
|
+
if (value == null) return value;
|
|
138
|
+
if (typeof value === 'string') return truncate(value, MAX_LOOP_PAYLOAD);
|
|
139
|
+
try {
|
|
140
|
+
const s = JSON.stringify(value);
|
|
141
|
+
if (s.length <= MAX_LOOP_PAYLOAD) return value;
|
|
142
|
+
return truncatedJsonSentinel(s.length);
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function boundDreamEventData(eventType, eventData) {
|
|
149
|
+
if (eventType !== 'dream_loop' || !eventData || typeof eventData !== 'object') return eventData;
|
|
150
|
+
return {
|
|
151
|
+
...eventData,
|
|
152
|
+
systemPrompt: truncateJsonValue(eventData.systemPrompt),
|
|
153
|
+
messages: truncateJsonValue(eventData.messages),
|
|
154
|
+
response: truncateJsonValue(eventData.response),
|
|
155
|
+
rawRequest: truncateJsonValue(eventData.rawRequest),
|
|
156
|
+
rawResponse: truncateJsonValue(eventData.rawResponse),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
128
160
|
/**
|
|
129
161
|
* DebugTrace — SQLite-backed debug trace.
|
|
130
162
|
*/
|
|
@@ -229,11 +261,7 @@ export class DebugTrace {
|
|
|
229
261
|
try {
|
|
230
262
|
const s = JSON.stringify(v);
|
|
231
263
|
if (s.length <= MAX_LOOP_PAYLOAD) return s;
|
|
232
|
-
return JSON.stringify(
|
|
233
|
-
__truncated: true,
|
|
234
|
-
originalBytes: s.length,
|
|
235
|
-
maxBytes: MAX_LOOP_PAYLOAD,
|
|
236
|
-
});
|
|
264
|
+
return JSON.stringify(truncatedJsonSentinel(s.length));
|
|
237
265
|
} catch { return null; }
|
|
238
266
|
};
|
|
239
267
|
// For raw request/response, accept either a pre-stringified blob
|
|
@@ -304,7 +332,8 @@ export class DebugTrace {
|
|
|
304
332
|
logEvent({ traceId, eventType, eventData = null }) {
|
|
305
333
|
const id = randomUUID();
|
|
306
334
|
const now = Date.now();
|
|
307
|
-
const
|
|
335
|
+
const boundedData = boundDreamEventData(eventType, eventData);
|
|
336
|
+
const data = boundedData != null ? JSON.stringify(boundedData) : null;
|
|
308
337
|
this.#prepare('insertEvent', `
|
|
309
338
|
INSERT INTO trace_events (id, trace_id, event_type, event_data, created_at)
|
|
310
339
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -312,6 +341,19 @@ export class DebugTrace {
|
|
|
312
341
|
return id;
|
|
313
342
|
}
|
|
314
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Compatibility helper used by older engine/dream call sites.
|
|
346
|
+
* @param {string} eventType
|
|
347
|
+
* @param {unknown} eventData
|
|
348
|
+
* @returns {string}
|
|
349
|
+
*/
|
|
350
|
+
event(eventType, eventData = null) {
|
|
351
|
+
const traceId = (eventData && typeof eventData === 'object' && (eventData.turnId || eventData.runId))
|
|
352
|
+
? String(eventData.turnId || eventData.runId)
|
|
353
|
+
: String(eventType || 'event');
|
|
354
|
+
return this.logEvent({ traceId, eventType, eventData });
|
|
355
|
+
}
|
|
356
|
+
|
|
315
357
|
// ─── Read API ────────────────────────────────────────────────
|
|
316
358
|
|
|
317
359
|
/**
|
|
@@ -477,7 +519,9 @@ export class DebugTrace {
|
|
|
477
519
|
const dreamEvents = [];
|
|
478
520
|
if (dreamLim > 0) {
|
|
479
521
|
const eventRows = this.#db.prepare(`
|
|
480
|
-
SELECT * FROM trace_events
|
|
522
|
+
SELECT * FROM trace_events
|
|
523
|
+
WHERE event_type IN ('dream_progress', 'dream_loop', 'dream_turn_open', 'dream_turn_close', 'dream_run')
|
|
524
|
+
ORDER BY created_at DESC, rowid DESC LIMIT ?
|
|
481
525
|
`).all(Math.max(dreamLim * 5, dreamLim));
|
|
482
526
|
for (const er of eventRows) {
|
|
483
527
|
const data = parseJsonSafe(er.event_data) || {};
|
|
@@ -488,7 +532,12 @@ export class DebugTrace {
|
|
|
488
532
|
const isThisGroup = evtGroupId === groupId || target === `group/${groupId}`;
|
|
489
533
|
if (!isBroadcast && !isThisGroup) continue;
|
|
490
534
|
}
|
|
491
|
-
dreamEvents.push({
|
|
535
|
+
dreamEvents.push({
|
|
536
|
+
type: data.type || (er.event_type === 'dream_progress' ? 'dream_progress' : er.event_type),
|
|
537
|
+
...data,
|
|
538
|
+
at: er.created_at,
|
|
539
|
+
ts: data.ts || data.at || er.created_at,
|
|
540
|
+
});
|
|
492
541
|
if (dreamEvents.length >= dreamLim) break;
|
|
493
542
|
}
|
|
494
543
|
dreamEvents.reverse();
|
|
@@ -638,6 +687,7 @@ export class NullTrace {
|
|
|
638
687
|
endTurn() {}
|
|
639
688
|
logTool() { return 'null'; }
|
|
640
689
|
logEvent() { return 'null'; }
|
|
690
|
+
event() { return 'null'; }
|
|
641
691
|
queryByMessage() { return { turns: [], tools: [], events: [] }; }
|
|
642
692
|
queryByTrace() { return { turns: [], tools: [], events: [] }; }
|
|
643
693
|
queryRecent() { return []; }
|
|
@@ -128,29 +128,128 @@ function makeLlm(session) {
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
// Emit complete loop event (request + response) to the debug panel.
|
|
131
|
+
const latencyMs = Date.now() - startedMs;
|
|
132
|
+
const usage = normalizeDreamUsage(r?.usage || {});
|
|
133
|
+
if (!session._dreamMetrics) session._dreamMetrics = createDreamMetrics({ turnId });
|
|
134
|
+
session._dreamMetrics.llmCallCount += 1;
|
|
135
|
+
session._dreamMetrics.inputTokens += usage.inputTokens;
|
|
136
|
+
session._dreamMetrics.outputTokens += usage.outputTokens;
|
|
137
|
+
session._dreamMetrics.totalTokens += usage.totalTokens;
|
|
138
|
+
session._dreamMetrics.byPass[pass] = session._dreamMetrics.byPass[pass] || createDreamPassMetrics();
|
|
139
|
+
session._dreamMetrics.byPass[pass].llmCallCount += 1;
|
|
140
|
+
session._dreamMetrics.byPass[pass].inputTokens += usage.inputTokens;
|
|
141
|
+
session._dreamMetrics.byPass[pass].outputTokens += usage.outputTokens;
|
|
142
|
+
session._dreamMetrics.byPass[pass].totalTokens += usage.totalTokens;
|
|
143
|
+
session._dreamMetrics.byPass[pass].durationMs += latencyMs;
|
|
144
|
+
const loopEvent = stampDreamScope(session, {
|
|
145
|
+
type: 'loop',
|
|
146
|
+
turnId,
|
|
147
|
+
loopNumber,
|
|
148
|
+
pass,
|
|
149
|
+
model: model || 'unknown',
|
|
150
|
+
systemPrompt: effectiveSystem,
|
|
151
|
+
messages: [{ role: 'user', content: prompt }],
|
|
152
|
+
response: typeof r?.text === 'string' ? r.text : '',
|
|
153
|
+
toolCalls: [],
|
|
154
|
+
usage,
|
|
155
|
+
latencyMs,
|
|
156
|
+
ttfbMs: null,
|
|
157
|
+
stopReason: 'end_turn',
|
|
158
|
+
rawRequest: null,
|
|
159
|
+
rawResponse: null,
|
|
160
|
+
});
|
|
161
|
+
persistDreamTrace(session, 'dream_loop', loopEvent);
|
|
131
162
|
if (typeof session._dreamProgressSink === 'function') {
|
|
132
|
-
session._dreamProgressSink(
|
|
133
|
-
type: 'loop',
|
|
134
|
-
turnId,
|
|
135
|
-
loopNumber,
|
|
136
|
-
model: model || 'unknown',
|
|
137
|
-
systemPrompt: effectiveSystem,
|
|
138
|
-
messages: [{ role: 'user', content: prompt }],
|
|
139
|
-
response: typeof r?.text === 'string' ? r.text : '',
|
|
140
|
-
toolCalls: [],
|
|
141
|
-
usage: r?.usage || { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
142
|
-
latencyMs: Date.now() - startedMs,
|
|
143
|
-
ttfbMs: null,
|
|
144
|
-
stopReason: 'end_turn',
|
|
145
|
-
rawRequest: null,
|
|
146
|
-
rawResponse: null,
|
|
147
|
-
});
|
|
163
|
+
session._dreamProgressSink(loopEvent);
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
return (r && r.text) ? r.text : '';
|
|
151
167
|
};
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
|
|
171
|
+
function stampDreamScope(session, evt) {
|
|
172
|
+
if (!evt || typeof evt !== 'object') return evt;
|
|
173
|
+
if (evt.groupId || !session?._dreamActiveGroupId) return evt;
|
|
174
|
+
return { ...evt, groupId: session._dreamActiveGroupId };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function finiteNumber(v) {
|
|
178
|
+
const n = Number(v);
|
|
179
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeDreamUsage(usage = {}) {
|
|
183
|
+
const inputTokens = finiteNumber(
|
|
184
|
+
usage.inputTokens ?? usage.input_tokens ?? usage.promptTokens ?? usage.prompt_tokens,
|
|
185
|
+
);
|
|
186
|
+
const outputTokens = finiteNumber(
|
|
187
|
+
usage.outputTokens ?? usage.output_tokens ?? usage.completionTokens ?? usage.completion_tokens,
|
|
188
|
+
);
|
|
189
|
+
const explicitTotal = finiteNumber(usage.totalTokens ?? usage.total_tokens);
|
|
190
|
+
return {
|
|
191
|
+
inputTokens,
|
|
192
|
+
outputTokens,
|
|
193
|
+
totalTokens: explicitTotal || inputTokens + outputTokens,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function createDreamPassMetrics() {
|
|
198
|
+
return { llmCallCount: 0, inputTokens: 0, outputTokens: 0, totalTokens: 0, durationMs: 0 };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function createDreamMetrics({ turnId, startedAt = Date.now() } = {}) {
|
|
202
|
+
return {
|
|
203
|
+
turnId: turnId || 'dream',
|
|
204
|
+
startedAt,
|
|
205
|
+
durationMs: 0,
|
|
206
|
+
llmCallCount: 0,
|
|
207
|
+
inputTokens: 0,
|
|
208
|
+
outputTokens: 0,
|
|
209
|
+
totalTokens: 0,
|
|
210
|
+
byPass: {},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function finalizeDreamMetrics(metrics, durationMs) {
|
|
215
|
+
const m = metrics || createDreamMetrics();
|
|
216
|
+
return {
|
|
217
|
+
turnId: m.turnId || 'dream',
|
|
218
|
+
startedAt: m.startedAt || null,
|
|
219
|
+
durationMs: finiteNumber(durationMs),
|
|
220
|
+
llmCallCount: finiteNumber(m.llmCallCount),
|
|
221
|
+
inputTokens: finiteNumber(m.inputTokens),
|
|
222
|
+
outputTokens: finiteNumber(m.outputTokens),
|
|
223
|
+
totalTokens: finiteNumber(m.totalTokens),
|
|
224
|
+
passBreakdown: Object.fromEntries(Object.entries(m.byPass || {}).map(([pass, rec]) => [pass, {
|
|
225
|
+
llmCallCount: finiteNumber(rec.llmCallCount),
|
|
226
|
+
inputTokens: finiteNumber(rec.inputTokens),
|
|
227
|
+
outputTokens: finiteNumber(rec.outputTokens),
|
|
228
|
+
totalTokens: finiteNumber(rec.totalTokens),
|
|
229
|
+
durationMs: finiteNumber(rec.durationMs),
|
|
230
|
+
}])),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function summarizeDreamResult(result = {}) {
|
|
235
|
+
return {
|
|
236
|
+
groups: Array.isArray(result.groups) ? result.groups.length : 0,
|
|
237
|
+
targets: Array.isArray(result.targets) ? result.targets.length : 0,
|
|
238
|
+
error: result.error || null,
|
|
239
|
+
skipped: !!result.skipped,
|
|
240
|
+
skippedReason: result.skippedReason || null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function persistDreamTrace(session, eventType, eventData) {
|
|
245
|
+
try {
|
|
246
|
+
if (typeof session?.trace?.event === 'function') session.trace.event(eventType, eventData);
|
|
247
|
+
else if (typeof session?.trace?.logEvent === 'function') {
|
|
248
|
+
session.trace.logEvent({ traceId: String(eventData?.turnId || eventData?.runId || eventType), eventType, eventData });
|
|
249
|
+
}
|
|
250
|
+
} catch { /* trace must never break dream */ }
|
|
251
|
+
}
|
|
252
|
+
|
|
154
253
|
/**
|
|
155
254
|
* Create a v2 dream scheduler bound to a session and wire its progress
|
|
156
255
|
* events into the engine's sub-agent event sink (web-bridge translates
|
|
@@ -162,20 +261,21 @@ function makeLlm(session) {
|
|
|
162
261
|
export function createV2DreamScheduler(session) {
|
|
163
262
|
const onProgress = (evt) => {
|
|
164
263
|
try {
|
|
264
|
+
const stampedEvt = stampDreamScope(session, evt);
|
|
165
265
|
const sink = session.engine?.subAgentEventSink || null;
|
|
166
266
|
// We don't have a sub-agent id; emit on the engine's standard
|
|
167
267
|
// event channel instead. session.engine exposes setSubAgentEventSink
|
|
168
268
|
// for nested events; for top-level dream, we route through trace.
|
|
169
269
|
if (typeof session.trace?.event === 'function') {
|
|
170
|
-
session.trace.event('dream_progress',
|
|
270
|
+
session.trace.event('dream_progress', stampedEvt);
|
|
171
271
|
}
|
|
172
272
|
if (typeof session._dreamProgressSink === 'function') {
|
|
173
|
-
session._dreamProgressSink(
|
|
273
|
+
session._dreamProgressSink(stampedEvt);
|
|
174
274
|
}
|
|
175
275
|
// Best-effort console for debug builds.
|
|
176
276
|
if (session.config?.debug) {
|
|
177
277
|
// eslint-disable-next-line no-console
|
|
178
|
-
console.log('[dream-v2]',
|
|
278
|
+
console.log('[dream-v2]', stampedEvt);
|
|
179
279
|
}
|
|
180
280
|
} catch { /* never let progress reporting kill the run */ }
|
|
181
281
|
};
|
|
@@ -190,16 +290,19 @@ export function createV2DreamScheduler(session) {
|
|
|
190
290
|
const startedAt = Date.now();
|
|
191
291
|
session._dreamTurnId = turnId;
|
|
192
292
|
session._dreamLoopCounter = 0;
|
|
293
|
+
session._dreamMetrics = createDreamMetrics({ turnId, startedAt });
|
|
193
294
|
|
|
295
|
+
const turnOpen = stampDreamScope(session, {
|
|
296
|
+
type: 'turn_open',
|
|
297
|
+
turnId,
|
|
298
|
+
userPrompt: '[dream] automatic memory consolidation',
|
|
299
|
+
vpId: null,
|
|
300
|
+
groupId: null,
|
|
301
|
+
at: startedAt,
|
|
302
|
+
});
|
|
303
|
+
persistDreamTrace(session, 'dream_turn_open', turnOpen);
|
|
194
304
|
if (typeof session._dreamProgressSink === 'function') {
|
|
195
|
-
session._dreamProgressSink(
|
|
196
|
-
type: 'turn_open',
|
|
197
|
-
turnId,
|
|
198
|
-
userPrompt: '[dream] automatic memory consolidation',
|
|
199
|
-
vpId: null,
|
|
200
|
-
groupId: null,
|
|
201
|
-
at: startedAt,
|
|
202
|
-
});
|
|
305
|
+
session._dreamProgressSink(turnOpen);
|
|
203
306
|
}
|
|
204
307
|
|
|
205
308
|
return runDream({
|
|
@@ -208,14 +311,35 @@ export function createV2DreamScheduler(session) {
|
|
|
208
311
|
scopeFilter: Array.isArray(opts.scopeFilter) ? opts.scopeFilter : undefined,
|
|
209
312
|
}).then((result) => {
|
|
210
313
|
// Bug 2: emit turn_close when the dream pass completes.
|
|
314
|
+
result.trigger = opts.manual ? 'manual' : 'auto';
|
|
315
|
+
if (session._dreamActiveGroupId && !result.groupId) result.groupId = session._dreamActiveGroupId;
|
|
316
|
+
const metrics = finalizeDreamMetrics(session._dreamMetrics, Date.now() - startedAt);
|
|
317
|
+
result.metrics = metrics;
|
|
318
|
+
result.durationMs = metrics.durationMs;
|
|
319
|
+
result.llmCallCount = metrics.llmCallCount;
|
|
320
|
+
result.inputTokens = metrics.inputTokens;
|
|
321
|
+
result.outputTokens = metrics.outputTokens;
|
|
322
|
+
result.totalTokens = metrics.totalTokens;
|
|
323
|
+
result.passBreakdown = metrics.passBreakdown;
|
|
324
|
+
const turnClose = stampDreamScope(session, {
|
|
325
|
+
type: 'turn_close',
|
|
326
|
+
turnId,
|
|
327
|
+
totalMs: metrics.durationMs,
|
|
328
|
+
totalTokens: metrics.totalTokens,
|
|
329
|
+
loopCount: metrics.llmCallCount,
|
|
330
|
+
metrics,
|
|
331
|
+
});
|
|
332
|
+
persistDreamTrace(session, 'dream_turn_close', turnClose);
|
|
333
|
+
persistDreamTrace(session, 'dream_run', stampDreamScope(session, {
|
|
334
|
+
type: 'dream_run',
|
|
335
|
+
turnId,
|
|
336
|
+
phase: 'result',
|
|
337
|
+
status: result?.error ? 'error' : 'done',
|
|
338
|
+
metrics,
|
|
339
|
+
resultSummary: summarizeDreamResult(result),
|
|
340
|
+
}));
|
|
211
341
|
if (typeof session._dreamProgressSink === 'function') {
|
|
212
|
-
session._dreamProgressSink(
|
|
213
|
-
type: 'turn_close',
|
|
214
|
-
turnId,
|
|
215
|
-
totalMs: Date.now() - startedAt,
|
|
216
|
-
totalTokens: 0,
|
|
217
|
-
loopCount: session._dreamLoopCounter || 0,
|
|
218
|
-
});
|
|
342
|
+
session._dreamProgressSink(turnClose);
|
|
219
343
|
}
|
|
220
344
|
return result;
|
|
221
345
|
});
|
|
@@ -31,8 +31,10 @@ import { ToolRegistry } from '../tools/registry.js';
|
|
|
31
31
|
import { buildSpawnedPreamble } from './spawned-prompt.js';
|
|
32
32
|
|
|
33
33
|
const RESTRICTED_TOOLS = new Set([
|
|
34
|
-
'
|
|
35
|
-
'
|
|
34
|
+
'SpawnAgent',
|
|
35
|
+
'Agent', // legacy alias
|
|
36
|
+
'PromptAgent',
|
|
37
|
+
'SendMessage', // legacy alias
|
|
36
38
|
'WaitAgent',
|
|
37
39
|
'CloseAgent',
|
|
38
40
|
'ListAgents',
|
|
@@ -38,7 +38,7 @@ export function buildSpawnedPreamble({ parentName, parentVpId, agentName, missio
|
|
|
38
38
|
m || '(无具体任务说明)',
|
|
39
39
|
'',
|
|
40
40
|
'## 行为约束',
|
|
41
|
-
'- 不要再 spawn sub-agent(你已经没有
|
|
41
|
+
'- 不要再 spawn sub-agent(你已经没有 SpawnAgent / PromptAgent / WaitAgent / CloseAgent 工具)。',
|
|
42
42
|
'- 不要 route_forward 给别的 VP,不要 ask_user。',
|
|
43
43
|
'- 完成时直接以 markdown 自由文本回复(end_turn)。建议结构:"## 结果" / "## 关键发现" / "## 遗留问题"。',
|
|
44
44
|
'- 失败/不可行也要明确说出来,不要假装完成。父 VP 会读你的最终消息。',
|
|
@@ -55,7 +55,7 @@ export function buildSpawnedPreamble({ parentName, parentVpId, agentName, missio
|
|
|
55
55
|
m || '(no mission body provided)',
|
|
56
56
|
'',
|
|
57
57
|
'## Constraints',
|
|
58
|
-
'- Do NOT spawn further sub-agents (
|
|
58
|
+
'- Do NOT spawn further sub-agents (SpawnAgent / PromptAgent / WaitAgent / CloseAgent are not in your toolset).',
|
|
59
59
|
'- Do NOT use route_forward to other VPs. Do NOT ask_user.',
|
|
60
60
|
'- When done, reply in free markdown (end_turn). Suggested structure: "## Result" / "## Key findings" / "## Open questions".',
|
|
61
61
|
'- If the mission is infeasible or you fail, say so plainly. The parent will read your final message.',
|
package/unify/tools/agent.js
CHANGED
|
@@ -174,7 +174,8 @@ export function tickAgent(agentId, delta = {}, now = Date.now()) {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
export default defineTool({
|
|
177
|
-
name: '
|
|
177
|
+
name: 'SpawnAgent',
|
|
178
|
+
aliases: ['Agent'],
|
|
178
179
|
description: `Create a sub-agent to work on an independent task in parallel.
|
|
179
180
|
|
|
180
181
|
Sub-agents run in their own context and can be given a concrete mission
|
|
@@ -189,7 +190,7 @@ Guidelines:
|
|
|
189
190
|
- Give a clear, focused mission — what "done" looks like
|
|
190
191
|
- Use expected_output to pin the structure you want back
|
|
191
192
|
- Always set a budget for unbounded missions
|
|
192
|
-
- Use
|
|
193
|
+
- Use PromptAgent to communicate, WaitAgent to collect results, CloseAgent to finalize`,
|
|
193
194
|
parameters: {
|
|
194
195
|
type: 'object',
|
|
195
196
|
properties: {
|
|
@@ -302,7 +303,7 @@ Guidelines:
|
|
|
302
303
|
persona: spec.persona || null,
|
|
303
304
|
budget: spec.budget || null,
|
|
304
305
|
status: agent.status,
|
|
305
|
-
message: `Sub-agent "${name}" spawned (${agentId}). Use WaitAgent to collect its first turn output,
|
|
306
|
+
message: `Sub-agent "${name}" spawned (${agentId}). Use WaitAgent to collect its first turn output, PromptAgent to give it more work, CloseAgent to finish.`,
|
|
306
307
|
});
|
|
307
308
|
},
|
|
308
309
|
});
|
package/unify/tools/index.js
CHANGED
|
@@ -18,7 +18,6 @@ import exitWorktree from './exit-worktree.js';
|
|
|
18
18
|
|
|
19
19
|
// --- P0 Core tools ---
|
|
20
20
|
import askUser from './ask-user.js';
|
|
21
|
-
import openSourceMessage from './open-source-message.js';
|
|
22
21
|
import webSearch from './web-search.js';
|
|
23
22
|
import webFetch from './web-fetch.js';
|
|
24
23
|
import historySearch from './history-search.js';
|
|
@@ -66,7 +65,6 @@ import { jsRepl, jsReplReset } from './js-repl.js';
|
|
|
66
65
|
import notebookEdit from './notebook-edit.js';
|
|
67
66
|
import imageGeneration from './image-generation.js';
|
|
68
67
|
import viewImage from './view-image.js';
|
|
69
|
-
import requestPermissions from './request-permissions.js';
|
|
70
68
|
|
|
71
69
|
/**
|
|
72
70
|
* All built-in tools, flattened into a single array.
|
|
@@ -82,7 +80,6 @@ export const allTools = [
|
|
|
82
80
|
|
|
83
81
|
// P0 Core
|
|
84
82
|
askUser,
|
|
85
|
-
openSourceMessage,
|
|
86
83
|
webSearch,
|
|
87
84
|
webFetch,
|
|
88
85
|
historySearch,
|
|
@@ -117,7 +114,6 @@ export const allTools = [
|
|
|
117
114
|
notebookEdit,
|
|
118
115
|
imageGeneration,
|
|
119
116
|
viewImage,
|
|
120
|
-
requestPermissions,
|
|
121
117
|
];
|
|
122
118
|
|
|
123
119
|
/**
|
package/unify/tools/registry.js
CHANGED
|
@@ -226,6 +226,17 @@ export class ToolRegistry {
|
|
|
226
226
|
register(tool) {
|
|
227
227
|
if (!tool || !tool.name) throw new Error('Tool must have a name');
|
|
228
228
|
this.#tools.set(tool.name, tool);
|
|
229
|
+
// Legacy-name aliases: keep historical jsonl tool_calls resolvable
|
|
230
|
+
// after a rename. The alias entry shares the same tool object so
|
|
231
|
+
// execute/has lookups succeed, but `getToolDefs()` dedupes by tool
|
|
232
|
+
// identity so the LLM only sees the canonical name.
|
|
233
|
+
if (Array.isArray(tool.aliases)) {
|
|
234
|
+
for (const alias of tool.aliases) {
|
|
235
|
+
if (typeof alias === 'string' && alias && alias !== tool.name) {
|
|
236
|
+
this.#tools.set(alias, tool);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
229
240
|
return this;
|
|
230
241
|
}
|
|
231
242
|
|
|
@@ -269,10 +280,21 @@ export class ToolRegistry {
|
|
|
269
280
|
|
|
270
281
|
/**
|
|
271
282
|
* Get all registered tools (unfiltered).
|
|
283
|
+
*
|
|
284
|
+
* Dedupes alias entries — when a tool is registered with `aliases`,
|
|
285
|
+
* the same ToolDef object lives at multiple Map keys. We return each
|
|
286
|
+
* tool object at most once (under its canonical `tool.name`).
|
|
272
287
|
* @returns {import('./types.js').ToolDef[]}
|
|
273
288
|
*/
|
|
274
289
|
getAllTools() {
|
|
275
|
-
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
const out = [];
|
|
292
|
+
for (const tool of this.#tools.values()) {
|
|
293
|
+
if (seen.has(tool)) continue;
|
|
294
|
+
seen.add(tool);
|
|
295
|
+
out.push(tool);
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
276
298
|
}
|
|
277
299
|
|
|
278
300
|
/**
|
|
@@ -291,11 +313,12 @@ export class ToolRegistry {
|
|
|
291
313
|
}
|
|
292
314
|
|
|
293
315
|
/**
|
|
294
|
-
* Get all registered tool names
|
|
316
|
+
* Get all registered tool names (canonical only — aliases are excluded
|
|
317
|
+
* so debug surfaces like the tool-stats panel show one row per tool).
|
|
295
318
|
* @returns {string[]}
|
|
296
319
|
*/
|
|
297
320
|
getToolNames() {
|
|
298
|
-
return
|
|
321
|
+
return this.getAllTools().map(t => t.name);
|
|
299
322
|
}
|
|
300
323
|
|
|
301
324
|
/**
|
|
@@ -337,12 +360,12 @@ export class ToolRegistry {
|
|
|
337
360
|
|
|
338
361
|
/** Number of registered tools. */
|
|
339
362
|
get size() {
|
|
340
|
-
return this
|
|
363
|
+
return this.getAllTools().length;
|
|
341
364
|
}
|
|
342
365
|
|
|
343
|
-
/** All registered tool names. */
|
|
366
|
+
/** All registered tool names (canonical only; aliases excluded). */
|
|
344
367
|
get names() {
|
|
345
|
-
return
|
|
368
|
+
return this.getAllTools().map(t => t.name);
|
|
346
369
|
}
|
|
347
370
|
}
|
|
348
371
|
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* send-message.js — Send a
|
|
2
|
+
* send-message.js — Send a follow-up prompt to a sub-agent.
|
|
3
|
+
*
|
|
4
|
+
* Tool name: PromptAgent (canonical) / SendMessage (legacy alias for
|
|
5
|
+
* historical jsonl replay — see registry alias map).
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { defineTool } from './types.js';
|
|
6
9
|
import { getAgentRegistry } from './agent.js';
|
|
7
10
|
|
|
8
11
|
export default defineTool({
|
|
9
|
-
name: '
|
|
10
|
-
|
|
12
|
+
name: 'PromptAgent',
|
|
13
|
+
aliases: ['SendMessage'],
|
|
14
|
+
description: `Send a follow-up prompt to a sub-agent you previously spawned.
|
|
11
15
|
|
|
12
|
-
Use this to give
|
|
13
|
-
The
|
|
16
|
+
Use this to give the sub-agent more work, additional instructions, or relay
|
|
17
|
+
information. The prompt is queued for the agent to process on its next turn.`,
|
|
14
18
|
parameters: {
|
|
15
19
|
type: 'object',
|
|
16
20
|
properties: {
|
package/unify/tools/types.js
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
*/
|
|
75
75
|
export function defineTool({
|
|
76
76
|
name,
|
|
77
|
+
aliases,
|
|
77
78
|
description,
|
|
78
79
|
parameters,
|
|
79
80
|
execute,
|
|
@@ -94,6 +95,12 @@ export function defineTool({
|
|
|
94
95
|
isReadOnly,
|
|
95
96
|
isDestructive,
|
|
96
97
|
};
|
|
98
|
+
// Legacy tool-name aliases. Registered as extra lookup keys so old
|
|
99
|
+
// jsonl tool_calls (e.g. `SendMessage` → `PromptAgent`) keep resolving,
|
|
100
|
+
// but excluded from the LLM-visible catalogue.
|
|
101
|
+
if (Array.isArray(aliases) && aliases.length > 0) {
|
|
102
|
+
def.aliases = aliases.slice();
|
|
103
|
+
}
|
|
97
104
|
// Only attach `timeoutMs` when the tool author opts in. Leaving it
|
|
98
105
|
// unset means ToolRegistry.execute uses DEFAULT_TOOL_TIMEOUT_MS — set
|
|
99
106
|
// to <= 0 to disable the per-tool timeout entirely.
|
|
@@ -10,7 +10,7 @@ export default defineTool({
|
|
|
10
10
|
description: `Wait for a sub-agent to complete its task and retrieve the result.
|
|
11
11
|
|
|
12
12
|
Returns the agent's final result or current status if still running.
|
|
13
|
-
Use after sending a task to an agent via
|
|
13
|
+
Use after sending a task to an agent via PromptAgent.`,
|
|
14
14
|
parameters: {
|
|
15
15
|
type: 'object',
|
|
16
16
|
properties: {
|
package/unify/web-bridge.js
CHANGED
|
@@ -3222,6 +3222,13 @@ export function normalizeDreamResult(result) {
|
|
|
3222
3222
|
|
|
3223
3223
|
return {
|
|
3224
3224
|
success,
|
|
3225
|
+
durationMs: Number.isFinite(Number(result?.durationMs)) ? Number(result.durationMs) : 0,
|
|
3226
|
+
llmCallCount: Number.isFinite(Number(result?.llmCallCount)) ? Number(result.llmCallCount) : 0,
|
|
3227
|
+
inputTokens: Number.isFinite(Number(result?.inputTokens)) ? Number(result.inputTokens) : 0,
|
|
3228
|
+
outputTokens: Number.isFinite(Number(result?.outputTokens)) ? Number(result.outputTokens) : 0,
|
|
3229
|
+
totalTokens: Number.isFinite(Number(result?.totalTokens)) ? Number(result.totalTokens) : 0,
|
|
3230
|
+
metrics: result?.metrics || null,
|
|
3231
|
+
passBreakdown: result?.passBreakdown || result?.metrics?.passBreakdown || null,
|
|
3225
3232
|
skipped,
|
|
3226
3233
|
skippedReason,
|
|
3227
3234
|
groupsProcessed,
|
|
@@ -3260,13 +3267,15 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
3260
3267
|
// group or different) overlapping the same inflight pass used to set
|
|
3261
3268
|
// the module-level groupId slot, race the sink wrapping, and let the
|
|
3262
3269
|
// second `finally` restore the original sink while the first run was
|
|
3263
|
-
// still emitting events. We now refuse
|
|
3264
|
-
//
|
|
3265
|
-
//
|
|
3266
|
-
//
|
|
3267
|
-
//
|
|
3268
|
-
//
|
|
3269
|
-
|
|
3270
|
+
// still emitting events. We now refuse scoped triggers while ANY dream
|
|
3271
|
+
// pass is already running: a scoped manual click during an unscoped
|
|
3272
|
+
// auto run must not install `_dreamActiveGroupId` or wrap the sink,
|
|
3273
|
+
// otherwise auto-run events can be persisted under the clicked group.
|
|
3274
|
+
// The scheduler also short-circuits the underlying run for same-group,
|
|
3275
|
+
// and a different group's filter would have been silently dropped
|
|
3276
|
+
// anyway (see dream-v2/schedule.js inflight reuse), so the user-facing
|
|
3277
|
+
// semantics are unchanged ("you already asked").
|
|
3278
|
+
if (groupId && (inflightScopedDreamGroups.size > 0 || session.dreamScheduler.isRunning)) {
|
|
3270
3279
|
const skippedResult = {
|
|
3271
3280
|
skipped: true,
|
|
3272
3281
|
skippedReason: 'already-running',
|
|
@@ -3289,6 +3298,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
3289
3298
|
// OTHER groupIds chain (last-installed wins) but each restoration
|
|
3290
3299
|
// unwinds back to its predecessor.
|
|
3291
3300
|
const originalSink = session?._dreamProgressSink;
|
|
3301
|
+
if (groupId) session._dreamActiveGroupId = groupId;
|
|
3292
3302
|
if (groupId && typeof originalSink === 'function') {
|
|
3293
3303
|
inflightScopedDreamGroups.add(groupId);
|
|
3294
3304
|
session._dreamProgressSink = (evt) => {
|
|
@@ -3345,6 +3355,7 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
3345
3355
|
});
|
|
3346
3356
|
} finally {
|
|
3347
3357
|
// Restore the original sink and release the per-group inflight lock.
|
|
3358
|
+
if (groupId && session?._dreamActiveGroupId === groupId) session._dreamActiveGroupId = null;
|
|
3348
3359
|
if (groupId && typeof originalSink === 'function') {
|
|
3349
3360
|
session._dreamProgressSink = originalSink;
|
|
3350
3361
|
inflightScopedDreamGroups.delete(groupId);
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* open-source-message.js — task-334f R6 §Δ24.4.
|
|
3
|
-
*
|
|
4
|
-
* Low-level random access: given a (groupId, msgId), fetch the raw message
|
|
5
|
-
* from the group's jsonl log. Used when a VP has an exact pointer but does
|
|
6
|
-
* not want to run the memory_trace wrapper (5% case: audit / debug).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { defineTool } from './types.js';
|
|
10
|
-
|
|
11
|
-
export default defineTool({
|
|
12
|
-
name: 'open_source_message',
|
|
13
|
-
description: `Open a single source message by (groupId, msgId).
|
|
14
|
-
|
|
15
|
-
This is the low-level random-access primitive. Prefer memory_trace if you are
|
|
16
|
-
starting from a memory entry. Returns JSON: { message } or { error }.`,
|
|
17
|
-
parameters: {
|
|
18
|
-
type: 'object',
|
|
19
|
-
properties: {
|
|
20
|
-
groupId: { type: 'string', description: 'Group id' },
|
|
21
|
-
msgId: { type: 'string', description: 'Message id' },
|
|
22
|
-
},
|
|
23
|
-
required: ['groupId', 'msgId'],
|
|
24
|
-
},
|
|
25
|
-
isConcurrencySafe: () => true,
|
|
26
|
-
isReadOnly: () => true,
|
|
27
|
-
async execute(input, ctx) {
|
|
28
|
-
const { groupId, msgId } = input || {};
|
|
29
|
-
if (!groupId || !msgId) {
|
|
30
|
-
return JSON.stringify({ error: 'groupId and msgId required' });
|
|
31
|
-
}
|
|
32
|
-
const coordinator = ctx?.coordinator;
|
|
33
|
-
if (!coordinator || typeof coordinator.openGroup !== 'function') {
|
|
34
|
-
return JSON.stringify({ error: 'group coordinator not available' });
|
|
35
|
-
}
|
|
36
|
-
const group = coordinator.openGroup(groupId);
|
|
37
|
-
if (!group) return JSON.stringify({ error: `group not found: ${groupId}` });
|
|
38
|
-
|
|
39
|
-
const iter = typeof group.readMessageRange === 'function'
|
|
40
|
-
? group.readMessageRange(msgId, msgId)
|
|
41
|
-
: group.streamMessages();
|
|
42
|
-
for (const msg of iter) {
|
|
43
|
-
if (msg.id === msgId) {
|
|
44
|
-
return JSON.stringify({ message: msg });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return JSON.stringify({ error: `message not found: ${msgId} in ${groupId}` });
|
|
48
|
-
},
|
|
49
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* request-permissions.js — Request permission for dangerous operations.
|
|
3
|
-
*
|
|
4
|
-
* When an operation is flagged as destructive, this tool requests
|
|
5
|
-
* explicit user permission before proceeding.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { defineTool } from './types.js';
|
|
9
|
-
|
|
10
|
-
export default defineTool({
|
|
11
|
-
name: 'RequestPermissions',
|
|
12
|
-
description: `Request permission from the user for a potentially dangerous operation.
|
|
13
|
-
|
|
14
|
-
Use this before executing destructive operations like:
|
|
15
|
-
- Deleting files or directories
|
|
16
|
-
- Running commands that modify system state
|
|
17
|
-
- Force-pushing to git
|
|
18
|
-
- Resetting databases
|
|
19
|
-
|
|
20
|
-
The user must explicitly approve before you proceed.`,
|
|
21
|
-
parameters: {
|
|
22
|
-
type: 'object',
|
|
23
|
-
properties: {
|
|
24
|
-
operation: {
|
|
25
|
-
type: 'string',
|
|
26
|
-
description: 'Description of the operation that needs permission',
|
|
27
|
-
},
|
|
28
|
-
reason: {
|
|
29
|
-
type: 'string',
|
|
30
|
-
description: 'Why this operation is necessary',
|
|
31
|
-
},
|
|
32
|
-
risk_level: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
enum: ['low', 'medium', 'high', 'critical'],
|
|
35
|
-
description: 'Risk level of the operation',
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
required: ['operation'],
|
|
39
|
-
},
|
|
40
|
-
isConcurrencySafe: () => false,
|
|
41
|
-
isReadOnly: () => true,
|
|
42
|
-
async execute(input, ctx) {
|
|
43
|
-
const { operation, reason, risk_level = 'medium' } = input;
|
|
44
|
-
if (!operation) return JSON.stringify({ error: 'operation is required' });
|
|
45
|
-
|
|
46
|
-
// In a full integration, this would use the ask_user mechanism
|
|
47
|
-
// to get explicit permission. For now, return a structured request.
|
|
48
|
-
return JSON.stringify({
|
|
49
|
-
type: 'permission_request',
|
|
50
|
-
operation,
|
|
51
|
-
reason: reason || 'Operation requires explicit permission',
|
|
52
|
-
riskLevel: risk_level,
|
|
53
|
-
message: `⚠️ Permission required for: ${operation}` +
|
|
54
|
-
(reason ? `\nReason: ${reason}` : '') +
|
|
55
|
-
`\nRisk level: ${risk_level}`,
|
|
56
|
-
hint: 'User must explicitly approve this operation before proceeding.',
|
|
57
|
-
});
|
|
58
|
-
},
|
|
59
|
-
});
|