@tyvm/knowhow 0.0.106 → 0.0.108-dev.126b29e

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 (87) hide show
  1. package/README.md +45 -0
  2. package/package.json +8 -2
  3. package/scripts/publish.sh +86 -0
  4. package/src/agents/base/base.ts +44 -4
  5. package/src/agents/tools/execCommand.ts +49 -6
  6. package/src/chat/modules/AgentModule.ts +55 -30
  7. package/src/chat/modules/SessionsModule.ts +7 -2
  8. package/src/cli.ts +7 -2
  9. package/src/clients/anthropic.ts +27 -18
  10. package/src/clients/gemini.ts +14 -2
  11. package/src/clients/http.ts +4 -0
  12. package/src/clients/openai.ts +12 -1
  13. package/src/clients/pricing/openai.ts +1 -0
  14. package/src/clients/types.ts +35 -1
  15. package/src/clients/xai.ts +11 -1
  16. package/src/cloudWorker.ts +75 -1
  17. package/src/index.ts +17 -0
  18. package/src/plugins/embedding.ts +11 -6
  19. package/src/plugins/vim.ts +5 -16
  20. package/src/services/KnowhowClient.ts +22 -2
  21. package/src/services/S3.ts +10 -0
  22. package/src/services/modules/types.ts +2 -0
  23. package/src/worker.ts +65 -1
  24. package/src/workers/tools/index.ts +2 -0
  25. package/src/workers/tools/reloadConfig.ts +84 -0
  26. package/tests/clients/AIClient.test.ts +1 -1
  27. package/tests/clients/anthropic.test.ts +202 -0
  28. package/tests/services/WorkerReloadConfig.test.ts +141 -0
  29. package/ts_build/package.json +8 -2
  30. package/ts_build/src/agents/base/base.d.ts +1 -0
  31. package/ts_build/src/agents/base/base.js +31 -4
  32. package/ts_build/src/agents/base/base.js.map +1 -1
  33. package/ts_build/src/agents/tools/execCommand.d.ts +1 -1
  34. package/ts_build/src/agents/tools/execCommand.js +39 -5
  35. package/ts_build/src/agents/tools/execCommand.js.map +1 -1
  36. package/ts_build/src/chat/modules/AgentModule.d.ts +1 -1
  37. package/ts_build/src/chat/modules/AgentModule.js +39 -19
  38. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  39. package/ts_build/src/chat/modules/SessionsModule.js +7 -2
  40. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  41. package/ts_build/src/cli.js +8 -2
  42. package/ts_build/src/cli.js.map +1 -1
  43. package/ts_build/src/clients/anthropic.d.ts +5 -5
  44. package/ts_build/src/clients/anthropic.js +27 -18
  45. package/ts_build/src/clients/anthropic.js.map +1 -1
  46. package/ts_build/src/clients/gemini.js +10 -1
  47. package/ts_build/src/clients/gemini.js.map +1 -1
  48. package/ts_build/src/clients/http.js +3 -0
  49. package/ts_build/src/clients/http.js.map +1 -1
  50. package/ts_build/src/clients/openai.js +11 -1
  51. package/ts_build/src/clients/openai.js.map +1 -1
  52. package/ts_build/src/clients/pricing/openai.js +1 -0
  53. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  54. package/ts_build/src/clients/types.d.ts +14 -1
  55. package/ts_build/src/clients/xai.js +11 -1
  56. package/ts_build/src/clients/xai.js.map +1 -1
  57. package/ts_build/src/cloudWorker.d.ts +9 -0
  58. package/ts_build/src/cloudWorker.js +36 -0
  59. package/ts_build/src/cloudWorker.js.map +1 -1
  60. package/ts_build/src/index.js +14 -0
  61. package/ts_build/src/index.js.map +1 -1
  62. package/ts_build/src/plugins/embedding.js +4 -3
  63. package/ts_build/src/plugins/embedding.js.map +1 -1
  64. package/ts_build/src/plugins/vim.js +3 -9
  65. package/ts_build/src/plugins/vim.js.map +1 -1
  66. package/ts_build/src/services/KnowhowClient.d.ts +12 -0
  67. package/ts_build/src/services/KnowhowClient.js +11 -0
  68. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  69. package/ts_build/src/services/S3.js +7 -0
  70. package/ts_build/src/services/S3.js.map +1 -1
  71. package/ts_build/src/services/modules/types.d.ts +2 -0
  72. package/ts_build/src/worker.js +38 -0
  73. package/ts_build/src/worker.js.map +1 -1
  74. package/ts_build/src/workers/tools/index.d.ts +2 -0
  75. package/ts_build/src/workers/tools/index.js +4 -1
  76. package/ts_build/src/workers/tools/index.js.map +1 -1
  77. package/ts_build/src/workers/tools/reloadConfig.d.ts +14 -0
  78. package/ts_build/src/workers/tools/reloadConfig.js +48 -0
  79. package/ts_build/src/workers/tools/reloadConfig.js.map +1 -0
  80. package/ts_build/tests/clients/AIClient.test.js +1 -1
  81. package/ts_build/tests/clients/AIClient.test.js.map +1 -1
  82. package/ts_build/tests/clients/anthropic.test.d.ts +1 -0
  83. package/ts_build/tests/clients/anthropic.test.js +159 -0
  84. package/ts_build/tests/clients/anthropic.test.js.map +1 -0
  85. package/ts_build/tests/services/WorkerReloadConfig.test.d.ts +1 -0
  86. package/ts_build/tests/services/WorkerReloadConfig.test.js +86 -0
  87. package/ts_build/tests/services/WorkerReloadConfig.test.js.map +1 -0
