@goondan/openharness-base 0.1.8 → 0.1.9
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/dist/index.d.ts +84 -0
- package/dist/index.js +537 -0
- package/package.json +14 -9
- package/src/__tests__/basic-system-prompt.test.ts +0 -186
- package/src/__tests__/compaction-summarize.test.ts +0 -282
- package/src/__tests__/logging.test.ts +0 -200
- package/src/__tests__/message-window.test.ts +0 -194
- package/src/__tests__/required-tools-guard.test.ts +0 -207
- package/src/__tests__/tool-search.test.ts +0 -187
- package/src/__tests__/tools.test.ts +0 -332
- package/src/extensions/basic-system-prompt.ts +0 -48
- package/src/extensions/compaction-summarize.ts +0 -104
- package/src/extensions/logging.ts +0 -42
- package/src/extensions/message-window.ts +0 -23
- package/src/extensions/required-tools-guard.ts +0 -24
- package/src/extensions/tool-search.ts +0 -38
- package/src/index.ts +0 -16
- package/src/tools/bash.ts +0 -38
- package/src/tools/file-system.ts +0 -83
- package/src/tools/http-fetch.ts +0 -64
- package/src/tools/json-query.ts +0 -71
- package/src/tools/text-transform.ts +0 -59
- package/src/tools/wait.ts +0 -46
- package/tsconfig.json +0 -8
- package/vitest.config.ts +0 -7
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { MessageWindow } from "../extensions/message-window.js";
|
|
3
|
-
import type {
|
|
4
|
-
ExtensionApi,
|
|
5
|
-
StepMiddleware,
|
|
6
|
-
StepContext,
|
|
7
|
-
StepResult,
|
|
8
|
-
ConversationState,
|
|
9
|
-
Message,
|
|
10
|
-
MessageEvent,
|
|
11
|
-
} from "@goondan/openharness-types";
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Helpers
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
|
|
17
|
-
function makeMessages(count: number): Message[] {
|
|
18
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
19
|
-
id: `msg-${i}`,
|
|
20
|
-
data: {
|
|
21
|
-
role: "user" as const,
|
|
22
|
-
content: `Message ${i}`,
|
|
23
|
-
},
|
|
24
|
-
}));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function makeMockConversationState(
|
|
28
|
-
messages: Message[],
|
|
29
|
-
): ConversationState & { emitted: MessageEvent[] } {
|
|
30
|
-
const emitted: MessageEvent[] = [];
|
|
31
|
-
return {
|
|
32
|
-
messages,
|
|
33
|
-
events: [],
|
|
34
|
-
emitted,
|
|
35
|
-
emit: vi.fn((event: MessageEvent) => {
|
|
36
|
-
emitted.push(event);
|
|
37
|
-
}),
|
|
38
|
-
restore: vi.fn(),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function makeMockApi(conversation: ConversationState): {
|
|
43
|
-
api: ExtensionApi;
|
|
44
|
-
registeredMiddleware: Array<{
|
|
45
|
-
level: string;
|
|
46
|
-
handler: StepMiddleware;
|
|
47
|
-
options?: { priority?: number };
|
|
48
|
-
}>;
|
|
49
|
-
} {
|
|
50
|
-
const registeredMiddleware: Array<{
|
|
51
|
-
level: string;
|
|
52
|
-
handler: StepMiddleware;
|
|
53
|
-
options?: { priority?: number };
|
|
54
|
-
}> = [];
|
|
55
|
-
|
|
56
|
-
const api: ExtensionApi = {
|
|
57
|
-
pipeline: {
|
|
58
|
-
register: vi.fn(
|
|
59
|
-
(level: string, handler: StepMiddleware, options?: { priority?: number }) => {
|
|
60
|
-
registeredMiddleware.push({ level, handler, options });
|
|
61
|
-
},
|
|
62
|
-
) as unknown as ExtensionApi["pipeline"]["register"],
|
|
63
|
-
},
|
|
64
|
-
tools: {
|
|
65
|
-
register: vi.fn(),
|
|
66
|
-
remove: vi.fn(),
|
|
67
|
-
list: vi.fn(() => []),
|
|
68
|
-
},
|
|
69
|
-
on: vi.fn(),
|
|
70
|
-
conversation,
|
|
71
|
-
runtime: {
|
|
72
|
-
agent: {
|
|
73
|
-
name: "test-agent",
|
|
74
|
-
model: { provider: "openai", model: "gpt-4o" },
|
|
75
|
-
extensions: [],
|
|
76
|
-
tools: [],
|
|
77
|
-
},
|
|
78
|
-
agents: {},
|
|
79
|
-
connections: {},
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
return { api, registeredMiddleware };
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function makeStepContext(conversation: ConversationState): StepContext {
|
|
87
|
-
return {
|
|
88
|
-
turnId: "turn-1",
|
|
89
|
-
agentName: "test-agent",
|
|
90
|
-
conversationId: "conv-1",
|
|
91
|
-
conversation,
|
|
92
|
-
stepNumber: 1,
|
|
93
|
-
abortSignal: new AbortController().signal,
|
|
94
|
-
input: {
|
|
95
|
-
name: "test-event",
|
|
96
|
-
content: [{ type: "text", text: "hello" }],
|
|
97
|
-
properties: {},
|
|
98
|
-
source: {
|
|
99
|
-
connector: "test-connector",
|
|
100
|
-
connectionName: "test",
|
|
101
|
-
receivedAt: new Date().toISOString(),
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
llm: { chat: vi.fn().mockResolvedValue({ text: "mock" }) },
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const stubStepResult: StepResult = {
|
|
109
|
-
toolCalls: [],
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Tests
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
describe("MessageWindow", () => {
|
|
117
|
-
it("creates an Extension with name 'message-window'", () => {
|
|
118
|
-
const ext = MessageWindow({ maxMessages: 5 });
|
|
119
|
-
expect(ext.name).toBe("message-window");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("registers step middleware via api.pipeline.register", () => {
|
|
123
|
-
const conversation = makeMockConversationState([]);
|
|
124
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
125
|
-
|
|
126
|
-
const ext = MessageWindow({ maxMessages: 5 });
|
|
127
|
-
ext.register(api);
|
|
128
|
-
|
|
129
|
-
expect(api.pipeline.register).toHaveBeenCalledOnce();
|
|
130
|
-
expect(registeredMiddleware[0].level).toBe("step");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("does NOT emit truncate when messages are within maxMessages", async () => {
|
|
134
|
-
const messages = makeMessages(3);
|
|
135
|
-
const conversation = makeMockConversationState(messages);
|
|
136
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
137
|
-
|
|
138
|
-
const ext = MessageWindow({ maxMessages: 5 });
|
|
139
|
-
ext.register(api);
|
|
140
|
-
|
|
141
|
-
const middleware = registeredMiddleware[0].handler;
|
|
142
|
-
const ctx = makeStepContext(conversation);
|
|
143
|
-
const next = vi.fn(async () => stubStepResult);
|
|
144
|
-
|
|
145
|
-
await middleware(ctx, next);
|
|
146
|
-
|
|
147
|
-
expect(conversation.emit).not.toHaveBeenCalled();
|
|
148
|
-
expect(next).toHaveBeenCalledOnce();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("emits truncate when messages exceed maxMessages", async () => {
|
|
152
|
-
const messages = makeMessages(8);
|
|
153
|
-
const conversation = makeMockConversationState(messages);
|
|
154
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
155
|
-
|
|
156
|
-
const ext = MessageWindow({ maxMessages: 5 });
|
|
157
|
-
ext.register(api);
|
|
158
|
-
|
|
159
|
-
const middleware = registeredMiddleware[0].handler;
|
|
160
|
-
const ctx = makeStepContext(conversation);
|
|
161
|
-
const next = vi.fn(async () => stubStepResult);
|
|
162
|
-
|
|
163
|
-
await middleware(ctx, next);
|
|
164
|
-
|
|
165
|
-
expect(conversation.emit).toHaveBeenCalledOnce();
|
|
166
|
-
const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted[0];
|
|
167
|
-
expect(emitted.type).toBe("truncate");
|
|
168
|
-
if (emitted.type === "truncate") {
|
|
169
|
-
expect(emitted.keepLast).toBe(5);
|
|
170
|
-
}
|
|
171
|
-
expect(next).toHaveBeenCalledOnce();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("emits truncate with exact maxMessages count", async () => {
|
|
175
|
-
const messages = makeMessages(6);
|
|
176
|
-
const conversation = makeMockConversationState(messages);
|
|
177
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
178
|
-
|
|
179
|
-
const ext = MessageWindow({ maxMessages: 5 });
|
|
180
|
-
ext.register(api);
|
|
181
|
-
|
|
182
|
-
const middleware = registeredMiddleware[0].handler;
|
|
183
|
-
const ctx = makeStepContext(conversation);
|
|
184
|
-
const next = vi.fn(async () => stubStepResult);
|
|
185
|
-
|
|
186
|
-
await middleware(ctx, next);
|
|
187
|
-
|
|
188
|
-
expect(conversation.emit).toHaveBeenCalledOnce();
|
|
189
|
-
const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted[0];
|
|
190
|
-
if (emitted.type === "truncate") {
|
|
191
|
-
expect(emitted.keepLast).toBe(5);
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
});
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { RequiredToolsGuard } from "../extensions/required-tools-guard.js";
|
|
3
|
-
import type {
|
|
4
|
-
ExtensionApi,
|
|
5
|
-
TurnMiddleware,
|
|
6
|
-
TurnContext,
|
|
7
|
-
TurnResult,
|
|
8
|
-
ConversationState,
|
|
9
|
-
ToolDefinition,
|
|
10
|
-
} from "@goondan/openharness-types";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Helpers
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
function makeMockConversationState(): ConversationState {
|
|
17
|
-
return {
|
|
18
|
-
messages: [],
|
|
19
|
-
events: [],
|
|
20
|
-
emit: vi.fn(),
|
|
21
|
-
restore: vi.fn(),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function makeMockApi(
|
|
26
|
-
conversation: ConversationState,
|
|
27
|
-
availableTools: ToolDefinition[] = [],
|
|
28
|
-
): {
|
|
29
|
-
api: ExtensionApi;
|
|
30
|
-
registeredMiddleware: Array<{
|
|
31
|
-
level: string;
|
|
32
|
-
handler: TurnMiddleware;
|
|
33
|
-
options?: { priority?: number };
|
|
34
|
-
}>;
|
|
35
|
-
} {
|
|
36
|
-
const registeredMiddleware: Array<{
|
|
37
|
-
level: string;
|
|
38
|
-
handler: TurnMiddleware;
|
|
39
|
-
options?: { priority?: number };
|
|
40
|
-
}> = [];
|
|
41
|
-
|
|
42
|
-
const api: ExtensionApi = {
|
|
43
|
-
pipeline: {
|
|
44
|
-
register: vi.fn(
|
|
45
|
-
(level: string, handler: TurnMiddleware, options?: { priority?: number }) => {
|
|
46
|
-
registeredMiddleware.push({ level, handler, options });
|
|
47
|
-
},
|
|
48
|
-
) as unknown as ExtensionApi["pipeline"]["register"],
|
|
49
|
-
},
|
|
50
|
-
tools: {
|
|
51
|
-
register: vi.fn(),
|
|
52
|
-
remove: vi.fn(),
|
|
53
|
-
list: vi.fn(() => availableTools as readonly ToolDefinition[]),
|
|
54
|
-
},
|
|
55
|
-
on: vi.fn(),
|
|
56
|
-
conversation,
|
|
57
|
-
runtime: {
|
|
58
|
-
agent: {
|
|
59
|
-
name: "test-agent",
|
|
60
|
-
model: { provider: "openai", model: "gpt-4o" },
|
|
61
|
-
extensions: [],
|
|
62
|
-
tools: [],
|
|
63
|
-
},
|
|
64
|
-
agents: {},
|
|
65
|
-
connections: {},
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return { api, registeredMiddleware };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function makeTurnContext(conversation: ConversationState): TurnContext {
|
|
73
|
-
return {
|
|
74
|
-
turnId: "turn-1",
|
|
75
|
-
agentName: "test-agent",
|
|
76
|
-
conversationId: "conv-1",
|
|
77
|
-
conversation,
|
|
78
|
-
abortSignal: new AbortController().signal,
|
|
79
|
-
input: {
|
|
80
|
-
name: "test-event",
|
|
81
|
-
content: [{ type: "text", text: "hello" }],
|
|
82
|
-
properties: {},
|
|
83
|
-
source: {
|
|
84
|
-
connector: "test-connector",
|
|
85
|
-
connectionName: "test",
|
|
86
|
-
receivedAt: new Date().toISOString(),
|
|
87
|
-
},
|
|
88
|
-
},
|
|
89
|
-
llm: { chat: vi.fn().mockResolvedValue({ text: "mock" }) },
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function makeDummyTool(name: string): ToolDefinition {
|
|
94
|
-
return {
|
|
95
|
-
name,
|
|
96
|
-
description: `Tool ${name}`,
|
|
97
|
-
parameters: { type: "object", properties: {} },
|
|
98
|
-
handler: async () => ({ type: "text", text: "ok" }),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const stubTurnResult: TurnResult = {
|
|
103
|
-
turnId: "turn-1",
|
|
104
|
-
agentName: "test-agent",
|
|
105
|
-
conversationId: "conv-1",
|
|
106
|
-
status: "completed",
|
|
107
|
-
steps: [],
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
// Tests
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
|
|
114
|
-
describe("RequiredToolsGuard", () => {
|
|
115
|
-
it("creates an Extension with name 'required-tools-guard'", () => {
|
|
116
|
-
const ext = RequiredToolsGuard({ tools: ["my_tool"] });
|
|
117
|
-
expect(ext.name).toBe("required-tools-guard");
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it("registers turn middleware via api.pipeline.register", () => {
|
|
121
|
-
const conversation = makeMockConversationState();
|
|
122
|
-
const { api, registeredMiddleware } = makeMockApi(conversation);
|
|
123
|
-
|
|
124
|
-
const ext = RequiredToolsGuard({ tools: ["my_tool"] });
|
|
125
|
-
ext.register(api);
|
|
126
|
-
|
|
127
|
-
expect(api.pipeline.register).toHaveBeenCalledOnce();
|
|
128
|
-
expect(registeredMiddleware[0].level).toBe("turn");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("calls next() when all required tools are present", async () => {
|
|
132
|
-
const conversation = makeMockConversationState();
|
|
133
|
-
const tools = [makeDummyTool("tool_a"), makeDummyTool("tool_b")];
|
|
134
|
-
const { api, registeredMiddleware } = makeMockApi(conversation, tools);
|
|
135
|
-
|
|
136
|
-
const ext = RequiredToolsGuard({ tools: ["tool_a", "tool_b"] });
|
|
137
|
-
ext.register(api);
|
|
138
|
-
|
|
139
|
-
const middleware = registeredMiddleware[0].handler;
|
|
140
|
-
const ctx = makeTurnContext(conversation);
|
|
141
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
142
|
-
|
|
143
|
-
const result = await middleware(ctx, next);
|
|
144
|
-
|
|
145
|
-
expect(next).toHaveBeenCalledOnce();
|
|
146
|
-
expect(result).toBe(stubTurnResult);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("throws error when required tools are missing", async () => {
|
|
150
|
-
const conversation = makeMockConversationState();
|
|
151
|
-
const tools = [makeDummyTool("tool_a")];
|
|
152
|
-
const { api, registeredMiddleware } = makeMockApi(conversation, tools);
|
|
153
|
-
|
|
154
|
-
const ext = RequiredToolsGuard({ tools: ["tool_a", "tool_b"] });
|
|
155
|
-
ext.register(api);
|
|
156
|
-
|
|
157
|
-
const middleware = registeredMiddleware[0].handler;
|
|
158
|
-
const ctx = makeTurnContext(conversation);
|
|
159
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
160
|
-
|
|
161
|
-
await expect(middleware(ctx, next)).rejects.toThrow("tool_b");
|
|
162
|
-
expect(next).not.toHaveBeenCalled();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("throws when no tools are registered at all", async () => {
|
|
166
|
-
const conversation = makeMockConversationState();
|
|
167
|
-
const { api, registeredMiddleware } = makeMockApi(conversation, []);
|
|
168
|
-
|
|
169
|
-
const ext = RequiredToolsGuard({ tools: ["required_tool"] });
|
|
170
|
-
ext.register(api);
|
|
171
|
-
|
|
172
|
-
const middleware = registeredMiddleware[0].handler;
|
|
173
|
-
const ctx = makeTurnContext(conversation);
|
|
174
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
175
|
-
|
|
176
|
-
await expect(middleware(ctx, next)).rejects.toThrow("required_tool");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("passes when no tools are required", async () => {
|
|
180
|
-
const conversation = makeMockConversationState();
|
|
181
|
-
const { api, registeredMiddleware } = makeMockApi(conversation, []);
|
|
182
|
-
|
|
183
|
-
const ext = RequiredToolsGuard({ tools: [] });
|
|
184
|
-
ext.register(api);
|
|
185
|
-
|
|
186
|
-
const middleware = registeredMiddleware[0].handler;
|
|
187
|
-
const ctx = makeTurnContext(conversation);
|
|
188
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
189
|
-
|
|
190
|
-
await expect(middleware(ctx, next)).resolves.toBe(stubTurnResult);
|
|
191
|
-
expect(next).toHaveBeenCalledOnce();
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("error message lists all missing tools", async () => {
|
|
195
|
-
const conversation = makeMockConversationState();
|
|
196
|
-
const { api, registeredMiddleware } = makeMockApi(conversation, []);
|
|
197
|
-
|
|
198
|
-
const ext = RequiredToolsGuard({ tools: ["tool_x", "tool_y", "tool_z"] });
|
|
199
|
-
ext.register(api);
|
|
200
|
-
|
|
201
|
-
const middleware = registeredMiddleware[0].handler;
|
|
202
|
-
const ctx = makeTurnContext(conversation);
|
|
203
|
-
const next = vi.fn(async () => stubTurnResult);
|
|
204
|
-
|
|
205
|
-
await expect(middleware(ctx, next)).rejects.toThrow(/tool_x.*tool_y.*tool_z/);
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { ToolSearch } from "../extensions/tool-search.js";
|
|
3
|
-
import type {
|
|
4
|
-
ExtensionApi,
|
|
5
|
-
ConversationState,
|
|
6
|
-
ToolDefinition,
|
|
7
|
-
ToolContext,
|
|
8
|
-
} from "@goondan/openharness-types";
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Helpers
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
function makeMockConversationState(): ConversationState {
|
|
15
|
-
return {
|
|
16
|
-
messages: [],
|
|
17
|
-
events: [],
|
|
18
|
-
emit: vi.fn(),
|
|
19
|
-
restore: vi.fn(),
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function makeMockApi(
|
|
24
|
-
conversation: ConversationState,
|
|
25
|
-
availableTools: ToolDefinition[] = [],
|
|
26
|
-
): {
|
|
27
|
-
api: ExtensionApi;
|
|
28
|
-
registeredTools: ToolDefinition[];
|
|
29
|
-
} {
|
|
30
|
-
const registeredTools: ToolDefinition[] = [...availableTools];
|
|
31
|
-
|
|
32
|
-
const api: ExtensionApi = {
|
|
33
|
-
pipeline: {
|
|
34
|
-
register: vi.fn() as unknown as ExtensionApi["pipeline"]["register"],
|
|
35
|
-
},
|
|
36
|
-
tools: {
|
|
37
|
-
register: vi.fn((tool: ToolDefinition) => {
|
|
38
|
-
registeredTools.push(tool);
|
|
39
|
-
}),
|
|
40
|
-
remove: vi.fn(),
|
|
41
|
-
list: vi.fn(() => registeredTools as readonly ToolDefinition[]),
|
|
42
|
-
},
|
|
43
|
-
on: vi.fn(),
|
|
44
|
-
conversation,
|
|
45
|
-
runtime: {
|
|
46
|
-
agent: {
|
|
47
|
-
name: "test-agent",
|
|
48
|
-
model: { provider: "openai", model: "gpt-4o" },
|
|
49
|
-
extensions: [],
|
|
50
|
-
tools: [],
|
|
51
|
-
},
|
|
52
|
-
agents: {},
|
|
53
|
-
connections: {},
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return { api, registeredTools };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function makeDummyTool(name: string, description: string): ToolDefinition {
|
|
61
|
-
return {
|
|
62
|
-
name,
|
|
63
|
-
description,
|
|
64
|
-
parameters: { type: "object", properties: {} },
|
|
65
|
-
handler: async () => ({ type: "text", text: "ok" }),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function makeToolContext(): ToolContext {
|
|
70
|
-
return {
|
|
71
|
-
conversationId: "conv-1",
|
|
72
|
-
agentName: "test-agent",
|
|
73
|
-
abortSignal: new AbortController().signal,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
// Tests
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
|
|
81
|
-
describe("ToolSearch", () => {
|
|
82
|
-
it("creates an Extension with name 'tool-search'", () => {
|
|
83
|
-
const ext = ToolSearch();
|
|
84
|
-
expect(ext.name).toBe("tool-search");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("registers a meta-tool named 'search_tools'", () => {
|
|
88
|
-
const conversation = makeMockConversationState();
|
|
89
|
-
const { api, registeredTools } = makeMockApi(conversation);
|
|
90
|
-
|
|
91
|
-
const ext = ToolSearch();
|
|
92
|
-
ext.register(api);
|
|
93
|
-
|
|
94
|
-
expect(api.tools.register).toHaveBeenCalledOnce();
|
|
95
|
-
const searchTool = registeredTools.find((t) => t.name === "search_tools");
|
|
96
|
-
expect(searchTool).toBeDefined();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("search_tools returns tools matching keyword in name", async () => {
|
|
100
|
-
const conversation = makeMockConversationState();
|
|
101
|
-
const seedTools = [
|
|
102
|
-
makeDummyTool("weather_get", "Get current weather"),
|
|
103
|
-
makeDummyTool("calendar_add", "Add a calendar event"),
|
|
104
|
-
makeDummyTool("weather_forecast", "Get weather forecast"),
|
|
105
|
-
];
|
|
106
|
-
const { api } = makeMockApi(conversation, seedTools);
|
|
107
|
-
|
|
108
|
-
const ext = ToolSearch();
|
|
109
|
-
ext.register(api);
|
|
110
|
-
|
|
111
|
-
const searchTool = (api.tools.list() as ToolDefinition[]).find(
|
|
112
|
-
(t) => t.name === "search_tools",
|
|
113
|
-
)!;
|
|
114
|
-
const result = await searchTool.handler({ query: "weather" }, makeToolContext());
|
|
115
|
-
|
|
116
|
-
expect(result.type).toBe("json");
|
|
117
|
-
if (result.type === "json") {
|
|
118
|
-
const data = result.data as unknown as ToolDefinition[];
|
|
119
|
-
expect(data).toHaveLength(2);
|
|
120
|
-
expect(data.map((t) => t.name)).toContain("weather_get");
|
|
121
|
-
expect(data.map((t) => t.name)).toContain("weather_forecast");
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("search_tools returns tools matching keyword in description", async () => {
|
|
126
|
-
const conversation = makeMockConversationState();
|
|
127
|
-
const seedTools = [
|
|
128
|
-
makeDummyTool("tool_a", "Send an email to a recipient"),
|
|
129
|
-
makeDummyTool("tool_b", "Read a file from disk"),
|
|
130
|
-
];
|
|
131
|
-
const { api } = makeMockApi(conversation, seedTools);
|
|
132
|
-
|
|
133
|
-
const ext = ToolSearch();
|
|
134
|
-
ext.register(api);
|
|
135
|
-
|
|
136
|
-
const searchTool = (api.tools.list() as ToolDefinition[]).find(
|
|
137
|
-
(t) => t.name === "search_tools",
|
|
138
|
-
)!;
|
|
139
|
-
const result = await searchTool.handler({ query: "email" }, makeToolContext());
|
|
140
|
-
|
|
141
|
-
expect(result.type).toBe("json");
|
|
142
|
-
if (result.type === "json") {
|
|
143
|
-
const data = result.data as unknown as ToolDefinition[];
|
|
144
|
-
expect(data).toHaveLength(1);
|
|
145
|
-
expect(data[0].name).toBe("tool_a");
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("search_tools returns empty array when no match", async () => {
|
|
150
|
-
const conversation = makeMockConversationState();
|
|
151
|
-
const seedTools = [makeDummyTool("calculator", "Perform math operations")];
|
|
152
|
-
const { api } = makeMockApi(conversation, seedTools);
|
|
153
|
-
|
|
154
|
-
const ext = ToolSearch();
|
|
155
|
-
ext.register(api);
|
|
156
|
-
|
|
157
|
-
const searchTool = (api.tools.list() as ToolDefinition[]).find(
|
|
158
|
-
(t) => t.name === "search_tools",
|
|
159
|
-
)!;
|
|
160
|
-
const result = await searchTool.handler({ query: "nonexistent" }, makeToolContext());
|
|
161
|
-
|
|
162
|
-
expect(result.type).toBe("json");
|
|
163
|
-
if (result.type === "json") {
|
|
164
|
-
expect(result.data).toEqual([]);
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("search is case-insensitive", async () => {
|
|
169
|
-
const conversation = makeMockConversationState();
|
|
170
|
-
const seedTools = [makeDummyTool("WeatherTool", "Get Weather Data")];
|
|
171
|
-
const { api } = makeMockApi(conversation, seedTools);
|
|
172
|
-
|
|
173
|
-
const ext = ToolSearch();
|
|
174
|
-
ext.register(api);
|
|
175
|
-
|
|
176
|
-
const searchTool = (api.tools.list() as ToolDefinition[]).find(
|
|
177
|
-
(t) => t.name === "search_tools",
|
|
178
|
-
)!;
|
|
179
|
-
const result = await searchTool.handler({ query: "weather" }, makeToolContext());
|
|
180
|
-
|
|
181
|
-
expect(result.type).toBe("json");
|
|
182
|
-
if (result.type === "json") {
|
|
183
|
-
const data = result.data as unknown as ToolDefinition[];
|
|
184
|
-
expect(data).toHaveLength(1);
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
});
|