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
@@ -25,10 +25,8 @@ import * as path from "path";
25
25
 
26
26
  import {
27
27
  WEB_CONTENT_SYSTEM_PROMPT,
28
- BTW_SYSTEM_PROMPT,
29
28
  type SystemPromptBlock,
30
29
  } from "../prompts/index.js";
31
- import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
32
30
 
33
31
  /**
34
32
  * Interface for debug data saved during 400 errors
@@ -148,6 +146,18 @@ function getModelConfig(
148
146
  return config;
149
147
  }
150
148
 
149
+ /**
150
+ * Effective disable-thinking params for a model config. No default: these
151
+ * params are only sent when the user explicitly configures
152
+ * `models[X].disableThinkingOptions` (an empty object clears them), so a
153
+ * gateway that doesn't understand the params is never hit with them.
154
+ */
155
+ function effectiveDisableThinkingOptions(
156
+ modelConfig: ModelConfig,
157
+ ): Record<string, unknown> | undefined {
158
+ return modelConfig.disableThinkingOptions;
159
+ }
160
+
151
161
  export interface CallAgentOptions {
152
162
  // Resolved configuration
153
163
  gatewayConfig: GatewayConfig;
@@ -184,6 +194,10 @@ export interface CallAgentOptions {
184
194
  stage?: "start" | "streaming" | "running" | "end";
185
195
  }) => void;
186
196
  onReasoningUpdate?: (content: string) => void;
197
+
198
+ // Disable-thinking params for fast-model subagent calls (merged into the
199
+ // request; never used in the agent loop).
200
+ disableThinkingOptions?: Record<string, unknown>;
187
201
  }
188
202
 
