@tyvm/knowhow 0.0.142 → 0.0.144

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 (57) hide show
  1. package/package.json +1 -1
  2. package/src/agents/developer/developer.ts +4 -1
  3. package/src/agents/patcher/patcher.ts +1 -1
  4. package/src/agents/tools/connectAgent.ts +2 -0
  5. package/src/agents/tools/observe.ts +2 -0
  6. package/src/agents/tools/replyToParent.ts +1 -0
  7. package/src/agents/tools/runGenerate.ts +1 -0
  8. package/src/agents/tools/sendAgentMessage.ts +1 -0
  9. package/src/agents/tools/startAgentTask.ts +1 -0
  10. package/src/agents/tools/waitForAgentCompleted.ts +1 -0
  11. package/src/chat/modules/SystemModule.ts +4 -3
  12. package/src/clients/pricing/fireworks.ts +73 -25
  13. package/src/clients/types.ts +8 -0
  14. package/src/commands/sandbox.ts +139 -114
  15. package/src/services/Mcp.ts +3 -0
  16. package/src/services/Tools.ts +12 -2
  17. package/src/types.ts +2 -0
  18. package/tests/services/Tools.test.ts +13 -11
  19. package/tests/unit/commands/sandbox.test.ts +350 -0
  20. package/ts_build/package.json +1 -1
  21. package/ts_build/src/agents/developer/developer.js +4 -0
  22. package/ts_build/src/agents/developer/developer.js.map +1 -1
  23. package/ts_build/src/agents/patcher/patcher.js +1 -1
  24. package/ts_build/src/agents/patcher/patcher.js.map +1 -1
  25. package/ts_build/src/agents/tools/connectAgent.js +2 -0
  26. package/ts_build/src/agents/tools/connectAgent.js.map +1 -1
  27. package/ts_build/src/agents/tools/observe.js +2 -0
  28. package/ts_build/src/agents/tools/observe.js.map +1 -1
  29. package/ts_build/src/agents/tools/replyToParent.js +1 -0
  30. package/ts_build/src/agents/tools/replyToParent.js.map +1 -1
  31. package/ts_build/src/agents/tools/runGenerate.js +1 -0
  32. package/ts_build/src/agents/tools/runGenerate.js.map +1 -1
  33. package/ts_build/src/agents/tools/sendAgentMessage.js +1 -0
  34. package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -1
  35. package/ts_build/src/agents/tools/startAgentTask.js +1 -0
  36. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  37. package/ts_build/src/agents/tools/waitForAgentCompleted.js +1 -0
  38. package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -1
  39. package/ts_build/src/chat/modules/SystemModule.js +4 -3
  40. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  41. package/ts_build/src/clients/pricing/fireworks.d.ts +26 -0
  42. package/ts_build/src/clients/pricing/fireworks.js +51 -25
  43. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  44. package/ts_build/src/clients/types.d.ts +1 -0
  45. package/ts_build/src/commands/sandbox.js +79 -80
  46. package/ts_build/src/commands/sandbox.js.map +1 -1
  47. package/ts_build/src/services/Mcp.js.map +1 -1
  48. package/ts_build/src/services/Tools.js +7 -2
  49. package/ts_build/src/services/Tools.js.map +1 -1
  50. package/ts_build/src/types.d.ts +26 -0
  51. package/ts_build/src/types.js +2 -0
  52. package/ts_build/src/types.js.map +1 -1
  53. package/ts_build/tests/services/Tools.test.js +6 -4
  54. package/ts_build/tests/services/Tools.test.js.map +1 -1
  55. package/ts_build/tests/unit/commands/sandbox.test.d.ts +1 -0
  56. package/ts_build/tests/unit/commands/sandbox.test.js +258 -0
  57. package/ts_build/tests/unit/commands/sandbox.test.js.map +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.142",
3
+ "version": "0.0.144",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -13,6 +13,10 @@ export class DeveloperAgent extends BaseAgent {
13
13
  model: Models.anthropic.Sonnet4_6,
14
14
  provider: "anthropic",
15
15
  },
16
+ {
17
+ model: Models.openai.GPT_56_Sol,
18
+ provider: "openai",
19
+ },
16
20
  ]);
17
21
  }
18
22
 
