morpheus-cli 0.8.5 → 0.8.7

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.
Files changed (78) hide show
  1. package/dist/channels/telegram.js +43 -0
  2. package/dist/config/manager.js +3 -1
  3. package/dist/config/schemas.js +2 -0
  4. package/dist/http/api.js +70 -13
  5. package/dist/http/routers/chronos.js +12 -1
  6. package/dist/http/webhooks-router.js +11 -3
  7. package/dist/runtime/ISubagent.js +1 -0
  8. package/dist/runtime/apoc.js +49 -39
  9. package/dist/runtime/audit/repository.js +193 -6
  10. package/dist/runtime/chronos/repository.js +35 -0
  11. package/dist/runtime/keymaker.js +6 -30
  12. package/dist/runtime/memory/sati/index.js +22 -1
  13. package/dist/runtime/memory/sati/repository.js +39 -1
  14. package/dist/runtime/memory/sqlite.js +16 -3
  15. package/dist/runtime/neo.js +78 -34
  16. package/dist/runtime/oracle.js +68 -19
  17. package/dist/runtime/skills/tool.js +25 -0
  18. package/dist/runtime/subagent-utils.js +89 -0
  19. package/dist/runtime/tasks/repository.js +51 -0
  20. package/dist/runtime/tasks/worker.js +12 -2
  21. package/dist/runtime/telephonist.js +17 -9
  22. package/dist/runtime/tools/delegation-utils.js +120 -0
  23. package/dist/runtime/tools/index.js +0 -2
  24. package/dist/runtime/tools/time-verify-tools.js +15 -8
  25. package/dist/runtime/trinity.js +50 -34
  26. package/dist/runtime/webhooks/repository.js +31 -0
  27. package/dist/types/config.js +2 -0
  28. package/dist/types/pagination.js +1 -0
  29. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +1 -0
  30. package/dist/ui/assets/Chat-CjxeAQmd.js +41 -0
  31. package/dist/ui/assets/Chronos-BAjeLobF.js +1 -0
  32. package/dist/ui/assets/{ConfirmationModal-MyIaIK_Z.js → ConfirmationModal-fvgnOWTY.js} +1 -1
  33. package/dist/ui/assets/{Dashboard-C52jjru9.js → Dashboard-Ca5mSefz.js} +1 -1
  34. package/dist/ui/assets/{DeleteConfirmationModal-B0nDocEK.js → DeleteConfirmationModal-A8EmnHoa.js} +1 -1
  35. package/dist/ui/assets/{Logs-fDrGC9Lq.js → Logs-CYu7se7R.js} +1 -1
  36. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +1 -0
  37. package/dist/ui/assets/ModelPricing-DnSm_Nh-.js +1 -0
  38. package/dist/ui/assets/Notifications-CiljQzvM.js +1 -0
  39. package/dist/ui/assets/Pagination-JsiwxVNQ.js +1 -0
  40. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +1 -0
  41. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +9 -0
  42. package/dist/ui/assets/{Settings-Cgd4dJdc.js → Settings-OQlHAJoy.js} +6 -4
  43. package/dist/ui/assets/Skills-Crsybug0.js +7 -0
  44. package/dist/ui/assets/Smiths-wm90jRDT.js +1 -0
  45. package/dist/ui/assets/Tasks-C5FMu_Yu.js +1 -0
  46. package/dist/ui/assets/TrinityDatabases-BzYfecKI.js +1 -0
  47. package/dist/ui/assets/{UsageStats-EEwfbJ6C.js → UsageStats-CBo2vW2n.js} +1 -1
  48. package/dist/ui/assets/{WebhookManager-CyVUcscY.js → WebhookManager-0tDFkfHd.js} +1 -1
  49. package/dist/ui/assets/audit-B-F8XPLi.js +1 -0
  50. package/dist/ui/assets/chronos-BvMxfBQH.js +1 -0
  51. package/dist/ui/assets/{config-cslLZS3q.js → config-DteVgNGR.js} +1 -1
  52. package/dist/ui/assets/index-Cwqr-n0Y.js +10 -0
  53. package/dist/ui/assets/index-DcfyUdLI.css +1 -0
  54. package/dist/ui/assets/{mcp-M0iDC0mj.js → mcp-DxzodOdH.js} +1 -1
  55. package/dist/ui/assets/{skills-BvaaqiOT.js → skills--hAyQnmG.js} +1 -1
  56. package/dist/ui/assets/{stats-DALk3GOj.js → stats-Cibaisqd.js} +1 -1
  57. package/dist/ui/assets/vendor-icons-BVuQI-6R.js +1 -0
  58. package/dist/ui/index.html +3 -3
  59. package/dist/ui/sw.js +1 -1
  60. package/package.json +2 -1
  61. package/dist/runtime/tools/apoc-tool.js +0 -157
  62. package/dist/runtime/tools/neo-tool.js +0 -172
  63. package/dist/runtime/tools/trinity-tool.js +0 -157
  64. package/dist/ui/assets/Chat-Cx2OgATp.js +0 -38
  65. package/dist/ui/assets/Chronos--mut48fM.js +0 -1
  66. package/dist/ui/assets/MCPManager-CtRQzwM8.js +0 -1
  67. package/dist/ui/assets/ModelPricing-d4EYrGko.js +0 -1
  68. package/dist/ui/assets/Notifications-Dkqug57C.js +0 -1
  69. package/dist/ui/assets/SatiMemories-DykYVHgi.js +0 -1
  70. package/dist/ui/assets/SessionAudit-Bk0-DpW0.js +0 -9
  71. package/dist/ui/assets/Skills-DSi313oC.js +0 -7
  72. package/dist/ui/assets/Smiths-DLys0BWT.js +0 -1
  73. package/dist/ui/assets/Tasks-B1MbPNUQ.js +0 -1
  74. package/dist/ui/assets/TrinityDatabases-B5SeHOLt.js +0 -1
  75. package/dist/ui/assets/chronos-BVRpP__j.js +0 -1
  76. package/dist/ui/assets/index-CpVvCthh.js +0 -10
  77. package/dist/ui/assets/index-QQyZIsmH.css +0 -1
  78. package/dist/ui/assets/vendor-icons-DLvvGkeN.js +0 -1
