@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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { observe, stopObserving } from "../../../src/agents/tools/observe";
|
|
2
|
+
import { ObservationSource } from "../../../src/agents/base/base";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* observe is a self-referential tool: it reads the calling agent + ToolsService
|
|
6
|
+
* off _ctx, builds a tool-poll ObservationSource, and delegates to the calling
|
|
7
|
+
* agent's own observe()/stopObserving() methods (which own the per-agent
|
|
8
|
+
* observation registry). These tests use a fake caller that implements the same
|
|
9
|
+
* per-agent observe()/stopObserving() contract as BaseAgent.
|
|
10
|
+
*/
|
|
11
|
+
describe("observe / stopObserving", () => {
|
|
12
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
13
|
+
|
|
14
|
+
// Minimal faithful reimplementation of BaseAgent.observe/stopObserving so the
|
|
15
|
+
// delegation is exercised end-to-end without constructing a full BaseAgent.
|
|
16
|
+
function makeCaller() {
|
|
17
|
+
const messages: any[] = [];
|
|
18
|
+
const observations = new Map<string, any>();
|
|
19
|
+
let seq = 0;
|
|
20
|
+
const done = "done";
|
|
21
|
+
const caller: any = {
|
|
22
|
+
currentTaskId: "task-x",
|
|
23
|
+
name: "Tester",
|
|
24
|
+
status: "in_progress",
|
|
25
|
+
getEnabledToolNames: () => ["fakeTool"],
|
|
26
|
+
addPendingMessage: (m: any) => messages.push(m),
|
|
27
|
+
observe(source: ObservationSource, opts: any = {}) {
|
|
28
|
+
const {
|
|
29
|
+
onlyOnChange = true,
|
|
30
|
+
maxUpdates = 20,
|
|
31
|
+
maxDurationMs = 10 * 60 * 1000,
|
|
32
|
+
} = opts;
|
|
33
|
+
seq += 1;
|
|
34
|
+
const safeLabel = source.label.replace(/[^A-Za-z0-9]+/g, "_").slice(0, 24);
|
|
35
|
+
const id = `obs_${safeLabel}_${Date.now()}_${seq}`;
|
|
36
|
+
let last: string | undefined;
|
|
37
|
+
const emit = (datum: unknown) => {
|
|
38
|
+
if (caller.status === done) {
|
|
39
|
+
caller.stopObserving(id);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const str =
|
|
43
|
+
typeof datum === "string" ? datum : JSON.stringify(datum);
|
|
44
|
+
if (onlyOnChange && str === last) return;
|
|
45
|
+
last = str;
|
|
46
|
+
const obs = observations.get(id);
|
|
47
|
+
if (!obs) return;
|
|
48
|
+
obs.updates += 1;
|
|
49
|
+
caller.addPendingMessage({
|
|
50
|
+
role: "user",
|
|
51
|
+
content: `[observe:${id}] ${source.label} (update ${obs.updates}/${maxUpdates}):\n${str}`,
|
|
52
|
+
});
|
|
53
|
+
if (obs.updates >= maxUpdates) caller.stopObserving(id, "max updates reached");
|
|
54
|
+
};
|
|
55
|
+
const teardownPromise = Promise.resolve(source.start(emit));
|
|
56
|
+
const expiry = maxDurationMs
|
|
57
|
+
? setTimeout(() => caller.stopObserving(id, "max duration reached"), maxDurationMs)
|
|
58
|
+
: undefined;
|
|
59
|
+
expiry?.unref?.();
|
|
60
|
+
observations.set(id, { id, label: source.label, updates: 0, teardownPromise, expiry });
|
|
61
|
+
return id;
|
|
62
|
+
},
|
|
63
|
+
stopObserving(id?: string, reason = "stopped") {
|
|
64
|
+
const ids = id ? [id] : [...observations.keys()];
|
|
65
|
+
let stopped = 0;
|
|
66
|
+
for (const oid of ids) {
|
|
67
|
+
const obs = observations.get(oid);
|
|
68
|
+
if (!obs) continue;
|
|
69
|
+
if (obs.expiry) clearTimeout(obs.expiry);
|
|
70
|
+
Promise.resolve(obs.teardownPromise).then(
|
|
71
|
+
(fn: any) => typeof fn === "function" && fn()
|
|
72
|
+
);
|
|
73
|
+
observations.delete(oid);
|
|
74
|
+
stopped += 1;
|
|
75
|
+
}
|
|
76
|
+
return id
|
|
77
|
+
? stopped
|
|
78
|
+
? `Stopped observer ${id} (${reason}).`
|
|
79
|
+
: `No active observer found with id: ${id}.`
|
|
80
|
+
: `Stopped ${stopped} active observer(s) (${reason}).`;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return { caller, messages };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function makeCtx(toolResults: string[]) {
|
|
87
|
+
const { caller, messages } = makeCaller();
|
|
88
|
+
let call = 0;
|
|
89
|
+
const Tools = {
|
|
90
|
+
callTool: jest.fn(async () => {
|
|
91
|
+
const r = toolResults[Math.min(call, toolResults.length - 1)];
|
|
92
|
+
call++;
|
|
93
|
+
return r;
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
return { _ctx: { caller, Tools }, messages, Tools, caller };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
it("requires toolName", async () => {
|
|
100
|
+
const res = await observe({ toolName: "" });
|
|
101
|
+
expect(res).toMatch(/toolName is required/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("errors without a ToolsService on _ctx", async () => {
|
|
105
|
+
const res = await observe({ toolName: "fakeTool", _ctx: { caller: {} } });
|
|
106
|
+
expect(res).toMatch(/no ToolsService/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("errors without a caller on _ctx", async () => {
|
|
110
|
+
const res = await observe({
|
|
111
|
+
toolName: "fakeTool",
|
|
112
|
+
_ctx: { Tools: { callTool: jest.fn() } as any },
|
|
113
|
+
});
|
|
114
|
+
expect(res).toMatch(/no calling agent/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("polls the tool and delivers changed results to the caller", async () => {
|
|
118
|
+
const { _ctx, messages, Tools } = makeCtx(["a", "b", "b", "c"]);
|
|
119
|
+
|
|
120
|
+
const res = await observe({
|
|
121
|
+
toolName: "fakeTool",
|
|
122
|
+
intervalMs: 500, // clamped to min 500
|
|
123
|
+
maxUpdates: 4,
|
|
124
|
+
_ctx: _ctx as any,
|
|
125
|
+
});
|
|
126
|
+
expect(res).toMatch(/Started observing/);
|
|
127
|
+
|
|
128
|
+
// Wait for ~4 polls
|
|
129
|
+
await delay(2400);
|
|
130
|
+
|
|
131
|
+
// Tool should have been called several times
|
|
132
|
+
expect(Tools.callTool).toHaveBeenCalled();
|
|
133
|
+
// Delivered updates: only changed values (a, b, c) => 3 update messages.
|
|
134
|
+
const updateMsgs = messages.filter((m) =>
|
|
135
|
+
String(m.content).includes("[observe:")
|
|
136
|
+
);
|
|
137
|
+
expect(updateMsgs.length).toBeGreaterThanOrEqual(3);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("can be stopped early via stopObserving with the returned id", async () => {
|
|
141
|
+
const { _ctx, messages } = makeCtx(["x", "y", "z"]);
|
|
142
|
+
const res = await observe({
|
|
143
|
+
toolName: "fakeTool",
|
|
144
|
+
intervalMs: 500,
|
|
145
|
+
maxUpdates: 20,
|
|
146
|
+
_ctx: _ctx as any,
|
|
147
|
+
});
|
|
148
|
+
const idMatch = res.match(/id: (obs_[^)]+)\)/);
|
|
149
|
+
expect(idMatch).toBeTruthy();
|
|
150
|
+
const id = idMatch![1];
|
|
151
|
+
|
|
152
|
+
await delay(600);
|
|
153
|
+
const stopRes = await stopObserving({ id, _ctx: _ctx as any });
|
|
154
|
+
expect(stopRes).toContain(`Stopped observer ${id}`);
|
|
155
|
+
|
|
156
|
+
const before = messages.length;
|
|
157
|
+
await delay(1000);
|
|
158
|
+
// No further updates after stopping.
|
|
159
|
+
expect(messages.length).toBe(before);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("stopObserving with no id stops all observers on that agent", async () => {
|
|
163
|
+
const { _ctx } = makeCtx(["1"]);
|
|
164
|
+
await observe({ toolName: "fakeTool", intervalMs: 1000, _ctx: _ctx as any });
|
|
165
|
+
const res = await stopObserving({ _ctx: _ctx as any });
|
|
166
|
+
expect(res).toMatch(/Stopped \d+ active observer/);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("stopObserving on an unknown id returns a helpful message", async () => {
|
|
170
|
+
const { _ctx } = makeCtx(["1"]);
|
|
171
|
+
const res = await stopObserving({ id: "obs_does_not_exist", _ctx: _ctx as any });
|
|
172
|
+
expect(res).toMatch(/No active observer found/);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("errors without a caller on _ctx for stopObserving", async () => {
|
|
176
|
+
const res = await stopObserving({ id: "x" });
|
|
177
|
+
expect(res).toMatch(/no calling agent/);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { replyToParent } from "../../../src/agents/tools/replyToParent";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* These tests exercise the `_ctx`-based self-referential mechanism:
|
|
8
|
+
* replyToParent discovers who is calling (and which parent spawned them)
|
|
9
|
+
* purely from the per-call context object threaded in as the last positional
|
|
10
|
+
* argument by ToolsService.callTool.
|
|
11
|
+
*/
|
|
12
|
+
describe("replyToParent (_ctx self-referential tool)", () => {
|
|
13
|
+
let tmpCwd: string;
|
|
14
|
+
let originalCwd: string;
|
|
15
|
+
let agentsDir: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
originalCwd = process.cwd();
|
|
19
|
+
tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), "reply-parent-"));
|
|
20
|
+
process.chdir(tmpCwd);
|
|
21
|
+
agentsDir = path.join(tmpCwd, ".knowhow", "processes", "agents");
|
|
22
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
process.chdir(originalCwd);
|
|
27
|
+
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function makeTask(taskId: string, meta: Record<string, any> = {}) {
|
|
31
|
+
const dir = path.join(agentsDir, taskId);
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(dir, "input.txt"), "", "utf8");
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
path.join(dir, "metadata.json"),
|
|
36
|
+
JSON.stringify({ taskId, ...meta }, null, 2),
|
|
37
|
+
"utf8"
|
|
38
|
+
);
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it("returns a helpful message when there is no parent", async () => {
|
|
43
|
+
makeTask("child-1"); // no parentTaskId
|
|
44
|
+
const result = await replyToParent("hi", { taskId: "child-1" });
|
|
45
|
+
expect(result).toMatch(/No parent task found/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("uses parentTaskId directly from _ctx when provided", async () => {
|
|
49
|
+
const parentDir = makeTask("parent-1");
|
|
50
|
+
makeTask("child-2", { parentTaskId: "parent-1" });
|
|
51
|
+
|
|
52
|
+
const result = await replyToParent("progress update", {
|
|
53
|
+
taskId: "child-2",
|
|
54
|
+
parentTaskId: "parent-1",
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result).toMatch(/Sent message to parent task parent-1/);
|
|
58
|
+
const parentInput = fs.readFileSync(
|
|
59
|
+
path.join(parentDir, "input.txt"),
|
|
60
|
+
"utf8"
|
|
61
|
+
);
|
|
62
|
+
expect(parentInput).toContain("progress update");
|
|
63
|
+
expect(parentInput).toContain("[from:child-2]");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("falls back to reading parentTaskId from the caller's metadata.json", async () => {
|
|
67
|
+
const parentDir = makeTask("parent-2");
|
|
68
|
+
makeTask("child-3", { parentTaskId: "parent-2" });
|
|
69
|
+
|
|
70
|
+
// No parentTaskId in _ctx — must be discovered from child-3's metadata.
|
|
71
|
+
const result = await replyToParent("need help", { taskId: "child-3" });
|
|
72
|
+
|
|
73
|
+
expect(result).toMatch(/Sent message to parent task parent-2/);
|
|
74
|
+
const parentInput = fs.readFileSync(
|
|
75
|
+
path.join(parentDir, "input.txt"),
|
|
76
|
+
"utf8"
|
|
77
|
+
);
|
|
78
|
+
expect(parentInput).toContain("need help");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("reads taskId/name off _ctx.caller when taskId not set directly", async () => {
|
|
82
|
+
const parentDir = makeTask("parent-3");
|
|
83
|
+
makeTask("child-4", { parentTaskId: "parent-3" });
|
|
84
|
+
|
|
85
|
+
const fakeAgent = { currentTaskId: "child-4", name: "Researcher" };
|
|
86
|
+
const result = await replyToParent("done researching", {
|
|
87
|
+
caller: fakeAgent,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(result).toMatch(/Sent message to parent task parent-3/);
|
|
91
|
+
const parentInput = fs.readFileSync(
|
|
92
|
+
path.join(parentDir, "input.txt"),
|
|
93
|
+
"utf8"
|
|
94
|
+
);
|
|
95
|
+
expect(parentInput).toContain("[agent:Researcher]");
|
|
96
|
+
expect(parentInput).toContain("done researching");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("appends rather than clobbers existing parent input", async () => {
|
|
100
|
+
const parentDir = makeTask("parent-4");
|
|
101
|
+
fs.writeFileSync(
|
|
102
|
+
path.join(parentDir, "input.txt"),
|
|
103
|
+
"existing message\n",
|
|
104
|
+
"utf8"
|
|
105
|
+
);
|
|
106
|
+
makeTask("child-5", { parentTaskId: "parent-4" });
|
|
107
|
+
|
|
108
|
+
await replyToParent("second message", {
|
|
109
|
+
taskId: "child-5",
|
|
110
|
+
parentTaskId: "parent-4",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const parentInput = fs.readFileSync(
|
|
114
|
+
path.join(parentDir, "input.txt"),
|
|
115
|
+
"utf8"
|
|
116
|
+
);
|
|
117
|
+
expect(parentInput).toContain("existing message");
|
|
118
|
+
expect(parentInput).toContain("second message");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns error for empty message", async () => {
|
|
122
|
+
const result = await replyToParent(" ", { taskId: "whatever" });
|
|
123
|
+
expect(result).toMatch(/No message provided/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { sendAgentMessage } from "../../../src/agents/tools/sendAgentMessage";
|
|
5
|
+
|
|
6
|
+
describe("sendAgentMessage", () => {
|
|
7
|
+
let tmpCwd: string;
|
|
8
|
+
let originalCwd: string;
|
|
9
|
+
let agentsDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalCwd = process.cwd();
|
|
13
|
+
tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), "send-msg-"));
|
|
14
|
+
process.chdir(tmpCwd);
|
|
15
|
+
agentsDir = path.join(tmpCwd, ".knowhow", "processes", "agents");
|
|
16
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.chdir(originalCwd);
|
|
21
|
+
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function makeTask(taskId: string): string {
|
|
25
|
+
const dir = path.join(agentsDir, taskId);
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
it("requires taskId and message", async () => {
|
|
31
|
+
expect(await sendAgentMessage({ taskId: "", message: "hi" })).toMatch(
|
|
32
|
+
/taskId is required/
|
|
33
|
+
);
|
|
34
|
+
expect(await sendAgentMessage({ taskId: "x", message: "" })).toMatch(
|
|
35
|
+
/message is required/
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("errors when the target task dir doesn't exist", async () => {
|
|
40
|
+
const res = await sendAgentMessage({ taskId: "nope", message: "hi" });
|
|
41
|
+
expect(res).toMatch(/target task directory not found/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("appends an envelope with sender identity from _ctx", async () => {
|
|
45
|
+
makeTask("child-1");
|
|
46
|
+
const res = await sendAgentMessage({
|
|
47
|
+
taskId: "child-1",
|
|
48
|
+
message: "do the thing",
|
|
49
|
+
_ctx: { taskId: "parent-1", caller: { name: "Developer" } },
|
|
50
|
+
});
|
|
51
|
+
expect(res).toMatch(/Sent message to task child-1/);
|
|
52
|
+
const input = fs.readFileSync(
|
|
53
|
+
path.join(agentsDir, "child-1", "input.txt"),
|
|
54
|
+
"utf8"
|
|
55
|
+
);
|
|
56
|
+
expect(input).toContain("[from:parent-1/Developer] do the thing");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("uses explicit from when provided", async () => {
|
|
60
|
+
makeTask("child-2");
|
|
61
|
+
await sendAgentMessage({
|
|
62
|
+
taskId: "child-2",
|
|
63
|
+
message: "msg",
|
|
64
|
+
from: "orchestrator",
|
|
65
|
+
});
|
|
66
|
+
const input = fs.readFileSync(
|
|
67
|
+
path.join(agentsDir, "child-2", "input.txt"),
|
|
68
|
+
"utf8"
|
|
69
|
+
);
|
|
70
|
+
expect(input).toContain("[from:orchestrator] msg");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("delivers as /poke when poke:true (interrupts recipient)", async () => {
|
|
74
|
+
makeTask("child-3");
|
|
75
|
+
await sendAgentMessage({
|
|
76
|
+
taskId: "child-3",
|
|
77
|
+
message: "stop and reconsider",
|
|
78
|
+
from: "boss",
|
|
79
|
+
poke: true,
|
|
80
|
+
});
|
|
81
|
+
const input = fs.readFileSync(
|
|
82
|
+
path.join(agentsDir, "child-3", "input.txt"),
|
|
83
|
+
"utf8"
|
|
84
|
+
);
|
|
85
|
+
expect(input).toMatch(/^\/poke \[from:boss\] stop and reconsider/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("appends without clobbering existing pending messages", async () => {
|
|
89
|
+
const dir = makeTask("child-4");
|
|
90
|
+
fs.writeFileSync(path.join(dir, "input.txt"), "existing line\n", "utf8");
|
|
91
|
+
await sendAgentMessage({
|
|
92
|
+
taskId: "child-4",
|
|
93
|
+
message: "second",
|
|
94
|
+
from: "a",
|
|
95
|
+
});
|
|
96
|
+
const input = fs.readFileSync(path.join(dir, "input.txt"), "utf8");
|
|
97
|
+
expect(input).toBe("existing line\n[from:a] second\n");
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { waitForAgentCompleted } from "../../../src/agents/tools/waitForAgentCompleted";
|
|
5
|
+
|
|
6
|
+
describe("waitForAgentCompleted", () => {
|
|
7
|
+
let tmpCwd: string;
|
|
8
|
+
let originalCwd: string;
|
|
9
|
+
let agentsDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalCwd = process.cwd();
|
|
13
|
+
tmpCwd = fs.mkdtempSync(path.join(os.tmpdir(), "wait-agent-"));
|
|
14
|
+
process.chdir(tmpCwd);
|
|
15
|
+
agentsDir = path.join(tmpCwd, ".knowhow", "processes", "agents");
|
|
16
|
+
fs.mkdirSync(agentsDir, { recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.chdir(originalCwd);
|
|
21
|
+
fs.rmSync(tmpCwd, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function makeTask(taskId: string, meta: Record<string, any> = {}, status?: string) {
|
|
25
|
+
const dir = path.join(agentsDir, taskId);
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(dir, "metadata.json"),
|
|
29
|
+
JSON.stringify({ taskId, ...meta }, null, 2),
|
|
30
|
+
"utf8"
|
|
31
|
+
);
|
|
32
|
+
if (status !== undefined) {
|
|
33
|
+
fs.writeFileSync(path.join(dir, "status.txt"), status, "utf8");
|
|
34
|
+
}
|
|
35
|
+
return dir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
it("returns error when taskId missing", async () => {
|
|
39
|
+
const res = JSON.parse(await waitForAgentCompleted({ taskId: "" }));
|
|
40
|
+
expect(res.status).toBe("error");
|
|
41
|
+
expect(res.error).toMatch(/taskId is required/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns error when task directory does not exist", async () => {
|
|
45
|
+
const res = JSON.parse(await waitForAgentCompleted({ taskId: "nope" }));
|
|
46
|
+
expect(res.status).toBe("error");
|
|
47
|
+
expect(res.error).toMatch(/No agent task directory/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns immediately with structured result for a completed task", async () => {
|
|
51
|
+
makeTask("done-1", {
|
|
52
|
+
status: "completed",
|
|
53
|
+
totalCostUsd: 1.23,
|
|
54
|
+
result: "the final answer",
|
|
55
|
+
artifacts: ["a.txt"],
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const res = JSON.parse(await waitForAgentCompleted({ taskId: "done-1" }));
|
|
59
|
+
expect(res.status).toBe("completed");
|
|
60
|
+
expect(res.costUsd).toBe(1.23);
|
|
61
|
+
expect(res.finalAnswer).toBe("the final answer");
|
|
62
|
+
expect(res.artifacts).toEqual(["a.txt"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("falls back to last assistant message when no result field", async () => {
|
|
66
|
+
makeTask("done-2", {
|
|
67
|
+
status: "completed",
|
|
68
|
+
totalCostUsd: 0.5,
|
|
69
|
+
threads: [
|
|
70
|
+
[
|
|
71
|
+
{ role: "user", content: "do it" },
|
|
72
|
+
{ role: "assistant", content: "I did it" },
|
|
73
|
+
],
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const res = JSON.parse(await waitForAgentCompleted({ taskId: "done-2" }));
|
|
78
|
+
expect(res.finalAnswer).toBe("I did it");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("treats inProgress:false with no status as completed", async () => {
|
|
82
|
+
makeTask("done-3", { inProgress: false, totalCostUsd: 0.1, result: "ok" });
|
|
83
|
+
const res = JSON.parse(await waitForAgentCompleted({ taskId: "done-3" }));
|
|
84
|
+
expect(res.status).toBe("completed");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("times out on a still-running task", async () => {
|
|
88
|
+
makeTask("running-1", { status: "running", totalCostUsd: 0.2 });
|
|
89
|
+
const res = JSON.parse(
|
|
90
|
+
await waitForAgentCompleted({
|
|
91
|
+
taskId: "running-1",
|
|
92
|
+
timeoutMs: 300,
|
|
93
|
+
pollIntervalMs: 100,
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
expect(res.timedOut).toBe(true);
|
|
97
|
+
expect(res.status).toBe("running");
|
|
98
|
+
expect(res.costUsd).toBe(0.2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("resolves when a running task transitions to completed mid-poll", async () => {
|
|
102
|
+
const dir = makeTask("running-2", { status: "running", totalCostUsd: 0 });
|
|
103
|
+
|
|
104
|
+
// Flip to completed shortly after we start waiting.
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(dir, "metadata.json"),
|
|
108
|
+
JSON.stringify(
|
|
109
|
+
{
|
|
110
|
+
taskId: "running-2",
|
|
111
|
+
status: "completed",
|
|
112
|
+
totalCostUsd: 2.0,
|
|
113
|
+
result: "eventually done",
|
|
114
|
+
},
|
|
115
|
+
null,
|
|
116
|
+
2
|
|
117
|
+
),
|
|
118
|
+
"utf8"
|
|
119
|
+
);
|
|
120
|
+
}, 150);
|
|
121
|
+
|
|
122
|
+
const res = JSON.parse(
|
|
123
|
+
await waitForAgentCompleted({
|
|
124
|
+
taskId: "running-2",
|
|
125
|
+
timeoutMs: 3000,
|
|
126
|
+
pollIntervalMs: 100,
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
expect(res.status).toBe("completed");
|
|
130
|
+
expect(res.finalAnswer).toBe("eventually done");
|
|
131
|
+
expect(res.costUsd).toBe(2.0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("fails a task whose pid is dead without a terminal status", async () => {
|
|
135
|
+
// Use a pid that is virtually guaranteed not to exist.
|
|
136
|
+
makeTask("crashed-1", { status: "running", totalCostUsd: 0.3, pid: 999999 });
|
|
137
|
+
const res = JSON.parse(
|
|
138
|
+
await waitForAgentCompleted({
|
|
139
|
+
taskId: "crashed-1",
|
|
140
|
+
timeoutMs: 5000,
|
|
141
|
+
pollIntervalMs: 100,
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
expect(res.status).toBe("failed");
|
|
145
|
+
expect(res.error).toMatch(/exited without writing a terminal status/);
|
|
146
|
+
});
|
|
147
|
+
});
|