189
203
  export interface CallAgentResult {
@@ -238,6 +252,7 @@ export async function callAgent(
238
252
  onContentUpdate,
239
253
  onToolUpdate,
240
254
  onReasoningUpdate,
255
+ disableThinkingOptions,
241
256
  } = options;
242
257
 
243
258
  // Validate model config at call time
@@ -321,6 +336,7 @@ export async function callAgent(
321
336
  const openaiModelConfig = getModelConfig(model || modelConfig.model, {
322
337
  max_tokens: resolvedMaxTokens,
323
338
  ...(modelConfig.options || {}),
339
+ ...(disableThinkingOptions ?? {}),
324
340
  });
325
341
 
326
342
  // Determine if streaming is needed
@@ -842,10 +858,17 @@ export async function processWebContent(
842
858
  ? modelConfig.fastModelOptions || {}
843
859
  : modelConfig.options || {};
844
860
 
861
+ // Disable-thinking params only apply to the fast-model override path;
862
+ // the agent-model path is untouched.
863
+ const disableThinking = options.model
864
+ ? effectiveDisableThinkingOptions(modelConfig)
865
+ : undefined;
866
+
845
867
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
846
868
  temperature: 0.1,
847
869
  max_tokens: 4096,
848
870
  ...activeExtraParams,
871
+ ...(disableThinking || {}),
849
872
  });
850
873
 
851
874
  try {
@@ -894,204 +917,3 @@ export async function processWebContent(
894
917
  }
895
918
  }
896
919
 
897
- export interface BtwOptions {
898
- // Resolved configuration
899
- gatewayConfig: GatewayConfig;
900
- modelConfig: ModelConfig;
901
-
902
- // Parameters
903
- messages: ChatCompletionMessageParam[];
904
- question: string;
905
- abortSignal?: AbortSignal;
906
- model?: string;
907
- }
908
-
909
- export interface BtwResult {
910
- content: string;
911
- usage?: {
912
- prompt_tokens: number;
913
- completion_tokens: number;
914
- total_tokens: number;
915
- };
916
- }
917
-
918
- export async function btw(options: BtwOptions): Promise<BtwResult> {
919
- const { gatewayConfig, modelConfig, messages, question, abortSignal } =
920
- options;
921
-
922
- // Validate model config at call time
923
- validateModelConfig(modelConfig);
924
-
925
- // Apply global 1 QPS rate limit
926
- if (
927
- process.env.NODE_ENV !== "test" ||
928
- modelConfig.model === "rate-limit-test"
929
- ) {
930
- await acquireSlot(abortSignal);
931
- }
932
-
933
- // Create OpenAI client with injected configuration
934
- const openai = new OpenAIClient({
935
- apiKey: gatewayConfig.apiKey,
936
- baseURL: gatewayConfig.baseURL,
937
- defaultHeaders: gatewayConfig.defaultHeaders,
938
- fetchOptions: gatewayConfig.fetchOptions,
939
- fetch: gatewayConfig.fetch,
940
- });
941
-
942
- const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
943
- temperature: 0.1,
944
- max_tokens: 4096,
945
- ...(modelConfig.options || {}),
946
- });
947
-
948
- try {
949
- const response = await openai.chat.completions.create(
950
- {
951
- ...openaiModelConfig,
952
- messages: [
953
- {
954
- role: "system",
955
- content: BTW_SYSTEM_PROMPT,
956
- },
957
- ...messages,
958
- {
959
- role: "user",
960
- content: question,
961
- },
962
- ],
963
- },
964
- {
965
- signal: abortSignal,
966
- },
967
- );
968
-
969
- const result = response.choices[0]?.message?.content?.trim();
970
- if (!result) {
971
- throw new Error(
972
- "Failed to process side question: Empty response from AI",
973
- );
974
- }
975
- const usage = response.usage
976
- ? {
977
- prompt_tokens: response.usage.prompt_tokens,
978
- completion_tokens: response.usage.completion_tokens,
979
- total_tokens: response.usage.total_tokens,
980
- }
981
- : undefined;
982
-
983
- return {
984
- content: result,
985
- usage,
986
- };
987
- } catch (error) {
988
- if ((error as Error).name === "AbortError") {
989
- logger.info("Side question request was aborted");
990
- throw new Error("Side question request was aborted");
991
- }
992
- logger.error("Failed to process side question:", error);
993
- throw error;
994
- }
995
- }
996
-
997
- export interface EvaluateGoalOptions {
998
- gatewayConfig: GatewayConfig;
999
- modelConfig: ModelConfig;
1000
- model: string;
1001
- goalCondition: string;
1002
- messages: ChatCompletionMessageParam[];
1003
- abortSignal?: AbortSignal;
1004
- }
1005
-
1006
- export interface EvaluateGoalResult {
1007
- content: string;
1008
- usage?: {
1009
- prompt_tokens: number;
1010
- completion_tokens: number;
1011
- total_tokens: number;
1012
- };
1013
- }
1014
-
1015
- export async function evaluateGoal(
1016
- options: EvaluateGoalOptions,
1017
- ): Promise<EvaluateGoalResult> {
1018
- const {
1019
- gatewayConfig,
1020
- modelConfig,
1021
- model,
1022
- goalCondition,
1023
- messages,
1024
- abortSignal,
1025
- } = options;
1026
-
1027
- // Create OpenAI client with injected configuration (no rate limiter — bypasses 1 QPS)
1028
- const openai = new OpenAIClient({
1029
- apiKey: gatewayConfig.apiKey,
1030
- baseURL: gatewayConfig.baseURL,
1031
- defaultHeaders: gatewayConfig.defaultHeaders,
1032
- fetchOptions: gatewayConfig.fetchOptions,
1033
- fetch: gatewayConfig.fetch,
1034
- });
1035
-
1036
- const openaiModelConfig = getModelConfig(model, {
1037
- temperature: 0,
1038
- max_tokens: 200,
1039
- ...(modelConfig.fastModelOptions || {}),
1040
- });
1041
-
1042
- // Strip images from messages to reduce token usage (same as compact)
1043
- const cleanedMessages = messages.map((msg) => {
1044
- if (Array.isArray(msg.content)) {
1045
- const textParts = msg.content.filter(
1046
- (part) => part.type === "text",
1047
- ) as import("openai/resources.js").ChatCompletionContentPartText[];
1048
- const text = textParts.map((p) => p.text).join("\n");
1049
- return { ...msg, content: text || "(empty message)" };
1050
- }
1051
- return msg;
1052
- });
1053
-
1054
- try {
1055
- const response = await openai.chat.completions.create(
1056
- {
1057
- ...openaiModelConfig,
1058
- messages: [
1059
- {
1060
- role: "system",
1061
- content: GOAL_EVALUATION_SYSTEM_PROMPT,
1062
- },
1063
- ...cleanedMessages,
1064
- {
1065
- role: "user",
1066
- content: `Goal condition: ${goalCondition}\n\nHas this goal been achieved based on the conversation above?`,
1067
- },
1068
- ],
1069
- },
1070
- {
1071
- signal: abortSignal,
1072
- },
1073
- );
1074
-
1075
- const result = response.choices[0]?.message?.content?.trim();
1076
- if (!result) {
1077
- throw new Error("Goal evaluation returned empty response");
1078
- }
1079
-
1080
- const usage = response.usage
1081
- ? {
1082
- prompt_tokens: response.usage.prompt_tokens,
1083
- completion_tokens: response.usage.completion_tokens,
1084
- total_tokens: response.usage.total_tokens,
1085
- }
1086
- : undefined;
1087
-
1088
- return { content: result, usage };
1089
- } catch (error) {
1090
- if ((error as Error).name === "AbortError") {
1091
- logger.info("Goal evaluation was aborted");
1092
- throw new Error("Goal evaluation was aborted");
1093
- }
1094
- logger.error("Goal evaluation failed:", error);
1095
- throw error;
1096
- }
1097
- }
@@ -2,14 +2,32 @@ import * as path from "node:path";
2
2
  import * as fs from "node:fs/promises";
