@goondan/openharness-base 0.0.1-alpha4 → 0.0.1-alpha5

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 (105) hide show
  1. package/dist/index.d.ts +72 -5
  2. package/dist/index.js +506 -5
  3. package/package.json +11 -33
  4. package/src/__tests__/compaction-summarize.test.ts +234 -0
  5. package/src/__tests__/context-message.test.ts +203 -0
  6. package/src/__tests__/logging.test.ts +200 -0
  7. package/src/__tests__/message-window.test.ts +193 -0
  8. package/src/__tests__/required-tools-guard.test.ts +206 -0
  9. package/src/__tests__/tool-search.test.ts +187 -0
  10. package/src/__tests__/tools.test.ts +332 -0
  11. package/src/extensions/compaction-summarize.ts +58 -0
  12. package/src/extensions/context-message.ts +37 -0
  13. package/src/extensions/logging.ts +42 -0
  14. package/src/extensions/message-window.ts +23 -0
  15. package/src/extensions/required-tools-guard.ts +24 -0
  16. package/src/extensions/tool-search.ts +38 -0
  17. package/src/index.ts +16 -0
  18. package/src/tools/bash.ts +38 -0
  19. package/src/tools/file-system.ts +83 -0
  20. package/src/tools/http-fetch.ts +64 -0
  21. package/src/tools/json-query.ts +71 -0
  22. package/src/tools/text-transform.ts +59 -0
  23. package/src/tools/wait.ts +46 -0
  24. package/tsconfig.json +8 -0
  25. package/vitest.config.ts +7 -0
  26. package/README.md +0 -11
  27. package/dist/extensions/compaction.d.ts +0 -12
  28. package/dist/extensions/compaction.d.ts.map +0 -1
  29. package/dist/extensions/compaction.js +0 -159
  30. package/dist/extensions/compaction.js.map +0 -1
  31. package/dist/extensions/context-message.d.ts +0 -9
  32. package/dist/extensions/context-message.d.ts.map +0 -1
  33. package/dist/extensions/context-message.js +0 -446
  34. package/dist/extensions/context-message.js.map +0 -1
  35. package/dist/extensions/index.d.ts +0 -13
  36. package/dist/extensions/index.d.ts.map +0 -1
  37. package/dist/extensions/index.js +0 -7
  38. package/dist/extensions/index.js.map +0 -1
  39. package/dist/extensions/logging.d.ts +0 -11
  40. package/dist/extensions/logging.d.ts.map +0 -1
  41. package/dist/extensions/logging.js +0 -140
  42. package/dist/extensions/logging.js.map +0 -1
  43. package/dist/extensions/message-integrity.d.ts +0 -8
  44. package/dist/extensions/message-integrity.d.ts.map +0 -1
  45. package/dist/extensions/message-integrity.js +0 -88
  46. package/dist/extensions/message-integrity.js.map +0 -1
  47. package/dist/extensions/message-window.d.ts +0 -7
  48. package/dist/extensions/message-window.d.ts.map +0 -1
  49. package/dist/extensions/message-window.js +0 -60
  50. package/dist/extensions/message-window.js.map +0 -1
  51. package/dist/extensions/required-tools-guard.d.ts +0 -9
  52. package/dist/extensions/required-tools-guard.d.ts.map +0 -1
  53. package/dist/extensions/required-tools-guard.js +0 -74
  54. package/dist/extensions/required-tools-guard.js.map +0 -1
  55. package/dist/extensions/tool-search.d.ts +0 -10
  56. package/dist/extensions/tool-search.d.ts.map +0 -1
  57. package/dist/extensions/tool-search.js +0 -198
  58. package/dist/extensions/tool-search.js.map +0 -1
  59. package/dist/harness.yaml +0 -503
  60. package/dist/index.d.ts.map +0 -1
  61. package/dist/index.js.map +0 -1
  62. package/dist/manifests/base.d.ts +0 -8
  63. package/dist/manifests/base.d.ts.map +0 -1
  64. package/dist/manifests/base.js +0 -352
  65. package/dist/manifests/base.js.map +0 -1
  66. package/dist/manifests/index.d.ts +0 -3
  67. package/dist/manifests/index.d.ts.map +0 -1
  68. package/dist/manifests/index.js +0 -2
  69. package/dist/manifests/index.js.map +0 -1
  70. package/dist/tools/bash.d.ts +0 -8
  71. package/dist/tools/bash.d.ts.map +0 -1
  72. package/dist/tools/bash.js +0 -119
  73. package/dist/tools/bash.js.map +0 -1
  74. package/dist/tools/file-system.d.ts +0 -12
  75. package/dist/tools/file-system.d.ts.map +0 -1
  76. package/dist/tools/file-system.js +0 -117
  77. package/dist/tools/file-system.js.map +0 -1
  78. package/dist/tools/http-fetch.d.ts +0 -8
  79. package/dist/tools/http-fetch.d.ts.map +0 -1
  80. package/dist/tools/http-fetch.js +0 -149
  81. package/dist/tools/http-fetch.js.map +0 -1
  82. package/dist/tools/index.d.ts +0 -7
  83. package/dist/tools/index.d.ts.map +0 -1
  84. package/dist/tools/index.js +0 -7
  85. package/dist/tools/index.js.map +0 -1
  86. package/dist/tools/json-query.d.ts +0 -12
  87. package/dist/tools/json-query.d.ts.map +0 -1
  88. package/dist/tools/json-query.js +0 -176
  89. package/dist/tools/json-query.js.map +0 -1
  90. package/dist/tools/text-transform.d.ts +0 -16
  91. package/dist/tools/text-transform.d.ts.map +0 -1
  92. package/dist/tools/text-transform.js +0 -127
  93. package/dist/tools/text-transform.js.map +0 -1
  94. package/dist/tools/wait.d.ts +0 -6
  95. package/dist/tools/wait.d.ts.map +0 -1
  96. package/dist/tools/wait.js +0 -32
  97. package/dist/tools/wait.js.map +0 -1
  98. package/dist/types.d.ts +0 -4
  99. package/dist/types.d.ts.map +0 -1
  100. package/dist/types.js +0 -6
  101. package/dist/types.js.map +0 -1
  102. package/dist/utils.d.ts +0 -17
  103. package/dist/utils.d.ts.map +0 -1
  104. package/dist/utils.js +0 -159
  105. package/dist/utils.js.map +0 -1
