@dot-ai/pi 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/__tests__/pi.test.d.ts +1 -0
- package/dist/__tests__/pi.test.js +334 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +162 -0
- package/package.json +27 -0
- package/src/__tests__/pi.test.ts +445 -0
- package/src/index.ts +188 -0
- package/tsconfig.json +18 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Mock @dot-ai/core before importing the adapter
|
|
4
|
+
const mockRuntime = {
|
|
5
|
+
boot: vi.fn().mockResolvedValue(undefined),
|
|
6
|
+
processPrompt: vi.fn().mockResolvedValue({ formatted: 'test context', enriched: {}, capabilities: [] }),
|
|
7
|
+
learn: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
fire: vi.fn().mockResolvedValue([]),
|
|
9
|
+
fireToolCall: vi.fn().mockResolvedValue(null),
|
|
10
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
capabilities: [
|
|
12
|
+
{
|
|
13
|
+
name: 'memory_recall',
|
|
14
|
+
description: 'Search memory',
|
|
15
|
+
parameters: { query: { type: 'string' } },
|
|
16
|
+
promptSnippet: 'Use memory_recall to search',
|
|
17
|
+
promptGuidelines: 'Search before answering',
|
|
18
|
+
execute: vi.fn().mockResolvedValue({ text: 'memory result', details: { hits: 1 } }),
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
diagnostics: { extensions: [], usedTiers: [], providerStatus: {}, capabilityCount: 1 },
|
|
22
|
+
isBooted: true,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
vi.mock('@dot-ai/core', () => {
|
|
26
|
+
// Use a class so `new DotAiRuntime()` works correctly
|
|
27
|
+
const MockDotAiRuntime = vi.fn(function (this: unknown) {
|
|
28
|
+
return mockRuntime;
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
DotAiRuntime: MockDotAiRuntime,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
import dotAiPiExtension from '../index.js';
|
|
36
|
+
import { DotAiRuntime } from '@dot-ai/core';
|
|
37
|
+
|
|
38
|
+
// Helper to create a mock Pi API and capture handlers
|
|
39
|
+
function createMockPi() {
|
|
40
|
+
const handlers = new Map<string, (...args: unknown[]) => unknown>();
|
|
41
|
+
const mockPi = {
|
|
42
|
+
on: vi.fn((event: string, handler: (...args: unknown[]) => unknown) => {
|
|
43
|
+
handlers.set(event, handler);
|
|
44
|
+
}),
|
|
45
|
+
registerTool: vi.fn(),
|
|
46
|
+
};
|
|
47
|
+
return { mockPi, handlers };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('adapter-pi', () => {
|
|
51
|
+
// --- Registration tests (existing) ---
|
|
52
|
+
|
|
53
|
+
it('exports a default function', () => {
|
|
54
|
+
expect(typeof dotAiPiExtension).toBe('function');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('registers event handlers on pi API', () => {
|
|
58
|
+
const { mockPi } = createMockPi();
|
|
59
|
+
dotAiPiExtension(mockPi);
|
|
60
|
+
|
|
61
|
+
expect(mockPi.on).toHaveBeenCalledWith('session_start', expect.any(Function));
|
|
62
|
+
expect(mockPi.on).toHaveBeenCalledWith('before_agent_start', expect.any(Function));
|
|
63
|
+
expect(mockPi.on).toHaveBeenCalledWith('context', expect.any(Function));
|
|
64
|
+
expect(mockPi.on).toHaveBeenCalledWith('tool_call', expect.any(Function));
|
|
65
|
+
expect(mockPi.on).toHaveBeenCalledWith('tool_result', expect.any(Function));
|
|
66
|
+
expect(mockPi.on).toHaveBeenCalledWith('agent_end', expect.any(Function));
|
|
67
|
+
expect(mockPi.on).toHaveBeenCalledWith('session_shutdown', expect.any(Function));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('registers all expected pi events', () => {
|
|
71
|
+
const events: string[] = [];
|
|
72
|
+
const mockPi = {
|
|
73
|
+
on: vi.fn((event: string) => events.push(event)),
|
|
74
|
+
registerTool: vi.fn(),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
dotAiPiExtension(mockPi);
|
|
78
|
+
|
|
79
|
+
const expected = ['session_start', 'before_agent_start', 'context', 'tool_call', 'tool_result', 'agent_end', 'session_shutdown'];
|
|
80
|
+
for (const e of expected) {
|
|
81
|
+
expect(events).toContain(e);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- Handler behavior tests ---
|
|
86
|
+
|
|
87
|
+
describe('handler behavior', () => {
|
|
88
|
+
let handlers: Map<string, (...args: unknown[]) => unknown>;
|
|
89
|
+
let mockPi: ReturnType<typeof createMockPi>['mockPi'];
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
// Reset all mocks before each test group setup
|
|
93
|
+
vi.clearAllMocks();
|
|
94
|
+
|
|
95
|
+
const setup = createMockPi();
|
|
96
|
+
mockPi = setup.mockPi;
|
|
97
|
+
handlers = setup.handlers;
|
|
98
|
+
|
|
99
|
+
dotAiPiExtension(mockPi);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// --- session_start ---
|
|
103
|
+
|
|
104
|
+
describe('session_start handler', () => {
|
|
105
|
+
it('creates DotAiRuntime with cwd as workspaceRoot', async () => {
|
|
106
|
+
const handler = handlers.get('session_start')!;
|
|
107
|
+
await handler();
|
|
108
|
+
|
|
109
|
+
expect(DotAiRuntime).toHaveBeenCalledWith({
|
|
110
|
+
workspaceRoot: process.cwd(),
|
|
111
|
+
skipIdentities: true,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('boots the runtime', async () => {
|
|
116
|
+
const handler = handlers.get('session_start')!;
|
|
117
|
+
await handler();
|
|
118
|
+
|
|
119
|
+
expect(mockRuntime.boot).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('registers capabilities as pi tools via registerTool', async () => {
|
|
123
|
+
const handler = handlers.get('session_start')!;
|
|
124
|
+
await handler();
|
|
125
|
+
|
|
126
|
+
expect(mockPi.registerTool).toHaveBeenCalledTimes(1);
|
|
127
|
+
expect(mockPi.registerTool).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
name: 'memory_recall',
|
|
130
|
+
description: 'Search memory',
|
|
131
|
+
parameters: { query: { type: 'string' } },
|
|
132
|
+
promptSnippet: 'Use memory_recall to search',
|
|
133
|
+
promptGuidelines: 'Search before answering',
|
|
134
|
+
execute: expect.any(Function),
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('maps capability execute to return {content, details} shape', async () => {
|
|
140
|
+
const handler = handlers.get('session_start')!;
|
|
141
|
+
await handler();
|
|
142
|
+
|
|
143
|
+
const registeredTool = mockPi.registerTool.mock.calls[0][0] as {
|
|
144
|
+
execute: (input: Record<string, unknown>) => Promise<{ content: string; details?: unknown }>;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const result = await registeredTool.execute({ query: 'test' });
|
|
148
|
+
|
|
149
|
+
expect(result).toEqual({ content: 'memory result', details: { hits: 1 } });
|
|
150
|
+
expect(mockRuntime.capabilities[0].execute).toHaveBeenCalledWith({ query: 'test' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// --- before_agent_start ---
|
|
155
|
+
|
|
156
|
+
describe('before_agent_start handler', () => {
|
|
157
|
+
it('returns undefined when runtime is null (before session_start)', async () => {
|
|
158
|
+
// Use a fresh Pi instance without calling session_start
|
|
159
|
+
const { handlers: freshHandlers } = createMockPi();
|
|
160
|
+
const freshMockPi = { on: vi.fn((event: string, handler: (...args: unknown[]) => unknown) => freshHandlers.set(event, handler)), registerTool: vi.fn() };
|
|
161
|
+
// We need a fresh module state — since module-level runtime starts null, just use a new registration
|
|
162
|
+
// but same module, so runtime is whatever state it's in.
|
|
163
|
+
// Instead, test via the shutdown path to reset runtime to null.
|
|
164
|
+
const shutdownHandler = handlers.get('session_shutdown')!;
|
|
165
|
+
await shutdownHandler();
|
|
166
|
+
|
|
167
|
+
const handler = handlers.get('before_agent_start')!;
|
|
168
|
+
const result = await handler({ content: 'hello' });
|
|
169
|
+
expect(result).toBeUndefined();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('calls runtime.processPrompt with message content and returns systemPrompt', async () => {
|
|
173
|
+
// Boot the runtime first
|
|
174
|
+
await handlers.get('session_start')!();
|
|
175
|
+
|
|
176
|
+
const handler = handlers.get('before_agent_start')!;
|
|
177
|
+
const result = await handler({ content: 'What is the weather?' });
|
|
178
|
+
|
|
179
|
+
expect(mockRuntime.processPrompt).toHaveBeenCalledWith('What is the weather?');
|
|
180
|
+
expect(result).toEqual({ systemPrompt: 'test context' });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('passes empty string when content field is missing', async () => {
|
|
184
|
+
await handlers.get('session_start')!();
|
|
185
|
+
|
|
186
|
+
const handler = handlers.get('before_agent_start')!;
|
|
187
|
+
await handler({});
|
|
188
|
+
|
|
189
|
+
expect(mockRuntime.processPrompt).toHaveBeenCalledWith('');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('passes empty string when arg is undefined', async () => {
|
|
193
|
+
await handlers.get('session_start')!();
|
|
194
|
+
|
|
195
|
+
const handler = handlers.get('before_agent_start')!;
|
|
196
|
+
await handler(undefined);
|
|
197
|
+
|
|
198
|
+
expect(mockRuntime.processPrompt).toHaveBeenCalledWith('');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// --- context ---
|
|
203
|
+
|
|
204
|
+
describe('context handler', () => {
|
|
205
|
+
it('returns undefined when runtime is null', async () => {
|
|
206
|
+
const shutdownHandler = handlers.get('session_shutdown')!;
|
|
207
|
+
await shutdownHandler();
|
|
208
|
+
|
|
209
|
+
const handler = handlers.get('context')!;
|
|
210
|
+
const result = await handler({ messages: [{ role: 'user', content: 'hi' }] });
|
|
211
|
+
expect(result).toBeUndefined();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('fires context_modify event with messages and returns first result', async () => {
|
|
215
|
+
const modifierResult = { messages: [{ role: 'system', content: 'injected' }] };
|
|
216
|
+
mockRuntime.fire.mockResolvedValueOnce([modifierResult]);
|
|
217
|
+
|
|
218
|
+
await handlers.get('session_start')!();
|
|
219
|
+
|
|
220
|
+
const event = { messages: [{ role: 'user', content: 'hi' }] };
|
|
221
|
+
const handler = handlers.get('context')!;
|
|
222
|
+
const result = await handler(event);
|
|
223
|
+
|
|
224
|
+
expect(mockRuntime.fire).toHaveBeenCalledWith('context_modify', event);
|
|
225
|
+
expect(result).toBe(modifierResult);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('returns undefined when fire returns empty results', async () => {
|
|
229
|
+
mockRuntime.fire.mockResolvedValueOnce([]);
|
|
230
|
+
|
|
231
|
+
await handlers.get('session_start')!();
|
|
232
|
+
|
|
233
|
+
const handler = handlers.get('context')!;
|
|
234
|
+
const result = await handler({ messages: [{ role: 'user', content: 'hi' }] });
|
|
235
|
+
|
|
236
|
+
expect(result).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('skips fire call when messages are missing', async () => {
|
|
240
|
+
await handlers.get('session_start')!();
|
|
241
|
+
|
|
242
|
+
const handler = handlers.get('context')!;
|
|
243
|
+
const result = await handler({});
|
|
244
|
+
|
|
245
|
+
expect(mockRuntime.fire).not.toHaveBeenCalled();
|
|
246
|
+
expect(result).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// --- tool_call ---
|
|
251
|
+
|
|
252
|
+
describe('tool_call handler', () => {
|
|
253
|
+
it('returns undefined when runtime is null', async () => {
|
|
254
|
+
const shutdownHandler = handlers.get('session_shutdown')!;
|
|
255
|
+
await shutdownHandler();
|
|
256
|
+
|
|
257
|
+
const handler = handlers.get('tool_call')!;
|
|
258
|
+
const result = await handler({ tool: 'memory_recall', input: { query: 'test' } });
|
|
259
|
+
expect(result).toBeUndefined();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('calls runtime.fireToolCall with tool name and input', async () => {
|
|
263
|
+
await handlers.get('session_start')!();
|
|
264
|
+
|
|
265
|
+
const handler = handlers.get('tool_call')!;
|
|
266
|
+
await handler({ tool: 'memory_recall', input: { query: 'test' } });
|
|
267
|
+
|
|
268
|
+
expect(mockRuntime.fireToolCall).toHaveBeenCalledWith({
|
|
269
|
+
tool: 'memory_recall',
|
|
270
|
+
input: { query: 'test' },
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns block result when extension blocks the tool call', async () => {
|
|
275
|
+
const blockResult = { decision: 'block', reason: 'forbidden tool' };
|
|
276
|
+
mockRuntime.fireToolCall.mockResolvedValueOnce(blockResult);
|
|
277
|
+
|
|
278
|
+
await handlers.get('session_start')!();
|
|
279
|
+
|
|
280
|
+
const handler = handlers.get('tool_call')!;
|
|
281
|
+
const result = await handler({ tool: 'memory_recall', input: {} });
|
|
282
|
+
|
|
283
|
+
expect(result).toBe(blockResult);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('returns undefined when tool call is allowed (no block)', async () => {
|
|
287
|
+
mockRuntime.fireToolCall.mockResolvedValueOnce({ decision: 'allow' });
|
|
288
|
+
|
|
289
|
+
await handlers.get('session_start')!();
|
|
290
|
+
|
|
291
|
+
const handler = handlers.get('tool_call')!;
|
|
292
|
+
const result = await handler({ tool: 'memory_recall', input: {} });
|
|
293
|
+
|
|
294
|
+
expect(result).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('uses empty object for input when input field is missing', async () => {
|
|
298
|
+
await handlers.get('session_start')!();
|
|
299
|
+
|
|
300
|
+
const handler = handlers.get('tool_call')!;
|
|
301
|
+
await handler({ tool: 'memory_recall' });
|
|
302
|
+
|
|
303
|
+
expect(mockRuntime.fireToolCall).toHaveBeenCalledWith({
|
|
304
|
+
tool: 'memory_recall',
|
|
305
|
+
input: {},
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('skips fireToolCall when tool field is missing', async () => {
|
|
310
|
+
await handlers.get('session_start')!();
|
|
311
|
+
|
|
312
|
+
const handler = handlers.get('tool_call')!;
|
|
313
|
+
await handler({ input: {} });
|
|
314
|
+
|
|
315
|
+
expect(mockRuntime.fireToolCall).not.toHaveBeenCalled();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// --- tool_result ---
|
|
320
|
+
|
|
321
|
+
describe('tool_result handler', () => {
|
|
322
|
+
it('fires tool_result event with tool info', async () => {
|
|
323
|
+
await handlers.get('session_start')!();
|
|
324
|
+
|
|
325
|
+
const handler = handlers.get('tool_result')!;
|
|
326
|
+
await handler({ tool: 'memory_recall', result: { content: 'some result' }, isError: false });
|
|
327
|
+
|
|
328
|
+
expect(mockRuntime.fire).toHaveBeenCalledWith('tool_result', {
|
|
329
|
+
tool: 'memory_recall',
|
|
330
|
+
result: { content: 'some result' },
|
|
331
|
+
isError: false,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('uses defaults for missing result and isError fields', async () => {
|
|
336
|
+
await handlers.get('session_start')!();
|
|
337
|
+
|
|
338
|
+
const handler = handlers.get('tool_result')!;
|
|
339
|
+
await handler({ tool: 'memory_recall' });
|
|
340
|
+
|
|
341
|
+
expect(mockRuntime.fire).toHaveBeenCalledWith('tool_result', {
|
|
342
|
+
tool: 'memory_recall',
|
|
343
|
+
result: { content: '' },
|
|
344
|
+
isError: false,
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('skips fire when tool field is missing', async () => {
|
|
349
|
+
await handlers.get('session_start')!();
|
|
350
|
+
|
|
351
|
+
const handler = handlers.get('tool_result')!;
|
|
352
|
+
await handler({ result: { content: 'orphan result' } });
|
|
353
|
+
|
|
354
|
+
expect(mockRuntime.fire).not.toHaveBeenCalled();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('returns undefined when runtime is null', async () => {
|
|
358
|
+
const shutdownHandler = handlers.get('session_shutdown')!;
|
|
359
|
+
await shutdownHandler();
|
|
360
|
+
|
|
361
|
+
const handler = handlers.get('tool_result')!;
|
|
362
|
+
const result = await handler({ tool: 'memory_recall', result: { content: 'x' } });
|
|
363
|
+
expect(result).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// --- agent_end ---
|
|
368
|
+
|
|
369
|
+
describe('agent_end handler', () => {
|
|
370
|
+
it('calls runtime.learn with response text', async () => {
|
|
371
|
+
await handlers.get('session_start')!();
|
|
372
|
+
|
|
373
|
+
const handler = handlers.get('agent_end')!;
|
|
374
|
+
await handler({ response: 'The answer is 42.' });
|
|
375
|
+
|
|
376
|
+
expect(mockRuntime.learn).toHaveBeenCalledWith('The answer is 42.');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('skips learn when response is empty string', async () => {
|
|
380
|
+
await handlers.get('session_start')!();
|
|
381
|
+
|
|
382
|
+
const handler = handlers.get('agent_end')!;
|
|
383
|
+
await handler({ response: '' });
|
|
384
|
+
|
|
385
|
+
expect(mockRuntime.learn).not.toHaveBeenCalled();
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('skips learn when response field is missing', async () => {
|
|
389
|
+
await handlers.get('session_start')!();
|
|
390
|
+
|
|
391
|
+
const handler = handlers.get('agent_end')!;
|
|
392
|
+
await handler({});
|
|
393
|
+
|
|
394
|
+
expect(mockRuntime.learn).not.toHaveBeenCalled();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('returns undefined when runtime is null', async () => {
|
|
398
|
+
const shutdownHandler = handlers.get('session_shutdown')!;
|
|
399
|
+
await shutdownHandler();
|
|
400
|
+
|
|
401
|
+
const handler = handlers.get('agent_end')!;
|
|
402
|
+
const result = await handler({ response: 'some response' });
|
|
403
|
+
expect(result).toBeUndefined();
|
|
404
|
+
expect(mockRuntime.learn).not.toHaveBeenCalled();
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// --- session_shutdown ---
|
|
409
|
+
|
|
410
|
+
describe('session_shutdown handler', () => {
|
|
411
|
+
it('calls runtime.shutdown', async () => {
|
|
412
|
+
await handlers.get('session_start')!();
|
|
413
|
+
|
|
414
|
+
const handler = handlers.get('session_shutdown')!;
|
|
415
|
+
await handler();
|
|
416
|
+
|
|
417
|
+
expect(mockRuntime.shutdown).toHaveBeenCalled();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('sets runtime to null after shutdown (subsequent handlers return undefined)', async () => {
|
|
421
|
+
await handlers.get('session_start')!();
|
|
422
|
+
await handlers.get('session_shutdown')!();
|
|
423
|
+
|
|
424
|
+
// After shutdown, before_agent_start should return undefined (no runtime)
|
|
425
|
+
const result = await handlers.get('before_agent_start')!({ content: 'hello' });
|
|
426
|
+
expect(result).toBeUndefined();
|
|
427
|
+
expect(mockRuntime.processPrompt).not.toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('does nothing when runtime is already null', async () => {
|
|
431
|
+
// Do not call session_start — runtime is null from previous shutdown or fresh state
|
|
432
|
+
// Ensure we start from null by calling shutdown twice
|
|
433
|
+
await handlers.get('session_start')!();
|
|
434
|
+
await handlers.get('session_shutdown')!();
|
|
435
|
+
|
|
436
|
+
vi.clearAllMocks();
|
|
437
|
+
|
|
438
|
+
const handler = handlers.get('session_shutdown')!;
|
|
439
|
+
await handler(); // Should not throw
|
|
440
|
+
|
|
441
|
+
expect(mockRuntime.shutdown).not.toHaveBeenCalled();
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dot-ai Pi adapter — a pi-coding-agent extension.
|
|
3
|
+
*
|
|
4
|
+
* This IS a pi extension: export default function(pi: PiExtensionAPI)
|
|
5
|
+
* Bridges dot-ai's runtime to pi's extension API.
|
|
6
|
+
* Full fidelity — all tiers supported, zero degradation.
|
|
7
|
+
*/
|
|
8
|
+
import { DotAiRuntime } from '@dot-ai/core';
|
|
9
|
+
import type { RuntimeOptions } from '@dot-ai/core';
|
|
10
|
+
|
|
11
|
+
// Pi extension API types (structural — no runtime dep on pi)
|
|
12
|
+
interface PiExtensionAPI {
|
|
13
|
+
on(event: string, handler: (...args: unknown[]) => unknown): void;
|
|
14
|
+
registerTool(tool: {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
parameters: Record<string, unknown>;
|
|
18
|
+
execute(input: Record<string, unknown>): Promise<{ content: string; details?: unknown }>;
|
|
19
|
+
promptSnippet?: string;
|
|
20
|
+
promptGuidelines?: string;
|
|
21
|
+
}): void;
|
|
22
|
+
registerCommand?(command: {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
parameters?: Array<{ name: string; description: string; required?: boolean }>;
|
|
26
|
+
execute(args: Record<string, string>): Promise<{ output?: string } | void>;
|
|
27
|
+
completions?(prefix: string): string[] | Promise<string[]>;
|
|
28
|
+
}): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let runtime: DotAiRuntime | null = null;
|
|
32
|
+
|
|
33
|
+
export default function dotAiPiExtension(pi: PiExtensionAPI): void {
|
|
34
|
+
pi.on('session_start', async () => {
|
|
35
|
+
const workspaceRoot = process.cwd();
|
|
36
|
+
runtime = new DotAiRuntime({
|
|
37
|
+
workspaceRoot,
|
|
38
|
+
skipIdentities: true,
|
|
39
|
+
} satisfies RuntimeOptions);
|
|
40
|
+
await runtime.boot();
|
|
41
|
+
|
|
42
|
+
// Register capabilities as pi tools
|
|
43
|
+
for (const cap of runtime.capabilities) {
|
|
44
|
+
pi.registerTool({
|
|
45
|
+
name: cap.name,
|
|
46
|
+
description: cap.description,
|
|
47
|
+
parameters: cap.parameters,
|
|
48
|
+
promptSnippet: cap.promptSnippet,
|
|
49
|
+
promptGuidelines: cap.promptGuidelines,
|
|
50
|
+
async execute(input: Record<string, unknown>) {
|
|
51
|
+
const result = await cap.execute(input);
|
|
52
|
+
return { content: result.text, details: result.details };
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Register extension-contributed commands as pi commands
|
|
58
|
+
if (pi.registerCommand) {
|
|
59
|
+
for (const cmd of runtime.commands) {
|
|
60
|
+
const cmdDef = cmd;
|
|
61
|
+
pi.registerCommand({
|
|
62
|
+
name: cmdDef.name,
|
|
63
|
+
description: cmdDef.description,
|
|
64
|
+
parameters: cmdDef.parameters,
|
|
65
|
+
async execute(args: Record<string, string>) {
|
|
66
|
+
const ctx = {
|
|
67
|
+
workspaceRoot: process.cwd(),
|
|
68
|
+
events: { on: () => {}, off: () => {}, emit: () => {} },
|
|
69
|
+
};
|
|
70
|
+
const result = await cmdDef.execute(args, ctx);
|
|
71
|
+
return result ?? undefined;
|
|
72
|
+
},
|
|
73
|
+
completions: cmdDef.completions,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Log diagnostics
|
|
79
|
+
const diag = runtime.diagnostics;
|
|
80
|
+
if (diag.extensions.length > 0) {
|
|
81
|
+
process.stderr.write(`[dot-ai/pi] ${diag.extensions.length} extension(s) loaded\n`);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
pi.on('before_agent_start', async (...args: unknown[]) => {
|
|
86
|
+
if (!runtime) return;
|
|
87
|
+
const lastMessage = args[0] as { content?: string } | undefined;
|
|
88
|
+
const prompt = lastMessage?.content ?? '';
|
|
89
|
+
const result = await runtime.processPrompt(prompt);
|
|
90
|
+
const response: { systemPrompt: string; model?: string } = { systemPrompt: result.formatted };
|
|
91
|
+
// If routing suggests a model change, propagate it to Pi
|
|
92
|
+
if (result.routing?.model && result.routing.model !== 'default') {
|
|
93
|
+
response.model = result.routing.model;
|
|
94
|
+
}
|
|
95
|
+
return response;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
pi.on('context', async (...args: unknown[]) => {
|
|
99
|
+
if (!runtime) return;
|
|
100
|
+
const event = args[0] as { messages?: unknown[] } | undefined;
|
|
101
|
+
if (event?.messages) {
|
|
102
|
+
const results = await runtime.fire('context_modify', event);
|
|
103
|
+
if (results.length > 0) {
|
|
104
|
+
return results[0]; // Return first modifier result
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
pi.on('tool_call', async (...args: unknown[]) => {
|
|
110
|
+
if (!runtime) return;
|
|
111
|
+
const event = args[0] as { tool?: string; input?: Record<string, unknown> } | undefined;
|
|
112
|
+
if (event?.tool) {
|
|
113
|
+
const result = await runtime.fireToolCall({
|
|
114
|
+
tool: event.tool,
|
|
115
|
+
input: event.input ?? {},
|
|
116
|
+
});
|
|
117
|
+
if (result?.decision === 'block') {
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
pi.on('tool_result', async (...args: unknown[]) => {
|
|
124
|
+
if (!runtime) return;
|
|
125
|
+
const event = args[0] as { tool?: string; result?: { content: string }; isError?: boolean } | undefined;
|
|
126
|
+
if (event?.tool) {
|
|
127
|
+
await runtime.fire('tool_result', {
|
|
128
|
+
tool: event.tool,
|
|
129
|
+
result: event.result ?? { content: '' },
|
|
130
|
+
isError: event.isError ?? false,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
pi.on('turn_start', async (...args: unknown[]) => {
|
|
136
|
+
if (!runtime) return;
|
|
137
|
+
await runtime.fire('turn_start', args[0]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.on('turn_end', async (...args: unknown[]) => {
|
|
141
|
+
if (!runtime) return;
|
|
142
|
+
await runtime.fire('turn_end', args[0]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
pi.on('agent_start', async (...args: unknown[]) => {
|
|
146
|
+
if (!runtime) return;
|
|
147
|
+
await runtime.fire('agent_start', args[0]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// input: chain-transform — allows extensions to intercept/modify user input
|
|
151
|
+
pi.on('input', async (...args: unknown[]) => {
|
|
152
|
+
if (!runtime) return;
|
|
153
|
+
const event = args[0] as { input?: string } | undefined;
|
|
154
|
+
const input = event?.input ?? '';
|
|
155
|
+
if (!input) return;
|
|
156
|
+
// Fire as chain-transform: each extension may modify the input string
|
|
157
|
+
const results = await runtime.fire<{ input?: string; consumed?: boolean }>('input', { input });
|
|
158
|
+
// Apply transforms in order; stop if any extension consumes the input
|
|
159
|
+
let transformed = input;
|
|
160
|
+
for (const result of results) {
|
|
161
|
+
if (result.consumed) {
|
|
162
|
+
return { input: transformed, consumed: true };
|
|
163
|
+
}
|
|
164
|
+
if (result.input !== undefined) {
|
|
165
|
+
transformed = result.input;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (transformed !== input) {
|
|
169
|
+
return { input: transformed };
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
pi.on('agent_end', async (...args: unknown[]) => {
|
|
174
|
+
if (!runtime) return;
|
|
175
|
+
const event = args[0] as { response?: string } | undefined;
|
|
176
|
+
const response = event?.response ?? '';
|
|
177
|
+
if (response) {
|
|
178
|
+
await runtime.learn(response);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
pi.on('session_shutdown', async () => {
|
|
183
|
+
if (runtime) {
|
|
184
|
+
await runtime.shutdown();
|
|
185
|
+
runtime = null;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"composite": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"],
|
|
15
|
+
"references": [
|
|
16
|
+
{ "path": "../core" }
|
|
17
|
+
]
|
|
18
|
+
}
|