@tyvm/knowhow 0.0.143 → 0.0.145

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 (158) hide show
  1. package/bin/knowhow.js +22 -20
  2. package/package.json +1 -1
  3. package/scripts/migrate-usage-json.js +147 -0
  4. package/src/agents/base/base.ts +65 -10
  5. package/src/agents/tools/patch.ts +84 -2
  6. package/src/agents/tools/textSearch.ts +98 -3
  7. package/src/chat/modules/AgentModule.ts +129 -6
  8. package/src/chat/modules/SessionsModule.ts +6 -0
  9. package/src/chat/types.ts +14 -0
  10. package/src/chat.ts +11 -0
  11. package/src/cli.ts +2 -0
  12. package/src/clients/anthropic.ts +56 -1
  13. package/src/clients/contextLimits.ts +40 -0
  14. package/src/clients/gemini.ts +59 -5
  15. package/src/clients/http.ts +85 -4
  16. package/src/clients/index.ts +4 -0
  17. package/src/clients/openai.ts +179 -31
  18. package/src/clients/pricing/fireworks.ts +50 -1
  19. package/src/clients/pricing/index.ts +1 -1
  20. package/src/clients/pricing/models.ts +3 -1
  21. package/src/clients/pricing/openai.ts +2 -2
  22. package/src/clients/pricing/qwen.ts +80 -0
  23. package/src/clients/qwen.ts +16 -0
  24. package/src/clients/types.ts +69 -1
  25. package/src/clients/xai.ts +73 -4
  26. package/src/commands/agent.ts +20 -1
  27. package/src/commands/sandbox.ts +139 -114
  28. package/src/index.ts +9 -0
  29. package/src/processors/Base64ImageDetector.ts +46 -7
  30. package/src/processors/GemmaToolProcessor.ts +163 -0
  31. package/src/processors/index.ts +1 -0
  32. package/src/services/AgentSyncFs.ts +37 -0
  33. package/src/services/SessionManager.ts +14 -0
  34. package/src/services/Tools.ts +3 -0
  35. package/src/services/modules/computerUse.ts +216 -0
  36. package/src/services/modules/index.ts +47 -9
  37. package/src/services/modules/types.ts +23 -1
  38. package/src/types.ts +3 -0
  39. package/tests/clients/contextLimits.test.ts +4 -0
  40. package/tests/clients/openai.images.test.ts +105 -0
  41. package/tests/clients/pricing.test.ts +6 -0
  42. package/tests/compressor/usageBase64.json +159 -0
  43. package/tests/compressor/usageBase64.test.ts +175 -0
  44. package/tests/patching/fixtures/transcript-mangles/agent-sync-fs-nested-catch.json +9 -0
  45. package/tests/patching/fixtures/transcript-mangles/automation-functions-crossed.json +9 -0
  46. package/tests/patching/fixtures/transcript-mangles/fruit-ninja-confirmed-retirement.json +8 -0
  47. package/tests/patching/patchFileOutput.test.ts +112 -1
  48. package/tests/patching/transcriptMangleRegression.test.ts +125 -0
  49. package/tests/processors/Base64ImageDetector.test.ts +36 -1
  50. package/tests/unit/commands/sandbox.test.ts +350 -0
  51. package/ts_build/package.json +1 -1
  52. package/ts_build/src/agents/base/base.d.ts +8 -1
  53. package/ts_build/src/agents/base/base.js +37 -3
  54. package/ts_build/src/agents/base/base.js.map +1 -1
  55. package/ts_build/src/agents/tools/patch.d.ts +1 -0
  56. package/ts_build/src/agents/tools/patch.js +35 -2
  57. package/ts_build/src/agents/tools/patch.js.map +1 -1
  58. package/ts_build/src/agents/tools/textSearch.js +56 -2
  59. package/ts_build/src/agents/tools/textSearch.js.map +1 -1
  60. package/ts_build/src/chat/modules/AgentModule.d.ts +14 -1
  61. package/ts_build/src/chat/modules/AgentModule.js +73 -5
  62. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  63. package/ts_build/src/chat/modules/SessionsModule.js +4 -0
  64. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  65. package/ts_build/src/chat/types.d.ts +11 -0
  66. package/ts_build/src/chat.js +7 -0
  67. package/ts_build/src/chat.js.map +1 -1
  68. package/ts_build/src/cli.d.ts +2 -1
  69. package/ts_build/src/cli.js +1 -0
  70. package/ts_build/src/cli.js.map +1 -1
  71. package/ts_build/src/clients/anthropic.js +28 -1
  72. package/ts_build/src/clients/anthropic.js.map +1 -1
  73. package/ts_build/src/clients/contextLimits.js +25 -0
  74. package/ts_build/src/clients/contextLimits.js.map +1 -1
  75. package/ts_build/src/clients/gemini.js +48 -4
  76. package/ts_build/src/clients/gemini.js.map +1 -1
  77. package/ts_build/src/clients/http.d.ts +2 -0
  78. package/ts_build/src/clients/http.js +66 -9
  79. package/ts_build/src/clients/http.js.map +1 -1
  80. package/ts_build/src/clients/index.d.ts +1 -0
  81. package/ts_build/src/clients/index.js +4 -0
  82. package/ts_build/src/clients/index.js.map +1 -1
  83. package/ts_build/src/clients/openai.d.ts +6 -1
  84. package/ts_build/src/clients/openai.js +131 -33
  85. package/ts_build/src/clients/openai.js.map +1 -1
  86. package/ts_build/src/clients/pricing/fireworks.d.ts +4 -1
  87. package/ts_build/src/clients/pricing/fireworks.js +33 -1
  88. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  89. package/ts_build/src/clients/pricing/index.d.ts +1 -1
  90. package/ts_build/src/clients/pricing/index.js +6 -2
  91. package/ts_build/src/clients/pricing/index.js.map +1 -1
  92. package/ts_build/src/clients/pricing/models.d.ts +2 -1
  93. package/ts_build/src/clients/pricing/models.js +4 -1
  94. package/ts_build/src/clients/pricing/models.js.map +1 -1
  95. package/ts_build/src/clients/pricing/openai.js +2 -2
  96. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  97. package/ts_build/src/clients/pricing/qwen.d.ts +5 -0
  98. package/ts_build/src/clients/pricing/qwen.js +30 -0
  99. package/ts_build/src/clients/pricing/qwen.js.map +1 -0
  100. package/ts_build/src/clients/qwen.d.ts +4 -0
  101. package/ts_build/src/clients/qwen.js +15 -0
  102. package/ts_build/src/clients/qwen.js.map +1 -0
  103. package/ts_build/src/clients/types.d.ts +12 -0
  104. package/ts_build/src/clients/xai.d.ts +1 -0
  105. package/ts_build/src/clients/xai.js +56 -9
  106. package/ts_build/src/clients/xai.js.map +1 -1
  107. package/ts_build/src/commands/agent.js +10 -1
  108. package/ts_build/src/commands/agent.js.map +1 -1
  109. package/ts_build/src/commands/sandbox.js +79 -80
  110. package/ts_build/src/commands/sandbox.js.map +1 -1
  111. package/ts_build/src/index.d.ts +2 -0
  112. package/ts_build/src/index.js +1 -0
  113. package/ts_build/src/index.js.map +1 -1
  114. package/ts_build/src/processors/Base64ImageDetector.js +22 -3
  115. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  116. package/ts_build/src/processors/GemmaToolProcessor.d.ts +14 -0
  117. package/ts_build/src/processors/GemmaToolProcessor.js +97 -0
  118. package/ts_build/src/processors/GemmaToolProcessor.js.map +1 -0
  119. package/ts_build/src/processors/index.d.ts +1 -0
  120. package/ts_build/src/processors/index.js +3 -1
  121. package/ts_build/src/processors/index.js.map +1 -1
  122. package/ts_build/src/services/AgentSyncFs.js +17 -0
  123. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  124. package/ts_build/src/services/SessionManager.js +12 -0
  125. package/ts_build/src/services/SessionManager.js.map +1 -1
  126. package/ts_build/src/services/Tools.d.ts +2 -0
  127. package/ts_build/src/services/Tools.js.map +1 -1
  128. package/ts_build/src/services/modules/computerUse.d.ts +163 -0
  129. package/ts_build/src/services/modules/computerUse.js +3 -0
  130. package/ts_build/src/services/modules/computerUse.js.map +1 -0
  131. package/ts_build/src/services/modules/index.d.ts +1 -0
  132. package/ts_build/src/services/modules/index.js +23 -6
  133. package/ts_build/src/services/modules/index.js.map +1 -1
  134. package/ts_build/src/services/modules/types.d.ts +4 -0
  135. package/ts_build/src/types.d.ts +3 -0
  136. package/ts_build/src/types.js.map +1 -1
  137. package/ts_build/src/workers/tools/index.d.ts +1 -1
  138. package/ts_build/tests/clients/contextLimits.test.js +2 -0
  139. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  140. package/ts_build/tests/clients/openai.images.test.d.ts +1 -0
  141. package/ts_build/tests/clients/openai.images.test.js +94 -0
  142. package/ts_build/tests/clients/openai.images.test.js.map +1 -0
  143. package/ts_build/tests/clients/pricing.test.js +4 -0
  144. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  145. package/ts_build/tests/compressor/usageBase64.test.d.ts +1 -0
  146. package/ts_build/tests/compressor/usageBase64.test.js +109 -0
  147. package/ts_build/tests/compressor/usageBase64.test.js.map +1 -0
  148. package/ts_build/tests/patching/patchFileOutput.test.js +82 -0
  149. package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -1
  150. package/ts_build/tests/patching/transcriptMangleRegression.test.d.ts +1 -0
  151. package/ts_build/tests/patching/transcriptMangleRegression.test.js +104 -0
  152. package/ts_build/tests/patching/transcriptMangleRegression.test.js.map +1 -0
  153. package/ts_build/tests/processors/Base64ImageDetector.test.js +24 -0
  154. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  155. package/ts_build/tests/unit/commands/sandbox.test.d.ts +1 -0
  156. package/ts_build/tests/unit/commands/sandbox.test.js +258 -0
  157. package/ts_build/tests/unit/commands/sandbox.test.js.map +1 -0
  158. package/x-home.png +0 -0
