opencode-async-agent 1.0.1 → 1.0.2

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 (2) hide show
  1. package/dist/async-agent.js +361 -4
  2. package/package.json +1 -1
@@ -38,10 +38,106 @@ function formatDuration(startedAt, completedAt) {
38
38
  var MAX_RUN_TIME_MS = 15 * 60 * 1e3;
39
39
 
40
40
  // src/plugin/manager.ts
41
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
42
+ import { join } from "path";
41
43
  function parseModel(model) {
42
44
  const [providerID, ...rest] = model.split("/");
43
45
  return { providerID, modelID: rest.join("/") };
44
46
  }
47
+ var ANALYSIS_PROMPT = `You are a session analyst. Analyze the following AI task execution comprehensively so the main agent can make informed next decisions.
48
+
49
+ ## Analysis Criteria
50
+
51
+ ### 1. Anything AI Missed Based on Initial Prompt
52
+ - Compare the original user prompt against what was actually accomplished
53
+ - Identify any requirements, questions, or requests that were never addressed
54
+ - List promises made by the agent that were left unfulfilled
55
+
56
+ ### 2. Wrong Doings
57
+ - Identify incorrect assumptions or bad approaches taken
58
+ - Note any factual errors or wrong technical decisions
59
+ - Call out misinterpretations of the original prompt
60
+
61
+ ### 3. Gave Up / Shortcuts
62
+ - Did the agent abandon parts of the task prematurely?
63
+ - Were steps skipped or incomplete solutions used?
64
+ - Did the agent stop without exhausting reasonable options?
65
+ - Any signs of "good enough" attitude instead of thorough completion?
66
+
67
+ ### 4. Messed Up
68
+ - Did the agent break existing functionality?
69
+ - Were new problems introduced during the task?
70
+ - Any destructive actions or unintended side effects?
71
+
72
+ ### 5. Good Points / Choices
73
+ - What technical decisions were sound and should be replicated?
74
+ - What approaches worked well that future tasks should follow?
75
+ - Notable strengths in this session's execution
76
+
77
+ ### 6. Session Ended Properly or Stream Cut Out
78
+ - **Proper finish:** Agent concluded with clear result or summary
79
+ - **Stream cut out:** Session interrupted mid-task with no conclusion
80
+ - **Ambiguous end:** Final state unclear or incomplete explanation
81
+
82
+ ### 7. Overall Status on the Session
83
+ - Give the main agent a complete picture of what happened
84
+ - Was this session successful, partial, or a failure?
85
+ - Is the output reliable enough to base next decisions on?
86
+
87
+ ## Output Format
88
+
89
+ Provide your analysis in **markdown** with these exact sections:
90
+
91
+ ### Summary
92
+ [2-3 sentence summary of what happened]
93
+
94
+ ### What the AI Missed Based on Initial Prompt
95
+ [List anything not covered from the original prompt]
96
+
97
+ ### Wrong Doings
98
+ [Incorrect assumptions, bad approaches, factual errors]
99
+
100
+ ### Gave Up / Shortcuts
101
+ [Premature abandonment, skipped steps, incomplete solutions]
102
+
103
+ ### Messed Up
104
+ [Broke things, created new problems, unintended side effects]
105
+
106
+ ### Good Points / Choices
107
+ [Sound decisions, approaches worth replicating, notable strengths]
108
+
109
+ ### Session Completion
110
+ - **Status:** [Proper finish / Stream cut out / Ambiguous]
111
+ - **Details:** [explanation of how the session ended]
112
+
113
+ ### Overall Status
114
+ [Complete assessment: Is this session's output reliable for next decisions? What's the verdict?]
115
+
116
+ ### Next Action for Main Agent
117
+ [Specific recommendation on what the main agent should do next based on this session's outcome]
118
+
119
+ ---
120
+
121
+ ## Session Data
122
+
123
+ ### Initial User Prompt
124
+ \`\`\`
125
+ \${initialPrompt}
126
+ \`\`\`
127
+
128
+ ### Full Conversation
129
+ \`\`\`
130
+ \${formattedMessages}
131
+ \`\`\`
132
+
133
+ ### Session Metadata
134
+ - Agent: \${agent}
135
+ - Model: \${model}
136
+ - Duration: \${duration}
137
+ - Status: \${status}
138
+ - Started: \${startTime}
139
+ - Completed: \${completedTime}
140
+ `;
45
141
  var DelegationManager = class {
46
142
  delegations = /* @__PURE__ */ new Map();
47
143
  client;
@@ -54,6 +150,29 @@ var DelegationManager = class {
54
150
  calculateDuration(delegation) {
55
151
  return formatDuration(delegation.startedAt, delegation.completedAt);
56
152
  }
153
+ // ---- Parent session model ----
154
+ async getParentModel(parentSessionID) {
155
+ try {
156
+ const messagesResult = await this.client.session.messages({
157
+ path: { id: parentSessionID }
158
+ });
159
+ const messageData = messagesResult.data;
160
+ if (!messageData || messageData.length === 0) {
161
+ return null;
162
+ }
163
+ const lastUserMessage = [...messageData].reverse().find((m) => m.info.role === "user");
164
+ if (!lastUserMessage || !lastUserMessage.info.model) {
165
+ return null;
166
+ }
167
+ const model = lastUserMessage.info.model;
168
+ const modelString = `${model.providerID}/${model.modelID}`;
169
+ await this.debugLog(`Got parent model: ${modelString}`);
170
+ return modelString;
171
+ } catch (error) {
172
+ this.log.debug(`Failed to get parent model: ${error instanceof Error ? error.message : "Unknown error"}`);
173
+ return null;
174
+ }
175
+ }
57
176
  // ---- Core operations ----
58
177
  async delegate(input) {
59
178
  await this.debugLog(`delegate() called`);
@@ -80,12 +199,14 @@ ${available || "(none)"}`
80
199
  throw new Error("Failed to create delegation session");
81
200
  }
82
201
  const sessionID = sessionResult.data.id;
202
+ const parentModel = await this.getParentModel(input.parentSessionID);
83
203
  const delegation = {
84
204
  id: sessionID,
85
205
  sessionID,
86
206
  parentSessionID: input.parentSessionID,
87
207
  parentMessageID: input.parentMessageID,
88
208
  parentAgent: input.parentAgent,
209
+ parentModel,
89
210
  prompt: input.prompt,
90
211
  agent: input.agent,
91
212
  model: input.model,
@@ -279,6 +400,14 @@ ${available || "(none)"}`
279
400
  Use delegation_list() to see available delegations.`);
280
401
  }
281
402
  if (delegation.status === "running") {
403
+ if (args.ai) {
404
+ return `Delegation "${args.id}" is still running.
405
+
406
+ Status: ${delegation.status}
407
+ Started: ${delegation.startedAt.toISOString()}
408
+
409
+ Wait for completion notification, then call delegation_read() again. AI analysis only available for completed sessions.`;
410
+ }
282
411
  return `Delegation "${args.id}" is still running.
283
412
 
284
413
  Status: ${delegation.status}
@@ -296,6 +425,16 @@ Error: ${delegation.error}`;
296
425
  Duration: ${delegation.duration}`;
297
426
  return statusMessage;
298
427
  }
428
+ if (args.ai) {
429
+ let model = args.ai_model;
430
+ if (!model) {
431
+ model = delegation.parentModel || await this.getDefaultModel();
432
+ if (!model) {
433
+ return "\u274C ai_model required when ai=true and no default model configured (parent session has no model)";
434
+ }
435
+ }
436
+ return await this.analyzeSessionWithAI(delegation, model);
437
+ }
299
438
  if (!args.mode || args.mode === "simple") {
300
439
  return await this.getSimpleResult(delegation);
301
440
  }
@@ -528,6 +667,221 @@ To inspect session content(human): opencode -s ${delegation.id}`;
528
667
  async debugLog(msg) {
529
668
  this.log.debug(msg);
530
669
  }
670
+ // ---- AI Analysis ----
671
+ async getDefaultModel() {
672
+ try {
673
+ const result = await this.client.config.get();
674
+ const config = result.data;
675
+ return config?.model || null;
676
+ } catch (error) {
677
+ this.log.debug(`Failed to get default model: ${error instanceof Error ? error.message : "Unknown error"}`);
678
+ return null;
679
+ }
680
+ }
681
+ getModelInfo(modelId) {
682
+ const [provider, ...rest] = modelId.split("/");
683
+ if (!provider) {
684
+ throw new Error(`Invalid model format: "${modelId}". Expected "provider/model"`);
685
+ }
686
+ const modelIdOnly = rest.join("/");
687
+ try {
688
+ const modelJsonPath = join(process.env.HOME || "", ".cache", "opencode", "models.json");
689
+ const content = readFileSync(modelJsonPath, "utf-8");
690
+ const modelsData = JSON.parse(content);
691
+ if (!modelsData[provider]) {
692
+ throw new Error(`Provider "${provider}" not found in models.json`);
693
+ }
694
+ const providerData = modelsData[provider];
695
+ const apiUrl = providerData.api || providerData.baseUrl;
696
+ if (!apiUrl) {
697
+ throw new Error(`No API URL found for provider "${provider}"`);
698
+ }
699
+ return { provider, apiUrl, modelId: modelIdOnly };
700
+ } catch (error) {
701
+ if (error instanceof Error) throw error;
702
+ throw new Error(`Failed to parse models.json: ${error}`);
703
+ }
704
+ }
705
+ getApiKey(provider) {
706
+ try {
707
+ const authJsonPath = join(process.env.HOME || "", ".local", "share", "opencode", "auth.json");
708
+ const content = readFileSync(authJsonPath, "utf-8");
709
+ const authData = JSON.parse(content);
710
+ if (!authData[provider]) {
711
+ throw new Error(`Provider "${provider}" not found in auth.json`);
712
+ }
713
+ const providerAuth = authData[provider];
714
+ if (providerAuth.type !== "api") {
715
+ throw new Error(`Provider "${provider}" is not an API key type`);
716
+ }
717
+ return providerAuth.key;
718
+ } catch (error) {
719
+ if (error instanceof Error) throw error;
720
+ throw new Error(`Failed to parse auth.json: ${error}`);
721
+ }
722
+ }
723
+ formatSessionForAI(messages, delegation) {
724
+ let initialPrompt = delegation.prompt;
725
+ const parts = [];
726
+ for (const msg of messages) {
727
+ const role = msg.info.role.toUpperCase();
728
+ const timestamp = msg.info.time?.created ? new Date(msg.info.time.created).toISOString() : "unknown";
729
+ parts.push(`[${role}] ${timestamp}`);
730
+ for (const part of msg.parts) {
731
+ switch (part.type) {
732
+ case "text":
733
+ if (part.text) {
734
+ parts.push(part.text.trim());
735
+ }
736
+ break;
737
+ case "reasoning":
738
+ case "thinking":
739
+ const thinkingText = part.thinking || part.text || "";
740
+ if (thinkingText) {
741
+ parts.push(`[REASONING] ${thinkingText.slice(0, 2e3)}`);
742
+ }
743
+ break;
744
+ case "tool":
745
+ if (part.state) {
746
+ const toolInput = part.state.status === "pending" || part.state.status === "running" ? JSON.stringify(part.state.input || {}) : JSON.stringify(part.state.input || {});
747
+ parts.push(`[TOOL CALL] ${part.tool}: ${toolInput}`);
748
+ }
749
+ break;
750
+ case "tool_result":
751
+ const content = part.content || part.output || "";
752
+ parts.push(`[TOOL RESULT] ${content}`);
753
+ break;
754
+ case "file":
755
+ parts.push(`[FILE] ${part.filename || "unknown file"} (${part.mime})`);
756
+ break;
757
+ case "patch":
758
+ parts.push(`[PATCH] Code diff applied`);
759
+ break;
760
+ case "snapshot":
761
+ parts.push(`[SNAPSHOT] State snapshot`);
762
+ break;
763
+ case "agent":
764
+ parts.push(`[AGENT] Switched to: ${part.name || "unknown"}`);
765
+ break;
766
+ default:
767
+ parts.push(`[${part.type}] ${JSON.stringify(part).slice(0, 200)}`);
768
+ }
769
+ }
770
+ parts.push("");
771
+ }
772
+ return `# Full Conversation
773
+
774
+ ${parts.join("\n")}`;
775
+ }
776
+ async callAIForAnalysis(apiUrl, apiKey, model, prompt, timeoutMs = 6e4) {
777
+ const controller = new AbortController();
778
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
779
+ try {
780
+ const url = apiUrl.endsWith("/") ? `${apiUrl}chat/completions` : `${apiUrl}/chat/completions`;
781
+ const response = await fetch(url, {
782
+ method: "POST",
783
+ headers: {
784
+ "Authorization": `Bearer ${apiKey}`,
785
+ "Content-Type": "application/json"
786
+ },
787
+ body: JSON.stringify({
788
+ model,
789
+ messages: [{ role: "user", content: prompt }],
790
+ max_tokens: 4e3,
791
+ temperature: 0.3
792
+ }),
793
+ signal: controller.signal
794
+ });
795
+ clearTimeout(timeoutId);
796
+ if (!response.ok) {
797
+ const errorText = await response.text();
798
+ throw new Error(`API request failed: ${response.status} ${response.statusText}
799
+ ${errorText}`);
800
+ }
801
+ const data = await response.json();
802
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
803
+ throw new Error("Invalid API response format");
804
+ }
805
+ return data.choices[0].message.content;
806
+ } catch (error) {
807
+ if (error.name === "AbortError") {
808
+ throw new Error("AI analysis timed out after 60 seconds");
809
+ }
810
+ throw error;
811
+ }
812
+ }
813
+ logAnalysis(delegationId, model, result, error, duration) {
814
+ if (process.env.OC_ASYNC_DEBUG !== "true") {
815
+ return;
816
+ }
817
+ try {
818
+ const logDir = join(process.env.HOME || "", ".cache", "opencode-delegation-ai");
819
+ if (!existsSync(logDir)) {
820
+ mkdirSync(logDir, { recursive: true });
821
+ }
822
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
823
+ const filename = `analysis-${delegationId}-${timestamp}.json`;
824
+ const logEntry = {
825
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
826
+ delegationId,
827
+ model,
828
+ status: error ? "error" : "success",
829
+ durationMs: duration,
830
+ result: error ? void 0 : result,
831
+ error: error || void 0
832
+ };
833
+ writeFileSync(join(logDir, filename), JSON.stringify(logEntry, null, 2));
834
+ } catch (err) {
835
+ this.log.debug(`Failed to log analysis: ${err}`);
836
+ }
837
+ }
838
+ async analyzeSessionWithAI(delegation, model) {
839
+ const startTime = Date.now();
840
+ try {
841
+ const modelInfo = this.getModelInfo(model);
842
+ const apiKey = this.getApiKey(modelInfo.provider);
843
+ const messagesResult = await this.client.session.messages({
844
+ path: { id: delegation.sessionID }
845
+ });
846
+ const messageData = messagesResult.data;
847
+ if (!messageData || messageData.length === 0) {
848
+ return `Delegation "${delegation.id}" has no messages to analyze.`;
849
+ }
850
+ const formattedMessages = this.formatSessionForAI(messageData, delegation);
851
+ const initialPrompt = messageData.find((m) => m.info.role === "user")?.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n") || delegation.prompt;
852
+ const sessionMetadata = `
853
+ ### Session Metadata
854
+ - Agent: ${delegation.agent}
855
+ - Model: ${delegation.model || "unknown"}
856
+ - Duration: ${delegation.duration || "N/A"}
857
+ - Status: ${delegation.status}
858
+ - Started: ${delegation.startedAt.toISOString()}
859
+ - Completed: ${delegation.completedAt?.toISOString() || "N/A"}
860
+ `;
861
+ const fullPrompt = ANALYSIS_PROMPT.replace("${initialPrompt}", initialPrompt).replace("${formattedMessages}", formattedMessages).replace("${agent}", delegation.agent).replace("${model}", delegation.model || "unknown").replace("${duration}", delegation.duration || "N/A").replace("${status}", delegation.status).replace("${startTime}", delegation.startedAt.toISOString()).replace("${completedTime}", delegation.completedAt?.toISOString() || "N/A");
862
+ const analysis = await this.callAIForAnalysis(modelInfo.apiUrl, apiKey, modelInfo.modelId, fullPrompt);
863
+ const duration = Date.now() - startTime;
864
+ this.logAnalysis(delegation.id, model, analysis, void 0, duration);
865
+ const header = `# AI Analysis for Delegation: ${delegation.id}
866
+
867
+ **Agent:** ${delegation.agent}
868
+ **Analysis Model:** ${model}
869
+ **Duration:** ${delegation.duration || "N/A"}
870
+ **Analysis Time:** ${(duration / 1e3).toFixed(2)}s
871
+
872
+ ---
873
+
874
+ `;
875
+ return header + analysis;
876
+ } catch (error) {
877
+ const duration = Date.now() - startTime;
878
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
879
+ this.logAnalysis(delegation.id, model, "", errorMessage, duration);
880
+ return `\u274C AI analysis failed:
881
+
882
+ ${errorMessage}`;
883
+ }
884
+ }
531
885
  };
532
886
 
533
887
  // src/plugin/tools.ts
@@ -598,6 +952,7 @@ function createDelegationRead(manager) {
598
952
  Modes:
599
953
  - simple (default): Returns just the final result
600
954
  - full: Returns all messages in the session with timestamps
955
+ - ai (requires ai=true): Use AI to analyze and summarize the entire session execution
601
956
 
602
957
  Use filters to get specific parts of the conversation.`,
603
958
  args: {
@@ -606,7 +961,9 @@ Use filters to get specific parts of the conversation.`,
606
961
  include_thinking: tool.schema.boolean().optional().describe("Include thinking/reasoning blocks in full mode"),
607
962
  include_tools: tool.schema.boolean().optional().describe("Include tool results in full mode"),
608
963
  since_message_id: tool.schema.string().optional().describe("Return only messages after this message ID (full mode only)"),
609
- limit: tool.schema.number().optional().describe("Max messages to return, capped at 100 (full mode only)")
964
+ limit: tool.schema.number().optional().describe("Max messages to return, capped at 100 (full mode only)"),
965
+ ai: tool.schema.boolean().optional().describe("Use AI to analyze and summarize the session"),
966
+ ai_model: tool.schema.string().optional().describe("Model for AI analysis (e.g. 'minimax/MiniMax-M2.5'). Required when ai=true if no default model configured")
610
967
  },
611
968
  async execute(args, toolCtx) {
612
969
  if (!toolCtx?.sessionID) {
@@ -749,11 +1106,11 @@ You WILL be notified via \`<system-reminder>\`. Polling wastes tokens.
749
1106
  async function readBgAgentsConfig() {
750
1107
  const { homedir } = await import("os");
751
1108
  const { readFile } = await import("fs/promises");
752
- const { join } = await import("path");
753
- const configDir = join(homedir(), ".config", "opencode");
1109
+ const { join: join2 } = await import("path");
1110
+ const configDir = join2(homedir(), ".config", "opencode");
754
1111
  for (const name of ["async-agents.md", "async-agent.md"]) {
755
1112
  try {
756
- return await readFile(join(configDir, name), "utf-8");
1113
+ return await readFile(join2(configDir, name), "utf-8");
757
1114
  } catch {
758
1115
  }
759
1116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-async-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "main": "dist/async-agent.js",
5
5
  "files": ["dist"],
6
6
  "scripts": {