@tyvm/knowhow 0.0.137 → 0.0.140

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 (173) hide show
  1. package/autodoc/chat-guide.md +1 -0
  2. package/package.json +5 -2
  3. package/scripts/build-for-node.sh +4 -4
  4. package/src/agents/base/base.ts +254 -29
  5. package/src/agents/patcher/patcher.ts +1 -1
  6. package/src/agents/researcher/researcher.ts +2 -2
  7. package/src/agents/tools/agentCall.ts +6 -0
  8. package/src/agents/tools/connectAgent.ts +353 -0
  9. package/src/agents/tools/index.ts +6 -0
  10. package/src/agents/tools/list.ts +48 -0
  11. package/src/agents/tools/observe.ts +245 -0
  12. package/src/agents/tools/replyToParent.ts +122 -0
  13. package/src/agents/tools/runGenerate.ts +209 -0
  14. package/src/agents/tools/sendAgentMessage.ts +138 -0
  15. package/src/agents/tools/startAgentTask.ts +92 -5
  16. package/src/agents/tools/waitForAgentCompleted.ts +269 -0
  17. package/src/ai.ts +1 -0
  18. package/src/chat/modules/AgentModule.ts +89 -27
  19. package/src/chat/modules/SessionsModule.ts +6 -10
  20. package/src/clients/contextLimits.ts +26 -0
  21. package/src/clients/pricing/anthropic.ts +12 -0
  22. package/src/commands/agent.ts +22 -0
  23. package/src/commands/agents.ts +83 -0
  24. package/src/commands/hash.ts +3 -2
  25. package/src/commands/misc.ts +6 -0
  26. package/src/commands/services.ts +14 -0
  27. package/src/generate.ts +25 -3
  28. package/src/services/AgentService.ts +21 -3
  29. package/src/services/AgentSyncFs.ts +56 -0
  30. package/src/services/AgentSyncKnowhowWeb.ts +1 -1
  31. package/src/services/EventService.ts +4 -1
  32. package/src/services/KnowhowClient.ts +7 -0
  33. package/src/services/LazyToolsService.ts +7 -3
  34. package/src/services/MinimalToolsService.ts +15 -7
  35. package/src/services/SyncedAgentWatcher.ts +2 -0
  36. package/src/services/SyncerService.ts +35 -6
  37. package/src/services/Tools.ts +43 -3
  38. package/src/services/modules/index.ts +127 -46
  39. package/src/types.ts +5 -0
  40. package/tests/agents/interrupt.test.ts +11 -18
  41. package/tests/agents/observe-base.test.ts +183 -0
  42. package/tests/agents/tools/connectAgent.test.ts +138 -0
  43. package/tests/agents/tools/observe.test.ts +179 -0
  44. package/tests/agents/tools/replyToParent.test.ts +125 -0
  45. package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
  46. package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
  47. package/tests/chat/detach-attach-rendering.test.ts +315 -0
  48. package/tests/clients/contextLimits.test.ts +23 -1
  49. package/tests/clients/pricing.test.ts +20 -1
  50. package/tests/services/Tools.test.ts +172 -9
  51. package/tests/unit/modules/moduleLoading.test.ts +69 -0
  52. package/ts_build/package.json +5 -2
  53. package/ts_build/src/agents/base/base.d.ts +24 -1
  54. package/ts_build/src/agents/base/base.js +111 -16
  55. package/ts_build/src/agents/base/base.js.map +1 -1
  56. package/ts_build/src/agents/patcher/patcher.js +1 -1
  57. package/ts_build/src/agents/patcher/patcher.js.map +1 -1
  58. package/ts_build/src/agents/researcher/researcher.js +2 -2
  59. package/ts_build/src/agents/researcher/researcher.js.map +1 -1
  60. package/ts_build/src/agents/tools/agentCall.js.map +1 -1
  61. package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
  62. package/ts_build/src/agents/tools/connectAgent.js +284 -0
  63. package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
  64. package/ts_build/src/agents/tools/index.d.ts +6 -0
  65. package/ts_build/src/agents/tools/index.js +6 -0
  66. package/ts_build/src/agents/tools/index.js.map +1 -1
  67. package/ts_build/src/agents/tools/list.js +40 -0
  68. package/ts_build/src/agents/tools/list.js.map +1 -1
  69. package/ts_build/src/agents/tools/observe.d.ts +20 -0
  70. package/ts_build/src/agents/tools/observe.js +144 -0
  71. package/ts_build/src/agents/tools/observe.js.map +1 -0
  72. package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
  73. package/ts_build/src/agents/tools/replyToParent.js +118 -0
  74. package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
  75. package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
  76. package/ts_build/src/agents/tools/runGenerate.js +138 -0
  77. package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
  78. package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
  79. package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
  80. package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
  81. package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
  82. package/ts_build/src/agents/tools/startAgentTask.js +53 -6
  83. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  84. package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
  85. package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
  86. package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
  87. package/ts_build/src/ai.d.ts +1 -0
  88. package/ts_build/src/ai.js.map +1 -1
  89. package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
  90. package/ts_build/src/chat/modules/AgentModule.js +46 -21
  91. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  92. package/ts_build/src/chat/modules/SessionsModule.js +1 -6
  93. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  94. package/ts_build/src/clients/contextLimits.js +21 -0
  95. package/ts_build/src/clients/contextLimits.js.map +1 -1
  96. package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
  97. package/ts_build/src/clients/pricing/anthropic.js +9 -0
  98. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  99. package/ts_build/src/commands/agent.js +11 -0
  100. package/ts_build/src/commands/agent.js.map +1 -1
  101. package/ts_build/src/commands/agents.js +61 -0
  102. package/ts_build/src/commands/agents.js.map +1 -1
  103. package/ts_build/src/commands/hash.js +1 -1
  104. package/ts_build/src/commands/hash.js.map +1 -1
  105. package/ts_build/src/commands/misc.js +2 -0
  106. package/ts_build/src/commands/misc.js.map +1 -1
  107. package/ts_build/src/commands/services.js +10 -0
  108. package/ts_build/src/commands/services.js.map +1 -1
  109. package/ts_build/src/generate.d.ts +2 -0
  110. package/ts_build/src/generate.js +11 -3
  111. package/ts_build/src/generate.js.map +1 -1
  112. package/ts_build/src/services/AgentService.d.ts +4 -1
  113. package/ts_build/src/services/AgentService.js +9 -2
  114. package/ts_build/src/services/AgentService.js.map +1 -1
  115. package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
  116. package/ts_build/src/services/AgentSyncFs.js +35 -0
  117. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  118. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
  119. package/ts_build/src/services/EventService.d.ts +4 -1
  120. package/ts_build/src/services/EventService.js.map +1 -1
  121. package/ts_build/src/services/KnowhowClient.js +3 -0
  122. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  123. package/ts_build/src/services/LazyToolsService.d.ts +2 -2
  124. package/ts_build/src/services/LazyToolsService.js +2 -2
  125. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  126. package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
  127. package/ts_build/src/services/MinimalToolsService.js +6 -6
  128. package/ts_build/src/services/MinimalToolsService.js.map +1 -1
  129. package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
  130. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  131. package/ts_build/src/services/SyncerService.d.ts +2 -1
  132. package/ts_build/src/services/SyncerService.js +8 -2
  133. package/ts_build/src/services/SyncerService.js.map +1 -1
  134. package/ts_build/src/services/Tools.d.ts +6 -1
  135. package/ts_build/src/services/Tools.js +9 -3
  136. package/ts_build/src/services/Tools.js.map +1 -1
  137. package/ts_build/src/services/modules/index.d.ts +1 -1
  138. package/ts_build/src/services/modules/index.js +67 -27
  139. package/ts_build/src/services/modules/index.js.map +1 -1
  140. package/ts_build/src/types.d.ts +3 -0
  141. package/ts_build/src/types.js.map +1 -1
  142. package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
  143. package/ts_build/tests/agents/interrupt.test.js +3 -13
  144. package/ts_build/tests/agents/interrupt.test.js.map +1 -1
  145. package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
  146. package/ts_build/tests/agents/observe-base.test.js +149 -0
  147. package/ts_build/tests/agents/observe-base.test.js.map +1 -0
  148. package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
  149. package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
  150. package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
  151. package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
  152. package/ts_build/tests/agents/tools/observe.test.js +148 -0
  153. package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
  154. package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
  155. package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
  156. package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
  157. package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
  158. package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
  159. package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
  160. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
  161. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
  162. package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
  163. package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
  164. package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
  165. package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
  166. package/ts_build/tests/clients/contextLimits.test.js +11 -1
  167. package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
  168. package/ts_build/tests/clients/pricing.test.js +10 -1
  169. package/ts_build/tests/clients/pricing.test.js.map +1 -1
  170. package/ts_build/tests/services/Tools.test.js +126 -8
  171. package/ts_build/tests/services/Tools.test.js.map +1 -1
  172. package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
  173. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
