@goondan/openharness-base 0.1.7 → 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.
Files changed (98) hide show
  1. package/package.json +14 -9
  2. package/dist/extensions/compaction.d.ts +0 -12
  3. package/dist/extensions/compaction.d.ts.map +0 -1
  4. package/dist/extensions/compaction.js +0 -159
  5. package/dist/extensions/compaction.js.map +0 -1
  6. package/dist/extensions/context-message.d.ts +0 -9
  7. package/dist/extensions/context-message.d.ts.map +0 -1
  8. package/dist/extensions/context-message.js +0 -446
  9. package/dist/extensions/context-message.js.map +0 -1
  10. package/dist/extensions/index.d.ts +0 -13
  11. package/dist/extensions/index.d.ts.map +0 -1
  12. package/dist/extensions/index.js +0 -7
  13. package/dist/extensions/index.js.map +0 -1
  14. package/dist/extensions/logging.d.ts +0 -11
  15. package/dist/extensions/logging.d.ts.map +0 -1
  16. package/dist/extensions/logging.js +0 -140
  17. package/dist/extensions/logging.js.map +0 -1
  18. package/dist/extensions/message-window.d.ts +0 -7
  19. package/dist/extensions/message-window.d.ts.map +0 -1
  20. package/dist/extensions/message-window.js +0 -60
  21. package/dist/extensions/message-window.js.map +0 -1
  22. package/dist/extensions/required-tools-guard.d.ts +0 -9
  23. package/dist/extensions/required-tools-guard.d.ts.map +0 -1
  24. package/dist/extensions/required-tools-guard.js +0 -74
  25. package/dist/extensions/required-tools-guard.js.map +0 -1
  26. package/dist/extensions/tool-search.d.ts +0 -10
  27. package/dist/extensions/tool-search.d.ts.map +0 -1
  28. package/dist/extensions/tool-search.js +0 -198
  29. package/dist/extensions/tool-search.js.map +0 -1
  30. package/dist/harness.yaml +0 -503
  31. package/dist/index.d.ts.map +0 -1
  32. package/dist/index.js.map +0 -1
  33. package/dist/manifests/base.d.ts +0 -8
  34. package/dist/manifests/base.d.ts.map +0 -1
  35. package/dist/manifests/base.js +0 -352
  36. package/dist/manifests/base.js.map +0 -1
  37. package/dist/manifests/index.d.ts +0 -3
  38. package/dist/manifests/index.d.ts.map +0 -1
  39. package/dist/manifests/index.js +0 -2
  40. package/dist/manifests/index.js.map +0 -1
  41. package/dist/tools/bash.d.ts +0 -8
  42. package/dist/tools/bash.d.ts.map +0 -1
  43. package/dist/tools/bash.js +0 -119
  44. package/dist/tools/bash.js.map +0 -1
  45. package/dist/tools/file-system.d.ts +0 -12
  46. package/dist/tools/file-system.d.ts.map +0 -1
  47. package/dist/tools/file-system.js +0 -117
  48. package/dist/tools/file-system.js.map +0 -1
  49. package/dist/tools/http-fetch.d.ts +0 -8
  50. package/dist/tools/http-fetch.d.ts.map +0 -1
  51. package/dist/tools/http-fetch.js +0 -149
  52. package/dist/tools/http-fetch.js.map +0 -1
  53. package/dist/tools/index.d.ts +0 -7
  54. package/dist/tools/index.d.ts.map +0 -1
  55. package/dist/tools/index.js +0 -7
  56. package/dist/tools/index.js.map +0 -1
  57. package/dist/tools/json-query.d.ts +0 -12
  58. package/dist/tools/json-query.d.ts.map +0 -1
  59. package/dist/tools/json-query.js +0 -176
  60. package/dist/tools/json-query.js.map +0 -1
  61. package/dist/tools/text-transform.d.ts +0 -16
  62. package/dist/tools/text-transform.d.ts.map +0 -1
  63. package/dist/tools/text-transform.js +0 -127
  64. package/dist/tools/text-transform.js.map +0 -1
  65. package/dist/tools/wait.d.ts +0 -6
  66. package/dist/tools/wait.d.ts.map +0 -1
  67. package/dist/tools/wait.js +0 -32
  68. package/dist/tools/wait.js.map +0 -1
  69. package/dist/types.d.ts +0 -4
  70. package/dist/types.d.ts.map +0 -1
  71. package/dist/types.js +0 -6
  72. package/dist/types.js.map +0 -1
  73. package/dist/utils.d.ts +0 -17
  74. package/dist/utils.d.ts.map +0 -1
  75. package/dist/utils.js +0 -159
  76. package/dist/utils.js.map +0 -1
  77. package/src/__tests__/basic-system-prompt.test.ts +0 -186
  78. package/src/__tests__/compaction-summarize.test.ts +0 -282
  79. package/src/__tests__/logging.test.ts +0 -200
  80. package/src/__tests__/message-window.test.ts +0 -194
  81. package/src/__tests__/required-tools-guard.test.ts +0 -207
  82. package/src/__tests__/tool-search.test.ts +0 -187
  83. package/src/__tests__/tools.test.ts +0 -332
  84. package/src/extensions/basic-system-prompt.ts +0 -48
  85. package/src/extensions/compaction-summarize.ts +0 -104
  86. package/src/extensions/logging.ts +0 -42
  87. package/src/extensions/message-window.ts +0 -23
  88. package/src/extensions/required-tools-guard.ts +0 -24
  89. package/src/extensions/tool-search.ts +0 -38
  90. package/src/index.ts +0 -16
  91. package/src/tools/bash.ts +0 -38
  92. package/src/tools/file-system.ts +0 -83
  93. package/src/tools/http-fetch.ts +0 -64
  94. package/src/tools/json-query.ts +0 -71
  95. package/src/tools/text-transform.ts +0 -59
  96. package/src/tools/wait.ts +0 -46
  97. package/tsconfig.json +0 -8
  98. package/vitest.config.ts +0 -7
