@goondan/openharness-base 0.0.1-alpha3 → 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 -162
  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 -451
  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 -155
  105. package/dist/utils.js.map +0 -1
@@ -0,0 +1,234 @@
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 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
+ };
105
+ }
106
+
107
+ const stubStepResult: StepResult = {
108
+ toolCalls: [],
109
+ };
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Tests
113
+ // ---------------------------------------------------------------------------
114
+
115
+ describe("CompactionSummarize", () => {
116
+ it("creates an Extension with name 'compaction-summarize'", () => {
117
+ const ext = CompactionSummarize({ threshold: 10 });
118
+ expect(ext.name).toBe("compaction-summarize");
119
+ });
120
+
121
+ it("registers step middleware via api.pipeline.register", () => {
122
+ const conversation = makeMockConversationState([]);
123
+ const { api, registeredMiddleware } = makeMockApi(conversation);
124
+
125
+ const ext = CompactionSummarize({ threshold: 10 });
126
+ ext.register(api);
127
+
128
+ expect(api.pipeline.register).toHaveBeenCalledOnce();
129
+ expect(registeredMiddleware[0].level).toBe("step");
130
+ });
131
+
132
+ it("does NOT compact when messages are below threshold", async () => {
133
+ const messages = makeMessages(5);
134
+ const conversation = makeMockConversationState(messages);
135
+ const { api, registeredMiddleware } = makeMockApi(conversation);
136
+
137
+ const ext = CompactionSummarize({ threshold: 10 });
138
+ ext.register(api);
139
+
140
+ const middleware = registeredMiddleware[0].handler;
141
+ const ctx = makeStepContext(conversation);
142
+ const next = vi.fn(async () => stubStepResult);
143
+
144
+ await middleware(ctx, next);
145
+
146
+ expect(conversation.emit).not.toHaveBeenCalled();
147
+ expect(next).toHaveBeenCalledOnce();
148
+ });
149
+
150
+ it("compacts when messages exceed threshold", async () => {
151
+ const messages = makeMessages(12);
152
+ const conversation = makeMockConversationState(messages);
153
+ const { api, registeredMiddleware } = makeMockApi(conversation);
154
+
155
+ const ext = CompactionSummarize({ threshold: 10 });
156
+ ext.register(api);
157
+
158
+ const middleware = registeredMiddleware[0].handler;
159
+ const ctx = makeStepContext(conversation);
160
+ const next = vi.fn(async () => stubStepResult);
161
+
162
+ await middleware(ctx, next);
163
+
164
+ const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
165
+
166
+ // keepCount = floor(10/2) = 5, removeCount = 12 - 5 = 7
167
+ // Should have 7 remove events + 1 append event
168
+ const removeEvents = emitted.filter((e) => e.type === "remove");
169
+ const appendEvents = emitted.filter((e) => e.type === "append");
170
+
171
+ expect(removeEvents).toHaveLength(7);
172
+ expect(appendEvents).toHaveLength(1);
173
+
174
+ // The appended message should be a system summary
175
+ const appendEvent = appendEvents[0];
176
+ if (appendEvent.type === "append") {
177
+ expect(appendEvent.message.data.role).toBe("system");
178
+ expect(appendEvent.message.data.content).toContain("[Summary of earlier conversation]:");
179
+ expect(appendEvent.message.metadata?.__createdBy).toBe("compaction-summarize");
180
+ }
181
+
182
+ expect(next).toHaveBeenCalledOnce();
183
+ });
184
+
185
+ it("removes the oldest messages (not the newest)", async () => {
186
+ const messages = makeMessages(12);
187
+ const conversation = makeMockConversationState(messages);
188
+ const { api, registeredMiddleware } = makeMockApi(conversation);
189
+
190
+ const ext = CompactionSummarize({ threshold: 10 });
191
+ ext.register(api);
192
+
193
+ const middleware = registeredMiddleware[0].handler;
194
+ const ctx = makeStepContext(conversation);
195
+ const next = vi.fn(async () => stubStepResult);
196
+
197
+ await middleware(ctx, next);
198
+
199
+ const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
200
+ const removeEvents = emitted.filter(
201
+ (e): e is Extract<typeof e, { type: "remove" }> => e.type === "remove",
202
+ );
203
+
204
+ // Should remove msg-0 through msg-6 (the oldest 7)
205
+ const removedIds = removeEvents.map((e) => e.messageId);
206
+ expect(removedIds).toContain("msg-0");
207
+ expect(removedIds).toContain("msg-6");
208
+ expect(removedIds).not.toContain("msg-7");
209
+ expect(removedIds).not.toContain("msg-11");
210
+ });
211
+
212
+ it("summary content includes text from removed messages", async () => {
213
+ const messages = makeMessages(12);
214
+ const conversation = makeMockConversationState(messages);
215
+ const { api, registeredMiddleware } = makeMockApi(conversation);
216
+
217
+ const ext = CompactionSummarize({ threshold: 10 });
218
+ ext.register(api);
219
+
220
+ const middleware = registeredMiddleware[0].handler;
221
+ const ctx = makeStepContext(conversation);
222
+ const next = vi.fn(async () => stubStepResult);
223
+
224
+ await middleware(ctx, next);
225
+
226
+ const emitted = (conversation as ReturnType<typeof makeMockConversationState>).emitted;
227
+ const appendEvent = emitted.find((e) => e.type === "append");
228
+ if (appendEvent && appendEvent.type === "append") {
229
+ const content = appendEvent.message.data.content as string;
230
+ // Should include text from the removed messages
231
+ expect(content).toContain("Message 0");
232
+ }
233
+ });
234
+ });
@@ -0,0 +1,203 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { ContextMessage } from "../extensions/context-message.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
+ };
86
+ }
87
+
88
+ const stubTurnResult: TurnResult = {
89
+ turnId: "turn-1",
90
+ agentName: "test-agent",
91
+ conversationId: "conv-1",
92
+ status: "completed",
93
+ steps: [],
94
+ };
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Tests
98
+ // ---------------------------------------------------------------------------
99
+
100
+ describe("ContextMessage", () => {
101
+ // Test 1: creates Extension with correct name
102
+ it("creates an Extension with name 'context-message'", () => {
103
+ const ext = ContextMessage("You are a helpful assistant.");
104
+ expect(ext.name).toBe("context-message");
105
+ });
106
+
107
+ // Test 2: register() adds turn middleware via api.pipeline.register
108
+ it("register() calls api.pipeline.register with level 'turn'", () => {
109
+ const conversation = makeMockConversationState();
110
+ const { api, registeredMiddleware } = makeMockApi(conversation);
111
+
112
+ const ext = ContextMessage("Hello");
113
+ ext.register(api);
114
+
115
+ expect(api.pipeline.register).toHaveBeenCalledOnce();
116
+ expect(registeredMiddleware).toHaveLength(1);
117
+ expect(registeredMiddleware[0].level).toBe("turn");
118
+ expect(typeof registeredMiddleware[0].handler).toBe("function");
119
+ });
120
+
121
+ // Test 3: middleware is registered with HIGH priority (low number)
122
+ it("registers turn middleware with priority 10 (high priority)", () => {
123
+ const conversation = makeMockConversationState();
124
+ const { api, registeredMiddleware } = makeMockApi(conversation);
125
+
126
+ const ext = ContextMessage("Hello");
127
+ ext.register(api);
128
+
129
+ expect(registeredMiddleware[0].options?.priority).toBe(10);
130
+ });
131
+
132
+ // Test 4: middleware prepends system message to conversation and text matches
133
+ it("middleware emits append event with system message matching the text argument", async () => {
134
+ const conversation = makeMockConversationState();
135
+ const { api, registeredMiddleware } = makeMockApi(conversation);
136
+ const systemText = "You are a helpful assistant.";
137
+
138
+ const ext = ContextMessage(systemText);
139
+ ext.register(api);
140
+
141
+ const middleware = registeredMiddleware[0].handler;
142
+ const ctx = makeTurnContext(conversation);
143
+ const next = vi.fn(async () => stubTurnResult);
144
+
145
+ await middleware(ctx, next);
146
+
147
+ expect(conversation.emit).toHaveBeenCalledOnce();
148
+ const emitted = conversation.emitted[0];
149
+ expect(emitted.type).toBe("append");
150
+ if (emitted.type === "append") {
151
+ expect(emitted.message.data.role).toBe("system");
152
+ expect(emitted.message.data.content).toBe(systemText);
153
+ expect(emitted.message.metadata?.__createdBy).toBe("context-message");
154
+ expect(typeof emitted.message.id).toBe("string");
155
+ }
156
+ });
157
+
158
+ // Test 5: middleware calls next() after emitting
159
+ it("middleware calls next() after emitting the system message", async () => {
160
+ const conversation = makeMockConversationState();
161
+ const { api, registeredMiddleware } = makeMockApi(conversation);
162
+
163
+ const ext = ContextMessage("Some context");
164
+ ext.register(api);
165
+
166
+ const middleware = registeredMiddleware[0].handler;
167
+ const ctx = makeTurnContext(conversation);
168
+ const next = vi.fn(async () => stubTurnResult);
169
+
170
+ const result = await middleware(ctx, next);
171
+
172
+ expect(next).toHaveBeenCalledOnce();
173
+ expect(result).toBe(stubTurnResult);
174
+ });
175
+
176
+ // Test 6: multiple ContextMessage extensions → multiple system messages
177
+ it("multiple ContextMessage extensions each prepend their own system message", async () => {
178
+ const conversation = makeMockConversationState();
179
+ const { api: api1, registeredMiddleware: rm1 } = makeMockApi(conversation);
180
+ const { api: api2, registeredMiddleware: rm2 } = makeMockApi(conversation);
181
+
182
+ const ext1 = ContextMessage("First context");
183
+ const ext2 = ContextMessage("Second context");
184
+
185
+ ext1.register(api1);
186
+ ext2.register(api2);
187
+
188
+ const ctx = makeTurnContext(conversation);
189
+ const next = vi.fn(async () => stubTurnResult);
190
+
191
+ await rm1[0].handler(ctx, next);
192
+ await rm2[0].handler(ctx, next);
193
+
194
+ expect(conversation.emitted).toHaveLength(2);
195
+
196
+ const texts = conversation.emitted
197
+ .filter((e): e is Extract<typeof e, { type: "append" }> => e.type === "append")
198
+ .map((e) => e.message.data.content);
199
+
200
+ expect(texts).toContain("First context");
201
+ expect(texts).toContain("Second context");
202
+ });
203
+ });
@@ -0,0 +1,200 @@
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
+ });