@@ -493,6 +493,76 @@ async function cmdAttach(taskIdOrIndex: string | undefined, opts: { index?: stri
493
493
  await startChat(`/attach ${resolvedId}`);
494
494
  }
495
495
 
496
+ /**
497
+ * Print the final answer (the result of the agent's finalAnswer tool call) for
498
+ * a completed task. Reads `metadata.result`, falling back to the last assistant
499
+ * message in the thread if `result` isn't present. This lets a human read what
500
+ * a finished agent concluded with, even if they never attached before it ended.
501
+ */
502
+ async function cmdAnswer(
503
+ taskIdOrIndex: string | undefined,
504
+ opts: { index?: string; raw: boolean }
505
+ ): Promise<void> {
506
+ const value = opts.index ?? taskIdOrIndex;
507
+ if (!value) {
508
+ console.error("Provide a task ID or use -i <number> to select by index.");
509
+ process.exit(1);
510
+ }
511
+
512
+ const resolvedId = resolveTaskId(value, true);
513
+ if (!resolvedId) {
514
+ console.error(`Task not found: ${value}`);
515
+ const rows = buildRows(true);
516
+ if (rows.length > 0) {
517
+ console.error("\nAvailable tasks:");
518
+ rows.forEach((r, i) => console.error(` ${i + 1}. ${r.displayId}`));
519
+ }
520
+ process.exit(1);
521
+ return;
522
+ }
523
+
524
+ const meta = loadMeta(resolvedId);
525
+ if (!meta) {
526
+ console.error(`No metadata found for task: ${resolvedId}`);
527
+ process.exit(1);
528
+ return;
529
+ }
530
+
531
+ // Prefer the stored finalAnswer result; fall back to the last assistant message.
532
+ let answer: string | undefined =
533
+ typeof meta.result === "string" && meta.result.trim()
534
+ ? meta.result
535
+ : undefined;
536
+
537
+ if (!answer) {
538
+ const threads: any[][] = meta.threads ?? [];
539
+ if (threads.length > 0) {
540
+ answer = lastAssistantMessage(threads[threads.length - 1]);
541
+ }
542
+ }
543
+
544
+ if (opts.raw) {
545
+ console.log(answer ?? "");
546
+ return;
547
+ }
548
+
549
+ const status = getStatus(resolvedId, meta);
550
+ if (!answer) {
551
+ console.log(`\n📭 No final answer available for task: ${resolvedId}`);
552
+ console.log(` Status: ${status}`);
553
+ if (status !== "completed") {
554
+ console.log(` The agent hasn't finished yet — attach with 'knowhow agents attach' to follow it.`);
555
+ }
556
+ return;
557
+ }
558
+
559
+ console.log(`\n💬 Final answer for task: ${resolvedId}`);
560
+ console.log(` Agent: ${meta.agentName ?? "?"} | Status: ${status} | Cost: ${meta.totalCostUsd != null ? "$" + (meta.totalCostUsd as number).toFixed(4) : "?"}`);
561
+ console.log("─".repeat(80));
562
+ console.log(answer);
563
+ console.log("─".repeat(80) + "\n");
564
+ }
565
+
496
566
 
497
567
 
498
568
  // ---------------------------------------------------------------------------
@@ -553,4 +623,17 @@ export function addAgentsCommand(program: Command): void {
553
623
  .action(async (taskId: string | undefined, opts: { index?: string }) => {
554
624
  await cmdAttach(taskId, opts);
555
625
  });
626
+
627
+ agents
628
+ .command("answer [taskId]")
629
+ .description(
630
+ "Print the final answer (finalAnswer result) of a completed agent task, formatted for reading. " +
631
+ "Reads metadata.result, falling back to the last assistant message. " +
632
+ "Accepts a full task ID, partial substring, or -i <number> for row index from 'agents list'."
633
+ )
634
+ .option("-i, --index <number>", "Select task by row number from 'agents list'")
635
+ .option("--raw", "Print only the raw answer text (no header/formatting)", false)
636
+ .action(async (taskId: string | undefined, opts: { index?: string; raw: boolean }) => {
637
+ await cmdAnswer(taskId, opts);
638
+ });
556
639
  }
@@ -90,8 +90,9 @@ export function addHashCommand(program: Command): void {
90
90
 
91
91
  const currentHash = hashFiles(inputFiles);
92
92
 
93
- // --save mode: record into .hashes.json under hashes[name][inputGlob] = hash
94
- if (opts.save) {
93
+ // Standalone --save mode (no --run): record hash immediately and exit.
94
+ // When --run is also present, --save is handled inside the --run branch below.
95
+ if (opts.save && !opts.run) {
95
96
  if (!hashes[opts.name]) hashes[opts.name] = {};
96
97
  hashes[opts.name][opts.input] = currentHash;
97
98
  await saveHashes(hashes);
@@ -66,6 +66,10 @@ export function addGenerateCommand(program: Command): void {
66
66
  "--sync-fs",
67
67
  "Enable filesystem sync for agent-driven sources (creates .knowhow/processes/agents/<taskId>/ so tasks appear in `knowhow agents list`)"
68
68
  )
69
+ .option(
70
+ "--sync-remote",
71
+ "Push agent-driven sources' work to a remote Knowhow task (identified by the source's taskId)"
72
+ )
69
73
  .option(
70
74
  "--force",
71
75
  "Skip hash checks and regenerate all matching sources regardless of whether inputs/outputs have changed"
@@ -83,6 +87,7 @@ export function addGenerateCommand(program: Command): void {
83
87
  name?: string;
84
88
  filter?: string;
85
89
  syncFs?: boolean;
90
+ syncRemote?: boolean;
86
91
  force?: boolean;
87
92
  concurrency?: number;
88
93
  plan?: boolean;
@@ -99,6 +104,7 @@ export function addGenerateCommand(program: Command): void {
99
104
  name: options.name,
100
105
  filter: options.filter,
101
106
  syncFs: options.syncFs,
107
+ syncRemote: options.syncRemote,
102
108
  force: options.force,
103
109
  concurrency: options.concurrency,
104
110
  });
@@ -49,6 +49,20 @@ export async function setupServices() {
49
49
 
50
50
  Tools.addContext("Mcp", Mcp);
51
51
 
52
+ // The `agentCall` tool + function are registered by AgentService on the
53
+ // global ToolsService (AllTools), not on the LazyToolsService we build here.
54
+ // Copy it across so agents/scripts using this Tools instance can delegate to
55
+ // other agents via agentCall (matching startAgentTask availability).
56
+ try {
57
+ const agentCallDef = AllTools.getTool?.("agentCall");
58
+ const agentCallFn = AllTools.getFunction?.("agentCall");
59
+ if (agentCallDef) Tools.addTool(agentCallDef);
60
+ if (agentCallFn) Tools.setFunction("agentCall", agentCallFn);
61
+ } catch (_) {
62
+ // Non-fatal: if agentCall isn't registered yet, agents can still use
63
+ // startAgentTask for delegation.
64
+ }
65
+
52
66
  Agents.setAgentContext(agentContext);
53
67
 
54
68
  console.log("🔌 Connecting to MCP...");
package/src/generate.ts CHANGED
@@ -51,6 +51,12 @@ export interface GenerateOptions {
51
51
  * so they appear in `knowhow agents list`. Overrides the per-source syncFs flag.
52
52
  */
53
53
  syncFs?: boolean;
54
+ /**
55
+ * When true, agent-driven sources push their work to a remote Knowhow task
56
+ * (identified by the source's taskId). Overrides the per-source syncRemote
57
+ * flag. Requires the sources to run as agents with a known taskId.
58
+ */
59
+ syncRemote?: boolean;
54
60
  /** When true, skip hash checks and regenerate all matching sources regardless of changes */
55
61
  force?: boolean;
56
62
  /**
@@ -58,6 +64,14 @@ export interface GenerateOptions {
58
64
  * dependency wave. Defaults to 3. Set to 1 for fully sequential execution.
59
65
  */
60
66
  concurrency?: number;
67
+ /**
68
+ * In-memory generation sources. When provided, these are used INSTEAD of the
69
+ * sources defined in `.knowhow/knowhow.json`. This enables callers (e.g. an
70
+ * agent using the `runGenerate` tool) to define arbitrary pipelines at
71
+ * runtime without persisting them to config — supporting complex task
72
+ * orchestration with `dependsOn`, per-source `agent`, analysis, etc.
73
+ */
74
+ sources?: GenerationSource[];
61
75
  }
62
76
 
63
77
  // ---------------------------------------------------------------------------
@@ -416,12 +430,14 @@ async function runSource(
416
430
  console.log("Generating", source.input, "to", source.output);
417
431
 
418
432
  const effectiveSyncFs = options.syncFs ?? source.syncFs;
433
+ const effectiveSyncRemote = options.syncRemote ?? source.syncRemote;
419
434
  const agentOpts: AgentOptions | undefined = source.agent
420
435
  ? {
421
436
  syncFs: effectiveSyncFs,
437
+ syncRemote: effectiveSyncRemote,
422
438
  taskId:
423
439
  source.taskId ??
424
- (effectiveSyncFs
440
+ (effectiveSyncFs || effectiveSyncRemote
425
441
  ? `generate:${source.output.replace(/[^a-zA-Z0-9_.-]/g, "_")}`
426
442
  : undefined),
427
443
  maxTimeLimit: source.maxTimeLimit,
@@ -469,8 +485,14 @@ async function runSource(
469
485
  // ---------------------------------------------------------------------------
470
486
 
471
487
  export async function generate(options: GenerateOptions = {}): Promise<void> {
472
- const config = await getConfig();
473
- const allSources = config.sources ?? [];
488
+ let allSources: GenerationSource[];
489
+ if (options.sources && options.sources.length > 0) {
490
+ // In-memory pipeline definition — bypass config entirely.
491
+ allSources = options.sources;
492
+ } else {
493
+ const config = await getConfig();
494
+ allSources = config.sources ?? [];
495
+ }
474
496
  const sources = allSources.filter((s) => sourceMatchesFilter(s, options));
475
497
 
476
498
  if ((options.name || options.filter) && sources.length === 0) {
@@ -104,11 +104,29 @@ export class AgentService {
104
104
  }
105
105
  }
106
106
 
107
- public async callAgent(name: string, query: string): Promise<string> {
107
+ /**
108
+ * Run a registered agent and return both its final answer and the cost it
109
+ * incurred. Returning cost (from the agent's own `getTotalCostUsd()`) lets
110
+ * callers — notably the script sandbox — account subagent spend against a
111
+ * budget. The `answer` string preserves the previous plain-string behavior
112
+ * for consumers that only care about the response.
113
+ */
114
+ public async callAgent(
115
+ name: string,
116
+ query: string
117
+ ): Promise<{ answer: string; costUsd: number }> {
108
118
  const agent = this.agents.get(name);
109
119
  if (!agent) {
110
- return `Agent ${name} not found. Options are: ${this.listAgents()}`;
120
+ return {
121
+ answer: `Agent ${name} not found. Options are: ${this.listAgents()}`,
122
+ costUsd: 0,
123
+ };
111
124
  }
112
- return agent.call(query);
125
+ const answer = await agent.call(query);
126
+ const costUsd =
127
+ typeof (agent as any).getTotalCostUsd === "function"
128
+ ? (agent as any).getTotalCostUsd()
129
+ : 0;
130
+ return { answer, costUsd };
113
131
  }
114
132
  }
@@ -11,6 +11,8 @@ export interface FsSyncOptions {
11
11
  taskId: string;
12
12
  prompt: string;
13
13
  agentName?: string;
14
+ /** The taskId of the parent agent that spawned this task, if any. */
15
+ parentTaskId?: string;
14
16
  }
15
17
 
16
18
  /**
@@ -62,8 +64,15 @@ export class AgentSyncFs {
62
64
  await this.writeInput("");
63
65
  await this.writeMetadata({
64
66
  taskId: this.taskId,
67
+ // Record the OS process id of this agent subprocess so orchestrating
68
+ // tools (e.g. waitForAgentCompleted) can detect a crashed-but-not-
69
+ // finalized task via process-liveness. This is the child's own pid,
70
+ // so it needs no coordination with the spawner and never touches
71
+ // execCommand's child processes.
72
+ pid: process.pid,
65
73
  prompt: options.prompt,
66
74
  agentName: options.agentName || "unknown",
75
+ ...(options.parentTaskId ? { parentTaskId: options.parentTaskId } : {}),
67
76
  startTime: new Date().toISOString(),
68
77
  status: "running",
69
78
  });
@@ -492,11 +501,58 @@ export class AgentSyncFs {
492
501
  console.error(`❌ Error cleaning up ${entry.name}:`, error);
493
502
  }
494
503
  }
504
+
505
+ // Also clean up old execCommand background log files in .knowhow/processes/*.txt
506
+ await AgentSyncFs.cleanupOldProcessLogs(threeDaysMs);
495
507
  } catch (error) {
496
508
  console.error(`❌ Error during cleanup:`, error);
497
509
  }
498
510
  }
499
511
 
512
+ /**
513
+ * Clean up old execCommand background log files (.knowhow/processes/*.txt).
514
+ * These are created by the execCommand tool when running background/long-lived
515
+ * commands, and are never cleaned up otherwise.
516
+ */
517
+ private static async cleanupOldProcessLogs(maxAgeMs: number): Promise<void> {
518
+ try {
519
+ // execCommand writes logs to the parent of the agents dir: .knowhow/processes
520
+ const processesPath = path.dirname(AgentSyncFs.sharedBasePath);
521
+
522
+ try {
523
+ await fs.access(processesPath);
524
+ } catch {
525
+ return;
526
+ }
527
+
528
+ const entries = await fs.readdir(processesPath, { withFileTypes: true });
529
+ const now = Date.now();
530
+
531
+ for (const entry of entries) {
532
+ // Only target the .txt log files that execCommand creates; leave the
533
+ // agents/ directory (and any other subdirectories) alone.
534
+ if (!entry.isFile() || !entry.name.endsWith(".txt")) continue;
535
+
536
+ const logPath = path.join(processesPath, entry.name);
537
+
538
+ try {
539
+ const stats = await fs.stat(logPath);
540
+ const age = now - stats.mtimeMs;
541
+
542
+ if (age > maxAgeMs) {
543
+ console.log(`🧹 Cleaning up old process log file: ${entry.name}`);
544
+ await fs.rm(logPath, { force: true });
545
+ }
546
+ } catch (error) {
547
+ // Skip if we can't stat or delete
548
+ console.error(`❌ Error cleaning up log ${entry.name}:`, error);
549
+ }
550
+ }
551
+ } catch (error) {
552
+ console.error(`❌ Error during process log cleanup:`, error);
553
+ }
554
+ }
555
+
500
556
  /**
501
557
  * Start periodic cleanup process
502
558
  */
@@ -13,7 +13,7 @@ import { wait } from "../utils";
13
13
 
14
14
  export interface SyncOptions {
15
15
  messageId?: string;
16
- existingKnowhowTaskId?: string;
16
+ syncRemote?: boolean;
17
17
  prompt: string;
18
18
  }
19
19
 
@@ -255,7 +255,10 @@ export class EventService extends EventEmitter {
255
255
  this.emit(this.eventTypes.agentsRegister, { name: agent.name, agent });
256
256
  }
257
257
 
258
- callAgent(name: string, query: string): Promise<string> {
258
+ callAgent(
259
+ name: string,
260
+ query: string
261
+ ): Promise<{ answer: string; costUsd: number }> {
259
262
  return new Promise((resolve, reject) => {
260
263
  this.emit(this.eventTypes.agentsCall, { name, query, resolve, reject });
261
264
  });
@@ -122,6 +122,13 @@ export interface GitCredentialResponse {
122
122
  }
123
123
 
124
124
  export function loadKnowhowJwt(): string {
125
+ // Check env var first — this is how cloud workers/CI runners get auth
126
+ // (injected via KNOWHOW_JWT env var in the runner's .env file by GithubRunnerService).
127
+ // This avoids depending on a potentially stale .knowhow/.jwt file from the snapshot.
128
+ if (process.env.KNOWHOW_JWT) {
129
+ return process.env.KNOWHOW_JWT;
130
+ }
131
+
125
132
  const jwtFile = path.join(process.cwd(), ".knowhow", ".jwt");
126
133
  if (!fs.existsSync(jwtFile)) {
127
134
  return "";
@@ -1,5 +1,5 @@
1
1
  import { minimatch } from "minimatch";
2
- import { ToolsService, ToolContext } from "./Tools";
2
+ import { ToolsService, ToolContext, ToolCallContext } from "./Tools";
3
3
  import { Tool } from "../clients/types";
4
4
  import { ToolCall } from "../clients/types";
5
5
  import {
@@ -140,7 +140,11 @@ export class LazyToolsService extends ToolsService {
140
140
  }
141
141
 
142
142
  // Override callTool to auto-enable tools that are in allTools but not yet enabled
143
- async callTool(toolCall: ToolCall, enabledTools?: string[]) {
143
+ async callTool(
144
+ toolCall: ToolCall,
145
+ enabledTools?: string[],
146
+ callContext?: ToolCallContext
147
+ ) {
144
148
  const functionName = toolCall.function.name;
145
149
 
146
150
  // If the tool isn't currently visible but exists in allTools, auto-enable it.
@@ -160,7 +164,7 @@ export class LazyToolsService extends ToolsService {
160
164
 
161
165
  // Always use the current enabled tool names after any auto-enable above,
162
166
  // so the base class check sees the freshly-enabled tool in the allowed list.
163
- return super.callTool(toolCall, this.getToolNames());
167
+ return super.callTool(toolCall, this.getToolNames(), callContext);
164
168
  }
165
169
 
166
170
  // Internal: Update visible tools based on patterns
@@ -1,4 +1,4 @@
1
- import { ToolsService, ToolContext } from "./Tools";
1
+ import { ToolsService, ToolContext, ToolCallContext } from "./Tools";
2
2
  import { Tool, ToolCall } from "../clients/types";
3
3
  import { callTool } from "../agents/tools/minimal/callTool";
4
4
  import { inspectTools } from "../agents/tools/minimal/inspectTools";
@@ -107,7 +107,11 @@ export class MinimalToolsService extends ToolsService {
107
107
  * Used both by direct calls (agent calls the tool by its real name) and by
108
108
  * the callTool meta-tool (agent calls callTool({ name, args })).
109
109
  */
110
- private async dispatchToTool(toolCall: ToolCall, resolvedName: string) {
110
+ private async dispatchToTool(
111
+ toolCall: ToolCall,
112
+ resolvedName: string,
113
+ callContext?: ToolCallContext
114
+ ) {
111
115
  // Temporarily surface the target tool in the visible list so base
112
116
  // ToolsService.callTool can find its definition, then restore state.
113
117
  const alreadyVisible = super
@@ -123,7 +127,7 @@ export class MinimalToolsService extends ToolsService {
123
127
  }
124
128
  }
125
129
 
126
- const result = await super.callTool(toolCall, [resolvedName]);
130
+ const result = await super.callTool(toolCall, [resolvedName], callContext);
127
131
 
128
132
  // Remove the temporarily-added tool to keep the visible set stable
129
133
  if (!alreadyVisible) {
@@ -136,7 +140,11 @@ export class MinimalToolsService extends ToolsService {
136
140
  /**
137
141
  * Delegates to the full catalog for resolution, then runs via callToolByName.
138
142
  */
139
- async callTool(toolCall: ToolCall, enabledTools?: string[]): Promise<any> {
143
+ async callTool(
144
+ toolCall: ToolCall,
145
+ enabledTools?: string[],
146
+ callContext?: ToolCallContext
147
+ ): Promise<any> {
140
148
  const functionName = toolCall.function.name;
141
149
 
142
150
  // Base tools (callTool, inspectTools, finalAnswer) → standard dispatch
@@ -145,7 +153,7 @@ export class MinimalToolsService extends ToolsService {
145
153
  .some((t) => t.function.name === functionName);
146
154
 
147
155
  if (isBaseTool) {
148
- return super.callTool(toolCall, this.getToolNames());
156
+ return super.callTool(toolCall, this.getToolNames(), callContext);
149
157
  }
150
158
 
151
159
  // Extended tools: called directly by name (not via the callTool meta-tool).
@@ -158,10 +166,10 @@ export class MinimalToolsService extends ToolsService {
158
166
  );
159
167
 
160
168
  if (existsInCatalog) {
161
- return this.dispatchToTool(toolCall, functionName);
169
+ return this.dispatchToTool(toolCall, functionName, callContext);
162
170
  }
163
171
 
164
172
  // Not found anywhere - let the base class produce the standard error.
165
- return super.callTool(toolCall, [functionName]);
173
+ return super.callTool(toolCall, [functionName], callContext);
166
174
  }
167
175
  }
@@ -58,6 +58,8 @@ export interface AttachableAgent {
58
58
  unpause(): void | Promise<void>;
59
59
  kill(): void | Promise<void>;
60
60
  interrupt(message?: string): void | Promise<void>;
61
+ /** Force conversation compaction at the next loop iteration (local agents only). */
62
+ requestCompact?(): void;
61
63
  addPendingUserMessage(message: Message): void;
62
64
  }
63
65
 
@@ -13,10 +13,16 @@ export interface SyncerOptions {
13
13
  messageId?: string;
14
14
  /** Force fs sync even if messageId is provided */
15
15
  syncFs?: boolean;
16
- /** Use an existing Knowhow task ID instead of creating a new one */
17
- existingKnowhowTaskId?: string;
16
+ /**
17
+ * Push the agent's work to a remote Knowhow task. When set (and there is no
18
+ * messageId), the `taskId` is treated as the remote task ID to attach to and
19
+ * push updates to, rather than creating a new remote task.
20
+ */
21
+ syncRemote?: boolean;
18
22
  /** Agent name to persist in metadata.json */
19
23
  agentName?: string;
24
+ /** The taskId of the parent agent that spawned this task, if any. */
25
+ parentTaskId?: string;
20
26
  }
21
27
 
22
28
  export interface AgentSyncer {
@@ -37,7 +43,8 @@ export interface AgentSyncer {
37
43
  *
38
44
  * Decision logic:
39
45
  * - Always sets up AgentSyncFs (primary local syncer)
40
- * - If messageId is present AND syncFs is not forced AND no existingKnowhowTaskId → also sets up AgentSyncKnowhowWeb
46
+ * - If messageId is present AND syncFs is not forced → creates a new remote task via AgentSyncKnowhowWeb
47
+ * - If syncRemote is set (and no messageId) → attaches to the remote task identified by taskId
41
48
  */
42
49
  export class SyncerService implements AgentSyncer {
43
50
  private fsSync: AgentSyncFs;
@@ -59,11 +66,23 @@ export class SyncerService implements AgentSyncer {
59
66
  this.active = true;
60
67
  this.useWebSync = false;
61
68
 
62
- // Determine whether to use web sync
69
+ // Determine whether to CREATE a new remote (web) task: only when we have a
70
+ // messageId and aren't forcing fs-only. A messageId means "definitely
71
+ // remote" — create a fresh remote task from that message.
63
72
  const shouldUseWebSync =
64
73
  !!options.messageId &&
65
- !options.syncFs &&
66
- !options.existingKnowhowTaskId;
74
+ !options.syncFs;
75
+
76
+ // Determine whether to ATTACH to an already-existing remote task and push
77
+ // local work to it (no creation needed). This is the "--sync-remote" path:
78
+ // the caller wants to push work to the remote task identified by taskId.
79
+ // We only auto-attach when there is NO messageId (messageId always creates
80
+ // a fresh remote task above).
81
+ //
82
+ // This is orthogonal to fs sync: we can push to the remote task AND keep
83
+ // the local .knowhow/processes/agents/<id>/ files in sync at the same time.
84
+ const shouldAttachExisting =
85
+ !!options.syncRemote && !options.messageId && !!options.taskId;
67
86
 
68
87
  // Always create fs sync task first
69
88
  console.log(
@@ -73,6 +92,7 @@ export class SyncerService implements AgentSyncer {
73
92
  taskId: options.taskId,
74
93
  prompt: options.prompt,
75
94
  agentName: options.agentName,
95
+ parentTaskId: options.parentTaskId,
76
96
  });
77
97
 
78
98
  // Optionally create web sync task
@@ -89,6 +109,15 @@ export class SyncerService implements AgentSyncer {
89
109
  }
90
110
  }
91
111
 
112
+ // Attach live web sync to an existing remote task (push updates to it).
113
+ if (!shouldUseWebSync && shouldAttachExisting) {
114
+ this.useWebSync = true;
115
+ this.createdTaskId = options.taskId;
116
+ console.log(
117
+ `🌐 Attaching web sync to existing remote task: ${options.taskId}`
118
+ );
119
+ }
120
+
92
121
  return fsTaskId;
93
122
  }
94
123
 
@@ -16,6 +16,28 @@ import { PluginService } from "../plugins/plugins";
16
16
  import { McpService } from "./Mcp";
17
17
  import { BehaviorsService } from "./BehaviorsService";
18
18
 
19
+ /**
20
+ * Per-invocation context passed into a tool call as `_ctx`.
21
+ *
22
+ * This is threaded through `callTool` and delivered to the tool implementation
23
+ * as the LAST positional argument (for positional tools) or merged onto the
24
+ * args object under the `_ctx` key (for non-positional tools).
25
+ *
26
+ * It enables self-referential tools: the calling agent can pass itself in via
27
+ * `caller`, so tools can read `caller.getCurrentTaskId()`, call
28
+ * `caller.addPendingUserMessage(...)`, subscribe to its events, etc. — without
29
+ * relying on shared mutable state on the ToolsService (which would break under
30
+ * concurrent tool usage).
31
+ */
32
+ export interface ToolCallContext {
33
+ /** The agent (or other entity) that initiated this tool call. */
34
+ caller?: any;
35
+ /** The task id of the calling agent, if known. */
36
+ taskId?: string;
37
+ /** Arbitrary additional per-call context. */
38
+ [key: string]: any;
39
+ }
40
+
19
41
  export interface ToolContext {
20
42
  Agents?: AgentService;
21
43
  Events?: EventService;
@@ -174,7 +196,11 @@ export class ToolsService {
174
196
  this.addFunctions(functions);
175
197
  }
176
198
 
177
- async callTool(toolCall: ToolCall, enabledTools = this.getToolNames()) {
199
+ async callTool(
200
+ toolCall: ToolCall,
201
+ enabledTools = this.getToolNames(),
202
+ callContext?: ToolCallContext
203
+ ) {
178
204
  const functionName = toolCall.function.name;
179
205
  let functionArgs: any;
180
206
 
@@ -219,10 +245,21 @@ export class ToolsService {
219
245
  const properties = toolDefinition?.function?.parameters?.properties || {};
220
246
  const isPositional =
221
247
  toolDefinition?.function?.parameters?.positional || false;
222
- const fnArgs = isPositional
248
+
249
+ // Thread the per-call context (`_ctx`) into the invocation so tools can be
250
+ // self-referential (e.g. read the calling agent off `_ctx.caller`).
251
+ // - positional tools receive `_ctx` as the LAST positional argument
252
+ // - non-positional tools receive it merged onto the args object as `_ctx`
253
+ const ctx: ToolCallContext = { ...(callContext || {}), Tools: this };
254
+
255
+ const positionalArgs = isPositional
223
256
  ? Object.keys(properties).map((p) => functionArgs[p])
224
257
  : functionArgs;
225
258
 
259
+ const fnArgs = isPositional
260
+ ? [...positionalArgs, ctx]
261
+ : { ...functionArgs, _ctx: ctx };
262
+
226
263
  // Execute the function
227
264
  const rawResponse = isPositional
228
265
  ? functionToCall.call(this, ...fnArgs)
@@ -250,7 +287,10 @@ export class ToolsService {
250
287
  // Handle special case for parallel tool use
251
288
  if (functionName === "multi_tool_use.parallel") {
252
289
  // Extract tool_calls array from arguments
253
- const toolCallsArg = Array.isArray(fnArgs) ? fnArgs : [fnArgs];
290
+ // Use the original (un-`_ctx`-augmented) args for extraction.
291
+ const toolCallsArg = Array.isArray(functionArgs)
292
+ ? functionArgs
293
+ : [functionArgs];
254
294
  const toolCalls = Array.isArray(toolCallsArg) ? toolCallsArg : [];
255
295
 
256
296
  const calls = toolCalls as {