@@ -70,4 +74,3 @@ export class DeveloperAgent extends BaseAgent {
70
74
  ] as Message[];
71
75
  }
72
76
  }
73
-
@@ -81,7 +81,7 @@ export class PatchingAgent extends BaseAgent {
81
81
  this.setModelPreferences([
82
82
  { model: Models.anthropic.Sonnet4_6, provider: "anthropic" },
83
83
  { model: Models.openai.GPT_56_Sol, provider: "openai" },
84
- { model: Models.xai.GrokCodeFast, provider: "xai" },
84
+ { model: Models.xai.Grok4_5, provider: "xai" },
85
85
  ]);
86
86
  }
87
87
 
@@ -288,6 +288,7 @@ export const connectAgentDefinition: Tool = {
288
288
  parameters: {
289
289
  type: "object",
290
290
  positional: false,
291
+ usesContext: true,
291
292
  properties: {
292
293
  connections: {
293
294
  type: "array",
@@ -340,6 +341,7 @@ export const stopConnectionsDefinition: Tool = {
340
341
  parameters: {
341
342
  type: "object",
342
343
  positional: false,
344
+ usesContext: true,
343
345
  properties: {
344
346
  id: {
345
347
  type: "string",
@@ -182,6 +182,7 @@ export const observeDefinition: Tool = {
182
182
  parameters: {
183
183
  type: "object",
184
184
  positional: false,
185
+ usesContext: true,
185
186
  properties: {
186
187
  toolName: {
187
188
  type: "string",
@@ -232,6 +233,7 @@ export const stopObservingDefinition: Tool = {
232
233
  parameters: {
233
234
  type: "object",
234
235
  positional: false,
236
+ usesContext: true,
235
237
  properties: {
236
238
  id: {
237
239
  type: "string",
@@ -110,6 +110,7 @@ export const replyToParentDefinition: Tool = {
110
110
  parameters: {
111
111
  type: "object",
112
112
  positional: true,
113
+ usesContext: true,
113
114
  properties: {
114
115
  message: {
115
116
  type: "string",
@@ -142,6 +142,7 @@ export const runGenerateDefinition: Tool = {
142
142
  "When `sources` is omitted, it runs the sources defined in .knowhow/knowhow.json (optionally scoped by name/filter).",
143
143
  parameters: {
144
144
  type: "object",
145
+ usesContext: true,
145
146
  properties: {
146
147
  sources: {
147
148
  type: "array",
@@ -112,6 +112,7 @@ export const sendAgentMessageDefinition: Tool = {
112
112
  parameters: {
113
113
  type: "object",
114
114
  positional: false,
115
+ usesContext: true,
115
116
  properties: {
116
117
  taskId: {
117
118
  type: "string",
@@ -265,6 +265,7 @@ export const startAgentTaskDefinition: Tool = {
265
265
  "The return value includes the taskId and agent directory path when syncFs is used.",
266
266
  parameters: {
267
267
  type: "object",
268
+ usesContext: true,
268
269
  properties: {
269
270
  messageId: {
270
271
  type: "string",
@@ -246,6 +246,7 @@ export const waitForAgentCompletedDefinition: Tool = {
246
246
  parameters: {
247
247
  type: "object",
248
248
  positional: false,
249
+ usesContext: true,
249
250
  properties: {
250
251
  taskId: {
251
252
  type: "string",
@@ -102,11 +102,12 @@ export class SystemModule extends BaseChatModule {
102
102
 
103
103
  // Get default model for new provider
104
104
  const ChatModelDefaults = {
105
- openai: Models.openai.GPT_53_Codex,
105
+ openai: Models.openai.GPT_56_Sol,
106
106
  anthropic: Models.anthropic.Sonnet4_6,
107
- google: Models.google.Gemini_3_Flash_Preview,
108
- xai: Models.xai.Grok4_1_Fast_NonReasoning,
107
+ google: Models.google.Gemini_35_Flash_Lite,
108
+ xai: Models.xai.Grok4_5,
109
109
  knowhow: "anthropic/" + Models.anthropic.Sonnet4_6,
110
+ fireworks: Models.fireworks.KimiK3,
110
111
  };
111
112
 
112
113
  const newModel =
@@ -1,45 +1,93 @@
1
1
  import { ModelPricing } from "./types";
2
2
 
3
3
  /**
4
- * Fireworks AI pricing (USD per 1M tokens)
4
+ * Fireworks AI model IDs, pricing, and catalog.
5
+ * Single source of truth for all Fireworks models.
5
6
  * Source: https://fireworks.ai/pricing
6
7
  */
8
+
9
+ // ─── Model IDs ────────────────────────────────────────────────────────────────
10
+
11
+ export const FireworksModels = {
12
+ // Moonshot AI
13
+ KimiK3: "accounts/fireworks/models/kimi-k3",
14
+ KimiK2p7Code: "accounts/fireworks/models/kimi-k2p7-code",
15
+ KimiK2_6: "accounts/fireworks/models/kimi-k2-6",
16
+ KimiK2_5: "accounts/fireworks/models/kimi-k2-5",
17
+
18
+ // MiniMax
19
+ MinimaxM3: "accounts/fireworks/models/minimax-m3",
20
+ MinimaxM2_7: "accounts/fireworks/models/minimax-m2-7",
21
+ MinimaxM2_5: "accounts/fireworks/models/minimax-m2-5",
22
+
23
+ // Qwen
24
+ Qwen3p7Plus: "accounts/fireworks/models/qwen3p7-plus",
25
+
26
+ // Deprecated Qwen Models
27
+ Qwen3_6Plus: "accounts/fireworks/models/qwen3-6-plus",
28
+ Qwen3p6Plus: "accounts/fireworks/models/qwen3p6-plus",
29
+ Qwen3Vl30bA3bThinking: "accounts/fireworks/models/qwen3-vl-30b-a3b-thinking",
30
+ Qwen3Vl30bA3bInstruct: "accounts/fireworks/models/qwen3-vl-30b-a3b-instruct",
31
+ Qwen3_8b: "accounts/fireworks/models/qwen3-8b",
32
+
33
+ // Z.ai
34
+ Glm5p2: "accounts/fireworks/models/glm-5p2",
35
+ Glm5p1: "accounts/fireworks/models/glm-5p1",
36
+ Glm5: "accounts/fireworks/models/glm-5",
37
+ Glm4_7: "accounts/fireworks/models/glm-4-7",
38
+
39
+ // DeepSeek AI
40
+ DeepseekV4Pro: "accounts/fireworks/models/deepseek-v4-pro",
41
+ DeepseekV4Flash: "accounts/fireworks/models/deepseek-v4-flash",
42
+ DeepseekV3_2: "accounts/fireworks/models/deepseek-v3-2",
43
+ DeepseekV3_1: "accounts/fireworks/models/deepseek-v3-1",
44
+
45
+ // OpenAI OSS
46
+ GptOss120b: "accounts/fireworks/models/gpt-oss-120b",
47
+
48
+ // Embedding / Reranker
49
+ Qwen3Embedding8b: "accounts/fireworks/models/qwen3-embedding-8b",
50
+ Qwen3Reranker8b: "accounts/fireworks/models/qwen3-reranker-8b",
51
+ } as const;
52
+
53
+ // ─── Text model pricing (USD per 1M tokens) ───────────────────────────────────
54
+
7
55
  export const FireworksTextPricing: Record<string, ModelPricing> = {
8
56
  // Moonshot AI
9
- "accounts/fireworks/models/kimi-k3": { input: 3.0, cache_hit: 0.30, output: 15.0 },
10
- "accounts/fireworks/models/kimi-k2p7-code": { input: 0.95, cache_hit: 0.19, output: 4.0 },
11
- "accounts/fireworks/models/kimi-k2-6": { input: 0.95, cache_hit: 0.16, output: 4.0 },
12
- "accounts/fireworks/models/kimi-k2-5": { input: 0.60, cache_hit: 0.10, output: 3.0 },
57
+ [FireworksModels.KimiK3]: { input: 3.0, cache_hit: 0.30, output: 15.0 },
58
+ [FireworksModels.KimiK2p7Code]: { input: 0.95, cache_hit: 0.19, output: 4.0 },
59
+ [FireworksModels.KimiK2_6]: { input: 0.95, cache_hit: 0.16, output: 4.0 },
60
+ [FireworksModels.KimiK2_5]: { input: 0.60, cache_hit: 0.10, output: 3.0 },
13
61
 
14
62
  // MiniMax
15
- "accounts/fireworks/models/minimax-m3": { input: 0.30, cache_hit: 0.06, output: 1.20 },
16
- "accounts/fireworks/models/minimax-m2-7": { input: 0.30, cache_hit: 0.06, output: 1.20 },
17
- "accounts/fireworks/models/minimax-m2-5": { input: 0.30, cache_hit: 0.03, output: 1.20 },
63
+ [FireworksModels.MinimaxM3]: { input: 0.30, cache_hit: 0.06, output: 1.20 },
64
+ [FireworksModels.MinimaxM2_7]: { input: 0.30, cache_hit: 0.06, output: 1.20 },
65
+ [FireworksModels.MinimaxM2_5]: { input: 0.30, cache_hit: 0.03, output: 1.20 },
18
66
 
19
67
  // Qwen
20
- "accounts/fireworks/models/qwen3p7-plus": { input: 0.40, cache_hit: 0.08, output: 1.60 },
68
+ [FireworksModels.Qwen3p7Plus]: { input: 0.40, cache_hit: 0.08, output: 1.60 },
21
69
 
22
70
  // Deprecated QWEN Models
23
- "accounts/fireworks/models/qwen3-6-plus": { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true},
24
- "accounts/fireworks/models/qwen3p6-plus": { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true},
25
- "accounts/fireworks/models/qwen3-vl-30b-a3b-thinking": { input: 0.15, cache_hit: 0.08, output: 0.60, deprecated: true},
26
- "accounts/fireworks/models/qwen3-vl-30b-a3b-instruct": { input: 0.15, cache_hit: 0.08, output: 0.60, deprecated: true},
27
- "accounts/fireworks/models/qwen3-8b": { input: 0.20, cache_hit: 0.10, output: 0.20, deprecated: true},
71
+ [FireworksModels.Qwen3_6Plus]: { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true },
72
+ [FireworksModels.Qwen3p6Plus]: { input: 0.50, cache_hit: 0.10, output: 3.0, deprecated: true },
73
+ [FireworksModels.Qwen3Vl30bA3bThinking]: { input: 0.15, cache_hit: 0.08, output: 0.60, deprecated: true },
74
+ [FireworksModels.Qwen3Vl30bA3bInstruct]: { input: 0.15, cache_hit: 0.08, output: 0.60, deprecated: true },
75
+ [FireworksModels.Qwen3_8b]: { input: 0.20, cache_hit: 0.10, output: 0.20, deprecated: true },
28
76
 
29
77
  // Z.ai
30
- "accounts/fireworks/models/glm-5p2": { input: 1.40, cache_hit: 0.26, output: 4.40 },
31
- "accounts/fireworks/models/glm-5p1": { input: 1.40, cache_hit: 0.26, output: 4.40 },
32
- "accounts/fireworks/models/glm-5": { input: 1.00, cache_hit: 0.20, output: 3.20, deprecated: true },
33
- "accounts/fireworks/models/glm-4-7": { input: 0.60, cache_hit: 0.30, output: 2.20, deprecated: true},
78
+ [FireworksModels.Glm5p2]: { input: 1.40, cache_hit: 0.26, output: 4.40 },
79
+ [FireworksModels.Glm5p1]: { input: 1.40, cache_hit: 0.26, output: 4.40 },
80
+ [FireworksModels.Glm5]: { input: 1.00, cache_hit: 0.20, output: 3.20, deprecated: true },
81
+ [FireworksModels.Glm4_7]: { input: 0.60, cache_hit: 0.30, output: 2.20, deprecated: true },
34
82
 
35
83
  // DeepSeek AI
36
- "accounts/fireworks/models/deepseek-v4-pro": { input: 1.74, cache_hit: 0.15, output: 3.48 },
37
- "accounts/fireworks/models/deepseek-v4-flash": { input: 0.14, cache_hit: 0.03, output: 0.28 },
38
- "accounts/fireworks/models/deepseek-v3-2": { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
39
- "accounts/fireworks/models/deepseek-v3-1": { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
84
+ [FireworksModels.DeepseekV4Pro]: { input: 1.74, cache_hit: 0.15, output: 3.48 },
85
+ [FireworksModels.DeepseekV4Flash]: { input: 0.14, cache_hit: 0.03, output: 0.28 },
86
+ [FireworksModels.DeepseekV3_2]: { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
87
+ [FireworksModels.DeepseekV3_1]: { input: 0.56, cache_hit: 0.28, output: 1.68, deprecated: true },
40
88
 
41
89
  // OpenAI OSS
42
- "accounts/fireworks/models/gpt-oss-120b": { input: 0.15, cache_hit: 0.01, output: 0.60 },
90
+ [FireworksModels.GptOss120b]: { input: 0.15, cache_hit: 0.01, output: 0.60 },
43
91
  };
44
92
 
45
93
  /**
@@ -48,8 +96,8 @@ export const FireworksTextPricing: Record<string, ModelPricing> = {
48
96
  */
49
97
  export const FireworksEmbeddingPricing: Record<string, ModelPricing> = {
50
98
  // Qwen3 Embedding 8B — $0.10/M tokens, context 40k
51
- "accounts/fireworks/models/qwen3-embedding-8b": { input: 0.10, output: 0.10 },
99
+ [FireworksModels.Qwen3Embedding8b]: { input: 0.10, output: 0.10 },
52
100
 
53
101
  // Qwen3 Reranker 8B — $0.20/M tokens, context 40k
54
- "accounts/fireworks/models/qwen3-reranker-8b": { input: 0.20, output: 0.20 },
102
+ [FireworksModels.Qwen3Reranker8b]: { input: 0.20, output: 0.20 },
55
103
  };
@@ -40,6 +40,14 @@ export interface Tool {
40
40
  parameters: {
41
41
  type: string;
42
42
  positional?: boolean;
43
+ /**
44
+ * When true, this tool is a local/in-memory (functional) tool that needs
45
+ * the per-call agent context. ToolsService will inject `_ctx` into the
46
+ * invocation (last positional arg for positional tools, or the `_ctx` key
47
+ * for non-positional tools). Tools that don't set this (e.g. MCP tools,
48
+ * which are serialized over the wire) never receive `_ctx`.
49
+ */
50
+ usesContext?: boolean;
43
51
  properties: {
44
52
  [key: string]: ToolProp;
45
53
  };
@@ -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
- }
@@ -442,6 +442,9 @@ export class McpService {
442
442
 
443
443
  const realName = this.parseToolName(toolName);
444
444
  return async (args: any) => {
445
+ // MCP tools are registered without `usesContext`, so ToolsService.callTool
446
+ // never injects the per-call `_ctx` here — the args we receive are already
447
+ // safe to serialize over the wire (no circular BaseAgent/AI-client refs).
445
448
  console.log("Calling tool via mcp client", realName, "with args", args);
446
449
  const tool = await client.callTool(
447
450
  {
@@ -245,9 +245,15 @@ export class ToolsService {
245
245
  const properties = toolDefinition?.function?.parameters?.properties || {};
246
246
  const isPositional =
247
247
  toolDefinition?.function?.parameters?.positional || false;
248
+ const usesContext =
249
+ toolDefinition?.function?.parameters?.usesContext || false;
248
250
 
249
251
  // Thread the per-call context (`_ctx`) into the invocation so tools can be
250
252
  // self-referential (e.g. read the calling agent off `_ctx.caller`).
253
+ // Only tools that opt-in via `usesContext: true` receive `_ctx`. This keeps
254
+ // the context (which holds non-serializable, circular references like the
255
+ // calling agent / AI client) out of tools that don't need it — notably MCP
256
+ // tools, whose args are serialized to JSON and sent over the wire.
251
257
  // - positional tools receive `_ctx` as the LAST positional argument
252
258
  // - non-positional tools receive it merged onto the args object as `_ctx`
253
259
  const ctx: ToolCallContext = { ...(callContext || {}), Tools: this };
@@ -257,8 +263,12 @@ export class ToolsService {
257
263
  : functionArgs;
258
264
 
259
265
  const fnArgs = isPositional
260
- ? [...positionalArgs, ctx]
261
- : { ...functionArgs, _ctx: ctx };
266
+ ? usesContext
267
+ ? [...positionalArgs, ctx]
268
+ : positionalArgs
269
+ : usesContext
270
+ ? { ...functionArgs, _ctx: ctx }
271
+ : functionArgs;
262
272
 
263
273
  // Execute the function
264
274
  const rawResponse = isPositional