@agentuity/opencode 1.0.11 → 1.0.13

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 (48) hide show
  1. package/dist/agents/lead.d.ts +1 -1
  2. package/dist/agents/lead.d.ts.map +1 -1
  3. package/dist/agents/lead.js +9 -0
  4. package/dist/agents/lead.js.map +1 -1
  5. package/dist/agents/monitor.d.ts +1 -1
  6. package/dist/agents/monitor.d.ts.map +1 -1
  7. package/dist/agents/monitor.js +13 -0
  8. package/dist/agents/monitor.js.map +1 -1
  9. package/dist/background/manager.d.ts +4 -1
  10. package/dist/background/manager.d.ts.map +1 -1
  11. package/dist/background/manager.js +161 -3
  12. package/dist/background/manager.js.map +1 -1
  13. package/dist/background/types.d.ts +21 -0
  14. package/dist/background/types.d.ts.map +1 -1
  15. package/dist/plugin/hooks/cadence.d.ts +2 -1
  16. package/dist/plugin/hooks/cadence.d.ts.map +1 -1
  17. package/dist/plugin/hooks/cadence.js +57 -1
  18. package/dist/plugin/hooks/cadence.js.map +1 -1
  19. package/dist/plugin/plugin.d.ts.map +1 -1
  20. package/dist/plugin/plugin.js +196 -7
  21. package/dist/plugin/plugin.js.map +1 -1
  22. package/dist/sqlite/index.d.ts +3 -0
  23. package/dist/sqlite/index.d.ts.map +1 -0
  24. package/dist/sqlite/index.js +2 -0
  25. package/dist/sqlite/index.js.map +1 -0
  26. package/dist/sqlite/queries.d.ts +18 -0
  27. package/dist/sqlite/queries.d.ts.map +1 -0
  28. package/dist/sqlite/queries.js +41 -0
  29. package/dist/sqlite/queries.js.map +1 -0
  30. package/dist/sqlite/reader.d.ts +44 -0
  31. package/dist/sqlite/reader.d.ts.map +1 -0
  32. package/dist/sqlite/reader.js +526 -0
  33. package/dist/sqlite/reader.js.map +1 -0
  34. package/dist/sqlite/types.d.ts +110 -0
  35. package/dist/sqlite/types.d.ts.map +1 -0
  36. package/dist/sqlite/types.js +2 -0
  37. package/dist/sqlite/types.js.map +1 -0
  38. package/package.json +3 -3
  39. package/src/agents/lead.ts +9 -0
  40. package/src/agents/monitor.ts +13 -0
  41. package/src/background/manager.ts +174 -3
  42. package/src/background/types.ts +10 -0
  43. package/src/plugin/hooks/cadence.ts +72 -1
  44. package/src/plugin/plugin.ts +271 -23
  45. package/src/sqlite/index.ts +16 -0
  46. package/src/sqlite/queries.ts +50 -0
  47. package/src/sqlite/reader.ts +677 -0
  48. package/src/sqlite/types.ts +121 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/opencode",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Agentuity employees and contributors",
6
6
  "description": "Agentuity Open Code plugin with specialized AI coding agents",
@@ -40,13 +40,13 @@
40
40
  "prepublishOnly": "bun run clean && bun run build"
41
41
  },
42
42
  "dependencies": {
43
- "@agentuity/core": "1.0.11",
43
+ "@agentuity/core": "1.0.13",
44
44
  "@opencode-ai/plugin": "^1.1.36",
45
45
  "yaml": "^2.8.1",
46
46
  "zod": "^4.3.5"
47
47
  },
