@overmux/pi 0.0.1

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 (70) hide show
  1. package/README.md +118 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +40 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config-2jNv4tol.d.ts +34 -0
  6. package/dist/config-2jNv4tol.d.ts.map +1 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.js +47 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/extension.d.ts +19 -0
  11. package/dist/extension.d.ts.map +1 -0
  12. package/dist/extension.js +265 -0
  13. package/dist/extension.js.map +1 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +16 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/jsonl-tail.d.ts +13 -0
  19. package/dist/jsonl-tail.d.ts.map +1 -0
  20. package/dist/jsonl-tail.js +83 -0
  21. package/dist/jsonl-tail.js.map +1 -0
  22. package/dist/live-events-ClhFOMGW.js +497 -0
  23. package/dist/live-events-ClhFOMGW.js.map +1 -0
  24. package/dist/live-events-DAmf6RRx.d.ts +107 -0
  25. package/dist/live-events-DAmf6RRx.d.ts.map +1 -0
  26. package/dist/live-events.d.ts +2 -0
  27. package/dist/live-events.js +2 -0
  28. package/dist/notification-DzOd9cRc.d.ts +20 -0
  29. package/dist/notification-DzOd9cRc.d.ts.map +1 -0
  30. package/dist/notification.d.ts +2 -0
  31. package/dist/notification.js +54 -0
  32. package/dist/notification.js.map +1 -0
  33. package/dist/plugin.d.ts +90 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +213 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/dist/projection.d.ts +101 -0
  38. package/dist/projection.d.ts.map +1 -0
  39. package/dist/projection.js +550 -0
  40. package/dist/projection.js.map +1 -0
  41. package/dist/protocol-CsrnSPOv.d.ts +115 -0
  42. package/dist/protocol-CsrnSPOv.d.ts.map +1 -0
  43. package/dist/protocol.d.ts +2 -0
  44. package/dist/protocol.js +365 -0
  45. package/dist/protocol.js.map +1 -0
  46. package/dist/react.d.ts +139 -0
  47. package/dist/react.d.ts.map +1 -0
  48. package/dist/react.js +796 -0
  49. package/dist/react.js.map +1 -0
  50. package/dist/server.d.ts +5 -0
  51. package/dist/server.js +4 -0
  52. package/dist/session-status-CZLTo8Km.d.ts +71 -0
  53. package/dist/session-status-CZLTo8Km.d.ts.map +1 -0
  54. package/dist/styles.css +619 -0
  55. package/docs/index.md +10 -0
  56. package/package.json +115 -0
  57. package/src/cli.ts +59 -0
  58. package/src/config.ts +87 -0
  59. package/src/extension.ts +486 -0
  60. package/src/index.ts +14 -0
  61. package/src/jsonl-tail.ts +110 -0
  62. package/src/live-events.ts +578 -0
  63. package/src/notification.ts +109 -0
  64. package/src/plugin.ts +369 -0
  65. package/src/projection.ts +995 -0
  66. package/src/protocol.ts +805 -0
  67. package/src/react.tsx +1293 -0
  68. package/src/server.ts +15 -0
  69. package/src/session-status.ts +379 -0
  70. package/src/styles.css +619 -0