package/README.md CHANGED
@@ -362,3 +362,48 @@ For command usage and behavior details, see:
362
362
  - Website: https://knowhow.tyvm.ai
363
363
  - Twitter/X: https://x.com/micahriggan
364
364
  - npm: https://www.npmjs.com/package/@tyvm/knowhow
365
+
366
+ ---
367
+
368
+ ## 🚧 Dev & Nightly Releases
369
+
370
+ Knowhow publishes unstable/preview builds under separate npm dist-tags so that regular users on `latest` are never affected.
371
+
372
+ ### Installing a pre-release build
373
+
374
+ ```bash
375
+ # Latest nightly (date-stamped)
376
+ npm install -g @tyvm/knowhow@nightly
377
+
378
+ # Latest dev snapshot (git-hash-stamped)
379
+ npm install -g @tyvm/knowhow@dev
380
+
381
+ # Pin a specific pre-release version
382
+ npm install -g @tyvm/knowhow@0.0.108-nightly.20250428
383
+ ```
384
+
385
+ ### How dist-tags work
386
+
387
+ | Tag | Installed by default? | Audience |
388
+ |-----|-----------------------|---------|
389
+ | `latest` | ✅ Yes (`npm install @tyvm/knowhow`) | All stable users |
390
+ | `nightly` | ❌ No (must opt-in) | Testers / early adopters |
391
+ | `dev` | ❌ No (must opt-in) | Developers / contributors |
392
+
393
+ Publishing a `nightly` or `dev` release **never moves the `latest` tag**, so existing users won't receive unstable code through normal updates.
394
+
395
+ ### Publishing scripts (for maintainers)
396
+
397
+ ```bash
398
+ # Publish a date-stamped nightly (e.g. 0.0.108-nightly.20250428)
399
+ npm run publish:nightly
400
+
401
+ # Publish a git-hash-stamped dev snapshot (e.g. 0.0.108-dev.abc1234)
402
+ npm run publish:dev
403
+
404
+ # Publish a stable release to `latest`
405
+ npm version patch # or minor / major
406
+ npm run publish:stable
407
+ ```
408
+
409
+ > **Note:** `publish:nightly` and `publish:dev` automatically version-bump the `package.json` with a pre-release suffix before publishing and do **not** create a git tag, keeping your git history clean.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.106",
3
+ "version": "0.0.108-dev.126b29e",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -21,7 +21,13 @@
21
21
  "lint": "tslint ./src/**/*.ts",
22
22
  "lint:deps": "depcheck --config=.depcheckrc",
23
23
  "lint:all": "npm run lint && npm run lint:deps",
24
- "check:model-pricing": "ts-node scripts/check-model-pricing.ts"
24
+ "check:model-pricing": "ts-node scripts/check-model-pricing.ts",
25
+ "version:nightly": "bash scripts/publish.sh version:nightly",
26
+ "version:dev": "bash scripts/publish.sh version:dev",
27
+ "publish:nightly": "bash scripts/publish.sh nightly",
28
+ "publish:dev": "bash scripts/publish.sh dev",
29
+ "publish:latest": "bash scripts/publish.sh stable",
30
+ "publish:stable": "bash scripts/publish.sh stable"
25
31
  },
26
32
  "keywords": [],
