wave-agent-sdk 0.14.2 → 0.14.4

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 (45) hide show
  1. package/builtin/skills/settings/SKILL.md +1 -1
  2. package/builtin/skills/settings/SKILLS.md +3 -0
  3. package/builtin/skills/settings/SUBAGENTS.md +21 -2
  4. package/dist/managers/aiManager.d.ts +3 -0
  5. package/dist/managers/aiManager.d.ts.map +1 -1
  6. package/dist/managers/aiManager.js +107 -82
  7. package/dist/managers/forkedAgentManager.d.ts +1 -0
  8. package/dist/managers/forkedAgentManager.d.ts.map +1 -1
  9. package/dist/managers/forkedAgentManager.js +1 -0
  10. package/dist/managers/pluginManager.d.ts +1 -0
  11. package/dist/managers/pluginManager.d.ts.map +1 -1
  12. package/dist/managers/pluginManager.js +7 -0
  13. package/dist/managers/subagentManager.d.ts +6 -0
  14. package/dist/managers/subagentManager.d.ts.map +1 -1
  15. package/dist/managers/subagentManager.js +36 -0
  16. package/dist/services/autoMemoryService.d.ts.map +1 -1
  17. package/dist/services/autoMemoryService.js +1 -0
  18. package/dist/services/pluginLoader.d.ts +5 -0
  19. package/dist/services/pluginLoader.d.ts.map +1 -1
  20. package/dist/services/pluginLoader.js +29 -0
  21. package/dist/types/plugins.d.ts +2 -0
  22. package/dist/types/plugins.d.ts.map +1 -1
  23. package/dist/utils/constants.d.ts +2 -2
  24. package/dist/utils/constants.d.ts.map +1 -1
  25. package/dist/utils/constants.js +2 -2
  26. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  27. package/dist/utils/convertMessagesForAPI.js +27 -8
  28. package/dist/utils/stringUtils.d.ts +8 -0
  29. package/dist/utils/stringUtils.d.ts.map +1 -1
  30. package/dist/utils/stringUtils.js +45 -0
  31. package/dist/utils/subagentParser.d.ts +8 -1
  32. package/dist/utils/subagentParser.d.ts.map +1 -1
  33. package/dist/utils/subagentParser.js +18 -3
  34. package/package.json +1 -1
  35. package/src/managers/aiManager.ts +141 -110
  36. package/src/managers/forkedAgentManager.ts +3 -0
  37. package/src/managers/pluginManager.ts +10 -0
  38. package/src/managers/subagentManager.ts +49 -0
  39. package/src/services/autoMemoryService.ts +1 -0
  40. package/src/services/pluginLoader.ts +37 -0
  41. package/src/types/plugins.ts +2 -0
  42. package/src/utils/constants.ts +2 -2
  43. package/src/utils/convertMessagesForAPI.ts +31 -9
  44. package/src/utils/stringUtils.ts +43 -0
  45. package/src/utils/subagentParser.ts +31 -4
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/utils/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAmC,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAA6C,CAAC;AAE9E;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAA0C,CAAC;AAE3E;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AAEvE;;GAEG;AACH,eAAO,MAAM,6BAA6B,QAAQ,CAAC;AACnD,eAAO,MAAM,8BAA8B,OAAO,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/utils/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAmC,CAAC;AAE/D;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAA6C,CAAC;AAE9E;;GAEG;AACH,eAAO,MAAM,mBAAmB,QAA0C,CAAC;AAE3E;;GAEG;AACH,eAAO,MAAM,gBAAgB,QAAyC,CAAC;AAEvE;;GAEG;AACH,eAAO,MAAM,6BAA6B,SAAS,CAAC;AACpD,eAAO,MAAM,8BAA8B,QAAQ,CAAC"}
@@ -23,5 +23,5 @@ export const USER_MEMORY_FILE = path.join(DATA_DIRECTORY, "AGENTS.md");
23
23
  /**
24
24
  * AI related constants
25
25
  */
