wave-agent-sdk 1.0.0 → 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 (43) hide show
  1. package/dist/agent.d.ts +9 -20
  2. package/dist/agent.js +35 -97
  3. package/dist/managers/aiManager.d.ts +63 -8
  4. package/dist/managers/aiManager.js +274 -80
  5. package/dist/managers/messageManager.d.ts +9 -5
  6. package/dist/managers/messageManager.js +36 -12
  7. package/dist/managers/permissionManager.js +13 -11
  8. package/dist/managers/subagentManager.d.ts +6 -0
  9. package/dist/managers/subagentManager.js +33 -22
  10. package/dist/prompts/index.d.ts +0 -2
  11. package/dist/prompts/index.js +71 -47
  12. package/dist/services/aiService.d.ts +1 -34
  13. package/dist/services/aiService.js +18 -130
  14. package/dist/services/autoMemoryService.d.ts +27 -2
  15. package/dist/services/autoMemoryService.js +124 -36
  16. package/dist/services/configurationService.js +14 -1
  17. package/dist/services/session.d.ts +13 -0
  18. package/dist/services/session.js +64 -0
  19. package/dist/types/agent.d.ts +0 -2
  20. package/dist/types/config.d.ts +7 -0
  21. package/dist/types/core.d.ts +1 -1
  22. package/dist/utils/containerSetup.js +12 -3
  23. package/package.json +1 -1
  24. package/src/agent.ts +50 -110
  25. package/src/managers/aiManager.ts +370 -105
  26. package/src/managers/messageManager.ts +51 -23
  27. package/src/managers/permissionManager.ts +15 -13
  28. package/src/managers/subagentManager.ts +36 -25
  29. package/src/prompts/index.ts +75 -56
  30. package/src/services/aiService.ts +25 -203
  31. package/src/services/autoMemoryService.ts +145 -39
  32. package/src/services/configurationService.ts +16 -1
  33. package/src/services/session.ts +68 -0
  34. package/src/types/agent.ts +0 -6
  35. package/src/types/config.ts +7 -0
  36. package/src/types/core.ts +1 -1
  37. package/src/utils/containerSetup.ts +12 -4
  38. package/dist/constants/goalPrompts.d.ts +0 -1
  39. package/dist/constants/goalPrompts.js +0 -10
  40. package/dist/managers/goalManager.d.ts +0 -42
  41. package/dist/managers/goalManager.js +0 -177
  42. package/src/constants/goalPrompts.ts +0 -10
  43. package/src/managers/goalManager.ts +0 -232