48
48
  "devDependencies": {
49
- "@agentuity/test-utils": "1.0.11",
49
+ "@agentuity/test-utils": "1.0.13",
50
50
  "@types/bun": "latest",
51
51
  "bun-types": "latest",
52
52
  "typescript": "^5.9.0"
@@ -504,6 +504,14 @@ agentuity_background_output({ task_id: "bg_xxx" })
504
504
  agentuity_background_cancel({ task_id: "bg_xxx" })
505
505
  \`\`\`
506
506
 
507
+ **Session Dashboard (Lead-of-Leads Monitoring):**
508
+ \`\`\`
509
+ agentuity_session_dashboard({ session_id: "ses_xxx" })
510
+ // Returns: hierarchy of child sessions with status, costs, active tools, and health summary
511
+ \`\`\`
512
+
513
+ Use \`agentuity_session_dashboard\` when orchestrating Lead-of-Leads to get a full view of all child sessions, their status, costs, and what they're currently doing — without needing to inspect each task individually.
514
+
507
515
  **Example - Parallel Security Review:**
508
516
  When asked to review multiple packages for security:
509
517
  1. Launch \`agentuity_background_task\` for each package with Scout
@@ -1315,6 +1323,7 @@ You (Parent Lead):
1315
1323
  - **No direct child-to-child communication** — Coordinate through PRD
1316
1324
  - **Parent handles integration** — After children complete, parent does any glue work
1317
1325
  - **Monitor watches tasks** — Use BackgroundMonitor to avoid polling loop exhausting context
1326
+ - **Session dashboard** — Use \`agentuity_session_dashboard\` to get a unified view of all child session states, costs, and health without inspecting each task individually
1318
1327
 
1319
1328
  ### Context Management
1320
1329
 
@@ -11,6 +11,19 @@ You are a background task monitor. Your ONLY job is to watch background tasks an
11
11
  3. When ALL tasks complete (or error), you report back to Lead
12
12
  4. You do NOT interpret results - just report completion status
13
13
 
14
+ ## Enhanced Inspection
15
+
16
+ When you need deeper insight into a task, use \`agentuity_background_inspect\` which returns:
17
+ - Full message history (not truncated)
18
+ - Active tool calls with status
19
+ - Todo items and their status
20
+ - Cost summary (total cost + tokens)
21
+ - Child session count (for nested Lead-of-Leads)
22
+
23
+ Use inspect when a task has been running for many poll cycles without completing — it can reveal what the agent is stuck on.
24
+
25
+ For a full session tree with all child sessions, costs, and health summary, use \`agentuity_session_dashboard({ session_id: "..." })\`. This is especially useful when monitoring Lead-of-Leads scenarios with multiple parallel workstreams.
26
+
14
27
  ## Polling Behavior
15
28
 
16
29
  - Poll every 10 seconds (wait between checks)
@@ -1,6 +1,7 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
2
  import { agents } from '../agents';
3
3
  import type { AgentDefinition } from '../agents';
4
+ import type { DBTextPart, OpenCodeDBReader } from '../sqlite';
4
5
  import type {
5
6
  BackgroundTask,
6
7
  BackgroundTaskConfig,
@@ -46,6 +47,7 @@ export class BackgroundManager {
46
47
  private config: BackgroundTaskConfig;
47
48
  private concurrency: ConcurrencyManager;
48
49
  private callbacks?: BackgroundManagerCallbacks;
50
+ private dbReader?: OpenCodeDBReader;
49
51
  private tasks = new Map<string, BackgroundTask>();
50
52
  private tasksByParent = new Map<string, Set<string>>();
51
53
  private tasksBySession = new Map<string, string>();
@@ -56,7 +58,8 @@ export class BackgroundManager {
56
58
  constructor(
57
59
  ctx: PluginInput,
58
60
  config?: BackgroundTaskConfig,
59
- callbacks?: BackgroundManagerCallbacks
61
+ callbacks?: BackgroundManagerCallbacks,
62
+ dbReader?: OpenCodeDBReader
60
63
  ) {
61
64
  this.ctx = ctx;
62
65
  this.config = { ...DEFAULT_BACKGROUND_CONFIG, ...config };
@@ -65,6 +68,7 @@ export class BackgroundManager {
65
68
  limits: buildConcurrencyLimits(this.config),
66
69
  });
67
70
  this.callbacks = callbacks;
71
+ this.dbReader = dbReader;
68
72
  }
69
73
 
70
74
  async launch(input: LaunchInput): Promise<BackgroundTask> {
@@ -122,6 +126,64 @@ export class BackgroundManager {
122
126
  if (!task?.sessionId) return undefined;
123
127
 
124
128
  try {
129
+ if (this.dbReader?.isAvailable()) {
130
+ const session = this.dbReader.getSession(task.sessionId);
131
+ const messageCount = this.dbReader.getMessageCount(task.sessionId);
132
+ const messageLimit = messageCount > 0 ? messageCount : 100;
133
+ const messageRows = this.dbReader.getMessages(task.sessionId, {
134
+ limit: messageLimit,
135
+ offset: 0,
136
+ });
137
+ const textParts = this.dbReader.getTextParts(task.sessionId, {
138
+ limit: Math.max(messageLimit * 5, 200),
139
+ });
140
+ const partsByMessage = groupTextPartsByMessage(textParts);
141
+ const messages = messageRows
142
+ .sort((a, b) => a.timeCreated - b.timeCreated)
143
+ .map((message) => ({
144
+ info: {
145
+ role: message.role,
146
+ agent: message.agent,
147
+ model: message.model,
148
+ cost: message.cost,
149
+ tokens: message.tokens,
150
+ error: message.error,
151
+ timeCreated: message.timeCreated,
152
+ timeUpdated: message.timeUpdated,
153
+ },
154
+ parts: buildTextParts(partsByMessage.get(message.id)),
155
+ }));
156
+ const activeTools = this.dbReader.getActiveToolCalls(task.sessionId).map((tool) => ({
157
+ tool: tool.tool,
158
+ status: tool.status,
159
+ callId: tool.callId,
160
+ }));
161
+ const todos = this.dbReader.getTodos(task.sessionId).map((todo) => ({
162
+ content: todo.content,
163
+ status: todo.status,
164
+ priority: todo.priority,
165
+ }));
166
+ const cost = this.dbReader.getSessionCost(task.sessionId);
167
+ const childSessionCount = this.dbReader.getChildSessions(task.sessionId).length;
168
+
169
+ return {
170
+ taskId: task.id,
171
+ sessionId: task.sessionId,
172
+ status: task.status,
173
+ session,
174
+ messages,
175
+ lastActivity: task.progress?.lastUpdate?.toISOString(),
176
+ messageCount,
177
+ activeTools,
178
+ todos,
179
+ costSummary: {
180
+ totalCost: cost.totalCost,
181
+ totalTokens: cost.totalTokens,
182
+ },
183
+ childSessionCount,
184
+ };
185
+ }
186
+
125
187
  // Get session details
126
188
  const sessionResponse = await this.ctx.client.session.get({
127
189
  path: { id: task.sessionId },
@@ -183,7 +245,8 @@ export class BackgroundManager {
183
245
  throwOnError: false,
184
246
  });
185
247
 
186
- const children = unwrapResponse<Array<unknown>>(childrenResponse) ?? [];
248
+ const rawChildren = unwrapResponse<Array<unknown>>(childrenResponse);
249
+ const children = Array.isArray(rawChildren) ? rawChildren : [];
187
250
  for (const child of children) {
188
251
  const childSession = child as { id?: string; status?: { type?: string } };
189
252
  if (!childSession.id) continue;
@@ -235,12 +298,68 @@ export class BackgroundManager {
235
298
  let recovered = 0;
236
299
 
237
300
  try {
301
+ if (this.dbReader?.isAvailable()) {
302
+ const parentSessionId = process.env.AGENTUITY_OPENCODE_SESSION;
303
+ if (parentSessionId) {
304
+ const sessions = this.dbReader.getChildSessions(parentSessionId);
305
+ for (const sess of sessions) {
306
+ if (!sess.title?.startsWith('{')) continue;
307
+
308
+ try {
309
+ const metadata = JSON.parse(sess.title) as {
310
+ taskId?: string;
311
+ agent?: string;
312
+ description?: string;
313
+ createdAt?: string;
314
+ };
315
+
316
+ if (!metadata.taskId || !metadata.taskId.startsWith('bg_')) continue;
317
+ if (this.tasks.has(metadata.taskId)) continue;
318
+
319
+ const agentName = metadata.agent ?? 'unknown';
320
+ const task: BackgroundTask = {
321
+ id: metadata.taskId,
322
+ sessionId: sess.id,
323
+ parentSessionId: sess.parentId ?? '',
324
+ agent: agentName,
325
+ description: metadata.description ?? '',
326
+ prompt: '',
327
+ status: this.mapDbStatusToTaskStatus(sess.id),
328
+ queuedAt: metadata.createdAt ? new Date(metadata.createdAt) : new Date(),
329
+ startedAt: metadata.createdAt ? new Date(metadata.createdAt) : new Date(),
330
+ concurrencyGroup: this.getConcurrencyGroup(agentName),
331
+ progress: {
332
+ toolCalls: 0,
333
+ lastUpdate: new Date(),
334
+ },
335
+ };
336
+
337
+ this.tasks.set(task.id, task);
338
+ this.tasksBySession.set(sess.id, task.id);
339
+
340
+ if (task.parentSessionId) {
341
+ const parentTasks =
342
+ this.tasksByParent.get(task.parentSessionId) ?? new Set();
343
+ parentTasks.add(task.id);
344
+ this.tasksByParent.set(task.parentSessionId, parentTasks);
345
+ }
346
+
347
+ recovered++;
348
+ } catch {
349
+ continue;
350
+ }
351
+ }
352
+ return recovered;
353
+ }
354
+ }
355
+
238
356
  // Get all sessions
239
357
  const sessionsResponse = await this.ctx.client.session.list({
240
358
  throwOnError: false,
241
359
  });
242
360
 
243
- const sessions = unwrapResponse<Array<unknown>>(sessionsResponse) ?? [];
361
+ const rawSessions = unwrapResponse<Array<unknown>>(sessionsResponse);
362
+ const sessions = Array.isArray(rawSessions) ? rawSessions : [];
244
363
 
245
364
  for (const session of sessions) {
246
365
  const sess = session as {
@@ -628,6 +747,18 @@ Use the agentuity_background_output tool with task_id "${task.id}" to view the r
628
747
 
629
748
  private async fetchLatestResult(sessionId: string): Promise<string | undefined> {
630
749
  try {
750
+ if (this.dbReader?.isAvailable()) {
751
+ const messages = this.dbReader.getMessages(sessionId, { limit: 100, offset: 0 });
752
+ const textParts = this.dbReader.getTextParts(sessionId, { limit: 300 });
753
+ const partsByMessage = groupTextPartsByMessage(textParts);
754
+ for (const message of messages) {
755
+ if (message.role !== 'assistant') continue;
756
+ const text = joinTextParts(partsByMessage.get(message.id));
757
+ if (text) return text;
758
+ }
759
+ return undefined;
760
+ }
761
+
631
762
  const messagesResult = await this.ctx.client.session.messages({
632
763
  path: { id: sessionId },
633
764
  throwOnError: true,
@@ -647,6 +778,23 @@ Use the agentuity_background_output tool with task_id "${task.id}" to view the r
647
778
  return undefined;
648
779
  }
649
780
 
781
+ private mapDbStatusToTaskStatus(sessionId: string): BackgroundTaskStatus {
782
+ if (!this.dbReader) return 'pending';
783
+ const status = this.dbReader.getSessionStatus(sessionId).status;
784
+ switch (status) {
785
+ case 'idle':
786
+ case 'archived':
787
+ return 'completed';
788
+ case 'active':
789
+ case 'compacting':
790
+ return 'running';
791
+ case 'error':
792
+ return 'error';
793
+ default:
794
+ return 'pending';
795
+ }
796
+ }
797
+
650
798
  private getConcurrencyGroup(agentName: string): string | undefined {
651
799
  const model = getAgentModel(agentName);
652
800
  if (!model) return undefined;
@@ -733,6 +881,29 @@ function extractTextFromParts(parts: Array<unknown>): string | undefined {
733
881
  return textParts.join('\n');
734
882
  }
735
883
 
884
+ function groupTextPartsByMessage(parts: DBTextPart[]): Map<string, DBTextPart[]> {
885
+ const grouped = new Map<string, DBTextPart[]>();
886
+ for (const part of parts) {
887
+ const list = grouped.get(part.messageId) ?? [];
888
+ list.push(part);
889
+ grouped.set(part.messageId, list);
890
+ }
891
+ for (const list of grouped.values()) {
892
+ list.sort((a, b) => a.timeCreated - b.timeCreated);
893
+ }
894
+ return grouped;
895
+ }
896
+
897
+ function buildTextParts(parts?: DBTextPart[]): Array<{ type: string; text: string }> {
898
+ if (!parts || parts.length === 0) return [];
899
+ return parts.map((part) => ({ type: 'text', text: part.text }));
900
+ }
901
+
902
+ function joinTextParts(parts?: DBTextPart[]): string | undefined {
903
+ if (!parts || parts.length === 0) return undefined;
904
+ return parts.map((part) => part.text).join('\n');
905
+ }
906
+
736
907
  function unwrapResponse<T>(result: unknown): T | undefined {
737
908
  if (typeof result === 'object' && result !== null && 'data' in result) {
738
909
  return (result as { data?: T }).data;
@@ -66,4 +66,14 @@ export interface TaskInspection {
66
66
  messages: Array<{ info: unknown; parts: unknown[] }>;
67
67
  /** Last activity timestamp from task progress */
68
68
  lastActivity?: string;
69
+ /** Total message count when available */
70
+ messageCount?: number;
71
+ /** Active tools with status when available */
72
+ activeTools?: Array<{ tool: string; status: string; callId: string }>;
73
+ /** Todo items when available */
74
+ todos?: Array<{ content: string; status: string; priority: string }>;
75
+ /** Cost summary when available */
76
+ costSummary?: { totalCost: number; totalTokens: number };
77
+ /** Count of child sessions (nested LoL) when available */
78
+ childSessionCount?: number;
69
79
  }
@@ -1,6 +1,7 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
2
  import type { CoderConfig } from '../../types';
3
3
  import type { BackgroundManager } from '../../background';
4
+ import type { OpenCodeDBReader, SessionTreeNode } from '../../sqlite';
4
5
 
5
6
  /** Compacting hook input/output types */
6
7
  type CompactingInput = { sessionID: string };
@@ -69,7 +70,8 @@ interface CadenceSessionState {
69
70
  export function createCadenceHooks(
70
71
  ctx: PluginInput,
71
72
  _config: CoderConfig,
72
- backgroundManager?: BackgroundManager
73
+ backgroundManager?: BackgroundManager,
74
+ dbReader?: OpenCodeDBReader
73
75
  ): CadenceHooks {
74
76
  const activeCadenceSessions = new Map<string, CadenceSessionState>();
75
77
 
@@ -320,6 +322,7 @@ ${taskList}
320
322
 
321
323
  **CRITICAL:** Task IDs and session IDs persist across compaction - these tasks are still running.
322
324
  Use \`agentuity_background_output({ task_id: "..." })\` to check their status.
325
+ Use \`agentuity_session_dashboard({ session_id: "..." })\` to get a full session tree with status, costs, and health summary for Lead-of-Leads monitoring.
323
326
 
324
327
  **Tip:** If you spawned child Leads for parallel work, delegate monitoring to BackgroundMonitor:
325
328
  \`\`\`typescript
@@ -363,6 +366,11 @@ After compaction:
363
366
  3. Lead will continue the loop from iteration ${state.iteration}
364
367
  4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?
365
368
  `);