3
3
  import { Container } from "../utils/container.js";
4
4
  import { MessageManager } from "../managers/messageManager.js";
5
- import { ForkedAgentManager } from "../managers/forkedAgentManager.js";
5
+ import { AIManager } from "../managers/aiManager.js";
6
6
  import { MemoryService } from "./memory.js";
7
7
  import { ConfigurationService } from "./configurationService.js";
8
8
  import { logger } from "../utils/globalLogger.js";
9
9
  import { isPathInside } from "../utils/pathSafety.js";
10
10
  import { buildAutoMemoryExtractionPrompt } from "../prompts/autoMemoryExtraction.js";
11
+ import {
12
+ READ_ONLY_COMMANDS,
13
+ splitBashCommand,
14
+ hasWriteRedirections,
15
+ hasCommandSubstitution,
16
+ hasProcessSubstitution,
17
+ hasSedInPlace,
18
+ isDangerousFind,
19
+ } from "../utils/bashParser.js";
11
20
  import type { Message } from "../types/index.js";
12
21
 
22
+ /**
23
+ * Message fed back to the extraction fork when the model requests a tool
24
+ * outside the allowed set (Bash rm, MCP tools, Agent, out-of-dir writes...).
25
+ */
26
+ const DENIED_TOOL_MESSAGE =
27
+ "This tool call was denied during auto-memory extraction. Available tools: " +
28
+ "Read, Grep, Glob, read-only Bash commands, and Write/Edit inside the " +
29
+ "memory directory only.";
30
+
13
31
  /**
14
32
  * Service responsible for managing the auto-memory background agent lifecycle.
15
33
  * Extracts and updates persistent project-level memory from conversation history.
@@ -17,6 +35,8 @@ import type { Message } from "../types/index.js";
17
35
  export class AutoMemoryService {
18
36
  private lastMemoryMessageId: string | null = null;
19
37
  private turnsSinceLastExtraction: number = 0;
38
+ private extractionInProgress: boolean = false;
39
+ private pendingExtraction: Promise<void> | null = null;
20
40
 
21
41
  constructor(private container: Container) {}
22
42
 
@@ -24,8 +44,8 @@ export class AutoMemoryService {
24
44
  return this.container.get<MessageManager>("MessageManager")!;
25
45
  }
26
46
 
27
- private get forkedAgentManager(): ForkedAgentManager {
28
- return this.container.get<ForkedAgentManager>("ForkedAgentManager")!;
47
+ private get aiManager(): AIManager {
48
+ return this.container.get<AIManager>("AIManager")!;
29
49
  }
30
50
 
31
51
  private get memoryService(): MemoryService {
@@ -71,7 +91,14 @@ export class AutoMemoryService {
71
91
  (m) =>
72
92
  m.role === "assistant" &&
73
93
  m.blocks.some((b) => {
74
- if (b.type === "tool" && (b.name === "Write" || b.name === "Edit")) {
94
+ if (
95
+ b.type === "tool" &&
96
+ (b.name === "Write" || b.name === "Edit") &&
97
+ // Only a successful manual write counts as a manual update. A
98
+ // denied/failed write didn't touch memory, so the extraction fork
99
+ // must still run or the information is lost.
100
+ b.success !== false
101
+ ) {
75
102
  try {
76
103
  const params = b.parameters ? JSON.parse(b.parameters) : null;
77
104
  const filePath = params?.file_path || params?.path;
@@ -98,22 +125,61 @@ export class AutoMemoryService {
98
125
  return;
99
126
  }
100
127
 
101
- // 3. Trigger background extraction using a forked subagent
102
- try {
103
- await this.runExtraction(workdir, messages);
104
- this.lastMemoryMessageId = messages[messages.length - 1].id || null;
105
- this.turnsSinceLastExtraction = 0;
106
- } catch (error) {
107
- logger.error("Auto-memory extraction failed to trigger:", error);
128
+ // 3. Concurrency guard: if an extraction is already in flight, skip this
129
+ // turn. turnsSinceLastExtraction is intentionally NOT reset so the next
130
+ // eligible turn retriggers the extraction.
131
+ if (this.extractionInProgress) {
132
+ logger.debug(
133
+ "Skipping auto-memory extraction: another extraction is still in progress.",
134
+ );
135
+ return;
108
136
  }
137
+
138
+ // 4. Trigger the perfect-fork extraction fire-and-forget. The message
139
+ // snapshot and new-message count are computed now; lastMemoryMessageId
140
+ // advances at trigger time so the next extraction starts from a later
141
+ // window even while this one runs.
142
+ const lastExtractedIndex = this.lastMemoryMessageId
143
+ ? messages.findIndex((m) => m.id === this.lastMemoryMessageId)
144
+ : -1;
145
+ const newMessageCount =
146
+ lastExtractedIndex === -1
147
+ ? messages.length
148
+ : messages.length - 1 - lastExtractedIndex;
149
+
150
+ this.turnsSinceLastExtraction = 0;
151
+ this.lastMemoryMessageId = messages[messages.length - 1].id || null;
152
+ this.extractionInProgress = true;
153
+ const extraction = this.runExtraction(workdir, messages, newMessageCount)
154
+ .catch((error) => {
155
+ logger.error("Auto-memory extraction failed:", error);
156
+ })
157
+ .finally(() => {
158
+ this.extractionInProgress = false;
159
+ this.pendingExtraction = null;
160
+ });
161
+ this.pendingExtraction = extraction;
162
+ }
163
+
164
+ /**
165
+ * Wait for an in-flight extraction to settle. Called from Agent.dispose so
166
+ * the process doesn't exit while the extraction fork is mid-flight.
167
+ */
168
+ async drain(): Promise<void> {
169
+ await this.pendingExtraction;
109
170
  }