@@ -0,0 +1,332 @@
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
+ });
@@ -0,0 +1,58 @@
1
+ import type { Extension, ExtensionApi } from "@goondan/openharness-types";
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ /**
5
+ * CompactionSummarize extension — when message count exceeds `threshold`,
6
+ * removes the oldest messages and prepends a summary system message.
7
+ *
8
+ * For MVP the "summary" is just the concatenation of removed message text.
9
+ */
10
+ export function CompactionSummarize(config: {
11
+ threshold: number;
12
+ summaryPrompt?: string;
13
+ }): Extension {
14
+ return {
15
+ name: "compaction-summarize",
16
+
17
+ register(api: ExtensionApi): void {
18
+ api.pipeline.register("step", async (ctx, next) => {
19
+ const messages = ctx.conversation.messages;
20
+ if (messages.length > config.threshold) {
21
+ const keepCount = Math.floor(config.threshold / 2);
22
+ const removeCount = messages.length - keepCount;
23
+ const toRemove = messages.slice(0, removeCount);
24
+
25
+ // Build a naive summary from removed messages
26
+ const summaryText = toRemove
27
+ .map((m) =>
28
+ typeof m.data.content === "string"
29
+ ? m.data.content
30
+ : JSON.stringify(m.data.content),
31
+ )
32
+ .join(" ");
33
+
34
+ // Remove old messages
35
+ for (const msg of toRemove) {
36
+ ctx.conversation.emit({ type: "remove", messageId: msg.id });
37
+ }
38
+
39
+ // Prepend summary
40
+ ctx.conversation.emit({
41
+ type: "append",
42
+ message: {
43
+ id: `summary-${randomUUID()}`,
44
+ data: {
45
+ role: "system",
46
+ content: `[Summary of earlier conversation]: ${summaryText}`,
47
+ },
48
+ metadata: {
49
+ __createdBy: "compaction-summarize",
50
+ },
51
+ },
52
+ });
53
+ }
54
+ return next();
55
+ });
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,37 @@
1
+ import type { Extension, ExtensionApi } from "@goondan/openharness-types";
2
+ import { randomUUID } from "node:crypto";
3
+
4
+ /**
5
+ * ContextMessage extension — prepends a system message to the conversation
6
+ * at the start of every turn.
7
+ *
8
+ * Priority 10 (HIGH) ensures it runs before other turn middleware.
9
+ */
10
+ export function ContextMessage(text: string): Extension {
11
+ return {
12
+ name: "context-message",
13
+
14
+ register(api: ExtensionApi): void {
15
+ api.pipeline.register(
16
+ "turn",
17
+ async (ctx, next) => {
18
+ ctx.conversation.emit({
19
+ type: "append",
20
+ message: {
21
+ id: `ctx-msg-${randomUUID()}`,
22
+ data: {
23
+ role: "system",
24
+ content: text,
25
+ },
26
+ metadata: {
27
+ __createdBy: "context-message",
28
+ },
29
+ },
30
+ });
31
+ return next();
32
+ },
33
+ { priority: 10 },
34
+ );
35
+ },
36
+ };
37
+ }
@@ -0,0 +1,42 @@
1
+ import type { Extension, ExtensionApi } from "@goondan/openharness-types";
2
+
3
+ /**
4
+ * Logging extension — subscribes to core events and logs them.
5
+ */
6
+ export function Logging(config?: { logger?: (msg: string) => void }): Extension {
7
+ const log = config?.logger ?? console.log;
8
+
9
+ return {
10
+ name: "logging",
11
+
12
+ register(api: ExtensionApi): void {
13
+ api.on("turn.start", (payload) => {
14
+ log(`[turn.start] ${JSON.stringify(payload)}`);
15
+ });
16
+
17
+ api.on("turn.done", (payload) => {
18
+ log(`[turn.done] ${JSON.stringify(payload)}`);
19
+ });
20
+
21
+ api.on("turn.error", (payload) => {
22
+ log(`[turn.error] ${JSON.stringify(payload)}`);
23
+ });
24
+
25
+ api.on("step.start", (payload) => {
26
+ log(`[step.start] ${JSON.stringify(payload)}`);
27
+ });
28
+
29
+ api.on("step.done", (payload) => {
30
+ log(`[step.done] ${JSON.stringify(payload)}`);
31
+ });
32
+
33
+ api.on("tool.start", (payload) => {
34
+ log(`[tool.start] ${JSON.stringify(payload)}`);
35
+ });
36
+
37
+ api.on("tool.done", (payload) => {
38
+ log(`[tool.done] ${JSON.stringify(payload)}`);
39
+ });
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,23 @@
1
+ import type { Extension, ExtensionApi } from "@goondan/openharness-types";
2
+
3
+ /**
4
+ * MessageWindow extension — truncates conversation history to keep only
5
+ * the most recent `maxMessages` messages before each step.
6
+ */
7
+ export function MessageWindow(config: { maxMessages: number }): Extension {
8
+ return {
9
+ name: "message-window",
10
+
11
+ register(api: ExtensionApi): void {
12
+ api.pipeline.register("step", async (ctx, next) => {
13
+ if (ctx.conversation.messages.length > config.maxMessages) {
14
+ ctx.conversation.emit({
15
+ type: "truncate",
16
+ keepLast: config.maxMessages,
17
+ });
18
+ }
19
+ return next();
20
+ });
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,24 @@
1
+ import type { Extension, ExtensionApi } from "@goondan/openharness-types";
2
+
3
+ /**
4
+ * RequiredToolsGuard extension — blocks a turn if any required tools are
5
+ * not registered.
6
+ */
7
+ export function RequiredToolsGuard(config: { tools: string[] }): Extension {
8
+ return {
9
+ name: "required-tools-guard",
10
+
11
+ register(api: ExtensionApi): void {
12
+ api.pipeline.register("turn", async (ctx, next) => {
13
+ const registered = api.tools.list().map((t) => t.name);
14
+ const missing = config.tools.filter((name) => !registered.includes(name));
15
+ if (missing.length > 0) {
16
+ throw new Error(
17
+ `RequiredToolsGuard: missing required tools: ${missing.join(", ")}`,
18
+ );
19
+ }
20
+ return next();
21
+ });
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,38 @@
1
+ import type { Extension, ExtensionApi, JsonValue } from "@goondan/openharness-types";
2
+
3
+ /**
4
+ * ToolSearch extension — registers a meta-tool `search_tools` that searches
5
+ * registered tool names and descriptions by keyword.
6
+ */
7
+ export function ToolSearch(): Extension {
8
+ return {
9
+ name: "tool-search",
10
+
11
+ register(api: ExtensionApi): void {
12
+ api.tools.register({
13
+ name: "search_tools",
14
+ description: "Search registered tools by keyword in name or description.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ query: {
19
+ type: "string",
20
+ description: "Keyword to search for in tool names and descriptions.",
21
+ },
22
+ },
23
+ required: ["query"],
24
+ },
25
+ handler: async (args) => {
26
+ const query = (args["query"] as string).toLowerCase();
27
+ const allTools = api.tools.list();
28
+ const matching = allTools.filter(
29
+ (t) =>
30
+ t.name.toLowerCase().includes(query) ||
31
+ t.description.toLowerCase().includes(query),
32
+ );
33
+ return { type: "json", data: matching as unknown as JsonValue };
34
+ },
35
+ });
36
+ },
37
+ };
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { ContextMessage } from "./extensions/context-message.js";
2
+ export { MessageWindow } from "./extensions/message-window.js";
3
+ export { CompactionSummarize } from "./extensions/compaction-summarize.js";
4
+ export { Logging } from "./extensions/logging.js";
5
+ export { ToolSearch } from "./extensions/tool-search.js";
6
+ export { RequiredToolsGuard } from "./extensions/required-tools-guard.js";
7
+
8
+ // Tools
9
+ export { BashTool } from "./tools/bash.js";
10
+ export type { BashToolConfig } from "./tools/bash.js";
11
+ export { FileReadTool, FileWriteTool, FileListTool } from "./tools/file-system.js";
12
+ export { HttpFetchTool } from "./tools/http-fetch.js";
13
+ export { JsonQueryTool } from "./tools/json-query.js";
14
+ export { TextTransformTool } from "./tools/text-transform.js";
15
+ export { WaitTool } from "./tools/wait.js";
16
+ export type { WaitToolConfig } from "./tools/wait.js";
@@ -0,0 +1,38 @@
1
+ import { exec } from "node:child_process";
2
+ import type { ToolDefinition, JsonObject, ToolContext } from "@goondan/openharness-types";
3
+
4
+ export interface BashToolConfig {
5
+ timeout?: number;
6
+ maxBuffer?: number;
7
+ }
8
+
9
+ export function BashTool(config: BashToolConfig = {}): ToolDefinition {
10
+ const { timeout = 30_000, maxBuffer = 1024 * 1024 } = config;
11
+
12
+ return {
13
+ name: "bash",
14
+ description: "Execute a shell command and return its output.",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ command: { type: "string", description: "The shell command to execute." },
19
+ cwd: { type: "string", description: "Optional working directory for the command." },
20
+ },
21
+ required: ["command"],
22
+ },
23
+ async handler(args: JsonObject, _ctx: ToolContext) {
24
+ const command = args["command"] as string;
25
+ const cwd = args["cwd"] as string | undefined;
26
+
27
+ return new Promise((resolve) => {
28
+ exec(command, { timeout, maxBuffer, cwd }, (error, stdout, stderr) => {
29
+ if (error) {
30
+ resolve({ type: "error", error: stderr || error.message });
31
+ } else {
32
+ resolve({ type: "text", text: stdout });
33
+ }
34
+ });
35
+ });
36
+ },
37
+ };
38
+ }