27
33
  "author": "Micah Riggan",
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ COMMAND="${1:-}"
5
+
6
+ get_current_version() {
7
+ node -e "process.stdout.write(require('./package.json').version)"
8
+ }
9
+
10
+ version_nightly() {
11
+ local version
12
+ version=$(get_current_version)
13
+ # Strip any existing pre-release suffix (e.g. -dev.xxx or -nightly.xxx)
14
+ version=$(echo "$version" | sed 's/-.*$//')
15
+ local stamp
16
+ stamp=$(date -u +%Y%m%d)
17
+ local pre="${version}-nightly.${stamp}"
18
+ local current
19
+ current=$(get_current_version)
20
+ if [ "$current" = "$pre" ]; then
21
+ echo "Version already set to: $pre (no change needed)"
22
+ else
23
+ echo "Bumping version to: $pre"
24
+ npm version "$pre" --no-git-tag-version
25
+ fi
26
+ }
27
+
28
+ version_dev() {
29
+ local version
30
+ version=$(get_current_version)
31
+ # Strip any existing pre-release suffix (e.g. -dev.xxx or -nightly.xxx)
32
+ version=$(echo "$version" | sed 's/-.*$//')
33
+ local hash
34
+ hash=$(git rev-parse --short HEAD)
35
+ local pre="${version}-dev.${hash}"
36
+ local current
37
+ current=$(get_current_version)
38
+ if [ "$current" = "$pre" ]; then
39
+ echo "Version already set to: $pre (no change needed)"
40
+ else
41
+ echo "Bumping version to: $pre"
42
+ npm version "$pre" --no-git-tag-version
43
+ fi
44
+ }
45
+
46
+ case "$COMMAND" in
47
+ nightly)
48
+ echo "🌙 Publishing nightly release..."
49
+ npm run compile
50
+ bash scripts/check-isolated-deps.sh
51
+ version_nightly
52
+ npm publish --tag nightly
53
+ echo "✅ Nightly published!"
54
+ ;;
55
+ dev)
56
+ echo "🔧 Publishing dev release..."
57
+ npm run compile
58
+ bash scripts/check-isolated-deps.sh
59
+ version_dev
60
+ npm publish --tag dev
61
+ echo "✅ Dev release published!"
62
+ ;;
63
+ stable|latest)
64
+ echo "🚀 Publishing stable release..."
65
+ npm run compile
66
+ bash scripts/check-isolated-deps.sh
67
+ npm publish --tag latest
68
+ echo "✅ Stable release published!"
69
+ ;;
70
+ version:nightly)
71
+ version_nightly
72
+ ;;
73
+ version:dev)
74
+ version_dev
75
+ ;;
76
+ *)
77
+ echo "Usage: $0 <nightly|dev|stable|version:nightly|version:dev>"
78
+ echo ""
79
+ echo " nightly - Compile, check deps, bump version with nightly stamp, publish to 'nightly' tag"
80
+ echo " dev - Compile, check deps, bump version with git hash, publish to 'dev' tag"
81
+ echo " stable - Compile, check deps, publish to 'latest' tag"
82
+ echo " version:nightly - Only bump version with nightly stamp (no publish)"
83
+ echo " version:dev - Only bump version with git hash (no publish)"
84
+ exit 1
85
+ ;;
86
+ esac
@@ -293,6 +293,33 @@ export abstract class BaseAgent implements IAgent {
293
293
  this.easyFinalAnswer = value;
294
294
  }
295
295
 
