@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.823",
3
+ "version": "0.1.826",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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 data = eventData != null ? JSON.stringify(eventData) : null;
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 WHERE event_type = 'dream_progress' ORDER BY created_at DESC LIMIT ?
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({ type: 'dream_progress', ...data, at: er.created_at, ts: data.ts || er.created_at });
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', evt);
270
+ session.trace.event('dream_progress', stampedEvt);
171
271
  }
172
272
  if (typeof session._dreamProgressSink === 'function') {
173
- session._dreamProgressSink(evt);
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]', evt);
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
- 'Agent',
35
- 'SendMessage',
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(你已经没有 Agent / SendMessage / WaitAgent / CloseAgent 工具)。',
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 (Agent / SendMessage / WaitAgent / CloseAgent are not in your toolset).',
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.',
@@ -174,7 +174,8 @@ export function tickAgent(agentId, delta = {}, now = Date.now()) {
174
174
  }
175
175
 
176
176
  export default defineTool({
177
- name: 'Agent',
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 SendMessage to communicate, WaitAgent to collect results, CloseAgent to finalize`,
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, SendMessage to give it more work, CloseAgent to finish.`,
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
  });
@@ -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
  /**
@@ -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
- return Array.from(this.#tools.values());
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 Array.from(this.#tools.keys());
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.#tools.size;
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 Array.from(this.#tools.keys());
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 message to a sub-agent.
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: 'SendMessage',
10
- description: `Send a message to a sub-agent.
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 tasks, provide instructions, or relay information to a sub-agent.
13
- The message is queued for the agent to process.`,
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: {
@@ -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 SendMessage.`,
13
+ Use after sending a task to an agent via PromptAgent.`,
14
14
  parameters: {
15
15
  type: 'object',
16
16
  properties: {
@@ -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 any second scoped trigger
3264
- // while ANY scoped pass is inflight the scheduler already
3265
- // short-circuits the underlying run for same-group, and a different
3266
- // group's filter would have been silently dropped anyway (see
3267
- // dream-v2/schedule.js inflight reuse), so the user-facing semantics
3268
- // are unchanged ("you already asked").
3269
- if (groupId && inflightScopedDreamGroups.size > 0) {
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
- });