110
171
 
111
172
  /**
112
- * Initialize and execute the background extraction subagent.
173
+ * Initialize and execute the extraction in a perfect fork: same system
174
+ * prompt, tools, model, and message prefix as the main conversation, so the
175
+ * prompt cache is reused. A tool gate confines the fork to read-only
176
+ * inspection and memory-directory writes. Runs in-process; callers treat it
177
+ * as fire-and-forget.
113
178
  */
114
179
  private async runExtraction(
115
180
  workdir: string,
116
181
  messages: Message[],
182
+ newMessageCount: number,
117
183
  ): Promise<void> {
118
184
  const memoryDir = this.memoryService.getAutoMemoryDirectory(workdir);
119
185
 
@@ -132,43 +198,83 @@ export class AutoMemoryService {
132
198
  // Ignore if directory doesn't exist yet
133
199
  }
134
200
 
135
- // Calculate how many new messages to analyze
136
- let newMessageCount = messages.length;
137
- if (this.lastMemoryMessageId) {
138
- const lastIndex = messages.findIndex(
139
- (m) => m.id === this.lastMemoryMessageId,
140
- );
141
- if (lastIndex !== -1) {
142
- newMessageCount = messages.length - 1 - lastIndex;
143
- }
144
- }
145
-
146
201
  const prompt = buildAutoMemoryExtractionPrompt(
147
202
  newMessageCount,
148
203
  existingMemoriesManifest,
149
204
  );
150
205
 
151
- // Execute the forked agent in background (fire-and-forget, decoupled from BackgroundTaskManager)
152
- await this.forkedAgentManager.forkAndExecute(
153
- "general-purpose",
206
+ await this.aiManager.runAutoMemoryFork(
154
207
  messages,
208
+ `${prompt}\n\nThe memory directory for this project is: ${memoryDir}`,
155
209
  {
156
- description: "Auto-memory extraction background agent",
157
- allowedTools: [
158
- "Read",
159
- "Glob",
160
- "Grep",
161
- `Write(${memoryDir}/**/*)`,
162
- `Edit(${memoryDir}/**/*)`,
163
- `Bash(rm ${memoryDir}/**/*)`,
164
- ],
165
- model: "fastModel", // Use fast model for background tasks to reduce latency and cost
166
- permissionModeOverride: "dontAsk", // Auto-deny out-of-scope writes without prompting user
167
210
  maxTurns: 5, // Limit turns to prevent verification rabbit-holes
211
+ canUseTool: (name, args) =>
212
+ this.isAllowedForkTool(name, args, memoryDir, workdir),
213
+ deniedToolMessage: DENIED_TOOL_MESSAGE,
168
214
  },
169
- `${prompt}\n\nThe memory directory for this project is: ${memoryDir}`,
170
215
  );
171
216
 
172
- logger.debug("Auto-memory extraction started in background.");
217
+ logger.debug("Auto-memory extraction completed.");
218
+ }
219
+
220
+ /**
221
+ * Tool gate for the extraction fork: Read/Grep/Glob are always allowed;
222
+ * Write/Edit only when the target path is inside the memory directory; Bash
223
+ * only for read-only commands (aligned with the permission manager's
224
+ * read-only bash classification). Everything else — Bash rm, MCP tools,
225
+ * Agent, out-of-dir writes — is denied.
226
+ */
227
+ private isAllowedForkTool(
228
+ name: string,
229
+ args: Record<string, unknown>,
230
+ memoryDir: string,
231
+ workdir: string,
232
+ ): boolean {
233
+ if (name === "Read" || name === "Grep" || name === "Glob") {
234
+ return true;
235
+ }
236
+
237
+ if (name === "Write" || name === "Edit") {
238
+ const filePath = args.file_path ?? args.path;
239
+ if (typeof filePath !== "string" || !filePath) return false;
240
+ const absolutePath = path.isAbsolute(filePath)
241
+ ? filePath
242
+ : path.resolve(workdir, filePath);
243
+ return isPathInside(absolutePath, memoryDir);
244
+ }
245
+
246
+ if (name === "Bash") {
247
+ const command = typeof args.command === "string" ? args.command : "";
248
+ return this.isReadOnlyBashCommand(command);
249
+ }
250
+
251
+ return false;
252
+ }
253
+
254
+ /**
255
+ * A bash command is read-only when every part is a READ_ONLY_COMMANDS entry
256
+ * without write redirections, command/process substitution, sed -i, or
257
+ * dangerous find flags. Mirrors PermissionManager.isAutoAllowedPart.
258
+ */
259
+ private isReadOnlyBashCommand(command: string): boolean {
260
+ if (!command.trim()) return false;
261
+ if (hasWriteRedirections(command)) return false;
262
+ if (hasCommandSubstitution(command)) return false;
263
+ if (hasProcessSubstitution(command)) return false;
264
+ if (hasSedInPlace(command)) return false;
265
+
266
+ const parts = splitBashCommand(command);
267
+ if (parts.length === 0) return false;
268
+
269
+ return parts.every((part) => {
270
+ const trimmed = part.trim();
271
+ if (!trimmed) return true;
272
+ const commandMatch = trimmed.match(/^(\w+)(\s+.*)?$/);
273
+ if (!commandMatch) return false;
274
+ const cmd = commandMatch[1];
275
+ if (!READ_ONLY_COMMANDS.includes(cmd)) return false;
276
+ if (cmd === "find" && isDangerousFind(part)) return false;
277
+ return true;
278
+ });
173
279
  }
174
280
  }