@@ -0,0 +1,16 @@
1
+ import { HttpClient } from "./http";
2
+ import { QwenTextPricing } from "./pricing/qwen";
3
+
4
+ /**
5
+ * QWEN Cloud client — OpenAI-compatible API
6
+ * https://dashscope-intl.aliyuncs.com/compatible-mode/v1
7
+ * Alibaba Cloud's Qwen model family via the international DashScope endpoint.
8
+ * Set env var QWEN_CLOUD_API_KEY to enable.
9
+ */
10
+ export class GenericQwenClient extends HttpClient {
11
+ constructor(apiKey = process.env.QWEN_CLOUD_API_KEY) {
12
+ super("https://dashscope-intl.aliyuncs.com/compatible-mode/v1");
13
+ if (apiKey) this.setJwt(apiKey);
14
+ this.setPrices(QwenTextPricing);
15
+ }
16
+ }
@@ -8,7 +8,10 @@ export type ModelModality =
8
8
 
9
9
  export type MessageContent =
10
10
  | { type: "text"; text: string }
11
- | { type: "image_url"; image_url: { url: string } }
11
+ | {
12
+ type: "image_url";
13
+ image_url: { url: string; detail?: "auto" | "low" | "high" };
14
+ }
12
15
  | { type: "audio_url"; audio_url: { url: string } }
