@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,315 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Tests for detach / re-attach rendering correctness.
|
|
4
|
+
*
|
|
5
|
+
* Bug: after calling /detach and then /attach again, the agent's tool calls
|
|
6
|
+
* and messages were not rendering to the console.
|
|
7
|
+
*
|
|
8
|
+
* Root cause: both SessionsModule.attachToFsAgent / attachToWebAgent AND
|
|
9
|
+
* AgentModule.attachedAgentChatLoop were calling wireAgentRendering().
|
|
10
|
+
* Since wireAgentRendering uses setListener (keyed, replaces-on-conflict),
|
|
11
|
+
* the second call on the SAME EventService replaced the first — but the
|
|
12
|
+
* watcher's `done` event handler registered by SessionsModule was also
|
|
13
|
+
* removed. More critically: after /detach, unwireAgentRendering() removes
|
|
14
|
+
* the keyed listeners from `_wireAgentEvents` and clears the stored ref.
|
|
15
|
+
* On re-attach a new FsSyncedAgentWatcher is created with a fresh
|
|
16
|
+
* EventService, but the double-wire means the stored `_wireAgentEvents`
|
|
17
|
+
* ref held by the AgentModule was pointing at the WRONG EventService
|
|
18
|
+
* (the one from the second wireAgentRendering call).
|
|
19
|
+
*
|
|
20
|
+
* The fix: SessionsModule no longer calls wireAgentRendering itself —
|
|
21
|
+
* it delegates entirely to attachedAgentChatLoop which already calls it.
|
|
22
|
+
*
|
|
23
|
+
* These tests verify the wireAgentRendering / unwireAgentRendering contract
|
|
24
|
+
* using real EventService instances so we catch regressions in EventService
|
|
25
|
+
* changes too.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { EventService } from "../../src/services/EventService";
|
|
29
|
+
|
|
30
|
+
// ─── Minimal harness that mirrors AgentModule's wire/unwire logic exactly ────
|
|
31
|
+
|
|
32
|
+
const EVENT_TYPES = {
|
|
33
|
+
done: "done",
|
|
34
|
+
toolCall: "tool:pre_call",
|
|
35
|
+
toolUsed: "tool:post_call",
|
|
36
|
+
agentSay: "agent:say",
|
|
37
|
+
threadUpdate: "thread_update",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
interface RenderEvent {
|
|
41
|
+
type: string;
|
|
42
|
+
taskId: string;
|
|
43
|
+
agentName: string;
|
|
44
|
+
data: any;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class RenderingHarness {
|
|
48
|
+
// Mirror of AgentModule's stored wire params
|
|
49
|
+
private _wireAgentEvents: EventService | undefined;
|
|
50
|
+
private _wireTaskId: string | undefined;
|
|
51
|
+
private _wireAgentName: string | undefined;
|
|
52
|
+
private _wireEventTypes:
|
|
53
|
+
| { toolCall?: string; toolUsed?: string; agentSay?: string; done: string }
|
|
54
|
+
| undefined;
|
|
55
|
+
|
|
56
|
+
public rendered: RenderEvent[] = [];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Mirrors AgentModule.wireAgentRendering exactly (uses real EventService.setListener).
|
|
60
|
+
*/
|
|
61
|
+
wireAgentRendering(
|
|
62
|
+
taskId: string,
|
|
63
|
+
agentEvents: EventService,
|
|
64
|
+
eventTypes: { toolCall?: string; toolUsed?: string; agentSay?: string; done: string },
|
|
65
|
+
agentName: string
|
|
66
|
+
): void {
|
|
67
|
+
if (eventTypes.toolCall) {
|
|
68
|
+
agentEvents.setListener(
|
|
69
|
+
{ key: "agentModule:render:toolCall", event: eventTypes.toolCall },
|
|
70
|
+
(data: any) => this.rendered.push({ type: "toolCall", taskId, agentName, data })
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (eventTypes.toolUsed) {
|
|
74
|
+
agentEvents.setListener(
|
|
75
|
+
{ key: "agentModule:render:toolUsed", event: eventTypes.toolUsed },
|
|
76
|
+
(data: any) => this.rendered.push({ type: "toolResult", taskId, agentName, data })
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (eventTypes.agentSay) {
|
|
80
|
+
agentEvents.setListener(
|
|
81
|
+
{ key: "agentModule:render:agentSay", event: eventTypes.agentSay },
|
|
82
|
+
(data: any) => this.rendered.push({ type: "agentMessage", taskId, agentName, data })
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
this._wireAgentEvents = agentEvents;
|
|
86
|
+
this._wireTaskId = taskId;
|
|
87
|
+
this._wireAgentName = agentName;
|
|
88
|
+
this._wireEventTypes = eventTypes;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mirrors AgentModule.unwireAgentRendering exactly.
|
|
93
|
+
*/
|
|
94
|
+
unwireAgentRendering(): void {
|
|
95
|
+
if (this._wireAgentEvents) {
|
|
96
|
+
this._wireAgentEvents.removeManagedListenersByPrefix("agentModule:render:");
|
|
97
|
+
this._wireAgentEvents = undefined;
|
|
98
|
+
}
|
|
99
|
+
this._wireTaskId = undefined;
|
|
100
|
+
this._wireAgentName = undefined;
|
|
101
|
+
this._wireEventTypes = undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
reset() {
|
|
105
|
+
this.rendered = [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Tests ────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
describe("detach / re-attach rendering", () => {
|
|
112
|
+
let harness: RenderingHarness;
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
harness = new RenderingHarness();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// 1. Basic: emitted events reach the renderer after wireAgentRendering ────────
|
|
119
|
+
|
|
120
|
+
it("emitted events reach the renderer after wireAgentRendering", () => {
|
|
121
|
+
const events = new EventService();
|
|
122
|
+
harness.wireAgentRendering("task-1", events, EVENT_TYPES, "Dev");
|
|
123
|
+
|
|
124
|
+
events.emit(EVENT_TYPES.toolCall, { toolCall: { function: { name: "execCommand" } } });
|
|
125
|
+
events.emit(EVENT_TYPES.agentSay, { message: "hello" });
|
|
126
|
+
events.emit(EVENT_TYPES.toolUsed, { toolCall: {}, functionResp: "output" });
|
|
127
|
+
|
|
128
|
+
expect(harness.rendered).toHaveLength(3);
|
|
129
|
+
expect(harness.rendered[0].type).toBe("toolCall");
|
|
130
|
+
expect(harness.rendered[1].type).toBe("agentMessage");
|
|
131
|
+
expect(harness.rendered[2].type).toBe("toolResult");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 2. After detach (unwire), old EventService no longer triggers renders ───────
|
|
135
|
+
|
|
136
|
+
it("old EventService does NOT render after unwireAgentRendering (detach)", () => {
|
|
137
|
+
const events = new EventService();
|
|
138
|
+
harness.wireAgentRendering("task-1", events, EVENT_TYPES, "Dev");
|
|
139
|
+
|
|
140
|
+
// Verify wired first
|
|
141
|
+
events.emit(EVENT_TYPES.toolCall, { toolCall: {} });
|
|
142
|
+
expect(harness.rendered).toHaveLength(1);
|
|
143
|
+
harness.reset();
|
|
144
|
+
|
|
145
|
+
// Detach
|
|
146
|
+
harness.unwireAgentRendering();
|
|
147
|
+
|
|
148
|
+
// Emit on the OLD EventService — nothing should render
|
|
149
|
+
events.emit(EVENT_TYPES.toolCall, { toolCall: {} });
|
|
150
|
+
events.emit(EVENT_TYPES.agentSay, { message: "zombie" });
|
|
151
|
+
events.emit(EVENT_TYPES.toolUsed, { toolCall: {}, functionResp: "output" });
|
|
152
|
+
|
|
153
|
+
expect(harness.rendered).toHaveLength(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// 3. Re-attach on a NEW EventService renders correctly ───────────────────────
|
|
157
|
+
|
|
158
|
+
it("events on a NEW EventService render after re-wiring (re-attach)", () => {
|
|
159
|
+
const oldEvents = new EventService();
|
|
160
|
+
harness.wireAgentRendering("task-1", oldEvents, EVENT_TYPES, "Dev");
|
|
161
|
+
harness.unwireAgentRendering();
|
|
162
|
+
|
|
163
|
+
// Re-attach: new watcher = new EventService
|
|
164
|
+
const newEvents = new EventService();
|
|
165
|
+
harness.wireAgentRendering("task-1", newEvents, EVENT_TYPES, "Dev");
|
|
166
|
+
|
|
167
|
+
newEvents.emit(EVENT_TYPES.toolCall, { toolCall: { function: { name: "readFile" } } });
|
|
168
|
+
newEvents.emit(EVENT_TYPES.agentSay, { message: "re-attached message" });
|
|
169
|
+
|
|
170
|
+
expect(harness.rendered).toHaveLength(2);
|
|
171
|
+
expect(harness.rendered[0].type).toBe("toolCall");
|
|
172
|
+
expect(harness.rendered[0].data.toolCall.function.name).toBe("readFile");
|
|
173
|
+
expect(harness.rendered[1].type).toBe("agentMessage");
|
|
174
|
+
expect(harness.rendered[1].data.message).toBe("re-attached message");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 4. Old EventService still silent after re-attach on new EventService ────────
|
|
178
|
+
|
|
179
|
+
it("old EventService stays silent after re-attach on new EventService", () => {
|
|
180
|
+
const oldEvents = new EventService();
|
|
181
|
+
harness.wireAgentRendering("task-1", oldEvents, EVENT_TYPES, "Dev");
|
|
182
|
+
harness.unwireAgentRendering();
|
|
183
|
+
|
|
184
|
+
const newEvents = new EventService();
|
|
185
|
+
harness.wireAgentRendering("task-1", newEvents, EVENT_TYPES, "Dev");
|
|
186
|
+
|
|
187
|
+
// New events render
|
|
188
|
+
newEvents.emit(EVENT_TYPES.agentSay, { message: "new" });
|
|
189
|
+
expect(harness.rendered).toHaveLength(1);
|
|
190
|
+
|
|
191
|
+
// Old events still silent
|
|
192
|
+
oldEvents.emit(EVENT_TYPES.toolCall, { toolCall: {} });
|
|
193
|
+
oldEvents.emit(EVENT_TYPES.agentSay, { message: "old" });
|
|
194
|
+
expect(harness.rendered).toHaveLength(1); // unchanged
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 5. Double-wire on the SAME EventService does NOT double-render ──────────────
|
|
198
|
+
//
|
|
199
|
+
// This is the bug that existed: SessionsModule.attachToFsAgent called
|
|
200
|
+
// wireAgentRendering, then attachedAgentChatLoop also called it.
|
|
201
|
+
// Since setListener is keyed (replaces), the net result is one listener —
|
|
202
|
+
// but the done handler registered by SessionsModule was lost.
|
|
203
|
+
// The fix removes the extra wireAgentRendering from SessionsModule.
|
|
204
|
+
|
|
205
|
+
it("calling wireAgentRendering twice on same EventService does NOT double-render", () => {
|
|
206
|
+
const events = new EventService();
|
|
207
|
+
|
|
208
|
+
// Simulate the OLD (buggy) behavior: SessionsModule wires, then attachedAgentChatLoop wires again
|
|
209
|
+
harness.wireAgentRendering("task-1", events, EVENT_TYPES, "Dev"); // first call
|
|
210
|
+
harness.wireAgentRendering("task-1", events, EVENT_TYPES, "Dev"); // second call (setListener replaces)
|
|
211
|
+
|
|
212
|
+
events.emit(EVENT_TYPES.toolCall, { toolCall: {} });
|
|
213
|
+
|
|
214
|
+
// setListener keyed — only ONE render expected, not two
|
|
215
|
+
expect(harness.rendered).toHaveLength(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 6. Full cycle: attach → emit → detach → re-attach → emit all render correctly
|
|
219
|
+
|
|
220
|
+
it("full attach → detach → re-attach cycle: all phases render correctly", () => {
|
|
221
|
+
// ── First attach ──────────────────────────────────────────────────────────
|
|
222
|
+
const watcher1 = new EventService();
|
|
223
|
+
harness.wireAgentRendering("task-A", watcher1, EVENT_TYPES, "Developer");
|
|
224
|
+
|
|
225
|
+
watcher1.emit(EVENT_TYPES.toolCall, { toolCall: { function: { name: "execCommand" } } });
|
|
226
|
+
watcher1.emit(EVENT_TYPES.agentSay, { message: "initial work" });
|
|
227
|
+
expect(harness.rendered).toHaveLength(2);
|
|
228
|
+
harness.reset();
|
|
229
|
+
|
|
230
|
+
// ── /detach ───────────────────────────────────────────────────────────────
|
|
231
|
+
harness.unwireAgentRendering();
|
|
232
|
+
|
|
233
|
+
// Events from watcher1 after detach should NOT render
|
|
234
|
+
watcher1.emit(EVENT_TYPES.toolCall, { toolCall: {} });
|
|
235
|
+
watcher1.emit(EVENT_TYPES.agentSay, { message: "zombie message" });
|
|
236
|
+
expect(harness.rendered).toHaveLength(0);
|
|
237
|
+
|
|
238
|
+
// ── Re-attach (new FsSyncedAgentWatcher creates a new EventService) ───────
|
|
239
|
+
const watcher2 = new EventService();
|
|
240
|
+
harness.wireAgentRendering("task-A", watcher2, EVENT_TYPES, "Developer");
|
|
241
|
+
|
|
242
|
+
watcher2.emit(EVENT_TYPES.toolCall, { toolCall: { function: { name: "readFile" } } });
|
|
243
|
+
watcher2.emit(EVENT_TYPES.agentSay, { message: "continuing after re-attach" });
|
|
244
|
+
|
|
245
|
+
// THE CRITICAL ASSERTION: renders must arrive after re-attach
|
|
246
|
+
expect(harness.rendered).toHaveLength(2);
|
|
247
|
+
expect(harness.rendered[0].type).toBe("toolCall");
|
|
248
|
+
expect(harness.rendered[0].data.toolCall.function.name).toBe("readFile");
|
|
249
|
+
expect(harness.rendered[1].type).toBe("agentMessage");
|
|
250
|
+
expect(harness.rendered[1].data.message).toBe("continuing after re-attach");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// 7. toolResult renders after re-attach ──────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
it("toolResult events render after re-attach", () => {
|
|
256
|
+
const events1 = new EventService();
|
|
257
|
+
harness.wireAgentRendering("task-1", events1, EVENT_TYPES, "Dev");
|
|
258
|
+
harness.unwireAgentRendering();
|
|
259
|
+
|
|
260
|
+
const events2 = new EventService();
|
|
261
|
+
harness.wireAgentRendering("task-1", events2, EVENT_TYPES, "Dev");
|
|
262
|
+
|
|
263
|
+
events2.emit(EVENT_TYPES.toolUsed, {
|
|
264
|
+
toolCall: { function: { name: "execCommand" } },
|
|
265
|
+
functionResp: "$ echo hello\nhello",
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
expect(harness.rendered).toHaveLength(1);
|
|
269
|
+
expect(harness.rendered[0].type).toBe("toolResult");
|
|
270
|
+
expect(harness.rendered[0].data.functionResp).toBe("$ echo hello\nhello");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// 8. Multiple sequential attach/detach cycles all work ───────────────────────
|
|
274
|
+
|
|
275
|
+
it("3 sequential attach/detach cycles all render correctly", () => {
|
|
276
|
+
for (let i = 0; i < 3; i++) {
|
|
277
|
+
harness.reset();
|
|
278
|
+
const events = new EventService();
|
|
279
|
+
harness.wireAgentRendering(`task-${i}`, events, EVENT_TYPES, "Dev");
|
|
280
|
+
|
|
281
|
+
events.emit(EVENT_TYPES.agentSay, { message: `cycle ${i}` });
|
|
282
|
+
expect(harness.rendered).toHaveLength(1);
|
|
283
|
+
expect(harness.rendered[0].data.message).toBe(`cycle ${i}`);
|
|
284
|
+
expect(harness.rendered[0].taskId).toBe(`task-${i}`);
|
|
285
|
+
|
|
286
|
+
// Detach: old events stop rendering
|
|
287
|
+
harness.unwireAgentRendering();
|
|
288
|
+
events.emit(EVENT_TYPES.agentSay, { message: "should not render" });
|
|
289
|
+
expect(harness.rendered).toHaveLength(1); // still 1, not 2
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// 9. hasManagedListener confirms listener is present/absent at right times ───
|
|
294
|
+
|
|
295
|
+
it("hasManagedListener reflects wired/unwired state correctly", () => {
|
|
296
|
+
const events = new EventService();
|
|
297
|
+
|
|
298
|
+
// Before wiring: no managed render listeners
|
|
299
|
+
expect(events.hasManagedListener("agentModule:render:toolCall")).toBe(false);
|
|
300
|
+
expect(events.hasManagedListener("agentModule:render:agentSay")).toBe(false);
|
|
301
|
+
|
|
302
|
+
harness.wireAgentRendering("task-1", events, EVENT_TYPES, "Dev");
|
|
303
|
+
|
|
304
|
+
// After wiring: listeners registered
|
|
305
|
+
expect(events.hasManagedListener("agentModule:render:toolCall")).toBe(true);
|
|
306
|
+
expect(events.hasManagedListener("agentModule:render:toolUsed")).toBe(true);
|
|
307
|
+
expect(events.hasManagedListener("agentModule:render:agentSay")).toBe(true);
|
|
308
|
+
|
|
309
|
+
harness.unwireAgentRendering();
|
|
310
|
+
|
|
311
|
+
// After detach: listeners removed
|
|
312
|
+
expect(events.hasManagedListener("agentModule:render:toolCall")).toBe(false);
|
|
313
|
+
expect(events.hasManagedListener("agentModule:render:agentSay")).toBe(false);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -4,6 +4,27 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Models } from "../../src/types";
|
|
6
6
|
import { ContextLimits, getModelContextLimit, DEFAULT_CONTEXT_LIMIT } from "../../src/clients/contextLimits";
|
|
7
|
+
import {
|
|
8
|
+
OpenAiTextPricing,
|
|
9
|
+
GeminiTextPricing,
|
|
10
|
+
AnthropicTextPricing,
|
|
11
|
+
XaiTextPricing,
|
|
12
|
+
} from "../../src/clients/pricing";
|
|
13
|
+
|
|
14
|
+
// Model ids explicitly marked deprecated across all pricing tables. Deprecated
|
|
15
|
+
// models are allowed to be absent from ContextLimits — they can be trimmed from
|
|
16
|
+
// Models.* / the context-limit map without breaking anything, since cost
|
|
17
|
+
// tracking reads the *TextPricing tables directly by model id.
|
|
18
|
+
const deprecatedModelIds = new Set<string>(
|
|
19
|
+
Object.entries({
|
|
20
|
+
...OpenAiTextPricing,
|
|
21
|
+
...GeminiTextPricing,
|
|
22
|
+
...AnthropicTextPricing,
|
|
23
|
+
...XaiTextPricing,
|
|
24
|
+
} as Record<string, { deprecated?: boolean }>)
|
|
25
|
+
.filter(([, p]) => p && p.deprecated === true)
|
|
26
|
+
.map(([id]) => id)
|
|
27
|
+
);
|
|
7
28
|
|
|
8
29
|
describe("ContextLimits", () => {
|
|
9
30
|
describe("coverage - all Models.* values have a recorded context limit", () => {
|
|
@@ -19,7 +40,8 @@ describe("ContextLimits", () => {
|
|
|
19
40
|
});
|
|
20
41
|
|
|
21
42
|
for (const [key, modelId] of modelEntries) {
|
|
22
|
-
|
|
43
|
+
const testFn = deprecatedModelIds.has(modelId) ? it.skip : it;
|
|
44
|
+
testFn(`${provider}.${key} (${modelId}) should have a context limit`, () => {
|
|
23
45
|
const limit = ContextLimits[modelId];
|
|
24
46
|
expect(limit).toBeDefined();
|
|
25
47
|
expect(typeof limit).toBe("number");
|
|
@@ -150,15 +150,34 @@ describe("Model Catalog Coverage", () => {
|
|
|
150
150
|
* Every model defined in Models.* and EmbeddingModels.* must have an entry
|
|
151
151
|
* in ALL_MODEL_CATALOG. This ensures the catalog stays in sync and is the
|
|
152
152
|
* single source of truth for model metadata and pricing.
|
|
153
|
+
*
|
|
154
|
+
* Exception: models flagged `deprecated` in their pricing entry are allowed to
|
|
155
|
+
* be absent from the catalog. Cost tracking for historical usage still works
|
|
156
|
+
* because calculateCost() reads the *TextPricing tables directly by model id,
|
|
157
|
+
* independently of the catalog — so deprecated models can be trimmed from the
|
|
158
|
+
* catalog/Models.* without breaking cost calculation.
|
|
153
159
|
*/
|
|
154
160
|
const catalogIds = new Set(ALL_MODEL_CATALOG.map((m) => m.id));
|
|
155
161
|
|
|
162
|
+
// Model ids explicitly marked deprecated across all pricing tables.
|
|
163
|
+
const deprecatedModelIds = new Set<string>(
|
|
164
|
+
Object.entries({
|
|
165
|
+
...OpenAiTextPricing,
|
|
166
|
+
...GeminiTextPricing,
|
|
167
|
+
...AnthropicTextPricing,
|
|
168
|
+
...XaiTextPricing,
|
|
169
|
+
} as Record<string, { deprecated?: boolean }>)
|
|
170
|
+
.filter(([, p]) => p && p.deprecated === true)
|
|
171
|
+
.map(([id]) => id)
|
|
172
|
+
);
|
|
173
|
+
|
|
156
174
|
describe("All Models.* entries are in the catalog", () => {
|
|
157
175
|
for (const [provider, providerModels] of Object.entries(Models)) {
|
|
158
176
|
for (const [modelKey, modelId] of Object.entries(
|
|
159
177
|
providerModels as Record<string, string>
|
|
160
178
|
)) {
|
|
161
|
-
|
|
179
|
+
const testFn = deprecatedModelIds.has(modelId) ? it.skip : it;
|
|
180
|
+
testFn(`Models.${provider}.${modelKey} (${modelId}) is in ALL_MODEL_CATALOG`, () => {
|
|
162
181
|
expect(catalogIds.has(modelId)).toBe(true);
|
|
163
182
|
});
|
|
164
183
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ToolsService, ToolContext } from "../../src/services/Tools";
|
|
1
|
+
import { ToolsService, ToolContext, ToolCallContext } from "../../src/services/Tools";
|
|
2
2
|
import { Tool, ToolCall } from "../../src/clients/types";
|
|
3
3
|
import { createPatternMatcher } from "../../src/services/types";
|
|
4
4
|
import { AgentService } from "../../src/services/AgentService";
|
|
@@ -268,7 +268,11 @@ describe("ToolsService", () => {
|
|
|
268
268
|
|
|
269
269
|
const result = await toolsService.callTool(mockToolCall);
|
|
270
270
|
|
|
271
|
-
|
|
271
|
+
// Non-positional tools receive `_ctx` merged onto the args object.
|
|
272
|
+
expect(mockFunction).toHaveBeenCalledWith(
|
|
273
|
+
expect.objectContaining({ input: "test value" })
|
|
274
|
+
);
|
|
275
|
+
expect(mockFunction.mock.calls[0][0]._ctx).toBeDefined();
|
|
272
276
|
expect(result.toolMessages).toHaveLength(1);
|
|
273
277
|
expect(result.toolMessages[0]).toEqual({
|
|
274
278
|
tool_call_id: "call_123",
|
|
@@ -312,7 +316,12 @@ describe("ToolsService", () => {
|
|
|
312
316
|
|
|
313
317
|
await toolsService.callTool(positionalCall);
|
|
314
318
|
|
|
315
|
-
|
|
319
|
+
// Positional tools receive `_ctx` as the LAST positional argument.
|
|
320
|
+
expect(mockFunction).toHaveBeenCalledWith(
|
|
321
|
+
"hello",
|
|
322
|
+
42,
|
|
323
|
+
expect.objectContaining({ Tools: expect.anything() })
|
|
324
|
+
);
|
|
316
325
|
});
|
|
317
326
|
|
|
318
327
|
it("should handle tool not enabled error", async () => {
|
|
@@ -433,7 +442,10 @@ describe("ToolsService", () => {
|
|
|
433
442
|
|
|
434
443
|
await toolsService.callTool(toolCallWithStringArgs);
|
|
435
444
|
|
|
436
|
-
expect(mockFunction).toHaveBeenCalledWith(
|
|
445
|
+
expect(mockFunction).toHaveBeenCalledWith(
|
|
446
|
+
expect.objectContaining({ input: "escaped\nstring" })
|
|
447
|
+
);
|
|
448
|
+
expect(mockFunction.mock.calls[0][0]._ctx).toBeDefined();
|
|
437
449
|
});
|
|
438
450
|
});
|
|
439
451
|
describe("Tool Override System", () => {
|
|
@@ -492,8 +504,9 @@ describe("ToolsService", () => {
|
|
|
492
504
|
|
|
493
505
|
const result = await toolsService.callTool(toolCall);
|
|
494
506
|
|
|
507
|
+
// Non-positional args now include `_ctx`; the override receives them in an array.
|
|
495
508
|
expect(overrideFunction).toHaveBeenCalledWith(
|
|
496
|
-
[{ input: "test" }],
|
|
509
|
+
[expect.objectContaining({ input: "test" })],
|
|
497
510
|
expect.any(Object)
|
|
498
511
|
);
|
|
499
512
|
expect(originalFunction).not.toHaveBeenCalled();
|
|
@@ -627,7 +640,9 @@ describe("ToolsService", () => {
|
|
|
627
640
|
const result = await toolsService.callTool(toolCall);
|
|
628
641
|
|
|
629
642
|
expect(wrapperFunction).toHaveBeenCalled();
|
|
630
|
-
expect(originalFunction).toHaveBeenCalledWith(
|
|
643
|
+
expect(originalFunction).toHaveBeenCalledWith(
|
|
644
|
+
expect.objectContaining({ input: "test" })
|
|
645
|
+
);
|
|
631
646
|
expect(result.functionResp).toBe("wrapped: original result");
|
|
632
647
|
});
|
|
633
648
|
|
|
@@ -841,7 +856,8 @@ describe("ToolsService", () => {
|
|
|
841
856
|
|
|
842
857
|
const result = await toolsService.callTool(noParamCall);
|
|
843
858
|
|
|
844
|
-
expect(mockFunction).toHaveBeenCalledWith(
|
|
859
|
+
expect(mockFunction).toHaveBeenCalledWith(expect.any(Object));
|
|
860
|
+
expect(mockFunction.mock.calls[0][0]._ctx).toBeDefined();
|
|
845
861
|
expect(result.functionResp).toBe("no param result");
|
|
846
862
|
});
|
|
847
863
|
|
|
@@ -897,14 +913,16 @@ describe("ToolsService", () => {
|
|
|
897
913
|
|
|
898
914
|
const result = await toolsService.callTool(complexCall);
|
|
899
915
|
|
|
900
|
-
expect(mockFunction).toHaveBeenCalledWith(
|
|
916
|
+
expect(mockFunction).toHaveBeenCalledWith(
|
|
917
|
+
expect.objectContaining({
|
|
901
918
|
config: {
|
|
902
919
|
settings: [
|
|
903
920
|
{ key: "timeout", value: 5000 },
|
|
904
921
|
{ key: "retries", value: 3 },
|
|
905
922
|
],
|
|
906
923
|
},
|
|
907
|
-
|
|
924
|
+
})
|
|
925
|
+
);
|
|
908
926
|
expect(result.functionResp).toBe("complex result");
|
|
909
927
|
});
|
|
910
928
|
|
|
@@ -1570,4 +1588,149 @@ describe("ToolsService", () => {
|
|
|
1570
1588
|
expect(foundTools.map(t => t.function.name)).toContain("tool2");
|
|
1571
1589
|
});
|
|
1572
1590
|
});
|
|
1591
|
+
|
|
1592
|
+
describe("_ctx per-call context threading", () => {
|
|
1593
|
+
const positionalTool: Tool = {
|
|
1594
|
+
type: "function",
|
|
1595
|
+
function: {
|
|
1596
|
+
name: "posCtxTool",
|
|
1597
|
+
parameters: {
|
|
1598
|
+
type: "object",
|
|
1599
|
+
positional: true,
|
|
1600
|
+
properties: {
|
|
1601
|
+
arg1: { type: "string" },
|
|
1602
|
+
},
|
|
1603
|
+
required: ["arg1"],
|
|
1604
|
+
},
|
|
1605
|
+
},
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
const nonPositionalTool: Tool = {
|
|
1609
|
+
type: "function",
|
|
1610
|
+
function: {
|
|
1611
|
+
name: "nonPosCtxTool",
|
|
1612
|
+
parameters: {
|
|
1613
|
+
type: "object",
|
|
1614
|
+
properties: {
|
|
1615
|
+
input: { type: "string" },
|
|
1616
|
+
},
|
|
1617
|
+
required: ["input"],
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
};
|
|
1621
|
+
|
|
1622
|
+
it("passes _ctx as the last positional argument for positional tools", async () => {
|
|
1623
|
+
const fn = jest.fn().mockResolvedValue("ok");
|
|
1624
|
+
toolsService.addTool(positionalTool);
|
|
1625
|
+
toolsService.setFunction("posCtxTool", fn);
|
|
1626
|
+
|
|
1627
|
+
const caller = { name: "TestAgent" };
|
|
1628
|
+
await toolsService.callTool(
|
|
1629
|
+
{
|
|
1630
|
+
id: "c1",
|
|
1631
|
+
type: "function",
|
|
1632
|
+
function: {
|
|
1633
|
+
name: "posCtxTool",
|
|
1634
|
+
arguments: JSON.stringify({ arg1: "hello" }),
|
|
1635
|
+
},
|
|
1636
|
+
},
|
|
1637
|
+
undefined,
|
|
1638
|
+
{ caller, taskId: "task-123" }
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
const callArgs = fn.mock.calls[0];
|
|
1642
|
+
expect(callArgs[0]).toBe("hello");
|
|
1643
|
+
const ctx = callArgs[1] as ToolCallContext;
|
|
1644
|
+
expect(ctx).toBeDefined();
|
|
1645
|
+
expect(ctx.caller).toBe(caller);
|
|
1646
|
+
expect(ctx.taskId).toBe("task-123");
|
|
1647
|
+
expect(ctx.Tools).toBe(toolsService);
|
|
1648
|
+
});
|
|
1649
|
+
|
|
1650
|
+
it("merges _ctx onto the args object for non-positional tools", async () => {
|
|
1651
|
+
const fn = jest.fn().mockResolvedValue("ok");
|
|
1652
|
+
toolsService.addTool(nonPositionalTool);
|
|
1653
|
+
toolsService.setFunction("nonPosCtxTool", fn);
|
|
1654
|
+
|
|
1655
|
+
const caller = { name: "TestAgent" };
|
|
1656
|
+
await toolsService.callTool(
|
|
1657
|
+
{
|
|
1658
|
+
id: "c2",
|
|
1659
|
+
type: "function",
|
|
1660
|
+
function: {
|
|
1661
|
+
name: "nonPosCtxTool",
|
|
1662
|
+
arguments: JSON.stringify({ input: "value" }),
|
|
1663
|
+
},
|
|
1664
|
+
},
|
|
1665
|
+
undefined,
|
|
1666
|
+
{ caller, taskId: "task-456" }
|
|
1667
|
+
);
|
|
1668
|
+
|
|
1669
|
+
const argObj = fn.mock.calls[0][0];
|
|
1670
|
+
expect(argObj.input).toBe("value");
|
|
1671
|
+
expect(argObj._ctx).toBeDefined();
|
|
1672
|
+
expect(argObj._ctx.caller).toBe(caller);
|
|
1673
|
+
expect(argObj._ctx.taskId).toBe("task-456");
|
|
1674
|
+
expect(argObj._ctx.Tools).toBe(toolsService);
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
it("still provides a _ctx (with Tools) when no callContext is passed", async () => {
|
|
1678
|
+
const fn = jest.fn().mockResolvedValue("ok");
|
|
1679
|
+
toolsService.addTool(nonPositionalTool);
|
|
1680
|
+
toolsService.setFunction("nonPosCtxTool", fn);
|
|
1681
|
+
|
|
1682
|
+
await toolsService.callTool({
|
|
1683
|
+
id: "c3",
|
|
1684
|
+
type: "function",
|
|
1685
|
+
function: {
|
|
1686
|
+
name: "nonPosCtxTool",
|
|
1687
|
+
arguments: JSON.stringify({ input: "value" }),
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
const argObj = fn.mock.calls[0][0];
|
|
1692
|
+
expect(argObj._ctx).toBeDefined();
|
|
1693
|
+
expect(argObj._ctx.Tools).toBe(toolsService);
|
|
1694
|
+
expect(argObj._ctx.caller).toBeUndefined();
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
it("does not leak _ctx into the multi_tool_use.parallel extraction", async () => {
|
|
1698
|
+
// The parallel path should extract calls from the ORIGINAL args, not the
|
|
1699
|
+
// _ctx-augmented ones.
|
|
1700
|
+
const parallelTool: Tool = {
|
|
1701
|
+
type: "function",
|
|
1702
|
+
function: {
|
|
1703
|
+
name: "multi_tool_use.parallel",
|
|
1704
|
+
parameters: { type: "object", properties: {} },
|
|
1705
|
+
},
|
|
1706
|
+
};
|
|
1707
|
+
const inner = jest
|
|
1708
|
+
.fn()
|
|
1709
|
+
.mockResolvedValue(["r1", "r2"]);
|
|
1710
|
+
toolsService.addTool(parallelTool);
|
|
1711
|
+
toolsService.setFunction("multi_tool_use.parallel", inner);
|
|
1712
|
+
|
|
1713
|
+
const result = await toolsService.callTool(
|
|
1714
|
+
{
|
|
1715
|
+
id: "c4",
|
|
1716
|
+
type: "function",
|
|
1717
|
+
function: {
|
|
1718
|
+
name: "multi_tool_use.parallel",
|
|
1719
|
+
arguments: JSON.stringify([
|
|
1720
|
+
{ recipient_name: "functions.a", parameters: {} },
|
|
1721
|
+
{ recipient_name: "functions.b", parameters: {} },
|
|
1722
|
+
]),
|
|
1723
|
+
},
|
|
1724
|
+
},
|
|
1725
|
+
undefined,
|
|
1726
|
+
{ caller: {}, taskId: "t" }
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1729
|
+
// Two tool messages, one per parallel call — proves extraction used the
|
|
1730
|
+
// original array shape rather than an _ctx-wrapped object.
|
|
1731
|
+
expect(result.toolMessages).toHaveLength(2);
|
|
1732
|
+
expect(result.toolMessages[0].name).toBe("a");
|
|
1733
|
+
expect(result.toolMessages[1].name).toBe("b");
|
|
1734
|
+
});
|
|
1735
|
+
});
|
|
1573
1736
|
});
|