@@ -619,16 +619,31 @@ export class ConfigurationService {
619
619
  baseConfig.fastModelOptions = fastModelSource.options;
620
620
  }
621
621
 
622
+ // Resolve fast-model disable-thinking params from models[fastModel].disableThinkingOptions
623
+ const fastModelDisableThinking =
624
+ fastModelSource && fastModelSource.disableThinkingOptions
625
+ ? fastModelSource.disableThinkingOptions
626
+ : undefined;
627
+ if (fastModelDisableThinking) {
628
+ baseConfig.disableThinkingOptions = fastModelDisableThinking;
629
+ }
630
+
622
631
  // Merge model-specific settings from configuration
623
632
  const modelSpecificConfig =
624
633
  resolvedAgentModel &&
625
634
  this.currentConfiguration?.models?.[resolvedAgentModel];
626
635
 
627
636
  if (modelSpecificConfig) {
628
- return {
637
+ const resolved: ModelConfig = {
629
638
  ...baseConfig,
630
639
  ...modelSpecificConfig,
631
640
  };
641
+ // Re-apply after the spread so the agent model's own
642
+ // disableThinkingOptions cannot clobber the fast-model value.
643
+ if (fastModelDisableThinking) {
644
+ resolved.disableThinkingOptions = fastModelDisableThinking;
645
+ }
646
+ return resolved;
632
647
  }
633
648
 
634
649
  return baseConfig;
@@ -612,6 +612,74 @@ export async function cleanupEmptyProjectDirectories(): Promise<void> {
612
612
  }
613
613
  }