26
- export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 96000; // Default token limit
27
- export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 8192; // Default output token limit
26
+ export const DEFAULT_WAVE_MAX_INPUT_TOKENS = 128000; // Default token limit
27
+ export const DEFAULT_WAVE_MAX_OUTPUT_TOKENS = 16384; // Default output token limit
@@ -1 +1 @@
1
- {"version":3,"file":"convertMessagesForAPI.d.ts","sourceRoot":"","sources":["../../src/utils/convertMessagesForAPI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAKjD,OAAO,EAEL,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AA0B7B;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,EAAE,GAClB,0BAA0B,EAAE,CAuO9B"}
1
+ {"version":3,"file":"convertMessagesForAPI.d.ts","sourceRoot":"","sources":["../../src/utils/convertMessagesForAPI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAKjD,OAAO,EAEL,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AAiC7B;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,OAAO,EAAE,GAClB,0BAA0B,EAAE,CAsP9B"}
@@ -1,9 +1,10 @@
1
1
  import { convertImageToBase64 } from "./messageOperations.js";
2
2
  import { taskNotificationToXml } from "./notificationXml.js";
3
- import { stripAnsiColors } from "./stringUtils.js";
3
+ import { recoverTruncatedJson, stripAnsiColors } from "./stringUtils.js";
4
4
  import { logger } from "./globalLogger.js";
5
5
  /**
6
- * Safely handle tool call parameters, ensuring a legal JSON string is returned
6
+ * Safely handle tool call parameters, ensuring a legal JSON string is returned.
7
+ * Attempts to recover truncated JSON (e.g., missing closing braces).
7
8
  * @param args Tool call parameters
8
9
  * @returns Legal JSON string
9
10
  */
@@ -16,12 +17,19 @@ function safeToolArguments(args) {
16
17
  JSON.parse(args);
17
18
  return args;
18
19
  }
19
- catch (error) {
20
- logger.error(`Invalid tool arguments: ${args}`, error);
21
- // If not valid JSON, return a fallback empty object with the original string as a comment or property
22
- return JSON.stringify({
23
- invalid_arguments: args,
24
- });
20
+ catch {
21
+ // Attempt to recover truncated JSON
22
+ const recovered = recoverTruncatedJson(args);
23
+ try {
24
+ JSON.parse(recovered);
25
+ return recovered;
26
+ }
27
+ catch {
28
+ // Truly malformed JSON — return sanitized fallback
29
+ return JSON.stringify({
30
+ invalid_arguments: args,
31
+ });
32
+ }
25
33
  }
26
34
  }
27
35
  /**
@@ -114,6 +122,16 @@ export function convertMessagesForAPI(messages) {
114
122
  .map((block) => (block.type === "text" ? block.content : ""))
115
123
  .join("\n");
116
124
  }
125
+ // Extract reasoning content from reasoning blocks
126
+ const reasoningBlocks = message.blocks.filter((block) => block.type === "reasoning" &&
127
+ block.content &&
128
+ block.content.trim().length > 0);
129
+ let reasoning_content;
130
+ if (reasoningBlocks.length > 0) {
131
+ reasoning_content = reasoningBlocks
132
+ .map((block) => (block.type === "reasoning" ? block.content : ""))
133
+ .join("\n");
134
+ }
117
135
  // Construct tool calls from tool blocks
118
136
  if (toolBlocks.length > 0) {
119
137
  tool_calls = toolBlocks
@@ -138,6 +156,7 @@ export function convertMessagesForAPI(messages) {
138
156
  role: "assistant",
139
157
  content: hasContent ? content : undefined,
140
158
  tool_calls,
159
+ ...(reasoning_content ? { reasoning_content } : {}),
141
160
  ...(message.additionalFields ? { ...message.additionalFields } : {}),
142
161
  };
143
162
  recentMessages.unshift(assistantMessage);
@@ -23,6 +23,14 @@ export declare const stripAnsiColors: (text: string) => string;
23
23
  * @returns Formatted line number prefix
24
24
  */
25
25
  export declare function formatLineNumberPrefix(lineNumber: number): string;
26
+ /**
27
+ * Attempt to recover truncated JSON (e.g., missing closing braces due to max tokens).
28
+ * Tracks brace depth and only recovers if there are unclosed `{` braces.
29
+ * Will NOT recover if there are unclosed `[` brackets (can't guess the content).
30
+ * @param jsonStr Potentially truncated JSON string
31
+ * @returns Recovered JSON string, or the original if unrecoverable
32
+ */
33
+ export declare function recoverTruncatedJson(jsonStr: string): string;
26
34
  /**
27
35
  * Efficiently get the last N lines of a string without splitting the whole string.
28
36
  */
@@ -1 +1 @@
1
- {"version":3,"file":"stringUtils.d.ts","sourceRoot":"","sources":["../../src/utils/stringUtils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgC/D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAK9C,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAehE"}
1
+ {"version":3,"file":"stringUtils.d.ts","sourceRoot":"","sources":["../../src/utils/stringUtils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAgC/D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAwBxB;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAK9C,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkC5D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAehE"}
@@ -77,6 +77,51 @@ export const stripAnsiColors = (text) => {
77
77
  export function formatLineNumberPrefix(lineNumber) {
78
78
  return `${lineNumber.toString().padStart(6)}\t`;
79
79
  }
80
+ /**
81
+ * Attempt to recover truncated JSON (e.g., missing closing braces due to max tokens).
82
+ * Tracks brace depth and only recovers if there are unclosed `{` braces.
83
+ * Will NOT recover if there are unclosed `[` brackets (can't guess the content).
84
+ * @param jsonStr Potentially truncated JSON string
85
+ * @returns Recovered JSON string, or the original if unrecoverable
86
+ */
87
+ export function recoverTruncatedJson(jsonStr) {
88
+ let braceDepth = 0;
89
+ let bracketDepth = 0;
90
+ let inString = false;
91
+ let escaped = false;
92
+ for (const ch of jsonStr) {
93
+ if (escaped) {
94
+ escaped = false;
95
+ continue;
96
+ }
97
+ if (ch === "\\" && inString) {
98
+ escaped = true;
99
+ continue;
100
+ }
101
+ if (ch === '"') {
102
+ inString = !inString;
103
+ continue;
104
+ }
105
+ if (!inString) {
106
+ if (ch === "{")
107
+ braceDepth++;
108
+ if (ch === "}")
109
+ braceDepth--;
110
+ if (ch === "[")
111
+ bracketDepth++;
112
+ if (ch === "]")
113
+ bracketDepth--;
114
+ }
115
+ }
116
+ // Build recovery suffix
117
+ let suffix = "";
118
+ if (inString)
119
+ suffix += '"'; // Close unclosed string
120
+ if (braceDepth > 0 && bracketDepth === 0) {
121
+ suffix += "}".repeat(braceDepth);
122
+ }
123
+ return suffix ? jsonStr + suffix : jsonStr;
124
+ }
80
125
  /**
81
126
  * Efficiently get the last N lines of a string without splitting the whole string.
82
127
  */
@@ -5,9 +5,16 @@ export interface SubagentConfiguration {
5
5
  model?: string;
6
6
  systemPrompt: string;
7
7
  filePath: string;
8
- scope: "project" | "user" | "builtin";
8
+ scope: "project" | "user" | "builtin" | "plugin";
9
9
  priority: number;
10
+ /** Plugin root directory path, set when scope is "plugin" */
11
+ pluginRoot?: string;
10
12
  }
13
+ /**
14
+ * Parse a plugin agent markdown file.
15
+ * Exposed as a public API for PluginLoader to use.
16
+ */
17
+ export declare function parseAgentFile(filePath: string, scope: "plugin", pluginRoot: string): SubagentConfiguration;
11
18
  /**
12
19
  * Load all subagent configurations from project and user directories, plus built-in subagents
13
20
  */
@@ -1 +1 @@
1
- {"version":3,"file":"subagentParser.d.ts","sourceRoot":"","sources":["../../src/utils/subagentParser.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAwLD;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAsBlC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAGvC"}
1
+ {"version":3,"file":"subagentParser.d.ts","sourceRoot":"","sources":["../../src/utils/subagentParser.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAkKD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,QAAQ,EACf,UAAU,EAAE,MAAM,GACjB,qBAAqB,CAEvB;AAqCD;;GAEG;AACH,wBAAsB,0BAA0B,CAC9C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAsBlC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAGvC"}
@@ -80,9 +80,9 @@ function validateConfiguration(config, filePath) {
80
80
  }
81
81
  }
82
82
  /**
83
- * Parse a single subagent markdown file
83
+ * Parse a single subagent markdown file with optional pluginRoot support
84
84
  */
85
- function parseSubagentFile(filePath, scope) {
85
+ function parseSubagentFile(filePath, scope, pluginRoot) {
86
86
  try {
87
87
  const content = readFileSync(filePath, "utf-8");
88
88
  const { frontmatter, body } = parseFrontmatter(content);
@@ -99,21 +99,36 @@ function parseSubagentFile(filePath, scope) {
99
99
  priority = 2;
100
100
  if (scope === "builtin")
101
101
  priority = 3;
102
+ if (scope === "plugin")
103
+ priority = 2; // Same priority as user-level
104
+ let systemPrompt = body;
105
+ // Substitute ${WAVE_PLUGIN_ROOT} for plugin scope at parse time
106
+ if (scope === "plugin" && pluginRoot) {
107
+ systemPrompt = systemPrompt.replace(/\$\{WAVE_PLUGIN_ROOT\}/g, pluginRoot);
108
+ }
102
109
  return {
103
110
  name: frontmatter.name,
104
111
  description: frontmatter.description,
105
112
  tools: frontmatter.tools,
106
113
  model: frontmatter.model,
107
- systemPrompt: body,
114
+ systemPrompt,
108
115
  filePath,
109
116
  scope,
110
117
  priority,
118
+ pluginRoot: scope === "plugin" ? pluginRoot : undefined,
111
119
  };
112
120
  }
113
121
  catch (error) {
114
122
  throw new Error(`Failed to parse subagent file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
115
123
  }
116
124
  }
125
+ /**
126
+ * Parse a plugin agent markdown file.
127
+ * Exposed as a public API for PluginLoader to use.
128
+ */
129
+ export function parseAgentFile(filePath, scope, pluginRoot) {
130
+ return parseSubagentFile(filePath, scope, pluginRoot);
131
+ }
117
132
  /**
118
133
  * Scan directory for subagent files
119
134
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -24,6 +24,7 @@ import type { SubagentManager } from "./subagentManager.js";
24
24
  import type { SkillManager } from "./skillManager.js";
25
25
  import { buildSystemPrompt } from "../prompts/index.js";
26
26
  import { Container } from "../utils/container.js";
27
+ import { recoverTruncatedJson } from "../utils/stringUtils.js";
27
28
  import { ConfigurationService } from "../services/configurationService.js";
28
29
  import type { NotificationQueue } from "./notificationQueue.js";
29
30
 
@@ -44,6 +45,8 @@ export interface AIManagerOptions {
44
45
  stream?: boolean;
45
46
  /**Optional model override (e.g. for subagents) */
46
47
  modelOverride?: string;
48
+ /**Optional max turns limit to prevent runaway recursion (e.g. for auto-memory extraction) */
49
+ maxTurns?: number;
47
50
  }
48
51
 
49
52
  export class AIManager {
@@ -59,6 +62,7 @@ export class AIManager {
59
62
  private modelOverride?: string;
60
63
  private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
61
64
  private consecutiveCompactionFailures: number = 0;
65
+ private readonly maxTurns?: number;
62
66
 
63
67
  // Service overrides
64
68
  constructor(
@@ -73,6 +77,7 @@ export class AIManager {
73
77
  this.callbacks = options.callbacks ?? {};
74
78
  this.modelOverride = options.modelOverride;
75
79
  this._onCwdChange = options.callbacks?.onCwdChange; // Initialize onCwdChange
80
+ this.maxTurns = options.maxTurns;
76
81
  }
77
82
 
78
83
  private get toolManager(): ToolManager {
@@ -814,34 +819,45 @@ export class AIManager {
814
819
  const toolName = functionToolCall.function?.name || "";
815
820
  // Safely parse tool parameters, handle tools without parameters
816
821
  let toolArgs: Record<string, unknown> = {};
822
+ let jsonRecovered = false;
817
823
  const argsString = functionToolCall.function?.arguments?.trim();
818
824
 
819
825
  if (!argsString || argsString === "") {
820
826
  // Tool without parameters, use empty object
821
827
  toolArgs = {};
822
828
  } else {
829
+ let recoveredArgs = argsString;
823
830
  try {
824
831
  toolArgs = JSON.parse(argsString);
825
- } catch (parseError) {
826
- // For non-empty but malformed JSON, still throw exception
827
- let errorMessage = `Failed to parse tool arguments`;
828
- if (result.finish_reason === "length") {
829
- errorMessage +=
830
- " (output truncated, please reduce your output)";
832
+ } catch {
833
+ // Attempt to recover truncated JSON (e.g., missing closing braces)
834
+ recoveredArgs = recoverTruncatedJson(argsString);
835
+ try {
836
+ toolArgs = JSON.parse(recoveredArgs);
837
+ jsonRecovered = true;
838
+ logger.warn(
839
+ `Recovered truncated JSON for tool "${toolName}"`,
840
+ );
841
+ } catch (parseError) {
842
+ let errorMessage = `Failed to parse tool arguments`;
843
+ if (result.finish_reason === "length") {
844
+ errorMessage +=
845
+ " (output truncated, please reduce your output)";
846
+ }
847
+ logger?.error(errorMessage, parseError);
848
+ this.messageManager.updateToolBlock({
849
+ id: toolId,
850
+ parameters: argsString,
851
+ result: errorMessage,
852
+ success: false,
853
+ error: errorMessage,
854
+ stage: "end",
855
+ name: toolName,
856
+ compactParams: "",
857
+ timestamp: Date.now(),
858
+ });
859
+ return;
831
860
  }
832
- logger?.error(errorMessage, parseError);
833
- this.messageManager.updateToolBlock({
834
- id: toolId,
835
- parameters: argsString,
836
- result: errorMessage,
837
- success: false,
838
- error: errorMessage,
839
- stage: "end",
840
- name: toolName,
841
- compactParams: "",
842
- timestamp: Date.now(),
843
- });
844
- return;
845
861
  }
846
862
  }
847
863
 
@@ -942,13 +958,20 @@ export class AIManager {
942
958
  context,
943
959
  );
944
960
 
961
+ // Build result content, adding truncation warning if JSON was recovered
962
+ let toolResultContent =
963
+ toolResult.content ||
964
+ (toolResult.error ? `Error: ${toolResult.error}` : "");
965
+ if (jsonRecovered) {
966
+ toolResultContent +=
967
+ "\n\n⚠️ Tool arguments were truncated (likely exceeded max output tokens). Please reduce your output or split into multiple tool calls.";
968
+ }
969
+
945
970
  // Update message state - tool execution completed
946
971
  this.messageManager.updateToolBlock({
947
972
  id: toolId,
948
973
  parameters: argsString,
949
- result:
950
- toolResult.content ||
951
- (toolResult.error ? `Error: ${toolResult.error}` : ""),
974
+ result: toolResultContent,
952
975
  success: toolResult.success,
953
976
  error: toolResult.error,
954
977
  stage: "end",
@@ -1001,108 +1024,116 @@ export class AIManager {
1001
1024
 
1002
1025
  // Check if there are tool operations or response was truncated, if so automatically initiate next AI service call
1003
1026
  if (toolCalls.length > 0 || result.finish_reason === "length") {
1004
- // Record committed snapshots to message history
1005
- if (this.reversionManager) {
1006
- const snapshots =
1007
- this.reversionManager.getAndClearCommittedSnapshots();
1008
- if (snapshots.length > 0) {
1009
- this.messageManager.addFileHistoryBlock(snapshots);
1010
- }
1011
- }
1012
-
1013
- // Check interruption status
1014
- const isCurrentlyAborted =
1015
- abortController.signal.aborted || toolAbortController.signal.aborted;
1016
-
1017
- // Check if all tools were manually backgrounded
1018
- const lastMessage =
1019
- this.messageManager.getMessages()[
1020
- this.messageManager.getMessages().length - 1
1021
- ];
1022
- const toolBlocks =
1023
- lastMessage?.blocks.filter(
1024
- (block): block is import("../types/messaging.js").ToolBlock =>
1025
- block.type === "tool",
1026
- ) || [];
1027
- const hasBackgrounded =
1028
- toolBlocks.length > 0 &&
1029
- toolBlocks.some((block) => block.isManuallyBackgrounded);
1030
-
1031
- if (hasBackgrounded) {
1032
- logger?.info(
1033
- "Some tools were manually backgrounded, stopping recursion.",
1027
+ // Check maxTurns limit before recursing
1028
+ if (this.maxTurns && recursionDepth + 1 >= this.maxTurns) {
1029
+ logger?.debug(
1030
+ `Max turns (${this.maxTurns}) reached, stopping recursion.`,
1034
1031
  );
1035
- } else if (!isCurrentlyAborted) {
1036
- // If response was truncated, add a hidden continuation message
1037
- if (result.finish_reason === "length") {
1038
- this.messageManager.addUserMessage({
1039
- content:
1040
- "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
1041
- isMeta: true,
1042
- });
1032
+ } else {
1033
+ // Record committed snapshots to message history
1034
+ if (this.reversionManager) {
1035
+ const snapshots =
1036
+ this.reversionManager.getAndClearCommittedSnapshots();
1037
+ if (snapshots.length > 0) {
1038
+ this.messageManager.addFileHistoryBlock(snapshots);
1039
+ }
1043
1040
  }
1044
1041
 
1045
- // Duplicate Tool Call Detection
1046
- if (toolCalls.length > 0) {
1047
- const messages = this.messageManager.getMessages();
1048
- // Find the most recent assistant message BEFORE the current one that has tool blocks
1049
- // The current assistant message is messages[messages.length - 1]
1050
- let previousAssistantWithTools: Message | undefined;
1051
- for (let i = messages.length - 2; i >= 0; i--) {
1052
- const msg = messages[i];
1053
- if (
1054
- msg.role === "assistant" &&
1055
- msg.blocks.some((b) => b.type === "tool")
1056
- ) {
1057
- previousAssistantWithTools = msg;
1058
- break;
1059
- }
1042
+ // Check interruption status
1043
+ const isCurrentlyAborted =
1044
+ abortController.signal.aborted ||
1045
+ toolAbortController.signal.aborted;
1046
+
1047
+ // Check if all tools were manually backgrounded
1048
+ const lastMessage =
1049
+ this.messageManager.getMessages()[
1050
+ this.messageManager.getMessages().length - 1
1051
+ ];
1052
+ const toolBlocks =
1053
+ lastMessage?.blocks.filter(
1054
+ (block): block is import("../types/messaging.js").ToolBlock =>
1055
+ block.type === "tool",
1056
+ ) || [];
1057
+ const hasBackgrounded =
1058
+ toolBlocks.length > 0 &&
1059
+ toolBlocks.some((block) => block.isManuallyBackgrounded);
1060
+
1061
+ if (hasBackgrounded) {
1062
+ logger?.info(
1063
+ "Some tools were manually backgrounded, stopping recursion.",
1064
+ );
1065
+ } else if (!isCurrentlyAborted) {
1066
+ // If response was truncated, add a hidden continuation message
1067
+ if (result.finish_reason === "length") {
1068
+ this.messageManager.addUserMessage({
1069
+ content:
1070
+ "Output token limit hit. Resume directly — no apology, no recap of what you were doing. Pick up mid-thought if that is where the cut happened. Break remaining work into smaller pieces.",
1071
+ isMeta: true,
1072
+ });
1060
1073
  }
1061
1074
 
1062
- if (previousAssistantWithTools) {
1063
- const previousToolBlocks =
1064
- previousAssistantWithTools.blocks.filter(
1065
- (b): b is import("../types/messaging.js").ToolBlock =>
1066
- b.type === "tool",
1067
- );
1075
+ // Duplicate Tool Call Detection
1076
+ if (toolCalls.length > 0) {
1077
+ const messages = this.messageManager.getMessages();
1078
+ // Find the most recent assistant message BEFORE the current one that has tool blocks
1079
+ // The current assistant message is messages[messages.length - 1]
1080
+ let previousAssistantWithTools: Message | undefined;
1081
+ for (let i = messages.length - 2; i >= 0; i--) {
1082
+ const msg = messages[i];
1083
+ if (
1084
+ msg.role === "assistant" &&
1085
+ msg.blocks.some((b) => b.type === "tool")
1086
+ ) {
1087
+ previousAssistantWithTools = msg;
1088
+ break;
1089
+ }
1090
+ }
1068
1091
 
1069
- for (const currentToolCall of toolCalls) {
1070
- const currentName = currentToolCall.function?.name;
1071
- const currentArgs = currentToolCall.function?.arguments;
1092
+ if (previousAssistantWithTools) {
1093
+ const previousToolBlocks =
1094
+ previousAssistantWithTools.blocks.filter(
1095
+ (b): b is import("../types/messaging.js").ToolBlock =>
1096
+ b.type === "tool",
1097
+ );
1072
1098
 
1073
- const isDuplicate = previousToolBlocks.some(
1074
- (prevBlock) =>
1075
- prevBlock.name === currentName &&
1076
- prevBlock.parameters === currentArgs,
1077
- );
1099
+ for (const currentToolCall of toolCalls) {
1100
+ const currentName = currentToolCall.function?.name;
1101
+ const currentArgs = currentToolCall.function?.arguments;
1078
1102
 
1079
- if (isDuplicate && currentName) {
1080
- const toolId = currentToolCall.id;
1081
- const lastMessage = messages[messages.length - 1];
1082
- const toolBlock = lastMessage.blocks.find(
1083
- (b): b is import("../types/messaging.js").ToolBlock =>
1084
- b.type === "tool" && b.id === toolId,
1103
+ const isDuplicate = previousToolBlocks.some(
1104
+ (prevBlock) =>
1105
+ prevBlock.name === currentName &&
1106
+ prevBlock.parameters === currentArgs,
1085
1107
  );
1086
- if (toolBlock) {
1087
- const warning = `\n\nNote: You just called this tool with the same arguments in the previous turn. Please ensure you are not in a loop and consider if you need to change your approach.`;
1088
- this.messageManager.updateToolBlock({
1089
- id: toolId,
1090
- result: (toolBlock.result || "") + warning,
1091
- stage: "end",
1092
- });
1108
+
1109
+ if (isDuplicate && currentName) {
1110
+ const toolId = currentToolCall.id;
1111
+ const lastMessage = messages[messages.length - 1];
1112
+ const toolBlock = lastMessage.blocks.find(
1113
+ (b): b is import("../types/messaging.js").ToolBlock =>
1114
+ b.type === "tool" && b.id === toolId,
1115
+ );
1116
+ if (toolBlock) {
1117
+ const warning = `\n\nNote: You just called this tool with the same arguments in the previous turn. Please ensure you are not in a loop and consider if you need to change your approach.`;
1118
+ this.messageManager.updateToolBlock({
1119
+ id: toolId,
1120
+ result: (toolBlock.result || "") + warning,
1121
+ stage: "end",
1122
+ });
1123
+ }
1093
1124
  }
1094
1125
  }
1095
1126
  }
1096
1127
  }
1097
- }
1098
1128
 
1099
- // Recursively call AI service, increment recursion depth, and pass same configuration
1100
- await this.sendAIMessage({
1101
- recursionDepth: recursionDepth + 1,
1102
- model,
1103
- allowedRules,
1104
- maxTokens,
1105
- });
1129
+ // Recursively call AI service, increment recursion depth, and pass same configuration
1130
+ await this.sendAIMessage({
1131
+ recursionDepth: recursionDepth + 1,
1132
+ model,
1133
+ allowedRules,
1134
+ maxTokens,
1135
+ });
1136
+ }
1106
1137
  }
1107
1138
  }
1108
1139
  } catch (error) {
@@ -47,6 +47,7 @@ export class ForkedAgentManager {
47
47
  allowedTools?: string[];
48
48
  model?: string;
49
49
  permissionModeOverride?: PermissionMode;
50
+ maxTurns?: number;
50
51
  },
51
52
  prompt: string,
52
53
  ): Promise<string> {
@@ -84,6 +85,7 @@ export class ForkedAgentManager {
84
85
  allowedTools?: string[];
85
86
  model?: string;
86
87
  permissionModeOverride?: PermissionMode;
88
+ maxTurns?: number;
87
89
  },
88
90
  prompt: string,
89
91
  ): Promise<void> {
@@ -103,6 +105,7 @@ export class ForkedAgentManager {
103
105
  allowedTools: parameters.allowedTools,
104
106
  model: parameters.model,
105
107
  permissionModeOverride: parameters.permissionModeOverride,
108
+ maxTurns: parameters.maxTurns,
106
109
  },
107
110
  false,
108
111
  );
@@ -7,6 +7,7 @@ import { HookManager } from "./hookManager.js";
7
7
  import { LspManager } from "./lspManager.js";
8
8
  import { McpManager } from "./mcpManager.js";
9
9
  import { SlashCommandManager } from "./slashCommandManager.js";
10
+ import { SubagentManager } from "./subagentManager.js";
10
11
  import { MarketplaceService } from "../services/MarketplaceService.js";
11
12
  import { ConfigurationService } from "../services/configurationService.js";
12
13
  import { Container } from "../utils/container.js";
@@ -53,6 +54,10 @@ export class PluginManager {
53
54
  return this.container.get<ConfigurationService>("ConfigurationService");
54
55
  }
55
56
 
57
+ private get subagentManager(): SubagentManager | undefined {
58
+ return this.container.get<SubagentManager>("SubagentManager");
59
+ }
60
+
56
61
  /**
57
62
  * Update enabled plugins configuration
58
63
  */
@@ -155,6 +160,7 @@ export class PluginManager {
155
160
  path: absolutePath,
156
161
  commands: PluginLoader.loadCommands(absolutePath),
157
162
  skills: await PluginLoader.loadSkills(absolutePath),
163
+ agents: await PluginLoader.loadAgents(absolutePath),
158
164
  lspConfig: await PluginLoader.loadLspConfig(absolutePath),
159
165
  mcpConfig: await PluginLoader.loadMcpConfig(absolutePath),
160
166
  hooksConfig: await PluginLoader.loadHooksConfig(absolutePath),
@@ -192,6 +198,10 @@ export class PluginManager {
192
198
  this.hookManager.registerPluginHooks(plugin.path, plugin.hooksConfig);
193
199
  }
194
200
 
201
+ if (this.subagentManager && plugin.agents.length > 0) {
202
+ this.subagentManager.registerPluginAgents(plugin.name, plugin.agents);
203
+ }
204
+
195
205
  this.plugins.set(manifest.name, plugin);
196
206
  logger?.info(`Loaded plugin: ${manifest.name} v${manifest.version}`);
197
207
  } catch (error) {