@@ -14,6 +14,7 @@ import { SQLiteChatMessageHistory } from '../runtime/memory/sqlite.js';
14
14
  import { SatiRepository } from '../runtime/memory/sati/repository.js';
15
15
  import { MCPManager } from '../config/mcp-manager.js';
16
16
  import { Construtor } from '../runtime/tools/factory.js';
17
+ import { AuditRepository } from '../runtime/audit/repository.js';
17
18
  function escapeHtml(text) {
18
19
  return text
19
20
  .replace(/&/g, '&')
@@ -310,8 +311,34 @@ export class TelegramAdapter {
310
311
  filePath = await this.downloadToTemp(fileLink);
311
312
  // Transcribe
312
313
  this.display.log(`Transcribing audio for @${user}...`, { source: 'Telephonist' });
314
+ const transcribeStart = Date.now();
313
315
  const { text, usage } = await this.telephonist.transcribe(filePath, 'audio/ogg', apiKey);
316
+ const transcribeDurationMs = Date.now() - transcribeStart;
314
317
  this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
318
+ // Audit: record telephonist execution
319
+ try {
320
+ const sessionId = await this.history.getCurrentSessionOrCreate();
321
+ AuditRepository.getInstance().insert({
322
+ session_id: sessionId,
323
+ event_type: 'telephonist',
324
+ agent: 'telephonist',
325
+ provider: config.audio.provider,
326
+ model: config.audio.model,
327
+ input_tokens: usage.input_tokens ?? null,
328
+ output_tokens: usage.output_tokens ?? null,
329
+ duration_ms: transcribeDurationMs,
330
+ status: 'success',
331
+ metadata: {
332
+ audio_duration_seconds: usage.audio_duration_seconds ?? duration,
333
+ text_preview: text.slice(0, 120),
334
+ text_length: text.length,
335
+ user,
336
+ },
337
+ });
338
+ }
339
+ catch {
340
+ // Audit failure never breaks the main flow
341
+ }
315
342
  // Reply with transcription (optional, maybe just process it?)
316
343
  // The prompt says "reply with the answer".
317
344
  // "Transcribe them... and process the resulting text as a standard user prompt."
@@ -352,6 +379,22 @@ export class TelegramAdapter {
352
379
  catch (error) {
353
380
  const detail = error?.cause?.message || error?.response?.data?.error?.message || error.message;
354
381
  this.display.log(`Audio processing error for @${user}: ${detail}`, { source: 'Telephonist', level: 'error' });
382
+ try {
383
+ const sessionId = await this.history.getCurrentSessionOrCreate();
384
+ AuditRepository.getInstance().insert({
385
+ session_id: sessionId,
386
+ event_type: 'telephonist',
387
+ agent: 'telephonist',
388
+ provider: this.config.get().audio.provider,
389
+ model: this.config.get().audio.model,
390
+ duration_ms: null,
391
+ status: 'error',
392
+ metadata: { error: detail, user },
393
+ });
394
+ }
395
+ catch {
396
+ // Audit failure never breaks the main flow
397
+ }
355
398
  await ctx.reply("Sorry, I failed to process your audio message.");
356
399
  }
357
400
  finally {
@@ -151,7 +151,9 @@ export class ConfigManager {
151
151
  base_url: config.sati.base_url || config.llm.base_url,
152
152
  context_window: config.sati.context_window !== undefined ? resolveNumeric('MORPHEUS_SATI_CONTEXT_WINDOW', config.sati.context_window, config.sati.context_window) : llmConfig.context_window,
153
153
  memory_limit: config.sati.memory_limit !== undefined ? resolveNumeric('MORPHEUS_SATI_MEMORY_LIMIT', config.sati.memory_limit, config.sati.memory_limit) : undefined,
154
- enabled_archived_sessions: resolveBoolean('MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS', config.sati.enabled_archived_sessions, true)
154
+ enabled_archived_sessions: resolveBoolean('MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS', config.sati.enabled_archived_sessions, true),
155
+ similarity_threshold: resolveNumeric('MORPHEUS_SATI_SIMILARITY_THRESHOLD', config.sati.similarity_threshold, 0.9),
156
+ evaluation_interval: resolveNumeric('MORPHEUS_SATI_EVALUATION_INTERVAL', config.sati.evaluation_interval, 1),
155
157
  };
156
158
  }
157
159
  // Apply precedence to Apoc config
@@ -21,6 +21,8 @@ export const LLMConfigSchema = z.object({
21
21
  export const SatiConfigSchema = LLMConfigSchema.extend({
22
22
  memory_limit: z.number().int().positive().optional(),
23
23
  enabled_archived_sessions: z.boolean().default(true),
24
+ similarity_threshold: z.number().min(0).max(1).default(0.9),
25
+ evaluation_interval: z.number().int().min(1).default(1),
24
26
  });
25
27
  export const ApocConfigSchema = LLMConfigSchema.extend({
26
28
  working_dir: z.string().optional(),
package/dist/http/api.js CHANGED
@@ -178,8 +178,30 @@ export function createApiRouter(oracle, chronosWorker) {
178
178
  duration_ms: row.duration_ms ?? null,
179
179
  provider: row.provider ?? null,
180
180
  model: row.model ?? null,
181
+ audio_duration_seconds: row.audio_duration_seconds ?? null,
182
+ sati_memories_count: null,
181
183
  };
182
184
  });
185
+ // Enrich AI messages with Sati memory recovery counts from audit events
186
+ const recoveryEvents = AuditRepository.getInstance()
187
+ .getBySession(id, { limit: 10_000 })
188
+ .filter(e => e.event_type === 'memory_recovery')
189
+ .sort((a, b) => a.created_at - b.created_at);
190
+ for (let ri = 0; ri < recoveryEvents.length; ri++) {
191
+ const ev = recoveryEvents[ri];
192
+ const windowEnd = recoveryEvents[ri + 1]?.created_at ?? Infinity;
193
+ const count = ev.metadata ? (JSON.parse(ev.metadata).memories_count ?? 0) : 0;
194
+ if (count === 0)
195
+ continue;
196
+ // Assign to the LAST ai message within this recovery's window
197
+ for (let mi = normalizedMessages.length - 1; mi >= 0; mi--) {
198
+ const m = normalizedMessages[mi];
199
+ if (m.type === 'ai' && m.created_at > ev.created_at && m.created_at < windowEnd) {
200
+ m.sati_memories_count = count;
201
+ break;
202
+ }
203
+ }
204
+ }
183
205
  // Convert DESC to ASC for UI rendering
184
206
  res.json(normalizedMessages.reverse());
185
207
  }
@@ -190,7 +212,16 @@ export function createApiRouter(oracle, chronosWorker) {
190
212
  sessionHistory.close();
191
213
  }
192
214
  });
193
- // --- Session Audit ---
215
+ // --- Audit ---
216
+ router.get('/audit/global', (_req, res) => {
217
+ try {
218
+ const summary = AuditRepository.getInstance().getGlobalSummary();
219
+ res.json(summary);
220
+ }
221
+ catch (err) {
222
+ res.status(500).json({ error: err.message });
223
+ }
224
+ });
194
225
  router.get('/sessions/:id/audit', (req, res) => {
195
226
  try {
196
227
  const { id } = req.params;
@@ -238,14 +269,25 @@ export function createApiRouter(oracle, chronosWorker) {
238
269
  const originChannel = req.query.origin_channel;
239
270
  const sessionId = req.query.session_id;
240
271
  const limit = req.query.limit;
272
+ const pageRaw = req.query.page;
273
+ const perPageRaw = req.query.per_page;
241
274
  const parsedStatus = typeof status === 'string' ? TaskStatusSchema.safeParse(status) : null;
242
275
  const parsedAgent = typeof agent === 'string' ? TaskAgentSchema.safeParse(agent) : null;
243
276
  const parsedOrigin = typeof originChannel === 'string' ? OriginChannelSchema.safeParse(originChannel) : null;
244
- const tasks = taskRepository.listTasks({
277
+ const filters = {
245
278
  status: parsedStatus?.success ? parsedStatus.data : undefined,
246
279
  agent: parsedAgent?.success ? parsedAgent.data : undefined,
247
280
  origin_channel: parsedOrigin?.success ? parsedOrigin.data : undefined,
248
281
  session_id: typeof sessionId === 'string' ? sessionId : undefined,
282
+ };
283
+ if (pageRaw !== undefined || perPageRaw !== undefined) {
284
+ const page = Math.max(1, parseInt(String(pageRaw ?? '1'), 10) || 1);
285
+ const per_page = Math.min(100, Math.max(1, parseInt(String(perPageRaw ?? '20'), 10) || 20));
286
+ const result = taskRepository.listTasksPaginated({ ...filters, page, per_page });
287
+ return res.json(result);
288
+ }
289
+ const tasks = taskRepository.listTasks({
290
+ ...filters,
249
291
  limit: typeof limit === 'string' ? Math.max(1, Math.min(500, Number(limit) || 200)) : 200,
250
292
  });
251
293
  res.json(tasks);
@@ -403,7 +445,8 @@ export function createApiRouter(oracle, chronosWorker) {
403
445
  provider: z.string().min(1),
404
446
  model: z.string().min(1),
405
447
  input_price_per_1m: z.number().nonnegative(),
406
- output_price_per_1m: z.number().nonnegative()
448
+ output_price_per_1m: z.number().nonnegative(),
449
+ audio_cost_per_second: z.number().nonnegative().nullable().optional(),
407
450
  });
408
451
  router.get('/model-pricing', (req, res) => {
409
452
  try {
@@ -435,7 +478,8 @@ export function createApiRouter(oracle, chronosWorker) {
435
478
  const { provider, model } = req.params;
436
479
  const partial = z.object({
437
480
  input_price_per_1m: z.number().nonnegative().optional(),
438
- output_price_per_1m: z.number().nonnegative().optional()
481
+ output_price_per_1m: z.number().nonnegative().optional(),
482
+ audio_cost_per_second: z.number().nonnegative().nullable().optional(),
439
483
  }).safeParse(req.body);
440
484
  if (!partial.success) {
441
485
  return res.status(400).json({ error: 'Invalid payload', details: partial.error.issues });
@@ -451,7 +495,10 @@ export function createApiRouter(oracle, chronosWorker) {
451
495
  provider,
452
496
  model,
453
497
  input_price_per_1m: partial.data.input_price_per_1m ?? existing.input_price_per_1m,
454
- output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m
498
+ output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m,
499
+ audio_cost_per_second: 'audio_cost_per_second' in partial.data
500
+ ? partial.data.audio_cost_per_second
501
+ : existing.audio_cost_per_second,
455
502
  });
456
503
  h.close();
457
504
  res.json({ success: true });
@@ -680,15 +727,25 @@ export function createApiRouter(oracle, chronosWorker) {
680
727
  router.get('/sati/memories', async (req, res) => {
681
728
  try {
682
729
  const repository = SatiRepository.getInstance();
683
- const memories = repository.getAllMemories();
684
- // Convert dates to ISO strings for JSON serialization
685
- const serializedMemories = memories.map(memory => ({
730
+ const pageRaw = req.query.page;
731
+ const perPageRaw = req.query.per_page;
732
+ const category = typeof req.query.category === 'string' && req.query.category !== 'all' ? req.query.category : undefined;
733
+ const importance = typeof req.query.importance === 'string' && req.query.importance !== 'all' ? req.query.importance : undefined;
734
+ const search = typeof req.query.search === 'string' && req.query.search.trim() ? req.query.search.trim() : undefined;
735
+ const serializeMemory = (memory) => ({
686
736
  ...memory,
687
- created_at: memory.created_at.toISOString(),
688
- updated_at: memory.updated_at.toISOString(),
689
- last_accessed_at: memory.last_accessed_at ? memory.last_accessed_at.toISOString() : null
690
- }));
691
- res.json(serializedMemories);
737
+ created_at: memory.created_at instanceof Date ? memory.created_at.toISOString() : memory.created_at,
738
+ updated_at: memory.updated_at instanceof Date ? memory.updated_at.toISOString() : memory.updated_at,
739
+ last_accessed_at: memory.last_accessed_at instanceof Date ? memory.last_accessed_at.toISOString() : (memory.last_accessed_at ?? null),
740
+ });
741
+ if (pageRaw !== undefined || perPageRaw !== undefined) {
742
+ const page = Math.max(1, parseInt(String(pageRaw ?? '1'), 10) || 1);
743
+ const per_page = Math.min(100, Math.max(1, parseInt(String(perPageRaw ?? '20'), 10) || 20));
744
+ const result = repository.getMemoriesPaginated({ category, importance, search, page, per_page });
745
+ return res.json({ ...result, data: result.data.map(serializeMemory) });
746
+ }
747
+ const memories = repository.getAllMemories();
748
+ res.json(memories.map(serializeMemory));
692
749
  }
693
750
  catch (error) {
694
751
  res.status(500).json({ error: error.message });
@@ -75,11 +75,22 @@ export function createChronosJobRouter(repo, _worker) {
75
75
  res.status(400).json({ error: err.message });
76
76
  }
77
77
  });
78
- // GET /api/chronos — list jobs
78
+ // GET /api/chronos — list jobs (paginated)
79
79
  router.get('/', (req, res) => {
80
80
  try {
81
81
  const enabled = req.query.enabled;
82
82
  const created_by = req.query.created_by;
83
+ const page = req.query.page ? Math.max(1, parseInt(String(req.query.page), 10) || 1) : undefined;
84
+ const per_page = req.query.per_page ? Math.min(100, Math.max(1, parseInt(String(req.query.per_page), 10) || 20)) : undefined;
85
+ if (page !== undefined || per_page !== undefined) {
86
+ const result = repo.listJobsPaginated({
87
+ enabled: enabled === 'true' ? true : enabled === 'false' ? false : undefined,
88
+ created_by,
89
+ page,
90
+ per_page,
91
+ });
92
+ return res.json(result);
93
+ }
83
94
  const jobs = repo.listJobs({
84
95
  enabled: enabled === 'true' ? true : enabled === 'false' ? false : undefined,
85
96
  created_by,
@@ -74,11 +74,19 @@ export function createWebhooksRouter() {
74
74
  // ─────────────────────────────────────────────────────────────────────────────
75
75
  router.get('/notifications', authMiddleware, (req, res) => {
76
76
  try {
77
- const { webhookId, unreadOnly } = req.query;
78
- const notifications = repo.listNotifications({
77
+ const { webhookId, unreadOnly, status, page: pageRaw, per_page: perPageRaw } = req.query;
78
+ const filters = {
79
79
  webhookId: typeof webhookId === 'string' ? webhookId : undefined,
80
80
  unreadOnly: unreadOnly === 'true',
81
- });
81
+ status: typeof status === 'string' && status !== 'all' ? status : undefined,
82
+ };
83
+ if (pageRaw !== undefined || perPageRaw !== undefined) {
84
+ const page = Math.max(1, parseInt(String(pageRaw ?? '1'), 10) || 1);
85
+ const per_page = Math.min(100, Math.max(1, parseInt(String(perPageRaw ?? '20'), 10) || 20));
86
+ const result = repo.listNotificationsPaginated({ ...filters, page, per_page });
87
+ return res.json(result);
88
+ }
89
+ const notifications = repo.listNotifications(filters);
82
90
  res.json(notifications);
83
91
  }
84
92
  catch (err) {
@@ -0,0 +1 @@
1
+ export {};
@@ -4,7 +4,8 @@ import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
5
5
  import { DisplayManager } from "./display.js";
6
6
  import { buildDevKit } from "../devkit/index.js";
7
- import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
7
+ import { extractRawUsage, persistAgentMessage, buildAgentResult, emitToolAuditEvents } from "./subagent-utils.js";
8
+ import { buildDelegationTool } from "./tools/delegation-utils.js";
8
9
  /**
9
10
  * Apoc is a subagent of Oracle specialized in devtools operations.
10
11
  * It receives delegated tasks from Oracle and executes them using DevKit tools
@@ -17,6 +18,7 @@ import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
17
18
  export class Apoc {
18
19
  static instance = null;
19
20
  static currentSessionId = undefined;
21
+ static _delegateTool = null;
20
22
  agent;
21
23
  config;
22
24
  display = DisplayManager.getInstance();
@@ -38,10 +40,10 @@ export class Apoc {
38
40
  }
39
41
  static resetInstance() {
40
42
  Apoc.instance = null;
43
+ Apoc._delegateTool = null;
41
44
  }
42
45
  async initialize() {
43
46
  const apocConfig = this.config.apoc || this.config.llm;
44
- // console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
45
47
  const devkit = ConfigManager.getInstance().getDevKitConfig();
46
48
  const timeout_ms = devkit.timeout_ms || this.config.apoc?.timeout_ms || 30_000;
47
49
  const personality = this.config.apoc?.personality || 'pragmatic_dev';
@@ -73,8 +75,9 @@ export class Apoc {
73
75
  * @param task Natural language task description
74
76
  * @param context Optional additional context from the ongoing conversation
75
77
  * @param sessionId Session to attribute token usage to (defaults to 'apoc')
78
+ * @param taskContext Optional Oracle task context (unused by Apoc directly — kept for ISubagent compatibility)
76
79
  */
77
- async execute(task, context, sessionId) {
80
+ async execute(task, context, sessionId, taskContext) {
78
81
  if (!this.agent) {
79
82
  await this.initialize();
80
83
  }
@@ -194,7 +197,7 @@ If refinement is triggered:
194
197
 
195
198
  2. Execute a second search cycle (Cycle 2).
196
199
  3. Repeat selection, navigation, extraction, verification.
197
- 4. Choose the stronger cycles evidence.
200
+ 4. Choose the stronger cycle's evidence.
198
201
  5. Do NOT perform more than 2 cycles.
199
202
 
200
203
  If Cycle 2 also fails:
@@ -239,10 +242,10 @@ LOW:
239
242
  OUTPUT FORMAT (STRICT)
240
243
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
241
244
 
242
- 1. Direct Answer
243
- 2. Evidence Summary
244
- 3. Sources (URLs)
245
- 4. Confidence Level (HIGH / MEDIUM / LOW)
245
+ 1. Direct Answer
246
+ 2. Evidence Summary
247
+ 3. Sources (URLs)
248
+ 4. Confidence Level (HIGH / MEDIUM / LOW)
246
249
  5. Completion Status (true / false)
247
250
 
248
251
  No conversational filler.
@@ -254,6 +257,7 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
254
257
  const userMessage = new HumanMessage(task);
255
258
  const messages = [systemMessage, userMessage];
256
259
  try {
260
+ const inputCount = messages.length;
257
261
  const startMs = Date.now();
258
262
  const response = await this.agent.invoke({ messages });
259
263
  const durationMs = Date.now() - startMs;
@@ -262,45 +266,51 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
262
266
  const content = typeof lastMessage.content === "string"
263
267
  ? lastMessage.content
264
268
  : JSON.stringify(lastMessage.content);
265
- // Aggregate token usage across all AI messages in this invocation
266
- const rawUsage = lastMessage.usage_metadata
267
- ?? lastMessage.response_metadata?.usage
268
- ?? lastMessage.response_metadata?.tokenUsage
269
- ?? lastMessage.usage;
270
- const inputTokens = rawUsage?.input_tokens ?? 0;
271
- const outputTokens = rawUsage?.output_tokens ?? 0;
269
+ const rawUsage = extractRawUsage(lastMessage);
272
270
  const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
273
271
  const targetSession = sessionId ?? Apoc.currentSessionId ?? "apoc";
274
- const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
275
- try {
276
- const persisted = new AIMessage(content);
277
- if (rawUsage)
278
- persisted.usage_metadata = rawUsage;
279
- persisted.provider_metadata = { provider: apocConfig.provider, model: apocConfig.model };
280
- persisted.agent_metadata = { agent: 'apoc' };
281
- persisted.duration_ms = durationMs;
282
- await history.addMessage(persisted);
283
- }
284
- finally {
285
- history.close();
286
- }
272
+ await persistAgentMessage('apoc', content, apocConfig, targetSession, rawUsage, durationMs);
273
+ emitToolAuditEvents(response.messages.slice(inputCount), targetSession, 'apoc');
287
274
  this.display.log("Apoc task completed.", { source: "Apoc" });
288
- return {
289
- output: content,
290
- usage: {
291
- provider: apocConfig.provider,
292
- model: apocConfig.model,
293
- inputTokens,
294
- outputTokens,
295
- durationMs,
296
- stepCount,
297
- },
298
- };
275
+ return buildAgentResult(content, apocConfig, rawUsage, durationMs, stepCount);
299
276
  }
300
277
  catch (err) {
301
278
  throw new ProviderError(this.config.apoc?.provider || this.config.llm.provider, err, "Apoc task execution failed");
302
279
  }
303
280
  }
281
+ createDelegateTool() {
282
+ if (!Apoc._delegateTool) {
283
+ Apoc._delegateTool = buildDelegationTool({
284
+ name: "apoc_delegate",
285
+ description: `Delegate a devtools task to Apoc, the specialized development subagent.
286
+
287
+ This tool enqueues a background task and returns an acknowledgement with task id.
288
+ Do not expect final execution output in the same response.
289
+ Each task must contain a single atomic action with a clear expected result.
290
+
291
+ Use this tool when the user asks for ANY of the following:
292
+ - File operations: read, write, create, delete files or directories
293
+ - Shell commands: run scripts, execute commands, check output
294
+ - Git: status, log, diff, commit, push, pull, clone, branch
295
+ - Package management: npm install/update/audit, yarn, package.json inspection
296
+ - Process management: list processes, kill processes, check ports
297
+ - Network: ping hosts, curl URLs, DNS lookups
298
+ - System info: environment variables, OS info, disk space, memory
299
+ - Internet search: search DuckDuckGo and verify facts by reading at least 3 sources via browser_navigate before reporting results.
300
+ - Browser automation: navigate websites (JS/SPA), inspect DOM, click elements, fill forms. Apoc will ask for missing user input (e.g. credentials, form fields) before proceeding.
301
+
302
+ Provide a clear natural language task description. Optionally provide context
303
+ from the current conversation to help Apoc understand the broader goal.`,
304
+ agentKey: "apoc",
305
+ agentLabel: "Apoc",
306
+ auditAgent: "apoc",
307
+ isSync: () => ConfigManager.getInstance().get().apoc?.execution_mode === 'sync',
308
+ notifyText: '🧑‍🔬 Apoc is executing your request...',
309
+ executeSync: (task, context, sessionId) => Apoc.getInstance().execute(task, context, sessionId),
310
+ });
311
+ }
312
+ return Apoc._delegateTool;
313
+ }
304
314
  /** Reload with updated config (called when settings change) */
305
315
  async reload() {
306
316
  this.config = ConfigManager.getInstance().get();