@@ -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
- });
@@ -1,332 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { BashTool } from "../tools/bash.js";
3
- import { FileReadTool, FileWriteTool, FileListTool } from "../tools/file-system.js";
4
- import { HttpFetchTool } from "../tools/http-fetch.js";
5
- import { JsonQueryTool } from "../tools/json-query.js";
6
- import { TextTransformTool } from "../tools/text-transform.js";
7
- import { WaitTool } from "../tools/wait.js";
8
- import type { ToolContext } from "@goondan/openharness-types";
9
- import { tmpdir } from "node:os";
10
- import { join } from "node:path";
11
- import { writeFile, mkdir } from "node:fs/promises";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Helpers
15
- // ---------------------------------------------------------------------------
16
-
17
- function makeCtx(abortSignal?: AbortSignal): ToolContext {
18
- return {
19
- conversationId: "conv-1",
20
- agentName: "test-agent",
21
- abortSignal: abortSignal ?? new AbortController().signal,
22
- };
23
- }
24
-
25
- // ---------------------------------------------------------------------------
26
- // All tools — consolidated schema validation
27
- // ---------------------------------------------------------------------------
28
-
29
- describe("All tools — schema structure", () => {
30
- it("every tool has name, description, non-empty required params, and a handler function", () => {
31
- const tools = [
32
- { factory: BashTool, expectedName: "bash", expectedRequired: ["command"] },
33
- { factory: FileReadTool, expectedName: "file_read", expectedRequired: ["path"] },
34
- { factory: FileWriteTool, expectedName: "file_write", expectedRequired: ["path", "content"] },
35
- { factory: FileListTool, expectedName: "file_list", expectedRequired: ["path"] },
36
- { factory: HttpFetchTool, expectedName: "http_fetch", expectedRequired: ["url"] },
37
- { factory: JsonQueryTool, expectedName: "json_query", expectedRequired: ["data", "path"] },
38
- { factory: TextTransformTool, expectedName: "text_transform", expectedRequired: ["text", "operation"] },
39
- { factory: WaitTool, expectedName: "wait", expectedRequired: ["ms"] },
40
- ];
41
-
42
- for (const { factory, expectedName, expectedRequired } of tools) {
43
- const tool = factory();
44
- expect(tool.name).toBe(expectedName);
45
- expect(typeof tool.description).toBe("string");
46
- expect(tool.description.length).toBeGreaterThan(0);
47
- expect(typeof tool.handler).toBe("function");
48
-
49
- const params = tool.parameters as { properties: Record<string, unknown>; required: string[] };
50
- for (const req of expectedRequired) {
51
- expect(params.properties).toHaveProperty(req);
52
- expect(params.required).toContain(req);
53
- }
54
- }
55
- });
56
- });
57
-
58
- // ---------------------------------------------------------------------------
59
- // BashTool
60
- // ---------------------------------------------------------------------------
61
-
62
- describe("BashTool", () => {
63
- it("returns stdout as text result on success", async () => {
64
- const tool = BashTool();
65
- const result = await tool.handler({ command: "echo hello" }, makeCtx());
66
- expect(result.type).toBe("text");
67
- if (result.type === "text") {
68
- expect(result.text.trim()).toBe("hello");
69
- }
70
- });
71
-
72
- it("returns error result on command failure", async () => {
73
- const tool = BashTool();
74
- const result = await tool.handler({ command: "cat /nonexistent_file_xyz_abc_123" }, makeCtx());
75
- expect(result.type).toBe("error");
76
- if (result.type === "error") {
77
- expect(typeof result.error).toBe("string");
78
- }
79
- });
80
- });
81
-
82
- // ---------------------------------------------------------------------------
83
- // FileReadTool
84
- // ---------------------------------------------------------------------------
85
-
86
- describe("FileReadTool", () => {
87
- let tmpFile: string;
88
-
89
- beforeEach(async () => {
90
- tmpFile = join(tmpdir(), `openharness-test-${Date.now()}.txt`);
91
- await writeFile(tmpFile, "hello file", "utf8");
92
- });
93
-
94
- it("reads file content successfully", async () => {
95
- const tool = FileReadTool();
96
- const result = await tool.handler({ path: tmpFile }, makeCtx());
97
- expect(result.type).toBe("text");
98
- if (result.type === "text") {
99
- expect(result.text).toBe("hello file");
100
- }
101
- });
102
-
103
- it("returns error for non-existent file", async () => {
104
- const tool = FileReadTool();
105
- const result = await tool.handler({ path: "/nonexistent/path/file.txt" }, makeCtx());
106
- expect(result.type).toBe("error");
107
- });
108
- });
109
-
110
- // ---------------------------------------------------------------------------
111
- // FileWriteTool
112
- // ---------------------------------------------------------------------------
113
-
114
- describe("FileWriteTool", () => {
115
- it("writes file successfully and returns text result", async () => {
116
- const tmpFile = join(tmpdir(), `openharness-write-${Date.now()}.txt`);
117
- const tool = FileWriteTool();
118
- const result = await tool.handler({ path: tmpFile, content: "written content" }, makeCtx());
119
- expect(result.type).toBe("text");
120
- if (result.type === "text") {
121
- expect(result.text).toContain(tmpFile);
122
- }
123
- });
124
-
125
- it("returns error when path is invalid", async () => {
126
- const tool = FileWriteTool();
127
- const result = await tool.handler({ path: "/nonexistent/deeply/nested/path.txt", content: "x" }, makeCtx());
128
- expect(result.type).toBe("error");
129
- });
130
- });
131
-
132
- // ---------------------------------------------------------------------------
133
- // FileListTool
134
- // ---------------------------------------------------------------------------
135
-
136
- describe("FileListTool", () => {
137
- let tmpDir: string;
138
-
139
- beforeEach(async () => {
140
- tmpDir = join(tmpdir(), `openharness-list-${Date.now()}`);
141
- await mkdir(tmpDir, { recursive: true });
142
- await writeFile(join(tmpDir, "a.txt"), "a");
143
- await writeFile(join(tmpDir, "b.txt"), "b");
144
- });
145
-
146
- it("lists directory entries as json result", async () => {
147
- const tool = FileListTool();
148
- const result = await tool.handler({ path: tmpDir }, makeCtx());
149
- expect(result.type).toBe("json");
150
- if (result.type === "json") {
151
- const entries = result.data as Array<{ name: string; type: string }>;
152
- expect(Array.isArray(entries)).toBe(true);
153
- const names = entries.map((e) => e.name);
154
- expect(names).toContain("a.txt");
155
- expect(names).toContain("b.txt");
156
- }
157
- });
158
-
159
- it("returns error for non-existent directory", async () => {
160
- const tool = FileListTool();
161
- const result = await tool.handler({ path: "/nonexistent/dir/xyz" }, makeCtx());
162
- expect(result.type).toBe("error");
163
- });
164
- });
165
-
166
- // ---------------------------------------------------------------------------
167
- // HttpFetchTool
168
- // ---------------------------------------------------------------------------
169
-
170
- describe("HttpFetchTool", () => {
171
- afterEach(() => {
172
- vi.restoreAllMocks();
173
- });
174
-
175
- it("returns json result with status, headers, body on success", async () => {
176
- const mockResponse = {
177
- status: 200,
178
- headers: {
179
- get: (name: string) => (name === "content-type" ? "application/json" : null),
180
- forEach: (cb: (value: string, key: string) => void) => {
181
- cb("application/json", "content-type");
182
- },
183
- },
184
- json: async () => ({ ok: true }),
185
- };
186
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse));
187
-
188
- const tool = HttpFetchTool();
189
- const result = await tool.handler({ url: "https://example.com/api" }, makeCtx());
190
-
191
- expect(result.type).toBe("json");
192
- if (result.type === "json") {
193
- const data = result.data as { status: number; headers: Record<string, string>; body: unknown };
194
- expect(data.status).toBe(200);
195
- expect(data.body).toEqual({ ok: true });
196
- }
197
- });
198
-
199
- it("returns error result when fetch throws", async () => {
200
- vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error")));
201
-
202
- const tool = HttpFetchTool();
203
- const result = await tool.handler({ url: "https://example.com/fail" }, makeCtx());
204
- expect(result.type).toBe("error");
205
- if (result.type === "error") {
206
- expect(result.error).toContain("Network error");
207
- }
208
- });
209
- });
210
-
211
- // ---------------------------------------------------------------------------
212
- // JsonQueryTool
213
- // ---------------------------------------------------------------------------
214
-
215
- describe("JsonQueryTool", () => {
216
- it("queries nested object with dot notation", async () => {
217
- const tool = JsonQueryTool();
218
- const data = { user: { name: "Alice", age: 30 } };
219
- const result = await tool.handler({ data, path: "$.user.name" }, makeCtx());
220
- expect(result.type).toBe("json");
221
- if (result.type === "json") {
222
- expect(result.data).toBe("Alice");
223
- }
224
- });
225
-
226
- it("queries array with bracket notation", async () => {
227
- const tool = JsonQueryTool();
228
- const data = { items: ["a", "b", "c"] };
229
- const result = await tool.handler({ data, path: "$.items[1]" }, makeCtx());
230
- expect(result.type).toBe("json");
231
- if (result.type === "json") {
232
- expect(result.data).toBe("b");
233
- }
234
- });
235
-
236
- it("returns root data for $ path", async () => {
237
- const tool = JsonQueryTool();
238
- const data = { x: 1 };
239
- const result = await tool.handler({ data, path: "$" }, makeCtx());
240
- expect(result.type).toBe("json");
241
- if (result.type === "json") {
242
- expect(result.data).toEqual(data);
243
- }
244
- });
245
-
246
- it("returns undefined for missing path", async () => {
247
- const tool = JsonQueryTool();
248
- const data = { a: 1 };
249
- const result = await tool.handler({ data, path: "$.b.c" }, makeCtx());
250
- expect(result.type).toBe("json");
251
- if (result.type === "json") {
252
- expect(result.data).toBeUndefined();
253
- }
254
- });
255
- });
256
-
257
- // ---------------------------------------------------------------------------
258
- // TextTransformTool
259
- // ---------------------------------------------------------------------------
260
-
261
- describe("TextTransformTool", () => {
262
- it("uppercase operation", async () => {
263
- const tool = TextTransformTool();
264
- const result = await tool.handler({ text: "hello world", operation: "uppercase" }, makeCtx());
265
- expect(result).toEqual({ type: "text", text: "HELLO WORLD" });
266
- });
267
-
268
- it("lowercase operation", async () => {
269
- const tool = TextTransformTool();
270
- const result = await tool.handler({ text: "HELLO WORLD", operation: "lowercase" }, makeCtx());
271
- expect(result).toEqual({ type: "text", text: "hello world" });
272
- });
273
-
274
- it("trim operation", async () => {
275
- const tool = TextTransformTool();
276
- const result = await tool.handler({ text: " spaces ", operation: "trim" }, makeCtx());
277
- expect(result).toEqual({ type: "text", text: "spaces" });
278
- });
279
-
280
- it("split operation returns json array", async () => {
281
- const tool = TextTransformTool();
282
- const result = await tool.handler(
283
- { text: "a,b,c", operation: "split", options: { delimiter: "," } },
284
- makeCtx(),
285
- );
286
- expect(result.type).toBe("json");
287
- if (result.type === "json") {
288
- expect(result.data).toEqual(["a", "b", "c"]);
289
- }
290
- });
291
-
292
- it("replace operation", async () => {
293
- const tool = TextTransformTool();
294
- const result = await tool.handler(
295
- { text: "foo bar foo", operation: "replace", options: { find: "foo", replacement: "baz" } },
296
- makeCtx(),
297
- );
298
- expect(result).toEqual({ type: "text", text: "baz bar baz" });
299
- });
300
- });
301
-
302
- // ---------------------------------------------------------------------------
303
- // WaitTool
304
- // ---------------------------------------------------------------------------
305
-
306
- describe("WaitTool", () => {
307
- it("waits and returns text result", async () => {
308
- const tool = WaitTool();
309
- const result = await tool.handler({ ms: 10 }, makeCtx());
310
- expect(result.type).toBe("text");
311
- if (result.type === "text") {
312
- expect(result.text).toBe("Waited 10ms");
313
- }
314
- });
315
-
316
- it("respects maxMs cap", async () => {
317
- const tool = WaitTool({ maxMs: 20 });
318
- const result = await tool.handler({ ms: 1000 }, makeCtx());
319
- expect(result.type).toBe("text");
320
- if (result.type === "text") {
321
- expect(result.text).toBe("Waited 20ms");
322
- }
323
- });
324
-
325
- it("rejects when aborted", async () => {
326
- const tool = WaitTool();
327
- const ac = new AbortController();
328
- const promise = tool.handler({ ms: 5000 }, makeCtx(ac.signal));
329
- ac.abort();
330
- await expect(promise).rejects.toThrow();
331
- });
332
- });
@@ -1,48 +0,0 @@
1
- import type { Extension, ExtensionApi } from "@goondan/openharness-types";
2
-
3
- const SYSTEM_MESSAGE_ID = "sys-basic-system-prompt";
4
-
5
- /**
6
- * BasicSystemPrompt extension — prepends a system message to the conversation
7
- * at the start of every turn.
8
- *
9
- * Uses a fixed message ID so the system prompt is only appended once;
10
- * subsequent turns detect the existing message and skip the append.
11
- *
12
- * Priority 10 (HIGH) ensures it runs before other turn middleware.
13
- */
14
- export function BasicSystemPrompt(text: string): Extension {
15
- return {
16
- name: "basic-system-prompt",
17
-
18
- register(api: ExtensionApi): void {
19
- api.pipeline.register(
20
- "turn",
21
- async (ctx, next) => {
22
- const alreadyExists = ctx.conversation.messages.some(
23
- (m) => m.id === SYSTEM_MESSAGE_ID,
24
- );
25
-
26
- if (!alreadyExists) {
27
- ctx.conversation.emit({
28
- type: "append",
29
- message: {
30
- id: SYSTEM_MESSAGE_ID,
31
- data: {
32
- role: "system",
33
- content: text,
34
- },
35
- metadata: {
36
- __createdBy: "basic-system-prompt",
37
- },
38
- },
39
- });
40
- }
41
-
42
- return next();
43
- },
44
- { priority: 10 },
45
- );
46
- },
47
- };
48
- }
@@ -1,104 +0,0 @@
1
- import type { Extension, ExtensionApi, Message, LlmChatOptions } from "@goondan/openharness-types";
2
- import { randomUUID } from "node:crypto";
3
-
4
- const DEFAULT_SUMMARY_PROMPT =
5
- "You are a conversation compactor. Summarize the following messages into a concise summary " +
6
- "that preserves all important context, decisions, facts, and action items. " +
7
- "Be thorough but brief. Output only the summary text, nothing else.";
8
-
9
- /**
10
- * Extract plain-text representation of a Message for summarization.
11
- */
12
- function messageToText(m: Message): string {
13
- const role = m.data.role;
14
- const content =
15
- typeof m.data.content === "string"
16
- ? m.data.content
17
- : JSON.stringify(m.data.content);
18
- return `[${role}]: ${content}`;
19
- }
20
-
21
- /**
22
- * CompactionSummarize extension — when message count exceeds `threshold`,
23
- * removes the oldest messages and replaces them with an LLM-generated summary.
24
- *
25
- * By default, uses the agent's own LLM (`ctx.llm`) to produce the summary.
26
- * A custom `summarizer` callback can override this for advanced use cases
27
- * (e.g. using a cheaper model, external API, or deterministic logic).
28
- *
29
- * @param config.threshold - Trigger compaction when messages exceed this count.
30
- * @param config.summaryPrompt - Custom system prompt for the LLM summarizer.
31
- * @param config.summarizer - Optional override: produce summary text from messages.
32
- */
33
- export function CompactionSummarize(config: {
34
- threshold: number;
35
- summaryPrompt?: string;
36
- /** LLM options for the summarization call (e.g. model override for cheaper summarization). */
37
- llmOptions?: LlmChatOptions;
38
- summarizer?: (messages: Message[]) => Promise<string>;
39
- }): Extension {
40
- return {
41
- name: "compaction-summarize",
42
-
43
- register(api: ExtensionApi): void {
44
- api.pipeline.register("step", async (ctx, next) => {
45
- const messages = ctx.conversation.messages;
46
- if (messages.length > config.threshold) {
47
- const keepCount = Math.floor(config.threshold / 2);
48
- const removeCount = messages.length - keepCount;
49
- const toRemove = messages.slice(0, removeCount);
50
-
51
- let summaryText: string;
52
-
53
- if (config.summarizer) {
54
- // User-provided summarizer takes precedence
55
- summaryText = await config.summarizer([...toRemove]);
56
- } else {
57
- // Default: LLM-based summarization via ctx.llm
58
- const transcript = toRemove.map(messageToText).join("\n");
59
- const prompt = config.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT;
60
-
61
- const llmResponse = await ctx.llm.chat(
62
- [
63
- { id: `compaction-sys-${randomUUID()}`, data: { role: "system", content: prompt }, metadata: {} },
64
- { id: `compaction-usr-${randomUUID()}`, data: { role: "user", content: transcript }, metadata: {} },
65
- ],
66
- [], // no tools needed for summarization
67
- ctx.abortSignal,
68
- config.llmOptions,
69
- );
70
-
71
- summaryText = llmResponse.text ?? transcript;
72
- }
73
-
74
- // Remove old messages, replace first with summary for stable ordering
75
- const [firstToRemove, ...restToRemove] = toRemove;
76
- // Replace the first message with the summary
77
- ctx.conversation.emit({
78
- type: "remove",
79
- messageId: firstToRemove.id,
80
- });
81
- for (const msg of restToRemove) {
82
- ctx.conversation.emit({ type: "remove", messageId: msg.id });
83
- }
84
-
85
- // Prepend summary as a system message
86
- ctx.conversation.emit({
87
- type: "append",
88
- message: {
89
- id: `summary-${randomUUID()}`,
90
- data: {
91
- role: "system",
92
- content: `[Summary of earlier conversation]: ${summaryText}`,
93
- },
94
- metadata: {
95
- __createdBy: "compaction-summarize",
96
- },
97
- },
98
- });
99
- }
100
- return next();
101
- });
102
- },
103
- };
104
- }