@@ -62,6 +62,22 @@ import {
62
62
  import { logOTelEvent } from "../telemetry/events.js";
63
63
  import type { BackgroundTask } from "../types/processes.js";
64
64
 
65
+ /** Result of a fork-path agent loop (compaction or auto-memory extraction). */
66
+ interface ForkLoopResult {
67
+ content?: string;
68
+ usage?: {
69
+ prompt_tokens: number;
70
+ completion_tokens: number;
71
+ total_tokens: number;
72
+ };
73
+ }
74
+
75
+ /** Max turns for the compaction fork: the model should summarize, not act. */
76
+ const MAX_FORK_TURNS = 3;
77
+
78
+ /** Max turns for the auto-memory extraction fork. */
79
+ const MAX_AUTO_MEMORY_FORK_TURNS = 5;
80
+
65
81
  // Truncate text to `max` chars and append a "… [+N chars]" marker when exceeded.
66
82
  // Used for background_tasks description/command fields (≤1000 chars per spec FR-063).
67
83
  function truncateWithMarker(text: string, max: number): string {
@@ -694,8 +710,7 @@ export class AIManager {
694
710
  } catch (compactError) {
695
711
  this.consecutiveCompactionFailures++;
696
712
  logger?.error(
697
- `Failed to compact messages (${this.consecutiveCompactionFailures} consecutive):`,
698
- compactError,
713
+ `Failed to compact messages (${this.consecutiveCompactionFailures} consecutive): ${compactError instanceof Error ? compactError.message : String(compactError)}`,
699
714
  );
700
715
  this.messageManager.addErrorBlock(
701
716
  `Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`,
@@ -745,27 +760,26 @@ export class AIManager {
745
760
  }
746
761
 
747
762
  /**
748
- * Fork-path compaction: run a bounded agent loop over a copy of the
749
- * conversation using the same system prompt, tools, model, and generation
750
- * params as the main loop, so the forked request prefix matches exactly
751
- * and the prompt cache is reused. Tool calls are denied locally (the model
752
- * is told to summarize, not act) and their rejections are fed back for
753
- * another turn. Returns undefined content when the model never produces
754
- * text; the caller treats that as a compaction failure.
763
+ * Fork-path loop: run a bounded agent loop over a copy of the conversation
764
+ * using the same system prompt, tools, model, and generation params as the
765
+ * main loop, so the forked request prefix matches exactly and the prompt
766
+ * cache is reused. A `canUseTool` gate decides whether each tool call
767
+ * executes locally (with a stripped context) or is denied and fed back to
768
+ * the model for another turn. Returns undefined content when the model never
769
+ * produces text; the caller treats that as a failure.
755
770
  */
756
- private async runCompactFork(
771
+ private async runForkLoop(
757
772
  historyMessages: ChatCompletionMessageParam[],
758
- compactPrompt: string,
773
+ prompt: string,
774
+ options: {
775
+ maxTurns: number;
776
+ /** Gate deciding which tool calls execute locally. When undefined, every tool call is denied. */
777
+ canUseTool?: (name: string, args: Record<string, unknown>) => boolean;
778
+ /** Message fed back to the model when a tool call is denied. */
779
+ deniedToolMessage?: string;
780
+ },
759
781
  abortSignal?: AbortSignal,
760
- ): Promise<{
761
- content?: string;
762
- usage?: {
763
- prompt_tokens: number;
764
- completion_tokens: number;
765
- total_tokens: number;
766
- };
767
- }> {
768
- const MAX_FORK_TURNS = 3;
782
+ ): Promise<ForkLoopResult> {
769
783
  const modelConfig = this.getModelConfig();
770
784
  const gatewayConfig = this.getGatewayConfig();
771
785
  const sessionId = this.messageManager.getSessionId();
@@ -783,21 +797,22 @@ export class AIManager {
783
797
  });
784
798
  }
785
799
 
786
- forkMessages.push({ role: "user", content: compactPrompt });
800
+ forkMessages.push({ role: "user", content: prompt });
787
801
 
788
802
  const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
789
803
  const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
790
804
 
791
- let totalUsage:
792
- | {
793
- prompt_tokens: number;
794
- completion_tokens: number;
795
- total_tokens: number;
796
- }
797
- | undefined;
805
+ // Fresh read-state map so Read/Edit state built up inside the fork never
806
+ // leaks into the main session's dedup and staleness tracking.
807
+ const forkReadFileState = new Map<
808
+ string,
809
+ { mtime: number; hash: string; offset?: number; limit?: number }
810
+ >();
811
+
812
+ let totalUsage: ForkLoopResult["usage"];
798
813
  let content: string | undefined;
799
814
 
800
- for (let turn = 0; turn < MAX_FORK_TURNS; turn++) {
815
+ for (let turn = 0; turn < options.maxTurns; turn++) {
801
816
  const result = await aiService.callAgent({
802
817
  gatewayConfig,
803
818
  modelConfig,
@@ -810,7 +825,7 @@ export class AIManager {
810
825
  toolChoice: this.toolChoiceOverride,
811
826
  // Stream so a slow reasoning model emits first bytes before the
812
827
  // gateway's idle timeout fires (non-streaming waits for the full
813
- // summary, which exceeds the timeout on large contexts).
828
+ // response, which exceeds the timeout on large contexts).
814
829
  stream: true,
815
830
  });
816
831
 
@@ -832,31 +847,329 @@ export class AIManager {
832
847
  }
833
848
 
834
849
  if (result.tool_calls && result.tool_calls.length > 0) {
835
- // Deny all tool calls locally and feed the rejections back so the
836
- // model gets another turn to produce the summary text.
850
+ const functionCalls = result.tool_calls.filter(
851
+ (tc) => tc.type === "function",
852
+ );
853
+ if (functionCalls.length === 0) break;
854
+
837
855
  forkMessages.push({
838
856
  role: "assistant",
839
857
  content: result.content ?? null,
840
- tool_calls: result.tool_calls,
858
+ tool_calls: functionCalls,
841
859
  });
842
- for (const toolCall of result.tool_calls) {
860
+
861
+ for (const toolCall of functionCalls) {
862
+ const name = toolCall.function?.name || "";
863
+ const args = this.parseForkToolArgs(toolCall.function?.arguments);
864
+ let toolContent: string;
865
+ if (options.canUseTool && options.canUseTool(name, args)) {
866
+ toolContent = await this.executeForkTool(
867
+ name,
868
+ args,
869
+ workdir,
870
+ sessionId,
871
+ abortSignal,
872
+ forkReadFileState,
873
+ );
874
+ } else {
875
+ toolContent =
876
+ options.deniedToolMessage ??
877
+ "Tool use is not allowed in this context";
878
+ }
843
879
  forkMessages.push({
844
880
  role: "tool",
845
881
  tool_call_id: toolCall.id,
846
- content: "Tool use is not allowed during compaction",
882
+ content: toolContent,
847
883
  });
848
884
  }
849
885
  continue;
850
886
  }
851
887
 
852
888
  // Neither text nor tool calls: retrying the identical request is
853
- // pointless, bail out and let the caller fail the compaction.
889
+ // pointless, bail out and let the caller fail.
854
890
  break;
855
891
  }
856
892
 
857
893
  return { content, usage: totalUsage };
858
894
  }
859
895
 
896
+ /**
897
+ * Fork-path compaction: deny all tool calls locally (the model is told to
898
+ * summarize, not act) and feed the rejections back for another turn.
899
+ */
900
+ private async runCompactFork(
901
+ historyMessages: ChatCompletionMessageParam[],
902
+ compactPrompt: string,
903
+ abortSignal?: AbortSignal,
904
+ ): Promise<ForkLoopResult> {
905
+ return this.runForkLoop(
906
+ historyMessages,
907
+ compactPrompt,
908
+ {
909
+ maxTurns: MAX_FORK_TURNS,
910
+ deniedToolMessage: "Tool use is not allowed during compaction",
911
+ },
912
+ abortSignal,
913
+ );
914
+ }
915
+
916
+ /**
917
+ * Auto-memory extraction via the perfect fork: the extraction prompt is run
918
+ * against the same request prefix as the main conversation (same system
919
+ * prompt, tools, model, and message history) so the prompt cache is reused.
920
+ * Gate-approved tools execute locally in a stripped context; everything else
921
+ * is denied. Usage is reported with operation_type "agent" so extraction
922
+ * token costs stay visible in session accounting.
923
+ */
924
+ public async runAutoMemoryFork(
925
+ messages: Message[],
926
+ prompt: string,
927
+ options: {
928
+ canUseTool: (name: string, args: Record<string, unknown>) => boolean;
929
+ deniedToolMessage?: string;
930
+ maxTurns?: number;
931
+ },
932
+ abortSignal?: AbortSignal,
933
+ ): Promise<ForkLoopResult> {
934
+ const modelConfig = this.getModelConfig();
935
+ const historyMessages = convertMessagesForAPI(messages, {
936
+ supportsVision: supportsVision(modelConfig.capabilities),
937
+ });
938
+ // Give the fork a real signal even when the caller has none, so tools
939
+ // (e.g. Bash's foreground path) always receive a well-formed context.
940
+ const signal = abortSignal ?? new AbortController().signal;
941
+ const result = await this.runForkLoop(
942
+ historyMessages,
943
+ prompt,
944
+ {
945
+ maxTurns: options.maxTurns ?? MAX_AUTO_MEMORY_FORK_TURNS,
946
+ canUseTool: options.canUseTool,
947
+ deniedToolMessage: options.deniedToolMessage,
948
+ },
949
+ signal,
950
+ );
951
+
952
+ if (result.usage && this.callbacks?.onUsageAdded) {
953
+ this.callbacks.onUsageAdded({
954
+ ...result.usage,
955
+ model: modelConfig.model,
956
+ operation_type: "agent",
957
+ });
958
+ }
959
+ return result;
960
+ }
961
+
962
+ /**
963
+ * Parse a fork tool call's JSON arguments, recovering truncated JSON the
964
+ * same way the main loop does. Unparseable arguments fall back to `{}` and
965
+ * are rejected by the gate or the tool's own parameter validation.
966
+ */
967
+ private parseForkToolArgs(
968
+ argsString: string | undefined,
969
+ ): Record<string, unknown> {
970
+ if (!argsString?.trim()) return {};
971
+ try {
972
+ return JSON.parse(argsString) as Record<string, unknown>;
973
+ } catch {
974
+ try {
975
+ return JSON.parse(recoverTruncatedJson(argsString)) as Record<
976
+ string,
977
+ unknown
978
+ >;
979
+ } catch {
980
+ return {};
981
+ }
982
+ }
983
+ }
984
+
985
+ /**
986
+ * Execute a single tool call inside a fork with a stripped context: no
987
+ * permission manager (never prompts the user), no message manager (no
988
+ * conditional-rule triggering), no messageId (no file-history snapshots),
989
+ * and no background task manager (commands run in the foreground). Only
990
+ * gate-approved tool names reach this path.
991
+ */
992
+ private async executeForkTool(
993
+ name: string,
994
+ args: Record<string, unknown>,
995
+ workdir: string,
996
+ sessionId: string | undefined,
997
+ abortSignal: AbortSignal | undefined,
998
+ readFileState: ToolContext["readFileState"],
999
+ ): Promise<string> {
1000
+ const plugin = this.toolManager.getTools().find((t) => t.name === name);
1001
+ if (!plugin) {
1002
+ return `Tool '${name}' not found`;
1003
+ }
1004
+ const context: ToolContext = {
1005
+ abortSignal,
1006
+ workdir,
1007
+ originalWorkdir: this.originalWorkdir,
1008
+ sessionId,
1009
+ taskManager: this.taskManager,
1010
+ readFileState,
1011
+ onShortResultUpdate: () => {},
1012
+ onResultUpdate: () => {},
1013
+ onCwdChange: () => {},
1014
+ };
1015
+ try {
1016
+ const result = await plugin.execute(args, context);
1017
+ if (result.content) return result.content;
1018
+ if (result.error) return `Error: ${result.error}`;
1019
+ return "";
1020
+ } catch (error) {
1021
+ const message = error instanceof Error ? error.message : String(error);
1022
+ logger?.error(`Fork tool execution failed for ${name}:`, error);
1023
+ return `Tool execution failed: ${message}`;
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Fork-path side question ("/btw"): run a single-turn fork of the
1029
+ * conversation using the same system prompt, tools, model, and generation
1030
+ * params as the main loop, so the forked request prefix matches exactly and
1031
+ * the prompt cache is reused. The in-progress assistant message (if any) is
1032
+ * stripped so the fork starts from the last completed request prefix. Tools
1033
+ * are never executed — the wrapped question instructs the model to answer
1034
+ * directly; an attempted tool call is surfaced as an error string.
1035
+ */
1036
+ async runBtwFork(
1037
+ question: string,
1038
+ abortSignal?: AbortSignal,
1039
+ onContent?: (content: string) => void,
1040
+ onReasoning?: (content: string) => void,
1041
+ ): Promise<{ content?: string; error?: string }> {
1042
+ const modelConfig = this.getModelConfig();
1043
+ const gatewayConfig = this.getGatewayConfig();
1044
+ const sessionId = this.messageManager.getSessionId();
1045
+ const workdir = this.getWorkdir();
1046
+
1047
+ const rawMessages = this.messageManager.getMessages();
1048
+
1049
+ // Strip the in-progress assistant message (a block still in "streaming"
1050
+ // stage) so the fork's request prefix matches the last completed
1051
+ // main-loop request and the prompt cache is reused.
1052
+ const lastMessage = rawMessages[rawMessages.length - 1];
1053
+ const hasInProgressMessage =
1054
+ lastMessage?.role === "assistant" &&
1055
+ lastMessage.blocks.some(
1056
+ (b) => "stage" in b && (b as { stage?: string }).stage === "streaming",
1057
+ );
1058
+
1059
+ const forkMessages: ChatCompletionMessageParam[] = convertMessagesForAPI(
1060
+ hasInProgressMessage ? rawMessages.slice(0, -1) : rawMessages,
1061
+ { supportsVision: supportsVision(modelConfig.capabilities) },
1062
+ );
1063
+
1064
+ // Mirror the main loop's memory injection so the request prefix matches.
1065
+ const { prependContent } =
1066
+ await this.messageManager.getMemoryForInjection();
1067
+ if (prependContent.trim()) {
1068
+ forkMessages.unshift({
1069
+ role: "user",
1070
+ content: wrapInSystemReminder(prependContent),
1071
+ });
1072
+ }
1073
+
1074
+ // Wrap the question with the side-question instructions (verbatim
1075
+ // Claude Code sideQuestion.ts) so the model answers directly.
1076
+ const wrappedQuestion = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.
1077
+
1078
+ IMPORTANT CONTEXT:
1079
+ - You are a separate, lightweight agent spawned to answer this one question
1080
+ - The main agent is NOT interrupted - it continues working independently in the background
1081
+ - You share the conversation context but are a completely separate instance
1082
+ - Do NOT reference being interrupted or what you were "previously doing" - that framing is incorrect
1083
+
1084
+ CRITICAL CONSTRAINTS:
1085
+ - You have NO tools available - you cannot read files, run commands, search, or take any actions
1086
+ - This is a one-off response - there will be no follow-up turns
1087
+ - You can ONLY provide information based on what you already know from the conversation context
1088
+ - NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
1089
+ - If you don't know the answer, say so - do not offer to look it up or investigate
1090
+
1091
+ Simply answer the question with the information you have.</system-reminder>
1092
+
1093
+ ${question}`;
1094
+ forkMessages.push({ role: "user", content: wrappedQuestion });
1095
+
1096
+ const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
1097
+ const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
1098
+
1099
+ try {
1100
+ // Surface partial output to the caller (e.g. the /btw overlay's
1101
+ // streaming display) as it arrives. Reasoning chunks from thinking
1102
+ // models stream through a separate channel when the caller supplies
1103
+ // one (webview hosts distinguish thinking from content so the panel
1104
+ // can drop thinking text once content starts); otherwise they fall
1105
+ // back to the content channel (CLI overlay mixes both).
1106
+ const streamToOverlay = (text: string) => {
1107
+ if (text.trim()) {
1108
+ onContent?.(text);
1109
+ }
1110
+ };
1111
+ const streamReasoning = (text: string) => {
1112
+ if (text.trim()) {
1113
+ if (onReasoning) {
1114
+ onReasoning(text);
1115
+ } else {
1116
+ onContent?.(text);
1117
+ }
1118
+ }
1119
+ };
1120
+ const result = await aiService.callAgent({
1121
+ gatewayConfig,
1122
+ modelConfig,
1123
+ messages: forkMessages,
1124
+ sessionId,
1125
+ abortSignal,
1126
+ workdir,
1127
+ tools: toolsConfig,
1128
+ systemPrompt,
1129
+ toolChoice: this.toolChoiceOverride,
1130
+ // Stream so a slow reasoning model emits first bytes before the
1131
+ // gateway's idle timeout fires (same rationale as runCompactFork).
1132
+ stream: true,
1133
+ onContentUpdate: streamToOverlay,
1134
+ onReasoningUpdate: streamReasoning,
1135
+ });
1136
+
1137
+ if (result.content?.trim()) {
1138
+ return { content: result.content };
1139
+ }
1140
+
1141
+ // A thinking model may emit only reasoning content (e.g. the stream
1142
+ // is truncated before the final answer); surface that instead of
1143
+ // falling through to "No response received".
1144
+ if (result.reasoning_content?.trim()) {
1145
+ return { content: result.reasoning_content };
1146
+ }
1147
+
1148
+ if (result.tool_calls && result.tool_calls.length > 0) {
1149
+ const firstFunctionCall = result.tool_calls.find(
1150
+ (call) => call.type === "function",
1151
+ );
1152
+ const toolName = firstFunctionCall?.function?.name ?? "a tool";
1153
+ return {
1154
+ error: `(The model tried to call ${toolName} instead of answering directly. Try rephrasing or ask in the main conversation.)`,
1155
+ };
1156
+ }
1157
+
1158
+ // Neither text nor tool calls.
1159
+ return { error: "No response received" };
1160
+ } catch (error) {
1161
+ // aiService.callAgent converts AbortError into a plain Error, so the
1162
+ // abort is detected via the signal itself; rethrow so the UI can
1163
+ // silently dismiss instead of showing an error.
1164
+ if (abortSignal?.aborted) {
1165
+ throw error;
1166
+ }
1167
+ return {
1168
+ error: `(API error: ${error instanceof Error ? error.message : String(error)})`,
1169
+ };
1170
+ }
1171
+ }
1172
+
860
1173
  /**
861
1174
  * Build post-compact context restoration content.
862
1175
  * Restores file reads, working directory, plan mode, skills, and background tasks.
@@ -1150,6 +1463,12 @@ export class AIManager {
1150
1463
  systemPrompt: mainSystemPrompt, // Pass custom system prompt
1151
1464
  maxTokens: maxTokens, // Pass max tokens override
1152
1465
  toolChoice: this.toolChoiceOverride, // Pass tool_choice override
1466
+ // Fast-model subagents send disable-thinking params only when
1467
+ // explicitly configured (never in the agent loop).
1468
+ disableThinkingOptions:
1469
+ this.modelOverride === "fastModel"
1470
+ ? this.getModelConfig().disableThinkingOptions
1471
+ : undefined,
1153
1472
  };
1154
1473
 
1155
1474
  // Prepend: AGENTS.md + user memory + unconditional rules as system-reminder
@@ -1578,77 +1897,19 @@ export class AIManager {
1578
1897
  }
1579
1898
  }
1580
1899
 
1581
- // Goal evaluation — supersedes Stop hooks when active
1582
- const goalManager = this.container.has("GoalManager")
1583
- ? this.container.get<import("./goalManager.js").GoalManager>(
1584
- "GoalManager",
1585
- )
1586
- : undefined;
1587
-
1588
- let goalContinuing = false;
1589
-
1590
- if (goalManager?.isGoalActive() && !this.subagentType) {
1591
- // 1. Increment turn count and check circuit breakers
1592
- goalManager.incrementTurnCount();
1593
- const circuitBreaker = goalManager.checkCircuitBreakers();
1594
-
1595
- if (circuitBreaker) {
1596
- goalManager.clearGoal();
1597
- logger?.info(`[Goal] ${circuitBreaker}`);
1598
- this.messageManager.addUserMessage({
1599
- content: `<system-reminder>${circuitBreaker}</system-reminder>`,
1600
- isMeta: true,
1601
- });
1602
- // Fall through to normal Stop hooks on the final turn
1603
- } else {
1604
- // 2. Evaluate goal
1605
- const evaluation = await goalManager.evaluateGoal(
1606
- abortController.signal,
1607
- );
1900
+ // Execute Stop hooks
1901
+ const shouldContinue = await this.executeStopHooks();
1608
1902
 
1609
- if (evaluation.isMet) {
1610
- goalManager.clearGoal();
1611
- logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
1612
- this.messageManager.addUserMessage({
1613
- content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
1614
- isMeta: true,
1615
- });
1616
- // Fall through to normal Stop hooks on the final turn
1617
- } else {
1618
- const goal = goalManager.getGoal()!;
1619
- goal.lastReason = evaluation.reason;
1620
- logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
1621
- this.messageManager.addUserMessage({
1622
- content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
1623
- isMeta: true,
1624
- });
1625
- // Keep loading state active to prevent UI flicker
1626
- this.setIsLoading(true);
1627
- goalContinuing = true;
1628
- // Restart outer loop to continue goal pursuit
1629
- shouldRestart = true;
1630
- turnOffset = 0;
1631
- }
1632
- }
1633
- }
1634
-
1635
- // Skip Stop hooks when goal evaluator is continuing the conversation
1636
- if (goalContinuing) {
1637
- // Goal evaluator supersedes Stop hooks
1638
- } else {
1639
- const shouldContinue = await this.executeStopHooks();
1640
-
1641
- // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
1642
- // restart the AI conversation cycle
1643
- if (shouldContinue) {
1644
- logger?.info(
1645
- `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
1646
- );
1903
+ // If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
1904
+ // restart the AI conversation cycle
1905
+ if (shouldContinue) {
1906
+ logger?.info(
1907
+ `${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
1908
+ );
1647
1909
 
1648
- // Restart the conversation to let AI fix the issues
1649
- shouldRestart = true;
1650
- turnOffset = 0;
1651
- }
1910
+ // Restart the conversation to let AI fix the issues
1911
+ shouldRestart = true;
1912
+ turnOffset = 0;
1652
1913
  }
1653
1914
  }
1654
1915
 
@@ -1934,6 +2195,8 @@ export class AIManager {
1934
2195
  id: toolId,
1935
2196
  shortResult,
1936
2197
  stage: "running",
2198
+ compactParams,
2199
+ name: toolName,
1937
2200
  });
1938
2201
  },
1939
2202
  onResultUpdate: (result: string) => {
@@ -1941,6 +2204,8 @@ export class AIManager {
1941
2204
  id: toolId,
1942
2205
  result,
1943
2206
  stage: "running",
2207
+ compactParams,
2208
+ name: toolName,
1944
2209
  });
1945
2210
  },
1946
2211
  onCwdChange: async (newCwd: string) => {