296
+ /**
297
+ * Detect if the model's response is a termination signal (e.g. "Done", "Complete", "Finished", "finalAnswer")
298
+ * This handles the case where an agent refuses to call finalAnswer and just says a short termination word.
299
+ */
300
+ protected isTerminationResponse(content: string): boolean {
301
+ const trimmed = content.trim();
302
+ // Short response (≤ 3 words) that matches a termination word/phrase exactly
303
+ const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
304
+ if (wordCount <= 3) {
305
+ const terminationPattern = /^(done|complete|completed|finished|final\s*answer|task\s*complete|all\s*done|that'?s\s*(all|it)|ok(ay)?|yes)[.!]*$/i;
306
+ if (terminationPattern.test(trimmed)) return true;
307
+ }
308
+
309
+ // Check if the first 1-3 words indicate task completion (for longer responses)
310
+ // e.g. "Task complete: ...", "All done.", "No further changes needed.", "Confirmed complete."
311
+ const firstWords = trimmed.split(/\s+/).slice(0, 3).join(" ");
312
+ const firstWordPattern = /^(task\s*(complete|completed|done|finished)|all\s*done|no\s*(further|more|additional|changes|action)|confirmed?\s*(complete|done|finished|one\s*last)|nothing\s*(more|further|else)|standing\s*by|everything\s*is|still\s*confirmed|acknowledged|done\s*and|complete\s*(and|\.)|completed\s*successfully|no\s*additional|verified\s*and)/i;
313
+ if (firstWordPattern.test(firstWords)) return true;
314
+
315
+ // If easyFinalAnswer mode is on, also match response starting with "✅" or numbered confirmation lists
316
+ if (this.easyFinalAnswer) {
317
+ if (trimmed.startsWith("✅") || /^[\d\.\-\*]/.test(trimmed)) return true;
318
+ }
319
+
320
+ return false;
321
+ }
322
+
296
323
  getEnabledTools() {
297
324
  return this.tools
298
325
  .getTools()
@@ -686,6 +713,7 @@ export abstract class BaseAgent implements IAgent {
686
713
  messages,
687
714
  tools: this.getEnabledTools(),
688
715
  tool_choice: "auto",
716
+ long_ttl_cache: this.runTime() > 300_000,
689
717
  });
690
718
 
691
719
  // If the agent was paused while the completion was in-flight, wait here
@@ -705,7 +733,7 @@ export abstract class BaseAgent implements IAgent {
705
733
  "warn"
706
734
  );
707
735
  const error = response as any;
708
- if ("response" in error && "data" in error.response) {
736
+ if (error != null && "response" in error && "data" in error.response) {
709
737
  this.log(
710
738
  `Response data: ${JSON.stringify(error.response.data, null, 2)}`,
711
739
  "warn"
@@ -826,6 +854,18 @@ export abstract class BaseAgent implements IAgent {
826
854
 
827
855
  // Early exit: not required to call tool
828
856
  const firstMessage = response.choices[0].message;
857
+ // Auto-detect termination words: if the model is just saying "Done", "Complete", etc.
858
+ if (
859
+ response.choices.length === 1 &&
860
+ firstMessage.content &&
861
+ this.isTerminationResponse(firstMessage.content)
862
+ ) {
863
+ this.log(`Termination word detected: "${firstMessage.content.trim()}", treating as finalAnswer`);
864
+ this.status = this.eventTypes.done;
865
+ this.agentEvents.emit(this.eventTypes.done, firstMessage.content);
866
+ return firstMessage.content;
867
+ }
868
+
829
869
  if (
830
870
  response.choices.length === 1 &&
831
871
  firstMessage.content &&
@@ -879,7 +919,7 @@ export abstract class BaseAgent implements IAgent {
879
919
  this.logStatus();
880
920
 
881
921
  const continuation = `<Workflow>
882
- workflow continues until you call one of ${this.requiredToolNames}.\n
922
+ workflow continues until you call one of ${JSON.stringify(this.requiredToolNames)}.\n
883
923
  ${statusMessage}
884
924
  </Workflow>`;
885
925
 
@@ -925,7 +965,7 @@ export abstract class BaseAgent implements IAgent {
925
965
 
926
966
  this.log(`Agent failed: ${e}`, "error");
927
967
 
928
- if ("response" in e && "data" in e.response) {
968
+ if (e != null && typeof e === "object" && "response" in e && "data" in (e as any).response) {
929
969
  this.log(
930
970
  `Error response data: ${JSON.stringify(e.response.data, null, 2)}`,
931
971
  "error"
@@ -1042,7 +1082,7 @@ export abstract class BaseAgent implements IAgent {
1042
1082
  toolCalls: ToolCall[],
1043
1083
  response: CompletionResponse
1044
1084
  ): { role: string; content: string } | null {
1045
- const outputTokens: number = response?.usage?.output_tokens || 0;
1085
+ const outputTokens: number = response?.usage?.completion_tokens || 0;
1046
1086
  const totalArgLength = toolCalls.reduce(
1047
1087
  (sum, tc) => sum + (tc.function?.arguments?.length || 0),
1048
1088
  0
@@ -3,6 +3,7 @@ import { promisify } from "util";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import * as os from "os";
6
+ import { services, ToolsService } from "../../services";
6
7
 
7
8
  export const execAsync = promisify(exec);
8
9
 
@@ -256,16 +257,32 @@ const execWithTimeout = async (
256
257
  };
257
258
 
258
259
  // Public tool
259
- export const execCommand = async (
260
+ export async function execCommand(
260
261
  command: string,
261
262
  timeout?: number,
262
263
  continueInBackground?: boolean,
263
264
  logFileName?: string
264
- ): Promise<string> => {
265
- if(!command || typeof command !== "string") {
265
+ ): Promise<string> {
266
+ if (!command || typeof command !== "string") {
266
267
  throw new Error("Invalid command. We received a non-string value. Please ensure you are sending strings of 4k tokens or less.");
267
268
  }
268
269
 
270
+ // Get context from bound ToolsService (same pattern as writeFile)
271
+ const toolService = (
272
+ this instanceof ToolsService ? this : services().Tools
273
+ ) as ToolsService;
274
+ const context = toolService.getContext();
275
+
276
+ // Emit pre-run blocking event — handlers can throw to block the command
277
+ if (context.Events) {
278
+ await context.Events.emitBlocking("exec:pre-run", {
279
+ command,
280
+ timeout,
281
+ continueInBackground,
282
+ logFileName,
283
+ });
284
+ }
285
+
269
286
  const { stdout, stderr, timedOut, killed, pid, logPath } =
270
287
  await execWithTimeout(command, {
271
288
  timeout,
@@ -298,6 +315,32 @@ export const execCommand = async (
298
315
  * : "";
299
316
  */
300
317
 
301
- // return `$ ${command}${statusMsg}\n${trimmed}${trimmedMsg}`;
302
- return `$ ${command}${statusMsg}\n${output}`;
303
- };
318
+ const result = `$ ${command}${statusMsg}\n${output}`;
319
+
320
+ // Emit post-run blocking event — handlers can append extra context
321
+ let eventResults: any[] = [];
322
+ if (context.Events) {
323
+ eventResults = await context.Events.emitBlocking("exec:post-run", {
324
+ command,
325
+ timeout,
326
+ continueInBackground,
327
+ logFileName,
328
+ stdout,
329
+ stderr,
330
+ timedOut,
331
+ killed,
332
+ pid,
333
+ logPath,
334
+ output,
335
+ });
336
+ }
337
+
338
+ // Append any additional context returned by post-run handlers
339
+ let eventResultsText = "";
340
+ if (eventResults && eventResults.length > 0) {
341
+ eventResultsText =
342
+ "\n\nAdditional Information:\n" + JSON.stringify(eventResults, null, 2);
343
+ }
344
+
345
+ return result + eventResultsText;
346
+ };
@@ -30,11 +30,7 @@ import {
30
30
  Base64ImageProcessor,
31
31
  } from "../../processors/index";
32
32
  import { TaskInfo } from "../types";
33
- import {
34
- createAgent,
35
- agentConstructors,
36
- AgentName,
37
- } from "../../agents";
33
+ import { createAgent, agentConstructors, AgentName } from "../../agents";
38
34
  import { ToolCallEvent } from "../../agents/base/base";
39
35
  import { KnowhowSimpleClient } from "../../services/KnowhowClient";
40
36
 
@@ -80,7 +76,6 @@ export class AgentModule extends BaseChatModule {
80
76
  this.remoteSyncModule = module;
81
77
  }
82
78
 
83
-
84
79
  getCommands(): ChatCommand[] {
85
80
  return [
86
81
  {
@@ -260,7 +255,10 @@ export class AgentModule extends BaseChatModule {
260
255
  if (context) {
261
256
  // Create a temporary agent instance to read its default model/provider
262
257
  const agentContext = services().Agents.getAgentContext();
263
- const tempAgent = createAgent(agentName as AgentName, agentContext) as BaseAgent;
258
+ const tempAgent = createAgent(
259
+ agentName as AgentName,
260
+ agentContext
261
+ ) as BaseAgent;
264
262
  context.selectedAgent = tempAgent;
265
263
  context.agentMode = true;
266
264
  context.currentAgent = agentName;
@@ -455,7 +453,6 @@ export class AgentModule extends BaseChatModule {
455
453
  const agentNames = Object.keys(agentConstructors);
456
454
 
457
455
  if (agentNames.length > 0) {
458
-
459
456
  console.log("\nAvailable agents:");
460
457
  agentNames.forEach((name) => {
461
458
  console.log(` - ${name}`);
@@ -503,39 +500,52 @@ export class AgentModule extends BaseChatModule {
503
500
  console.error(`Session ${sessionId} not found.`);
504
501
  return;
505
502
  }
506
- const lastThread = session.threads[session.threads.length - 1];
507
503
  console.log(`\n🔄 Resuming session: ${sessionId}`);
508
504
  console.log(`Agent: ${session.agentName}`);
509
505
  console.log(`Original task: ${session.initialInput}`);
510
506
  console.log(`Status: ${session.status}`);
511
507
 
512
- const reason = resumeReason
513
- ? `Reason for resuming: ${resumeReason}`
514
- : "";
515
-
516
- // Create resume prompt
517
- const resumePrompt = `You are resuming a previously started task. Here's the context:
518
- ORIGINAL REQUEST:
519
- ${session.initialInput}
520
-
521
- LAST Progress State:
522
- ${JSON.stringify(lastThread)}
523
-
524
- Please continue from where you left off and complete the original request.
525
- ${reason}
526
-
527
- `;
508
+ // Build resume prompt (same pattern as resumeFromMessages)
509
+ const resumePrompt = [
510
+ "You are resuming a previously started task.",
511
+ session.initialInput ? `ORIGINAL REQUEST: ${session.initialInput}` : "",
512
+ "Please continue from where you left off and complete the original request.",
513
+ resumeReason ? `Reason for resuming: ${resumeReason}` : "",
514
+ ]
515
+ .filter(Boolean)
516
+ .join("\n");
517
+
518
+ // Restore the full message history from the last thread
519
+ const threads = session.threads || [];
520
+ const lastThread = threads.length > 0 ? threads[threads.length - 1] : [];
521
+ const resumeMessages = [...lastThread];
522
+
523
+ // Append the resume prompt to the last user message (or add a new one)
524
+ const reversedIndex = [...lastThread]
525
+ .reverse()
526
+ .findIndex((e) => e.role === "user" && typeof e.content === "string");
527
+
528
+ if (reversedIndex === -1) {
529
+ resumeMessages.push({ role: "user", content: resumePrompt });
530
+ } else {
531
+ const actualIndex = lastThread.length - 1 - reversedIndex;
532
+ resumeMessages[actualIndex] = {
533
+ ...resumeMessages[actualIndex],
534
+ content: resumeMessages[actualIndex].content + `\n\n<Workflow>[RESUME CONTEXT]: ${resumePrompt}</Workflow>`,
535
+ };
536
+ }
528
537
 
529
538
  console.log("🚀 Session resuming...");
530
539
  const context = this.chatService?.getContext();
531
540
  const agentName = session.agentName || context.currentAgent;
541
+ const previousAgentMode = context?.agentMode;
532
542
 
533
543
  if (!agentName || !agentConstructors[agentName as AgentName]) {
534
544
  console.error(`Agent ${agentName} not found.`);
535
545
  return;
536
546
  }
537
547
 
538
- // Start agent with Knowhow task context if available
548
+ // Start agent with Knowhow task context and restored message history
539
549
  const { agent, taskId } = await this.setupAgent({
540
550
  agentName,
541
551
  input: resumePrompt,
@@ -544,7 +554,20 @@ Please continue from where you left off and complete the original request.
544
554
  chatHistory: [],
545
555
  run: false, // Don't run yet, we need to set up event listeners first
546
556
  });
547
- await this.attachedAgentChatLoop(taskId, agent, resumePrompt);
557
+
558
+ // After resume finishes, revert to normal chat (non-agent mode) so the
559
+ // user can start a fresh conversation instead of staying locked in agent mode.
560
+ agent.agentEvents.once(agent.eventTypes.done, () => {
561
+ const ctx = this.chatService?.getContext();
562
+ if (ctx && !previousAgentMode) {
563
+ ctx.agentMode = false;
564
+ ctx.selectedAgent = undefined;
565
+ ctx.currentAgent = undefined;
566
+ this.chatService?.disableMode("agent");
567
+ }
568
+ });
569
+
570
+ await this.attachedAgentChatLoop(taskId, agent, resumePrompt, resumeMessages);
548
571
  } catch (error) {
549
572
  console.error(
550
573
  `Failed to resume session ${sessionId}:`,
@@ -1033,7 +1056,8 @@ Please continue from where you left off and complete the original request.
1033
1056
  async attachedAgentChatLoop(
1034
1057
  taskId: string,
1035
1058
  agent: AttachableAgent,
1036
- initialInput?: string
1059
+ initialInput?: string,
1060
+ initialMessages?: any[]
1037
1061
  ): Promise<void> {
1038
1062
  try {
1039
1063
  let agentFinalOutput: string | undefined;
@@ -1075,7 +1099,7 @@ Please continue from where you left off and complete the original request.
1075
1099
 
1076
1100
  if (context.chatHistory) {
1077
1101
  const found = context.chatHistory.find((h) => h.taskId === taskId);
1078
- found.output = agentFinalOutput;
1102
+ if (found) found.output = agentFinalOutput;
1079
1103
  }
1080
1104
 
1081
1105
  resolve("done");
@@ -1088,7 +1112,8 @@ Please continue from where you left off and complete the original request.
1088
1112
  if (initialInput) {
1089
1113
  const taskInfo = this.taskRegistry.get(taskId);
1090
1114
  (agent as BaseAgent).call(
1091
- taskInfo?.formattedPrompt || taskInfo?.initialInput || initialInput
1115
+ taskInfo?.formattedPrompt || taskInfo?.initialInput || initialInput,
1116
+ initialMessages
1092
1117
  );
1093
1118
  }
1094
1119
  } catch (error) {
@@ -304,15 +304,20 @@ export class SessionsModule extends BaseChatModule {
304
304
  async handleResumeCommand(args: string[]): Promise<void> {
305
305
  const sessionManager = this.agentModule.getSessionManager();
306
306
 
307
- if (args.length === 0) {
307
+ if (args.length === 0 || args[0] === "--all") {
308
308
  // Interactive: show saved sessions for selection
309
- const savedSessions = sessionManager.listAvailableSessions();
309
+ const showAll = args[0] === "--all";
310
+ const allSessions = sessionManager.listAvailableSessions();
311
+ const savedSessions = showAll ? allSessions : allSessions.slice(0, 10);
310
312
  if (savedSessions.length === 0) {
311
313
  console.log("No saved sessions found to resume.");
312
314
  return;
313
315
  }
314
316
 
315
317
  this.printSavedSessionsTable(savedSessions);
318
+ if (!showAll && allSessions.length > 10) {
319
+ console.log(` ... and ${allSessions.length - 10} more. Use /resume --all to see all sessions.`);
320
+ }
316
321
 
317
322
  const allIds = savedSessions.map((s) => s.sessionId);
318
323
  const selectedId = await this.selectByNumber(
package/src/cli.ts CHANGED
@@ -530,12 +530,17 @@ async function main() {
530
530
  .description("Create or sync a cloud worker with your local knowhow config")
531
531
  .option("--create", "Create a new cloud worker with synced config and files")
532
532
  .option("--push <uid>", "Push/sync local config and files to an existing cloud worker")
533
+ .option("--pull <id>", "Pull the latest workerConfigJson from a cloud worker and update local config")
533
534
  .option("--name <name>", "Name for the cloud worker (used with --create)")
534
535
  .option("--dry-run", "Print what would be synced without doing it")
535
536
  .action(async (options) => {
536
537
  try {
537
- const { cloudWorker } = await import("./cloudWorker");
538
- await cloudWorker(options);
538
+ const { cloudWorker, pullCloudWorkerConfig } = await import("./cloudWorker");
539
+ if (options.pull) {
540
+ await pullCloudWorkerConfig({ id: options.pull });
541
+ } else {
542
+ await cloudWorker(options);
543
+ }
539
544
  } catch (error) {
540
545
  console.error("Error running cloudworker:", error);
541
546
  process.exit(1);
@@ -40,11 +40,11 @@ export class GenericAnthropicClient implements GenericClient {
40
40
  });
41
41
  }
42
42
 
43
- handleToolCaching(tools: Anthropic.Tool[]) {
43
+ handleToolCaching(tools: Anthropic.Tool[], longTtl = false) {
44
44
  const lastTool = tools[tools.length - 1];
45
45
 
46
46
  if (lastTool) {
47
- lastTool.cache_control = { type: "ephemeral" };
47
+ lastTool.cache_control = longTtl ? { type: "ephemeral", ttl: "1h" } as any : { type: "ephemeral" };
48
48
  }
49
49
  }
50
50
 
@@ -94,7 +94,7 @@ export class GenericAnthropicClient implements GenericClient {
94
94
  return cleaned;
95
95
  }
96
96
 
97
- transformTools(tools?: Tool[]): Anthropic.Tool[] {
97
+ transformTools(tools?: Tool[], longTtl = false): Anthropic.Tool[] {
98
98
  if (!tools) {
99
99
  return [];
100
100
  }
@@ -104,7 +104,7 @@ export class GenericAnthropicClient implements GenericClient {
104
104
  input_schema: this.cleanSchemaForAnthropic(tool.function.parameters) as any,
105
105
  }));
106
106
 
107
- this.handleToolCaching(transformed);
107
+ this.handleToolCaching(transformed, longTtl);
108
108
 
109
109
  return transformed;
110
110
  }
@@ -153,16 +153,16 @@ export class GenericAnthropicClient implements GenericClient {
153
153
  return messages;
154
154
  }
155
155
 
156
- cacheLastContent(message: MessageParam) {
156
+ cacheLastContent(message: MessageParam, longTtl = false) {
157
157
  if (Array.isArray(message.content)) {
158
158
  const lastMessage = message.content[message.content.length - 1];
159
159
  if (
160
160
  lastMessage.type !== "thinking" &&
161
161
  lastMessage.type !== "redacted_thinking"
162
162
  ) {
163
- lastMessage.cache_control = {
164
- type: "ephemeral",
165
- };
163
+ lastMessage.cache_control = longTtl
164
+ ? ({ type: "ephemeral", ttl: "1h" } as any)
165
+ : { type: "ephemeral" };
166
166
  }
167
167
  }
168
168
  }
@@ -179,7 +179,7 @@ export class GenericAnthropicClient implements GenericClient {
179
179
  }
180
180
  }
181
181
 
182
- handleMessageCaching(groupedMessages: MessageParam[]) {
182
+ handleMessageCaching(groupedMessages: MessageParam[], longTtl = false) {
183
183
  this.handleClearingCache(groupedMessages);
184
184
 
185
185
  // find the last two messages and mark them as ephemeral
@@ -189,7 +189,7 @@ export class GenericAnthropicClient implements GenericClient {
189
189
 
190
190
  for (const m of lastTwoUserMessages) {
191
191
  if (Array.isArray(m.content)) {
192
- this.cacheLastContent(m);
192
+ this.cacheLastContent(m, longTtl);
193
193
  }
194
194
  }
195
195
  }
@@ -203,11 +203,11 @@ export class GenericAnthropicClient implements GenericClient {
203
203
  }
204
204
  }
205
205
 
206
- transformMessages(messages: Message[]): MessageParam[] {
206
+ transformMessages(messages: Message[], longTtl = false): MessageParam[] {
207
207
  const toolCalls = messages.flatMap((msg) => msg.tool_calls || []);
208
208
  const claudeMessages: MessageParam[] = messages
209
209
  .filter((msg) => msg.role !== "system")
210
- .filter((msg) => msg.content)
210
+ .filter((msg) => msg.content || msg.role === "tool")
211
211
  .map((msg) => {
212
212
  if (msg.role === "tool") {
213
213
  const toolCall = toolCalls.find((tc) => tc.id === msg.tool_call_id);
@@ -302,7 +302,7 @@ export class GenericAnthropicClient implements GenericClient {
302
302
 
303
303
  const groupedMessages = this.combineMessages(claudeMessages);
304
304
 
305
- this.handleMessageCaching(groupedMessages);
305
+ this.handleMessageCaching(groupedMessages, longTtl);
306
306
 
307
307
  return groupedMessages;
308
308
  }
@@ -349,14 +349,15 @@ export class GenericAnthropicClient implements GenericClient {
349
349
  async createChatCompletion(
350
350
  options: CompletionOptions
351
351
  ): Promise<CompletionResponse> {
352
+ const longTtl = !!options.long_ttl_cache;
352
353
  const systemMessage = options.messages
353
354
  .filter((msg) => msg.role === "system")
354
355
  .map((msg) => msg.content || "")
355
356
  .join("\n");
356
357
 
357
- const claudeMessages = this.transformMessages(options.messages);
358
+ const claudeMessages = this.transformMessages(options.messages, longTtl);
358
359
 
359
- const tools = this.transformTools(options.tools);
360
+ const tools = this.transformTools(options.tools, longTtl);
360
361
  try {
361
362
  const response = await this.client.messages.create({
362
363
  model: options.model,
@@ -365,7 +366,7 @@ export class GenericAnthropicClient implements GenericClient {
365
366
  ? [
366
367
  {
367
368
  text: systemMessage,
368
- cache_control: { type: "ephemeral" },
369
+ cache_control: longTtl ? ({ type: "ephemeral", ttl: "1h" } as any) : { type: "ephemeral" },
369
370
  type: "text",
370
371
  },
371
372
  ]
@@ -412,11 +413,19 @@ export class GenericAnthropicClient implements GenericClient {
412
413
  }),
413
414
 
414
415
  model: options.model,
415
- usage: response.usage,
416
+ usage: response.usage ? {
417
+ input_tokens: response.usage.input_tokens ?? 0,
418
+ output_tokens: response.usage.output_tokens ?? 0,
419
+ prompt_tokens: response.usage.input_tokens ?? 0,
420
+ completion_tokens: response.usage.output_tokens ?? 0,
421
+ total_tokens: (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0),
422
+ cache_creation_input_tokens: response.usage.cache_creation_input_tokens ?? 0,
423
+ cache_read_input_tokens: response.usage.cache_read_input_tokens ?? 0,
424
+ } : undefined,
416
425
  usd_cost: this.calculateCost(options.model, response.usage),
417
426
  };
418
427
  } catch (err) {
419
- if ("headers" in err && err.headers["x-should-retry"] === "true") {
428
+ if ("headers" in err && err.headers?.["x-should-retry"] === "true") {
420
429
  console.warn("Retrying failed request", err);
421
430
  await wait(2500);
422
431
  return this.createChatCompletion(options);