13
16
  | { type: "video_url"; video_url: { url: string } };
14
17
 
@@ -23,6 +26,50 @@ export interface Message {
23
26
 
24
27
  export interface OutputMessage extends Message {
25
28
  content?: string | null;
29
+ /**
30
+ * The model's reasoning/thinking content, when the provider exposes it.
31
+ * - OpenAI Responses API: a human-readable summary of the model's reasoning
32
+ * (populated when reasoning_summary is requested).
33
+ */
34
+ reasoning_summary?: string;
35
+ /**
36
+ * OpenAI Responses API only (internal): the raw reasoning output items
37
+ * (carrying encrypted_content) captured from a stateless response. These are
38
+ * re-injected into the next request's input to preserve the reasoning chain.
39
+ * Not intended for display.
40
+ *
41
+ * @deprecated Prefer the provider-agnostic `_reasoning_details` slot. This is
42
+ * retained for backward compatibility with the OpenAI client only.
43
+ */
44
+ _reasoning_items?: any[];
45
+ /**
46
+ * Provider-agnostic (internal): an opaque, provider-tagged carrier for the
47
+ * model's reasoning artifact that must be round-tripped verbatim on the next
48
+ * tool-use turn to preserve reasoning continuity. Each provider stashes its
49
+ * own native artifact here and re-injects it only when the `provider` tag
50
+ * matches, so provider-specific payload never leaks into the generic
51
+ * (OpenAI-format) message shape.
52
+ *
53
+ * - openai / xai: Responses API `reasoning` output items (encrypted_content)
54
+ * - anthropic: `thinking` / `redacted_thinking` blocks (with signature/data)
55
+ * - gemini: `thoughtSignature` strings keyed by tool-call id (+ thought parts)
56
+ *
57
+ * Not intended for display.
58
+ */
59
+ _reasoning_details?: ReasoningDetails;
60
+ }
61
+
62
+ /**
63
+ * Opaque, provider-tagged reasoning artifact. The `items` are provider-native
64
+ * and only meaningful to the client whose id matches `provider`; any other
65
+ * client must ignore them (they are never mapped into a foreign provider's
66
+ * request payload).
67
+ */
68
+ export interface ReasoningDetails {
69
+ /** The provider that produced these items, e.g. "openai" | "xai" | "anthropic" | "gemini". */
70
+ provider: string;
71
+ /** Provider-native reasoning items to round-trip verbatim on the next turn. */
72
+ items: any[];
26
73
  }
27
74
 
28
75
  export interface ToolProp {
@@ -30,6 +77,7 @@ export interface ToolProp {
30
77
  description?: string;
31
78
  properties?: { [key: string]: ToolProp };
32
79
  items?: ToolProp;
80
+ enum?: any[];
33
81
  }
34
82
 
35
83
  export interface Tool {
@@ -114,6 +162,20 @@ export interface CompletionOptions extends RetryOptions {
114
162
  * Defaults to false (no summary displayed).
115
163
  */
116
164
  reasoning_summary?: boolean;
165
+ /**
166
+ * OpenAI Responses API only: when true, OpenAI stores the response server-side
167
+ * so subsequent turns can reference it via `previous_response_id` instead of
168
+ * resending the full reasoning/message history. Defaults to false (stateless).
169
+ * When false, the client automatically requests `reasoning.encrypted_content`
170
+ * and threads the reasoning items back in the next request's input.
171
+ */
172
+ store?: boolean;
173
+ /**
174
+ * OpenAI Responses API only: the id of a previously-stored response to continue
175
+ * from. Only meaningful when `store` was true on the prior request. When set,
176
+ * the model keeps its prior chain-of-thought instead of re-deriving it.
177
+ */
178
+ previous_response_id?: string;
117
179
  /**
118
180
  * When true, hints to the client that this task is long-running and it should
119
181
  * use a long-TTL cache where available.
@@ -165,6 +227,12 @@ export interface CompletionResponse {
165
227
  model: string;
166
228
  usage: TokenUsage | undefined;
167
229
  usd_cost?: number;
230
+ /**
231
+ * OpenAI Responses API only: the id of this response. When `store` was true,
232
+ * this can be passed as `previous_response_id` on the next request to continue
233
+ * the reasoning chain without resending history.
234
+ */
235
+ response_id?: string;
168
236
  }
169
237
 
170
238
  /** A single chunk yielded by a streaming completion. */
@@ -116,6 +116,20 @@ export class GenericXAIClient implements GenericClient {
116
116
  * Used for grok-4.20 reasoning/non-reasoning and multi-agent models.
117
117
  * Translates Chat Completions message format to Responses API format.
118
118
  */
119
+ /**
120
+ * Resolve the xai-native reasoning items (Responses API `reasoning` items
121
+ * with encrypted_content) to re-inject for an assistant message, reading the
122
+ * provider-agnostic `_reasoning_details` slot only when it was produced by
123
+ * this provider.
124
+ */
125
+ private getReasoningItems(msg: any): any[] {
126
+ const details = msg?._reasoning_details;
127
+ if (details && details.provider === "xai" && Array.isArray(details.items)) {
128
+ return details.items;
129
+ }
130
+ return [];
131
+ }
132
+
119
133
  async createChatResponse(
120
134
  options: CompletionOptions
121
135
  ): Promise<CompletionResponse> {
@@ -133,22 +147,46 @@ export class GenericXAIClient implements GenericClient {
133
147
  .trim() || undefined;
134
148
 
135
149
  // Convert chat messages to Responses API input items
150
+ // Responses API hard limit: 10,485,760 chars per function_call_output.output
151
+ const MAX_TOOL_OUTPUT = 10_485_000;
152
+ const truncateToolOutput = (s: string) =>
153
+ s.length > MAX_TOOL_OUTPUT
154
+ ? s.slice(0, MAX_TOOL_OUTPUT) + "\n\n[TRUNCATED: output exceeded 10MB API limit]"
155
+ : s;
136
156
  const input: any[] = nonSystemMessages.map((msg) => {
137
157
  if (msg.role === "tool") {
138
158
  return {
139
159
  type: "function_call_output",
140
160
  call_id: msg.tool_call_id,
141
- output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
161
+ output: truncateToolOutput(typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content)),
142
162
  };
143
163
  }
144
164
  if (msg.role === "assistant" && msg.tool_calls?.length) {
145
- return msg.tool_calls.map((tc) => ({
165
+ // Re-inject any captured reasoning items (encrypted_content) BEFORE the
166
+ // function_call items so the model keeps its chain-of-thought instead of
167
+ // re-deriving it from scratch each turn.
168
+ const reasoningItems = this.getReasoningItems(msg);
169
+ return [
170
+ ...reasoningItems,
171
+ ...msg.tool_calls.map((tc) => ({
146
172
  type: "function_call",
147
173
  id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
148
174
  call_id: tc.id,
149
175
  name: tc.function.name,
150
176
  arguments: tc.function.arguments,
151
- }));
177
+ })),
178
+ ];
179
+ }
180
+ if (msg.role === "assistant" && this.getReasoningItems(msg).length) {
181
+ // assistant message (no tool calls) that carried reasoning items →
182
+ // re-inject reasoning items, then the visible message content.
183
+ return [
184
+ ...this.getReasoningItems(msg),
185
+ {
186
+ role: msg.role,
187
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
188
+ },
189
+ ];
152
190
  }
153
191
  return {
154
192
  role: msg.role,
@@ -182,7 +220,18 @@ export class GenericXAIClient implements GenericClient {
182
220
  ...(options.max_tokens && { max_output_tokens: Math.max(options.max_tokens, 16_000) }),
183
221
  ...(reasoningEffort && { reasoning: { effort: reasoningEffort } }),
184
222
  ...(tools?.length && { tools, tool_choice: "auto" }),
185
- store: false,
223
+ // Reasoning persistence (same construct as OpenAI's Responses API):
224
+ // - store === true → xAI persists the response; next turn passes
225
+ // previous_response_id to continue the reasoning chain server-side.
226
+ // - store !== true → stateless; request encrypted reasoning content so
227
+ // we can thread the reasoning items back into the next request ourselves.
228
+ store: options.store === true,
229
+ ...(options.previous_response_id && {
230
+ previous_response_id: options.previous_response_id,
231
+ }),
232
+ ...(options.store !== true && reasoningEffort && {
233
+ include: ["reasoning.encrypted_content"],
234
+ }),
186
235
  };
187
236
 
188
237
  const response = await fetch("https://api.x.ai/v1/responses", {
@@ -218,6 +267,10 @@ export class GenericXAIClient implements GenericClient {
218
267
  // Collect text content and tool calls from output items
219
268
  let textContent: string | null = null;
220
269
  const toolCalls: any[] = [];
270
+ // Reasoning items (with encrypted_content) to thread back into the next
271
+ // turn, plus a human-readable summary of the model's thinking (if present).
272
+ const reasoningItems: any[] = [];
273
+ let reasoningSummary: string | null = null;
221
274
 
222
275
  for (const item of data.output ?? []) {
223
276
  if (item.type === "message") {
@@ -226,6 +279,17 @@ export class GenericXAIClient implements GenericClient {
226
279
  textContent = (textContent ?? "") + part.text;
227
280
  }
228
281
  }
282
+ } else if (item.type === "reasoning") {
283
+ // Keep the raw reasoning item so it can be re-injected next turn
284
+ // (carries encrypted_content when include was requested).
285
+ reasoningItems.push(item);
286
+ const summaryParts = item.summary ?? [];
287
+ for (const part of summaryParts) {
288
+ const text = typeof part === "string" ? part : part?.text;
289
+ if (text) {
290
+ reasoningSummary = (reasoningSummary ?? "") + text;
291
+ }
292
+ }
229
293
  } else if (item.type === "function_call") {
230
294
  toolCalls.push({
231
295
  id: item.call_id,
@@ -245,12 +309,17 @@ export class GenericXAIClient implements GenericClient {
245
309
  role: "assistant",
246
310
  content: textContent,
247
311
  ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
312
+ ...(reasoningSummary && { reasoning_summary: reasoningSummary }),
313
+ ...(reasoningItems.length > 0 && {
314
+ _reasoning_details: { provider: "xai", items: reasoningItems },
315
+ }),
248
316
  },
249
317
  },
250
318
  ],
251
319
  model: options.model,
252
320
  usage,
253
321
  usd_cost: usdCost,
322
+ response_id: data.id,
254
323
  };
255
324
  }
256
325
 
@@ -55,6 +55,10 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
55
55
  )
56
56
  .option("--model <model>", "Specific model for the provider")
57
57
  .option("--agent-name <name>", "Which agent to use", "Patcher")
58
+ .option(
59
+ "--reasoning-effort <effort>",
60
+ "Reasoning effort: none, low, medium, or high. Use 'none' to disable thinking on models that support it (e.g. for faster computer-use)."
61
+ )
58
62
  .option(
59
63
  "--max-time-limit <minutes>",
60
64
  "Time limit for agent execution (minutes)",
@@ -116,17 +120,29 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
116
120
  options.taskId,
117
121
  options.messageId
118
122
  );
123
+ // Restore the exact model/provider/reasoning the original run used
124
+ // (falls back to agent defaults when not persisted / remote-only).
125
+ const savedSettings = await agentModule.loadTaskSettings(
126
+ options.taskId,
127
+ options.messageId
128
+ );
119
129
  const resumeInput =
120
130
  options.input || "Please continue from where you left off.";
121
131
 
122
132
  await agentModule.initialize(chatService);
123
133
  const { taskCompleted: resumed } =
124
134
  await agentModule.resumeFromMessages({
125
- agentName: options.agentName || "Patcher",
135
+ agentName:
136
+ options.agentName || savedSettings.agentName || "Patcher",
126
137
  input: resumeInput,
127
138
  threads,
128
139
  messageId: options.messageId,
129
140
  taskId: options.taskId,
141
+ // CLI-provided model/provider still win over persisted ones.
142
+ model: options.model || savedSettings.model,
143
+ provider: options.provider || savedSettings.provider,
144
+ reasoningEffort: savedSettings.reasoningEffort,
145
+ summarizeReasoning: savedSettings.summarizeReasoning,
130
146
  });
131
147
  await resumed;
132
148
  return;
@@ -208,6 +224,9 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
208
224
  input,
209
225
  maxTimeLimit: parseInt(options.maxTimeLimit, 10),
210
226
  maxSpendLimit: parseFloat(options.maxSpendLimit),
227
+ ...(options.reasoningEffort
228
+ ? { reasoningEffort: options.reasoningEffort }
229
+ : {}),
211
230
  ...(behaviorSystemPrompt ? { systemPrompt: behaviorSystemPrompt } : {}),
212
231
  ...(behaviorModel ? { model: behaviorModel } : {}),
213
232
  run: true,
@@ -60,6 +60,7 @@ interface SandboxSnapshot {
60
60
  status: string;
61
61
  errorMsg?: string | null;
62
62
  statusMessage?: string | null;
63
+ regenerationSandboxId?: string | null;
63
64
  snapshotContent?: string;
64
65
  setupScript?: string | null;
65
66
  }
@@ -122,6 +123,51 @@ async function resolveSnapshot(
122
123
 
123
124
  // ─── Polling helpers ──────────────────────────────────────────────────────────
124
125
 
126
+ /**
127
+ * Poll a snapshot's status until regeneration is complete.
128
+ *
129
+ * The backend's POST /regenerate endpoint is fire-and-forget — it returns
130
+ * immediately with regenerationSandboxId set. We poll GET /snapshots/:id
131
+ * until one of:
132
+ * - regenerationSandboxId is null AND status === "ready" → success
133
+ * - status === "error" → failure
134
+ * - deadline exceeded → timeout
135
+ *
136
+ * statusMessage is printed whenever it changes so the user sees progress.
137
+ */
138
+ async function pollRegeneration(
139
+ sandboxId: string,
140
+ snapshotId: string,
141
+ timeoutMs: number
142
+ ): Promise<SandboxSnapshot> {
143
+ const deadline = Date.now() + timeoutMs;
144
+ let lastStatus = "";
145
+ let lastMessage = "";
146
+ while (Date.now() < deadline) {
147
+ const snap = await apiGet<SandboxSnapshot>(
148
+ `/sandboxes/${sandboxId}/snapshots/${snapshotId}`
149
+ );
150
+ if (snap.status !== lastStatus) {
151
+ console.log(` [snapshot] status: ${snap.status}`);
152
+ lastStatus = snap.status;
153
+ }
154
+ if (snap.statusMessage && snap.statusMessage !== lastMessage) {
155
+ console.log(` [snapshot] ${snap.statusMessage}`);
156
+ lastMessage = snap.statusMessage;
157
+ }
158
+ if (!snap.regenerationSandboxId && snap.status === "ready") return snap;
159
+ if (snap.status === "error") {
160
+ throw new Error(
161
+ `Snapshot regeneration failed: ${snap.errorMsg || snap.statusMessage || "(no details)"}`
162
+ );
163
+ }
164
+ await new Promise((r) => setTimeout(r, 5000));
165
+ }
166
+ throw new Error(
167
+ `Timeout waiting for snapshot regeneration to complete (${timeoutMs}ms). Last status: ${lastStatus}`
168
+ );
169
+ }
170
+
125
171
  async function pollSandboxStatus(
126
172
  sandboxId: string,
127
173
  targetStatuses: string[],
@@ -373,7 +419,7 @@ export function addSandboxCommand(program: Command): void {
373
419
  .option("--sandbox-name <name>", "Source sandbox name (alternative to --sandbox-id)")
374
420
  .option("--snapshot-id <id>", "Snapshot ID to regenerate")
375
421
  .option("--snapshot-name <name>", "Snapshot label to regenerate")
376
- .option("--no-wait", "Return immediately without waiting for snapshot to be ready")
422
+ .option("--no-wait", "Trigger regeneration and return immediately (do not poll for completion)")
377
423
  .option("--timeout-minutes <min>", "Max time for the full regen cycle", "60")
378
424
  .action(
379
425
  async (opts: {
@@ -381,20 +427,18 @@ export function addSandboxCommand(program: Command): void {
381
427
  sandboxName?: string;
382
428
  snapshotId?: string;
383
429
  snapshotName?: string;
384
- wait: boolean; // commander flips --no-wait → wait=false
430
+ wait: boolean;
385
431
  timeoutMinutes: string;
386
432
  }) => {
387
433
  const timeoutMs = Number(opts.timeoutMinutes) * 60 * 1000;
388
- const deadline = Date.now() + timeoutMs;
389
434
 
390
- // ── 1. Resolve sandbox ──────────────────────────────────────────────
435
+ // ── 1. Resolve sandbox & snapshot ──────────────────────────────────
391
436
  if (!opts.sandboxId && !opts.sandboxName) {
392
437
  throw new Error("Provide --sandbox-id or --sandbox-name");
393
438
  }
394
439
  const sandboxId =
395
440
  opts.sandboxId || (await resolveSandboxId(opts.sandboxName!));
396
441
 
397
- // ── 2. Resolve snapshot ─────────────────────────────────────────────
398
442
  if (!opts.snapshotId && !opts.snapshotName) {
399
443
  throw new Error("Provide --snapshot-id or --snapshot-name");
400
444
  }
@@ -410,132 +454,113 @@ export function addSandboxCommand(program: Command): void {
410
454
  );
411
455
  console.log(` Source sandbox: ${sandboxId}`);
412
456
 
413
- if (snapshot.status !== "ready") {
457
+ // ── 2. Validate preconditions ───────────────────────────────────────
458
+ if (snapshot.status !== "ready" && !snapshot.regenerationSandboxId) {
414
459
  throw new Error(
415
460
  `Snapshot must be in 'ready' status to regenerate (current: ${snapshot.status})`
416
461
  );
417
462
  }
418
- if (!snapshot.setupScript) {
419
- throw new Error(
420
- "Snapshot has no setupScript. Add one via the UI before regenerating."
421
- );
422
- }
423
463
 
424
- // ── 3. Fork ephemeral sandbox from the snapshot ─────────────────────
425
- console.log("\n📦 Step 1/4: Forking ephemeral sandbox from snapshot...");
426
- const ephemeralName = `regen-${snapshotId.slice(0, 8)}-${Date.now()}`;
427
- const forkResult = await apiPost<{ id?: string; sandboxId?: string }>(
428
- "/sandboxes/fork",
429
- { snapshotId, name: ephemeralName }
464
+ // ── 3. Kick off background regeneration via backend API ─────────────
465
+ // The backend POST /regenerate endpoint returns immediately with the
466
+ // updated snapshot (regenerationSandboxId set). It runs the full
467
+ // cycle (fork ephemeral → setupScript → snapshot) in the background.
468
+ console.log("\n📡 Triggering backend regeneration...");
469
+ const triggered = await apiPost<SandboxSnapshot>(
470
+ `/sandboxes/${sandboxId}/snapshots/${snapshotId}/regenerate`
430
471
  );
431
- const ephemeralId = forkResult.id || forkResult.sandboxId;
432
- if (!ephemeralId) {
433
- throw new Error(
434
- `Fork did not return a sandbox ID: ${JSON.stringify(forkResult)}`
472
+ console.log(` regenerationSandboxId: ${triggered.regenerationSandboxId || "(none)"}`);
473
+
474
+ if (!opts.wait) {
475
+ console.log(
476
+ `\n✅ Regeneration triggered. Monitor with:\n` +
477
+ ` knowhow sandbox snapshot-status --sandbox-id ${sandboxId} --snapshot-id ${snapshotId}`
435
478
  );
479
+ return;
436
480
  }
437
- console.log(` Ephemeral sandbox: ${ephemeralId} (${ephemeralName})`);
438
-
439
- // ── 4. Wait for ephemeral sandbox to be running ─────────────────────
440
- console.log("\n⏳ Step 2/4: Waiting for sandbox to start...");
441
- await pollSandboxStatus(
442
- ephemeralId,
443
- ["running"],
444
- Math.min(deadline - Date.now(), 10 * 60 * 1000),
445
- ephemeralName
481
+
482
+ // ── 4. Poll until complete ──────────────────────────────────────────
483
+ console.log("\n⏳ Waiting for regeneration to complete (Ctrl+C to stop polling)...");
484
+ const done = await pollRegeneration(sandboxId, snapshotId, timeoutMs);
485
+
486
+ console.log(
487
+ `\n✅ Snapshot regeneration complete!\n` +
488
+ ` Snapshot ID : ${done.id}\n` +
489
+ ` Label : ${done.label || "(none)"}\n` +
490
+ ` Status : ${done.status}`
446
491
  );
447
- console.log(" ✓ Running");
448
-
449
- // ── 5. Execute the setupScript ──────────────────────────────────────
450
- console.log("\n🔧 Step 3/4: Executing setupScript...");
451
- console.log(` Script length: ${snapshot.setupScript.length} chars`);
452
- let execResult: { stdout?: string; stderr?: string; exitCode?: number };
453
- try {
454
- execResult = await apiPost<{
455
- stdout?: string;
456
- stderr?: string;
457
- exitCode?: number;
458
- }>(`/sandboxes/${ephemeralId}/exec`, {
459
- command: "/bin/bash",
460
- args: ["-c", snapshot.setupScript],
461
- timeoutMs: Math.min(deadline - Date.now(), 25 * 60 * 1000),
462
- });
463
- } catch (err: any) {
464
- console.error(`\n❌ setupScript exec failed: ${err.message}`);
465
- await safeDestroy(ephemeralId);
466
- throw err;
467
- }
492
+ }
493
+ );
468
494
 
469
- if (execResult.stdout) {
470
- console.log("--- stdout ---");
471
- process.stdout.write(execResult.stdout);
472
- }
473
- if (execResult.stderr) {
474
- console.error("--- stderr ---");
475
- process.stderr.write(execResult.stderr);
495
+ // ── sandbox snapshot-status ──────────────────────────────────────────────────
496
+ sandboxCmd
497
+ .command("snapshot-status")
498
+ .description(
499
+ "Check and optionally wait for a snapshot's regeneration to complete.\n" +
500
+ "Useful after triggering regeneration with --no-wait."
501
+ )
502
+ .option("--sandbox-id <id>", "Sandbox ID")
503
+ .option("--sandbox-name <name>", "Sandbox name")
504
+ .option("--snapshot-id <id>", "Snapshot ID")
505
+ .option("--snapshot-name <name>", "Snapshot label")
506
+ .option("--wait", "Poll until regeneration completes (or times out)")
507
+ .option("--timeout-minutes <min>", "Max poll time in minutes", "60")
508
+ .option("--json", "Output raw JSON")
509
+ .action(
510
+ async (opts: {
511
+ sandboxId?: string;
512
+ sandboxName?: string;
513
+ snapshotId?: string;
514
+ snapshotName?: string;
515
+ wait?: boolean;
516
+ timeoutMinutes: string;
517
+ json?: boolean;
518
+ }) => {
519
+ if (!opts.sandboxId && !opts.sandboxName) {
520
+ throw new Error("Provide --sandbox-id or --sandbox-name");
476
521
  }
477
- if (execResult.exitCode != null && execResult.exitCode !== 0) {
478
- await safeDestroy(ephemeralId);
479
- throw new Error(`setupScript exited with code ${execResult.exitCode}`);
522
+ const sandboxId =
523
+ opts.sandboxId || (await resolveSandboxId(opts.sandboxName!));
524
+
525
+ if (!opts.snapshotId && !opts.snapshotName) {
526
+ throw new Error("Provide --snapshot-id or --snapshot-name");
480
527
  }
481
- console.log(" ✓ setupScript completed");
482
-
483
- // ── 6. Capture new snapshot with the same label ─────────────────────
484
- console.log("\n📸 Step 4/4: Capturing new snapshot...");
485
- const newLabel = snapshot.label || `regen-${snapshotId.slice(0, 8)}`;
486
- let createResult: { snapshot?: SandboxSnapshot; id?: string };
487
- try {
488
- createResult = await apiPost<{ snapshot?: SandboxSnapshot; id?: string }>(
489
- `/sandboxes/${ephemeralId}/snapshots`,
490
- {
491
- label: newLabel,
492
- description: snapshot.description,
493
- snapshotContent: snapshot.snapshotContent || "full",
494
- snapshotType: "full",
495
- }
528
+ const snapshot = opts.snapshotId
529
+ ? await apiGet<SandboxSnapshot>(
530
+ `/sandboxes/${sandboxId}/snapshots/${opts.snapshotId}`
531
+ )
532
+ : await resolveSnapshot(sandboxId, opts.snapshotName!);
533
+
534
+ if (opts.wait && snapshot.regenerationSandboxId) {
535
+ console.log("⏳ Waiting for regeneration to complete...");
536
+ const done = await pollRegeneration(
537
+ sandboxId,
538
+ snapshot.id,
539
+ Number(opts.timeoutMinutes) * 60 * 1000
496
540
  );
497
- } catch (err: any) {
498
- console.error(`\n❌ Snapshot creation failed: ${err.message}`);
499
- await safeDestroy(ephemeralId);
500
- throw err;
541
+ if (opts.json) {
542
+ console.log(JSON.stringify(done, null, 2));
543
+ } else {
544
+ console.log(
545
+ `✅ Done — status: ${done.status} label: ${done.label || "(none)"}`
546
+ );
547
+ }
548
+ return;
501
549
  }
502
550
 
503
- const newSnapshot = createResult.snapshot || (createResult as SandboxSnapshot);
504
- const newSnapshotId = newSnapshot.id;
505
- console.log(` New snapshot ID: ${newSnapshotId}`);
506
-
507
- if (opts.wait) {
508
- console.log(" Waiting for snapshot upload...");
509
- await pollSnapshotStatus(
510
- ephemeralId,
511
- newSnapshotId,
512
- ["ready"],
513
- Math.min(deadline - Date.now(), 20 * 60 * 1000)
551
+ if (opts.json) {
552
+ console.log(JSON.stringify(snapshot, null, 2));
553
+ } else {
554
+ const regenInfo = snapshot.regenerationSandboxId
555
+ ? `🔄 regenerating (ephemeral: ${snapshot.regenerationSandboxId})`
556
+ : `✅ ${snapshot.status}`;
557
+ console.log(
558
+ `Snapshot ${snapshot.id} label=${snapshot.label || "(none)"} ${regenInfo}`
514
559
  );
515
- console.log(" ✓ Snapshot ready");
560
+ if (snapshot.statusMessage) {
561
+ console.log(` message: ${snapshot.statusMessage}`);
562
+ }
516
563
  }
517
-
518
- // ── 7. Destroy ephemeral sandbox ────────────────────────────────────
519
- console.log("\n🗑 Destroying ephemeral sandbox...");
520
- await safeDestroy(ephemeralId);
521
- console.log(" ✓ Destroyed");
522
-
523
- console.log(
524
- `\n✅ Snapshot regeneration complete!\n` +
525
- ` Old snapshot ID : ${snapshotId}\n` +
526
- ` New snapshot ID : ${newSnapshotId}\n` +
527
- ` Label : ${newLabel}\n\n` +
528
- `ℹ The old snapshot still exists. Clean it up with:\n` +
529
- ` knowhow sandbox delete-snapshot --sandbox-id ${sandboxId} --snapshot-id ${snapshotId}`
530
- );
531
564
  }
532
565
  );
533
566
  }
534
-
535
- async function safeDestroy(sandboxId: string): Promise<void> {
536
- try {
537
- await apiDelete(`/sandboxes/${sandboxId}`);
538
- } catch (err: any) {
539
- console.warn(`⚠ Could not destroy sandbox ${sandboxId}: ${err.message}`);
540
- }
541
- }
package/src/index.ts CHANGED
@@ -51,6 +51,14 @@ export {
51
51
  } from "./generate";
52
52
 
53
53
  export * as clients from "./clients";
54
+ export type {
55
+ Tool,
56
+ ToolProp,
57
+ ToolCall,
58
+ Message,
59
+ CompletionOptions,
60
+ CompletionResponse,
61
+ } from "./clients/types";
54
62
  export * as agents from "./agents";
55
63
  export * as services from "./services";
56
64
  export * as embeddings from "./embeddings";
@@ -61,6 +69,7 @@ export * as ai from "./ai";
61
69
  // Export module system types for external modules
62
70
  export * from "./services/modules/types";
63
71
  export { ModulesService } from "./services/modules";
72
+ export * from "./services/modules/computerUse";
64
73
  // Export conversion types for external modules (e.g. knowhow-module-pdf)
65
74
  export * from "./services/conversion/types";
66
75
  export { ConversionService } from "./services/conversion/ConversionService";