@goondan/openharness-base 0.1.8 → 0.1.10

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.
@@ -1,186 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { BasicSystemPrompt } from "../extensions/basic-system-prompt.js";
3
- import type {
4
- ExtensionApi,
5
- TurnMiddleware,
6
- TurnContext,
7
- TurnResult,
8
- ConversationState,
9
- Message,
10
- MessageEvent,
11
- } from "@goondan/openharness-types";
12
-
13
- // ---------------------------------------------------------------------------
14
- // Helpers
15
- // ---------------------------------------------------------------------------
16
-
17
- function makeMockConversationState(): ConversationState & { emitted: MessageEvent[] } {
18
- const emitted: MessageEvent[] = [];
19
- const messages: Message[] = [];
20
- return {
21
- messages,
22
- events: [],
23
- emitted,
24
- emit: vi.fn((event: MessageEvent) => {
25
- emitted.push(event);
26
- if (event.type === "append") {
27
- messages.push(event.message);
28
- }
29
- }),
30
- restore: vi.fn(),
31
- };
32
- }
33
-
34
- function makeMockApi(conversation: ConversationState): {
35
- api: ExtensionApi;
36
- registeredMiddleware: Array<{ level: string; handler: TurnMiddleware; options?: { priority?: number } }>;
37
- } {
38
- const registeredMiddleware: Array<{ level: string; handler: TurnMiddleware; options?: { priority?: number } }> = [];
39
-
40
- const api: ExtensionApi = {
41
- pipeline: {
42
- register: vi.fn((level: string, handler: TurnMiddleware, options?: { priority?: number }) => {
43
- registeredMiddleware.push({ level, handler, options });
44
- }) as unknown as ExtensionApi["pipeline"]["register"],
45
- },
46
- tools: {
47
- register: vi.fn(),
48
- remove: vi.fn(),
49
- list: vi.fn(() => []),
50
- },
51
- on: vi.fn(),
52
- conversation,
53
- runtime: {
54
- agent: {
55
- name: "test-agent",
56
- model: { provider: "openai", model: "gpt-4o" },
57
- extensions: [],
58
- tools: [],
59
- },
60
- agents: {},
61
- connections: {},
62
- },
63
- };
64
-
65
- return { api, registeredMiddleware };
66
- }
67
-
68
- function makeTurnContext(conversation: ConversationState): TurnContext {
69
- return {
70
- turnId: "turn-1",
71
- agentName: "test-agent",
72
- conversationId: "conv-1",
73
- conversation,
74
- abortSignal: new AbortController().signal,
75
- input: {
76
- name: "test-event",
77
- content: [{ type: "text", text: "hello" }],
78
- properties: {},
79
- source: {
80
- connector: "test-connector",
81
- connectionName: "test",
82
- receivedAt: new Date().toISOString(),
83
- },
84
- },
85
- llm: { chat: vi.fn().mockResolvedValue({ text: "mock" }) },
86
- };
87
- }
88
-
89
- const stubTurnResult: TurnResult = {
90
- turnId: "turn-1",
91
- agentName: "test-agent",
92
- conversationId: "conv-1",
93
- status: "completed",
94
- steps: [],
95
- };
96
-
97
- // ---------------------------------------------------------------------------
98
- // Tests
99
- // ---------------------------------------------------------------------------
100
-
101
- describe("BasicSystemPrompt", () => {
102
- it("creates an Extension with name 'basic-system-prompt'", () => {
103
- const ext = BasicSystemPrompt("You are helpful.");
104
- expect(ext.name).toBe("basic-system-prompt");
105
- });
106
-
107
- it("register() calls api.pipeline.register with level 'turn'", () => {
108
- const conversation = makeMockConversationState();
109
- const { api, registeredMiddleware } = makeMockApi(conversation);
110
-
111
- const ext = BasicSystemPrompt("You are helpful.");
112
- ext.register(api);
113
-
114
- expect(api.pipeline.register).toHaveBeenCalledOnce();
115
- expect(registeredMiddleware).toHaveLength(1);
116
- expect(registeredMiddleware[0].level).toBe("turn");
117
- expect(typeof registeredMiddleware[0].handler).toBe("function");
118
- });
119
-
120
- it("registers turn middleware with priority 10 (high priority)", () => {
121
- const conversation = makeMockConversationState();
122
- const { api, registeredMiddleware } = makeMockApi(conversation);
123
-
124
- const ext = BasicSystemPrompt("You are helpful.");
125
- ext.register(api);
126
-
127
- expect(registeredMiddleware[0].options?.priority).toBe(10);
128
- });
129
-
130
- it("middleware appends a system message and calls next()", async () => {
131
- const conversation = makeMockConversationState();
132
- const { api, registeredMiddleware } = makeMockApi(conversation);
133
-
134
- const ext = BasicSystemPrompt("You are helpful.");
135
- ext.register(api);
136
-
137
- const middleware = registeredMiddleware[0].handler;
138
- const ctx = makeTurnContext(conversation);
139
- const next = vi.fn(async () => stubTurnResult);
140
-
141
- const result = await middleware(ctx, next);
142
-
143
- expect(next).toHaveBeenCalledOnce();
144
- expect(result).toBe(stubTurnResult);
145
-
146
- // Verify system message was emitted
147
- expect(conversation.emit).toHaveBeenCalledOnce();
148
- const emittedEvent = conversation.emitted[0];
149
- expect(emittedEvent.type).toBe("append");
150
- if (emittedEvent.type === "append") {
151
- expect(emittedEvent.message.id).toBe("sys-basic-system-prompt");
152
- expect(emittedEvent.message.data.role).toBe("system");
153
- expect(emittedEvent.message.data.content).toBe("You are helpful.");
154
- expect(emittedEvent.message.metadata?.__createdBy).toBe("basic-system-prompt");
155
- }
156
- });
157
-
158
- it("does not duplicate the system message on subsequent turns", async () => {
159
- const conversation = makeMockConversationState();
160
- const { api, registeredMiddleware } = makeMockApi(conversation);
161
-
162
- const ext = BasicSystemPrompt("You are helpful.");
163
- ext.register(api);
164
-
165
- const middleware = registeredMiddleware[0].handler;
166
- const next = vi.fn(async () => stubTurnResult);
167
-
168
- // First turn — should append
169
- const ctx1 = makeTurnContext(conversation);
170
- await middleware(ctx1, next);
171
- expect(conversation.emitted).toHaveLength(1);
172
-
173
- // Second turn — system message already in conversation.messages, should skip
174
- const ctx2 = makeTurnContext(conversation);
175
- await middleware(ctx2, next);
176
- expect(conversation.emitted).toHaveLength(1); // still 1, no new append
177
-
178
- // Third turn — still no duplication
179
- const ctx3 = makeTurnContext(conversation);
180
- await middleware(ctx3, next);
181
- expect(conversation.emitted).toHaveLength(1);
182
-
183
- // next() should have been called every turn
184
- expect(next).toHaveBeenCalledTimes(3);
185
- });
186
- });
@@ -1,282 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { CompactionSummarize } from "../extensions/compaction-summarize.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 makeMockLlmClient() {
87
- return {
88
- chat: vi.fn().mockResolvedValue({ text: "LLM-generated summary of the conversation." }),
89
- };
90
- }
91
-
92
- function makeStepContext(conversation: ConversationState): StepContext {
93
- return {
94
- turnId: "turn-1",
95
- agentName: "test-agent",
96
- conversationId: "conv-1",
97
- conversation,
98
- stepNumber: 1,
99
- abortSignal: new AbortController().signal,
100
- input: {
101
- name: "test-event",
102
- content: [{ type: "text", text: "hello" }],
103
- properties: {},
104
- source: {
105
- connector: "test-connector",
106
- connectionName: "test",
107
- receivedAt: new Date().toISOString(),
108
- },
109
- },
110
- llm: makeMockLlmClient(),
111
- };
112
- }
113
-
114
- const stubStepResult: StepResult = {
115
- toolCalls: [],
116
- };
117
-
118
- // ---------------------------------------------------------------------------
119
- // Tests
120
- // ---------------------------------------------------------------------------
121
-
122
- describe("CompactionSummarize", () => {
123
- it("creates an Extension with name 'compaction-summarize'", () => {
124
- const ext = CompactionSummarize({ threshold: 10 });
125
- expect(ext.name).toBe("compaction-summarize");
126
- });
127
-
128
- it("registers step middleware via api.pipeline.register", () => {
129
- const conversation = makeMockConversationState([]);
130
- const { api, registeredMiddleware } = makeMockApi(conversation);
131
-
132
- const ext = CompactionSummarize({ threshold: 10 });
133
- ext.register(api);
134
-
135
- expect(api.pipeline.register).toHaveBeenCalledOnce();
136
- expect(registeredMiddleware[0].level).toBe("step");
137
- });
138
-
139
- it("does NOT compact when messages are below threshold", async () => {
140
- const messages = makeMessages(5);
141
- const conversation = makeMockConversationState(messages);
142
- const { api, registeredMiddleware } = makeMockApi(conversation);
143
-
144
- const ext = CompactionSummarize({ threshold: 10 });
145
- ext.register(api);
146
-
147
- const middleware = registeredMiddleware[0].handler;
148
- const ctx = makeStepContext(conversation);
149
- const next = vi.fn(async () => stubStepResult);
150
-
151
- await middleware(ctx, next);
152
-
153
- expect(conversation.emit).not.toHaveBeenCalled();
154
- expect(next).toHaveBeenCalledOnce();
155
- });
156
-
157
- it("compacts when messages exceed threshold", async () => {
158
- const messages = makeMessages(12);
159
- const conversation = makeMockConversationState(messages);
160
- const { api, registeredMiddleware } = makeMockApi(conversation);
161
-
162
- const ext = CompactionSummarize({ threshold: 10 });
163
- ext.register(api);
164
-
165
- const middleware = registeredMiddleware[0].handler;
166
- const ctx = makeStepContext(conversation);
167
- const next = vi.fn(async () => stubStepResult);
168
-
169
- await middleware(ctx, next);
170
-
171
- const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
172
-
173
- // keepCount = floor(10/2) = 5, removeCount = 12 - 5 = 7
174
- // Should have 7 remove events + 1 append event
175
- const removeEvents = emitted.filter((e) => e.type === "remove");
176
- const appendEvents = emitted.filter((e) => e.type === "append");
177
-
178
- expect(removeEvents).toHaveLength(7);
179
- expect(appendEvents).toHaveLength(1);
180
-
181
- // The appended message should be a system summary
182
- const appendEvent = appendEvents[0];
183
- if (appendEvent.type === "append") {
184
- expect(appendEvent.message.data.role).toBe("system");
185
- expect(appendEvent.message.data.content).toContain("[Summary of earlier conversation]:");
186
- expect(appendEvent.message.metadata?.__createdBy).toBe("compaction-summarize");
187
- }
188
-
189
- expect(next).toHaveBeenCalledOnce();
190
- });
191
-
192
- it("removes the oldest messages (not the newest)", async () => {
193
- const messages = makeMessages(12);
194
- const conversation = makeMockConversationState(messages);
195
- const { api, registeredMiddleware } = makeMockApi(conversation);
196
-
197
- const ext = CompactionSummarize({ threshold: 10 });
198
- ext.register(api);
199
-
200
- const middleware = registeredMiddleware[0].handler;
201
- const ctx = makeStepContext(conversation);
202
- const next = vi.fn(async () => stubStepResult);
203
-
204
- await middleware(ctx, next);
205
-
206
- const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
207
- const removeEvents = emitted.filter(
208
- (e): e is Extract<typeof e, { type: "remove" }> => e.type === "remove",
209
- );
210
-
211
- // Should remove msg-0 through msg-6 (the oldest 7)
212
- const removedIds = removeEvents.map((e) => e.messageId);
213
- expect(removedIds).toContain("msg-0");
214
- expect(removedIds).toContain("msg-6");
215
- expect(removedIds).not.toContain("msg-7");
216
- expect(removedIds).not.toContain("msg-11");
217
- });
218
-
219
- it("summary content includes text from removed messages", async () => {
220
- const messages = makeMessages(12);
221
- const conversation = makeMockConversationState(messages);
222
- const { api, registeredMiddleware } = makeMockApi(conversation);
223
-
224
- const ext = CompactionSummarize({ threshold: 10 });
225
- ext.register(api);
226
-
227
- const middleware = registeredMiddleware[0].handler;
228
- const ctx = makeStepContext(conversation);
229
- const next = vi.fn(async () => stubStepResult);
230
-
231
- await middleware(ctx, next);
232
-
233
- // Verify LLM was called for summarization
234
- expect(ctx.llm.chat).toHaveBeenCalledOnce();
235
-
236
- const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
237
- const appendEvent = emitted.find((e) => e.type === "append");
238
- if (appendEvent && appendEvent.type === "append") {
239
- const content = appendEvent.message.data.content as string;
240
- // Should include the LLM-generated summary
241
- expect(content).toContain("LLM-generated summary of the conversation.");
242
- }
243
- });
244
-
245
- it("uses custom summarizer when provided", async () => {
246
- const messages = makeMessages(12);
247
- const conversation = makeMockConversationState(messages);
248
- const { api, registeredMiddleware } = makeMockApi(conversation);
249
-
250
- const summarizer = vi.fn(async (msgs: Message[]) => {
251
- return `Custom summary of ${msgs.length} messages`;
252
- });
253
-
254
- const ext = CompactionSummarize({ threshold: 10, summarizer });
255
- ext.register(api);
256
-
257
- const middleware = registeredMiddleware[0].handler;
258
- const ctx = makeStepContext(conversation);
259
- const next = vi.fn(async () => stubStepResult);
260
-
261
- await middleware(ctx, next);
262
-
263
- // summarizer should have been called with the 7 removed messages
264
- expect(summarizer).toHaveBeenCalledOnce();
265
- expect(summarizer).toHaveBeenCalledWith(
266
- expect.arrayContaining([
267
- expect.objectContaining({ id: "msg-0" }),
268
- expect.objectContaining({ id: "msg-6" }),
269
- ]),
270
- );
271
- expect(summarizer.mock.calls[0][0]).toHaveLength(7);
272
-
273
- const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
274
- const appendEvent = emitted.find((e) => e.type === "append");
275
- if (appendEvent && appendEvent.type === "append") {
276
- const content = appendEvent.message.data.content as string;
277
- expect(content).toContain("Custom summary of 7 messages");
278
- }
279
-
280
- expect(next).toHaveBeenCalledOnce();
281
- });
282
- });
@@ -1,200 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import { Logging } from "../extensions/logging.js";
3
- import type { ExtensionApi, ConversationState } from "@goondan/openharness-types";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Helpers
7
- // ---------------------------------------------------------------------------
8
-
9
- function makeMockConversationState(): ConversationState {
10
- return {
11
- messages: [],
12
- events: [],
13
- emit: vi.fn(),
14
- restore: vi.fn(),
15
- };
16
- }
17
-
18
- function makeMockApi(conversation: ConversationState): {
19
- api: ExtensionApi;
20
- eventListeners: Map<string, Array<(payload: unknown) => void>>;
21
- } {
22
- const eventListeners = new Map<string, Array<(payload: unknown) => void>>();
23
-
24
- const api: ExtensionApi = {
25
- pipeline: {
26
- register: vi.fn() as unknown as ExtensionApi["pipeline"]["register"],
27
- },
28
- tools: {
29
- register: vi.fn(),
30
- remove: vi.fn(),
31
- list: vi.fn(() => []),
32
- },
33
- on: vi.fn((event: string, listener: (payload: unknown) => void) => {
34
- if (!eventListeners.has(event)) {
35
- eventListeners.set(event, []);
36
- }
37
- eventListeners.get(event)!.push(listener);
38
- }),
39
- conversation,
40
- runtime: {
41
- agent: {
42
- name: "test-agent",
43
- model: { provider: "openai", model: "gpt-4o" },
44
- extensions: [],
45
- tools: [],
46
- },
47
- agents: {},
48
- connections: {},
49
- },
50
- };
51
-
52
- return { api, eventListeners };
53
- }
54
-
55
- function emit(
56
- eventListeners: Map<string, Array<(payload: unknown) => void>>,
57
- event: string,
58
- payload: unknown,
59
- ) {
60
- eventListeners.get(event)?.forEach((l) => l(payload));
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // Tests
65
- // ---------------------------------------------------------------------------
66
-
67
- describe("Logging", () => {
68
- it("creates an Extension with name 'logging'", () => {
69
- const ext = Logging();
70
- expect(ext.name).toBe("logging");
71
- });
72
-
73
- it("subscribes to all core events on register", () => {
74
- const conversation = makeMockConversationState();
75
- const { api } = makeMockApi(conversation);
76
-
77
- const ext = Logging();
78
- ext.register(api);
79
-
80
- expect(api.on).toHaveBeenCalledWith("turn.start", expect.any(Function));
81
- expect(api.on).toHaveBeenCalledWith("turn.done", expect.any(Function));
82
- expect(api.on).toHaveBeenCalledWith("turn.error", expect.any(Function));
83
- expect(api.on).toHaveBeenCalledWith("step.start", expect.any(Function));
84
- expect(api.on).toHaveBeenCalledWith("step.done", expect.any(Function));
85
- expect(api.on).toHaveBeenCalledWith("tool.start", expect.any(Function));
86
- expect(api.on).toHaveBeenCalledWith("tool.done", expect.any(Function));
87
- });
88
-
89
- it("logs turn.start event with custom logger", () => {
90
- const conversation = makeMockConversationState();
91
- const { api, eventListeners } = makeMockApi(conversation);
92
- const logger = vi.fn();
93
-
94
- const ext = Logging({ logger });
95
- ext.register(api);
96
-
97
- emit(eventListeners, "turn.start", { turnId: "t1" });
98
-
99
- expect(logger).toHaveBeenCalledOnce();
100
- expect(logger.mock.calls[0][0]).toContain("turn.start");
101
- });
102
-
103
- it("logs turn.done event", () => {
104
- const conversation = makeMockConversationState();
105
- const { api, eventListeners } = makeMockApi(conversation);
106
- const logger = vi.fn();
107
-
108
- const ext = Logging({ logger });
109
- ext.register(api);
110
-
111
- emit(eventListeners, "turn.done", { turnId: "t1", status: "completed" });
112
-
113
- expect(logger).toHaveBeenCalledOnce();
114
- expect(logger.mock.calls[0][0]).toContain("turn.done");
115
- });
116
-
117
- it("logs turn.error event", () => {
118
- const conversation = makeMockConversationState();
119
- const { api, eventListeners } = makeMockApi(conversation);
120
- const logger = vi.fn();
121
-
122
- const ext = Logging({ logger });
123
- ext.register(api);
124
-
125
- emit(eventListeners, "turn.error", { error: "oops" });
126
-
127
- expect(logger).toHaveBeenCalledOnce();
128
- expect(logger.mock.calls[0][0]).toContain("turn.error");
129
- });
130
-
131
- it("uses console.log as default logger", () => {
132
- const conversation = makeMockConversationState();
133
- const { api, eventListeners } = makeMockApi(conversation);
134
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
135
-
136
- const ext = Logging();
137
- ext.register(api);
138
-
139
- emit(eventListeners, "turn.start", { turnId: "t1" });
140
-
141
- expect(consoleSpy).toHaveBeenCalledOnce();
142
- consoleSpy.mockRestore();
143
- });
144
-
145
- it("logs step.start event with custom logger", () => {
146
- const conversation = makeMockConversationState();
147
- const { api, eventListeners } = makeMockApi(conversation);
148
- const logger = vi.fn();
149
-
150
- const ext = Logging({ logger });
151
- ext.register(api);
152
-
153
- emit(eventListeners, "step.start", { stepIndex: 0 });
154
-
155
- expect(logger).toHaveBeenCalledOnce();
156
- expect(logger.mock.calls[0][0]).toContain("step.start");
157
- expect(logger.mock.calls[0][0]).toContain("stepIndex");
158
- });
159
-
160
- it("logs step.done event with custom logger", () => {
161
- const conversation = makeMockConversationState();
162
- const { api, eventListeners } = makeMockApi(conversation);
163
- const logger = vi.fn();
164
-
165
- const ext = Logging({ logger });
166
- ext.register(api);
167
-
168
- emit(eventListeners, "step.done", { stepIndex: 0, toolCallCount: 2 });
169
-
170
- expect(logger).toHaveBeenCalledOnce();
171
- expect(logger.mock.calls[0][0]).toContain("step.done");
172
- });
173
-
174
- it("logs tool.start and tool.done events with custom logger", () => {
175
- const conversation = makeMockConversationState();
176
- const { api, eventListeners } = makeMockApi(conversation);
177
- const logger = vi.fn();
178
-
179
- const ext = Logging({ logger });
180
- ext.register(api);
181
-
182
- emit(eventListeners, "tool.start", { toolName: "bash", toolCallId: "tc-1" });
183
- emit(eventListeners, "tool.done", { toolName: "bash", toolCallId: "tc-1", result: { type: "text", text: "ok" } });
184
-
185
- expect(logger).toHaveBeenCalledTimes(2);
186
- expect(logger.mock.calls[0][0]).toContain("tool.start");
187
- expect(logger.mock.calls[0][0]).toContain("bash");
188
- expect(logger.mock.calls[1][0]).toContain("tool.done");
189
- });
190
-
191
- it("does NOT call pipeline.register (event-based, no middleware)", () => {
192
- const conversation = makeMockConversationState();
193
- const { api } = makeMockApi(conversation);
194
-
195
- const ext = Logging();
196
- ext.register(api);
197
-
198
- expect(api.pipeline.register).not.toHaveBeenCalled();
199
- });
200
- });