package/src/plugin.ts ADDED
@@ -0,0 +1,369 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import {
4
+ defineOperation,
5
+ type HandlerContext,
6
+ type StreamHandlerDefinition,
7
+ } from "overmux";
8
+ import {
9
+ defineResourceContract,
10
+ defineStreamContract,
11
+ noInputSchema,
12
+ } from "overmux";
13
+ import { z } from "zod";
14
+
15
+ import {
16
+ sendAbort,
17
+ sendSetModel,
18
+ sendSetThinkingLevel,
19
+ sendUserMessage,
20
+ type AbortResponse,
21
+ type UserMessageResponse,
22
+ } from "./protocol.js";
23
+ import {
24
+ createPiAgentConversationService,
25
+ piConversationSnapshotSchema,
26
+ piSessionMetadataSchema,
27
+ type PiAgentSession,
28
+ type PiConversationSnapshot,
29
+ } from "./projection.js";
30
+
31
+ type Awaitable<T> = T | Promise<T>;
32
+ type Dispose = () => void;
33
+ export type PiSessionSource = {
34
+ list: () => Awaitable<readonly PiAgentSession[]>;
35
+ subscribe: (
36
+ invalidate: () => void,
37
+ options: { signal: AbortSignal },
38
+ ) => Dispose | void;
39
+ };
40
+ type PiAgentListener = () => void;
41
+
42
+ export type PiAgents = {
43
+ get: (agentId: string) => Promise<PiAgentSession | undefined>;
44
+ list: () => Promise<readonly PiAgentSession[]>;
45
+ subscribe: (listener: PiAgentListener) => Dispose;
46
+ };
47
+
48
+ const piAgentSessionSchema = z
49
+ .object({
50
+ id: z.string().min(1),
51
+ liveEventsDir: z.string().min(1).optional(),
52
+ sessionFile: z.string().min(1),
53
+ sessionMetadata: piSessionMetadataSchema.optional(),
54
+ })
55
+ .strict();
56
+
57
+ const piSessionSchema = z.object({ agentId: z.string().min(1) }).strict();
58
+ const piMessageInputSchema = z
59
+ .object({
60
+ agentId: z.string().min(1),
61
+ deliverAs: z.enum(["steer", "followUp"]),
62
+ message: z.string().trim().min(1),
63
+ })
64
+ .strict();
65
+ const piMessageResultSchema = z.object({
66
+ delivery: z.enum(["immediate", "steer", "followUp"]),
67
+ requestId: z.string().min(1),
68
+ });
69
+ const piStopInputSchema = z.object({ agentId: z.string().min(1) }).strict();
70
+ const piStopResultSchema = z.object({ requestId: z.string().min(1) });
71
+ const piModelInputSchema = z
72
+ .object({
73
+ agentId: z.string().min(1),
74
+ provider: z.string().min(1),
75
+ id: z.string().min(1),
76
+ })
77
+ .strict();
78
+ const piThinkingInputSchema = z
79
+ .object({
80
+ agentId: z.string().min(1),
81
+ level: z.enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]),
82
+ })
83
+ .strict();
84
+ const piControlResultSchema = z.object({ requestId: z.string().min(1) });
85
+
86
+ const once = (dispose: Dispose): Dispose => {
87
+ let disposed = false;
88
+ return () => {
89
+ if (disposed) {
90
+ return;
91
+ }
92
+ disposed = true;
93
+ dispose();
94
+ };
95
+ };
96
+
97
+ const sameSessions = (
98
+ left: readonly PiAgentSession[],
99
+ right: readonly PiAgentSession[],
100
+ ): boolean =>
101
+ left.length === right.length &&
102
+ left.every(
103
+ (session, index) =>
104
+ session.id === right[index]?.id &&
105
+ session.sessionFile === right[index]?.sessionFile &&
106
+ session.liveEventsDir === right[index]?.liveEventsDir &&
107
+ JSON.stringify(session.sessionMetadata) ===
108
+ JSON.stringify(right[index]?.sessionMetadata),
109
+ );
110
+
111
+ export const definePiAgents = ({
112
+ liveEventsDir,
113
+ sessions,
114
+ }: {
115
+ liveEventsDir: string;
116
+ sessions: PiSessionSource;
117
+ }): PiAgents => {
118
+ const listeners = new Set<PiAgentListener>();
119
+ let cached: PiAgentSession[] = [];
120
+ let sourceDispose: Dispose | undefined;
121
+ let refreshQueue = Promise.resolve();
122
+
123
+ const refresh = async () => {
124
+ const result = refreshQueue.then(async () => {
125
+ const next = (await sessions.list())
126
+ .map((session) =>
127
+ piAgentSessionSchema.parse({ ...session, liveEventsDir }),
128
+ )
129
+ .sort((left, right) => left.id.localeCompare(right.id));
130
+ if (new Set(next.map(({ id }) => id)).size !== next.length) {
131
+ throw new Error("Pi agent discovery returned duplicate IDs");
132
+ }
133
+ if (!sameSessions(cached, next)) {
134
+ cached = next;
135
+ listeners.forEach((listener) => listener());
136
+ }
137
+ });
138
+ refreshQueue = result.catch(() => undefined);
139
+ await result;
140
+ return cached;
141
+ };
142
+
143
+ const stopSource = () => {
144
+ sourceDispose?.();
145
+ sourceDispose = undefined;
146
+ };
147
+
148
+ const startSource = () => {
149
+ if (sourceDispose) {
150
+ return;
151
+ }
152
+ const controller = new AbortController();
153
+ const dispose = sessions.subscribe(
154
+ () => void refresh().catch(() => undefined),
155
+ { signal: controller.signal },
156
+ );
157
+ sourceDispose = once(() => {
158
+ controller.abort();
159
+ dispose?.();
160
+ });
161
+ };
162
+
163
+ return {
164
+ get: async (agentId) => (await refresh()).find(({ id }) => id === agentId),
165
+ list: refresh,
166
+ subscribe: (listener) => {
167
+ listeners.add(listener);
168
+ startSource();
169
+ return () => {
170
+ listeners.delete(listener);
171
+ if (!listeners.size) {
172
+ stopSource();
173
+ }
174
+ };
175
+ },
176
+ };
177
+ };
178
+
179
+ const piSessionsContract = defineResourceContract({
180
+ input: noInputSchema,
181
+ output: z.array(piSessionSchema),
182
+ });
183
+
184
+ export const piSessionsResource = ({ agents }: { agents: PiAgents }) => ({
185
+ contract: piSessionsContract,
186
+ kind: "subscription" as const,
187
+ read: async (_input: void, _context: HandlerContext) =>
188
+ (await agents.list()).map(({ id }) => ({ agentId: id })),
189
+ subscribe: (
190
+ _input: void,
191
+ invalidate: () => void,
192
+ context: HandlerContext,
193
+ ) => {
194
+ const dispose = once(agents.subscribe(invalidate));
195
+ context.signal.addEventListener("abort", dispose, { once: true });
196
+ return once(() => {
197
+ context.signal.removeEventListener("abort", dispose);
198
+ dispose();
199
+ });
200
+ },
201
+ });
202
+
203
+ const piConversationContract = defineStreamContract({
204
+ clientMessage: z.never(),
205
+ input: z.object({ agentId: z.string().min(1) }).strict(),
206
+ serverMessage: piConversationSnapshotSchema,
207
+ });
208
+
209
+ const limitConversationEntries = (
210
+ snapshot: PiConversationSnapshot,
211
+ maxEntries: number | undefined,
212
+ ): PiConversationSnapshot =>
213
+ maxEntries === undefined
214
+ ? snapshot
215
+ : { ...snapshot, entries: snapshot.entries.slice(-maxEntries) };
216
+
217
+ export const piConversationStream = ({
218
+ agents,
219
+ maxEntries,
220
+ }: {
221
+ agents: PiAgents;
222
+ maxEntries?: number;
223
+ }): StreamHandlerDefinition<
224
+ typeof piConversationContract.input,
225
+ typeof piConversationContract.clientMessage,
226
+ typeof piConversationContract.serverMessage
227
+ > => {
228
+ if (
229
+ maxEntries !== undefined &&
230
+ (!Number.isSafeInteger(maxEntries) || maxEntries < 1)
231
+ ) {
232
+ throw new Error("Pi conversation maxEntries must be a positive integer");
233
+ }
234
+ return {
235
+ contract: piConversationContract,
236
+ open: async ({ agentId }, context) => {
237
+ const service = createPiAgentConversationService({
238
+ resolveAgent: async (id) => {
239
+ const agent = await agents.get(id);
240
+ if (!agent) {
241
+ throw new Error(`Pi agent not found: ${id}`);
242
+ }
243
+ return { ...agent, sessionId: agent.id };
244
+ },
245
+ });
246
+ try {
247
+ context.emit(
248
+ limitConversationEntries(
249
+ await service.getSnapshot(agentId),
250
+ maxEntries,
251
+ ),
252
+ );
253
+ const unsubscribe = service.subscribe(agentId, (snapshot) =>
254
+ context.emit(limitConversationEntries(snapshot, maxEntries)),
255
+ );
256
+ return {
257
+ dispose: once(() => {
258
+ unsubscribe();
259
+ service.dispose();
260
+ }),
261
+ };
262
+ } catch (error) {
263
+ service.dispose();
264
+ throw error;
265
+ }
266
+ },
267
+ };
268
+ };
269
+
270
+ const piMessageResult = async (
271
+ agent: PiAgentSession | undefined,
272
+ input: z.infer<typeof piMessageInputSchema>,
273
+ ): Promise<z.infer<typeof piMessageResultSchema>> => {
274
+ if (!agent) {
275
+ throw new Error(`Pi agent not found: ${input.agentId}`);
276
+ }
277
+ const response = await sendUserMessage(agent.id, {
278
+ deliverAs: input.deliverAs,
279
+ message: input.message,
280
+ requestId: randomUUID(),
281
+ });
282
+ if (!response?.ok) {
283
+ throw new Error("Pi agent is unavailable");
284
+ }
285
+ return response;
286
+ };
287
+
288
+ const piStopResult = async (
289
+ agent: PiAgentSession | undefined,
290
+ input: z.infer<typeof piStopInputSchema>,
291
+ ): Promise<z.infer<typeof piStopResultSchema>> => {
292
+ if (!agent) {
293
+ throw new Error(`Pi agent not found: ${input.agentId}`);
294
+ }
295
+ const requestId = randomUUID();
296
+ const response = await sendAbort(agent.id, { requestId });
297
+ if (!response?.ok) {
298
+ throw new Error("Pi agent is unavailable");
299
+ }
300
+ return { requestId };
301
+ };
302
+
303
+ const piControlResult = async (
304
+ agent: PiAgentSession | undefined,
305
+ input: { agentId: string },
306
+ send: (
307
+ agentId: string,
308
+ requestId: string,
309
+ ) => Promise<{ ok: boolean; error?: string } | undefined>,
310
+ ): Promise<z.infer<typeof piControlResultSchema>> => {
311
+ if (!agent) {
312
+ throw new Error(`Pi agent not found: ${input.agentId}`);
313
+ }
314
+ const requestId = randomUUID();
315
+ const response = await send(agent.id, requestId);
316
+ if (response?.ok) {
317
+ return { requestId };
318
+ }
319
+ if (response?.error === "not_found") {
320
+ throw new Error("Pi model was not found");
321
+ }
322
+ if (response?.error === "no_key") {
323
+ throw new Error("No API key is available for this Pi model");
324
+ }
325
+ throw new Error("Pi agent is unavailable");
326
+ };
327
+
328
+ export const piOperationHandlers = ({ agents }: { agents: PiAgents }) => ({
329
+ sendPiMessage: defineOperation({
330
+ handle: async (input) =>
331
+ piMessageResult(await agents.get(input.agentId), input),
332
+ input: piMessageInputSchema,
333
+ output: piMessageResultSchema,
334
+ }),
335
+ stopPiAgent: defineOperation({
336
+ handle: async (input) =>
337
+ piStopResult(await agents.get(input.agentId), input),
338
+ input: piStopInputSchema,
339
+ output: piStopResultSchema,
340
+ }),
341
+ setPiModel: defineOperation({
342
+ handle: async (input) =>
343
+ piControlResult(
344
+ await agents.get(input.agentId),
345
+ input,
346
+ (agentId, requestId) =>
347
+ sendSetModel(agentId, {
348
+ id: input.id,
349
+ provider: input.provider,
350
+ requestId,
351
+ }),
352
+ ),
353
+ input: piModelInputSchema,
354
+ output: piControlResultSchema,
355
+ }),
356
+ setPiThinkingLevel: defineOperation({
357
+ handle: async (input) =>
358
+ piControlResult(
359
+ await agents.get(input.agentId),
360
+ input,
361
+ (agentId, requestId) =>
362
+ sendSetThinkingLevel(agentId, { level: input.level, requestId }),
363
+ ),
364
+ input: piThinkingInputSchema,
365
+ output: piControlResultSchema,
366
+ }),
367
+ });
368
+
369
+ export type { AbortResponse, UserMessageResponse };