614
614
 
615
+ /**
616
+ * Clean up "ghost" session files that contain only meta messages
617
+ * (isMeta: true) — e.g. sessions where a SessionStart hook injected a
618
+ * system-reminder but no real user/assistant message was ever sent.
619
+ *
620
+ * Such sessions are no longer created thanks to lazy materialization in
621
+ * saveSession(); this one-time sweep removes files that predate that change
622
+ * so they stop showing up as "0 tokens / No content" entries in the resume
623
+ * list.
624
+ *
625
+ * @returns Promise that resolves to the number of files deleted
626
+ */
627
+ export async function cleanupMetaOnlySessions(): Promise<number> {
628
+ // Do not perform cleanup operations in test environment
629
+ if (process.env.NODE_ENV === "test") {
630
+ return 0;
631
+ }
632
+
633
+ let deletedCount = 0;
634
+ try {
635
+ const projectDirs = await fs.readdir(SESSION_DIR);
636
+
637
+ for (const projectDirName of projectDirs) {
638
+ const projectPath = join(SESSION_DIR, projectDirName);
639
+ try {
640
+ const stat = await fs.stat(projectPath);
641
+ if (!stat.isDirectory()) {
642
+ continue;
643
+ }
644
+
645
+ const files = await fs.readdir(projectPath);
646
+ for (const file of files) {
647
+ if (!file.endsWith(".jsonl")) {
648
+ continue;
649
+ }
650
+
651
+ const filePath = join(projectPath, file);
652
+ try {
653
+ // Fast path: a file whose last message is not meta contains a
654
+ // real message, so it can never be meta-only.
655
+ const jsonlHandler = new JsonlHandler();
656
+ const lastMessage = await jsonlHandler.getLastMessage(filePath);
657
+ if (!lastMessage?.isMeta) {
658
+ continue;
659
+ }
660
+
661
+ const messages = await jsonlHandler.read(filePath);
662
+ if (messages.length > 0 && messages.every((m) => m.isMeta)) {
663
+ await fs.unlink(filePath);
664
+ deletedCount++;
665
+ }
666
+ } catch {
667
+ // Skip corrupted or unreadable files
668
+ continue;
669
+ }
670
+ }
671
+ } catch {
672
+ // Skip directories we can't access
673
+ continue;
674
+ }
675
+ }
676
+ } catch {
677
+ // Ignore errors if base directory doesn't exist or can't be accessed
678
+ }
679
+
680
+ return deletedCount;
681
+ }
682
+
615
683
  /**
616
684
  * Check if a session exists in JSONL storage (new approach)
617
685
  *
@@ -119,10 +119,4 @@ export interface AgentCallbacks
119
119
  onCommandRunningChange?: (running: boolean) => void;
120
120
  onWorkdirChange?: (newCwd: string) => void;
121
121
  onQueuedMessagesChange?: (messages: QueuedMessage[]) => void;
122
- onGoalStateChange?: (
123
- active: boolean,
124
- condition?: string,
125
- elapsed?: string,
126
- ) => void;
127
- onGoalEvaluating?: (evaluating: boolean) => void;
128
122
  }
@@ -33,4 +33,11 @@ export interface ModelConfig {
33
33
  options?: Record<string, unknown>;
34
34
  /** Fast model generation params (resolved from models[fastModel].options) */
35
35
  fastModelOptions?: Record<string, unknown>;
36
+ /**
37
+ * Fast-model-only disable-thinking params passed through verbatim
38
+ * (e.g. `{ thinking: { type: "disabled" } }`). Applied only in fast-model
39
+ * scenarios (webFetch content processing, `model: fastModel` subagents),
40
+ * never in the agent loop. `{}` clears the default.
41
+ */
42
+ disableThinkingOptions?: Record<string, unknown>;
36
43
  }
package/src/types/core.ts CHANGED
@@ -25,7 +25,7 @@ export interface Usage {
25
25
  completion_tokens: number; // Tokens generated in completions
26
26
  total_tokens: number; // Sum of prompt + completion tokens
27
27
  model?: string; // Model used for the operation (e.g., "gpt-4", "gpt-3.5-turbo")
28
- operation_type?: "agent" | "compact" | "goal_evaluation"; // Type of operation that generated usage
28
+ operation_type?: "agent" | "compact"; // Type of operation that generated usage
29
29
 
30
30
  // Cache-related tokens (Claude top-level + OpenAI prompt_tokens_details)
31
31
  cache_read_input_tokens?: number; // Tokens read from cache (Claude) or cached_tokens (OpenAI prompt_tokens_details)