@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.
- package/autodoc/chat-guide.md +1 -0
- package/package.json +5 -2
- package/scripts/build-for-node.sh +4 -4
- package/src/agents/base/base.ts +254 -29
- package/src/agents/patcher/patcher.ts +1 -1
- package/src/agents/researcher/researcher.ts +2 -2
- package/src/agents/tools/agentCall.ts +6 -0
- package/src/agents/tools/connectAgent.ts +353 -0
- package/src/agents/tools/index.ts +6 -0
- package/src/agents/tools/list.ts +48 -0
- package/src/agents/tools/observe.ts +245 -0
- package/src/agents/tools/replyToParent.ts +122 -0
- package/src/agents/tools/runGenerate.ts +209 -0
- package/src/agents/tools/sendAgentMessage.ts +138 -0
- package/src/agents/tools/startAgentTask.ts +92 -5
- package/src/agents/tools/waitForAgentCompleted.ts +269 -0
- package/src/ai.ts +1 -0
- package/src/chat/modules/AgentModule.ts +89 -27
- package/src/chat/modules/SessionsModule.ts +6 -10
- package/src/clients/contextLimits.ts +26 -0
- package/src/clients/pricing/anthropic.ts +12 -0
- package/src/commands/agent.ts +22 -0
- package/src/commands/agents.ts +83 -0
- package/src/commands/hash.ts +3 -2
- package/src/commands/misc.ts +6 -0
- package/src/commands/services.ts +14 -0
- package/src/generate.ts +25 -3
- package/src/services/AgentService.ts +21 -3
- package/src/services/AgentSyncFs.ts +56 -0
- package/src/services/AgentSyncKnowhowWeb.ts +1 -1
- package/src/services/EventService.ts +4 -1
- package/src/services/KnowhowClient.ts +7 -0
- package/src/services/LazyToolsService.ts +7 -3
- package/src/services/MinimalToolsService.ts +15 -7
- package/src/services/SyncedAgentWatcher.ts +2 -0
- package/src/services/SyncerService.ts +35 -6
- package/src/services/Tools.ts +43 -3
- package/src/services/modules/index.ts +127 -46
- package/src/types.ts +5 -0
- package/tests/agents/interrupt.test.ts +11 -18
- package/tests/agents/observe-base.test.ts +183 -0
- package/tests/agents/tools/connectAgent.test.ts +138 -0
- package/tests/agents/tools/observe.test.ts +179 -0
- package/tests/agents/tools/replyToParent.test.ts +125 -0
- package/tests/agents/tools/sendAgentMessage.test.ts +99 -0
- package/tests/agents/tools/waitForAgentCompleted.test.ts +147 -0
- package/tests/chat/detach-attach-rendering.test.ts +315 -0
- package/tests/clients/contextLimits.test.ts +23 -1
- package/tests/clients/pricing.test.ts +20 -1
- package/tests/services/Tools.test.ts +172 -9
- package/tests/unit/modules/moduleLoading.test.ts +69 -0
- package/ts_build/package.json +5 -2
- package/ts_build/src/agents/base/base.d.ts +24 -1
- package/ts_build/src/agents/base/base.js +111 -16
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/patcher/patcher.js +1 -1
- package/ts_build/src/agents/patcher/patcher.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +2 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/agentCall.js.map +1 -1
- package/ts_build/src/agents/tools/connectAgent.d.ts +21 -0
- package/ts_build/src/agents/tools/connectAgent.js +284 -0
- package/ts_build/src/agents/tools/connectAgent.js.map +1 -0
- package/ts_build/src/agents/tools/index.d.ts +6 -0
- package/ts_build/src/agents/tools/index.js +6 -0
- package/ts_build/src/agents/tools/index.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +40 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/observe.d.ts +20 -0
- package/ts_build/src/agents/tools/observe.js +144 -0
- package/ts_build/src/agents/tools/observe.js.map +1 -0
- package/ts_build/src/agents/tools/replyToParent.d.ts +4 -0
- package/ts_build/src/agents/tools/replyToParent.js +118 -0
- package/ts_build/src/agents/tools/replyToParent.js.map +1 -0
- package/ts_build/src/agents/tools/runGenerate.d.ts +19 -0
- package/ts_build/src/agents/tools/runGenerate.js +138 -0
- package/ts_build/src/agents/tools/runGenerate.js.map +1 -0
- package/ts_build/src/agents/tools/sendAgentMessage.d.ts +11 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js +118 -0
- package/ts_build/src/agents/tools/sendAgentMessage.js.map +1 -0
- package/ts_build/src/agents/tools/startAgentTask.d.ts +7 -0
- package/ts_build/src/agents/tools/startAgentTask.js +53 -6
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/agents/tools/waitForAgentCompleted.d.ts +10 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js +229 -0
- package/ts_build/src/agents/tools/waitForAgentCompleted.js.map +1 -0
- package/ts_build/src/ai.d.ts +1 -0
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.d.ts +3 -1
- package/ts_build/src/chat/modules/AgentModule.js +46 -21
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +1 -6
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +21 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/clients/pricing/anthropic.d.ts +2 -0
- package/ts_build/src/clients/pricing/anthropic.js +9 -0
- package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
- package/ts_build/src/commands/agent.js +11 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/agents.js +61 -0
- package/ts_build/src/commands/agents.js.map +1 -1
- package/ts_build/src/commands/hash.js +1 -1
- package/ts_build/src/commands/hash.js.map +1 -1
- package/ts_build/src/commands/misc.js +2 -0
- package/ts_build/src/commands/misc.js.map +1 -1
- package/ts_build/src/commands/services.js +10 -0
- package/ts_build/src/commands/services.js.map +1 -1
- package/ts_build/src/generate.d.ts +2 -0
- package/ts_build/src/generate.js +11 -3
- package/ts_build/src/generate.js.map +1 -1
- package/ts_build/src/services/AgentService.d.ts +4 -1
- package/ts_build/src/services/AgentService.js +9 -2
- package/ts_build/src/services/AgentService.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +2 -0
- package/ts_build/src/services/AgentSyncFs.js +35 -0
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -1
- package/ts_build/src/services/EventService.d.ts +4 -1
- package/ts_build/src/services/EventService.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.js +3 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.d.ts +2 -2
- package/ts_build/src/services/LazyToolsService.js +2 -2
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/MinimalToolsService.d.ts +2 -2
- package/ts_build/src/services/MinimalToolsService.js +6 -6
- package/ts_build/src/services/MinimalToolsService.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +1 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/SyncerService.d.ts +2 -1
- package/ts_build/src/services/SyncerService.js +8 -2
- package/ts_build/src/services/SyncerService.js.map +1 -1
- package/ts_build/src/services/Tools.d.ts +6 -1
- package/ts_build/src/services/Tools.js +9 -3
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +1 -1
- package/ts_build/src/services/modules/index.js +67 -27
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/types.d.ts +3 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/tests/agents/interrupt.test.d.ts +0 -1
- package/ts_build/tests/agents/interrupt.test.js +3 -13
- package/ts_build/tests/agents/interrupt.test.js.map +1 -1
- package/ts_build/tests/agents/observe-base.test.d.ts +1 -0
- package/ts_build/tests/agents/observe-base.test.js +149 -0
- package/ts_build/tests/agents/observe-base.test.js.map +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js +142 -0
- package/ts_build/tests/agents/tools/connectAgent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/observe.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/observe.test.js +148 -0
- package/ts_build/tests/agents/tools/observe.test.js.map +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js +116 -0
- package/ts_build/tests/agents/tools/replyToParent.test.js.map +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js +112 -0
- package/ts_build/tests/agents/tools/sendAgentMessage.test.js.map +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js +147 -0
- package/ts_build/tests/agents/tools/waitForAgentCompleted.test.js.map +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.d.ts +1 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js +168 -0
- package/ts_build/tests/chat/detach-attach-rendering.test.js.map +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +11 -1
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -1
- package/ts_build/tests/clients/pricing.test.js +10 -1
- package/ts_build/tests/clients/pricing.test.js.map +1 -1
- package/ts_build/tests/services/Tools.test.js +126 -8
- package/ts_build/tests/services/Tools.test.js.map +1 -1
- package/ts_build/tests/unit/modules/moduleLoading.test.js +84 -0
- package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
package/src/commands/agents.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/hash.ts
CHANGED
|
@@ -90,8 +90,9 @@ export function addHashCommand(program: Command): void {
|
|
|
90
90
|
|
|
91
91
|
const currentHash = hashFiles(inputFiles);
|
|
92
92
|
|
|
93
|
-
// --save mode: record
|
|
94
|
-
|
|
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);
|
package/src/commands/misc.ts
CHANGED
|
@@ -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
|
});
|
package/src/commands/services.ts
CHANGED
|
@@ -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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
|
120
|
+
return {
|
|
121
|
+
answer: `Agent ${name} not found. Options are: ${this.listAgents()}`,
|
|
122
|
+
costUsd: 0,
|
|
123
|
+
};
|
|
111
124
|
}
|
|
112
|
-
|
|
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
|
*/
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
/**
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/services/Tools.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|