369
+
370
+ const dashboardSummary = buildSqliteDashboardSummary(dbReader, sessionId);
371
+ if (dashboardSummary) {
372
+ output.context.push(dashboardSummary);
373
+ }
366
374
  },
367
375
 
368
376
  /**
@@ -455,6 +463,69 @@ function isCadenceStop(text: string): boolean {
455
463
  );
456
464
  }
457
465
 
466
+ function buildSqliteDashboardSummary(
467
+ dbReader: OpenCodeDBReader | undefined,
468
+ parentSessionId: string
469
+ ): string | undefined {
470
+ if (!dbReader || !dbReader.isAvailable()) return undefined;
471
+
472
+ const dashboard = dbReader.getSessionDashboard(parentSessionId);
473
+ if (!dashboard.sessions.length) return undefined;
474
+
475
+ const lines: string[] = [];
476
+ for (const node of dashboard.sessions) {
477
+ appendDashboardNode(dbReader, node, 0, lines);
478
+ }
479
+
480
+ return `
481
+ ## SQLite Session Dashboard
482
+
483
+ - Parent session: ${parentSessionId}
484
+ - Total child cost: ${dashboard.totalCost}
485
+
486
+ ${lines.join('\n')}
487
+ `;
488
+ }
489
+
490
+ function appendDashboardNode(
491
+ reader: OpenCodeDBReader,
492
+ node: SessionTreeNode,
493
+ depth: number,
494
+ lines: string[]
495
+ ): void {
496
+ const status = reader.getSessionStatus(node.session.id);
497
+ const todoSummary = node.todoSummary
498
+ ? `${node.todoSummary.pending}/${node.todoSummary.total} pending`
499
+ : 'no todos';
500
+ const costSummary = node.costSummary
501
+ ? `${node.costSummary.totalCost} (${node.costSummary.totalTokens} tokens)`
502
+ : '0 (0 tokens)';
503
+ const label = formatSessionLabel(node.session.title);
504
+ const indent = `${' '.repeat(depth)}-`;
505
+
506
+ lines.push(
507
+ `${indent} ${label} [${node.session.id}] status: ${status.status}, tools: ${node.activeToolCount}, todos: ${todoSummary}, messages: ${node.messageCount}, cost: ${costSummary}`
508
+ );
509
+
510
+ for (const child of node.children) {
511
+ appendDashboardNode(reader, child, depth + 1, lines);
512
+ }
513
+ }
514
+
515
+ function formatSessionLabel(title: string): string {
516
+ if (!title) return 'Session';
517
+ if (title.startsWith('{')) {
518
+ try {
519
+ const parsed = JSON.parse(title) as { description?: string; taskId?: string };
520
+ if (parsed.description) return parsed.description;
521
+ if (parsed.taskId) return parsed.taskId;
522
+ } catch {
523
+ return title;
524
+ }
525
+ }
526
+ return title;
527
+ }
528
+
458
529
  function showToast(ctx: PluginInput, message: string): void {
459
530
  try {
460
531
  ctx.client.tui.showToast({ body: { message, variant: 'info' } });