@earendil-works/pi-server 0.84.4 → 0.85.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +66 -37
  3. package/dist/connection.d.ts +9 -5
  4. package/dist/connection.d.ts.map +1 -1
  5. package/dist/connection.js.map +1 -1
  6. package/dist/errors.d.ts +16 -18
  7. package/dist/errors.d.ts.map +1 -1
  8. package/dist/errors.js +23 -27
  9. package/dist/errors.js.map +1 -1
  10. package/dist/index.d.ts +0 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +0 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/listener.d.ts +1 -3
  15. package/dist/listener.d.ts.map +1 -1
  16. package/dist/listener.js.map +1 -1
  17. package/dist/server.d.ts +16 -6
  18. package/dist/server.d.ts.map +1 -1
  19. package/dist/server.js +268 -86
  20. package/dist/server.js.map +1 -1
  21. package/dist/session-router.d.ts +40 -0
  22. package/dist/session-router.d.ts.map +1 -0
  23. package/dist/session-router.js +264 -0
  24. package/dist/session-router.js.map +1 -0
  25. package/dist/testing/client.d.ts +6 -2
  26. package/dist/testing/client.d.ts.map +1 -1
  27. package/dist/testing/client.js +23 -3
  28. package/dist/testing/client.js.map +1 -1
  29. package/dist/testing/host.d.ts +57 -0
  30. package/dist/testing/host.d.ts.map +1 -0
  31. package/dist/testing/host.js +189 -0
  32. package/dist/testing/host.js.map +1 -0
  33. package/dist/testing/index.d.ts +1 -1
  34. package/dist/testing/index.d.ts.map +1 -1
  35. package/dist/testing/index.js +1 -1
  36. package/dist/testing/index.js.map +1 -1
  37. package/dist/testing/server.d.ts +8 -7
  38. package/dist/testing/server.d.ts.map +1 -1
  39. package/dist/testing/server.js +7 -7
  40. package/dist/testing/server.js.map +1 -1
  41. package/dist/transports/unix/address.d.ts +3 -0
  42. package/dist/transports/unix/address.d.ts.map +1 -0
  43. package/dist/transports/unix/address.js +9 -0
  44. package/dist/transports/unix/address.js.map +1 -0
  45. package/dist/transports/unix/index.d.ts +1 -0
  46. package/dist/transports/unix/index.d.ts.map +1 -1
  47. package/dist/transports/unix/index.js +1 -0
  48. package/dist/transports/unix/index.js.map +1 -1
  49. package/dist/transports/unix/listener.d.ts +2 -3
  50. package/dist/transports/unix/listener.d.ts.map +1 -1
  51. package/dist/transports/unix/listener.js +13 -24
  52. package/dist/transports/unix/listener.js.map +1 -1
  53. package/dist/transports/unix/preset.d.ts +5 -4
  54. package/dist/transports/unix/preset.d.ts.map +1 -1
  55. package/dist/transports/unix/preset.js +5 -4
  56. package/dist/transports/unix/preset.js.map +1 -1
  57. package/dist/transports/unix/types.d.ts +2 -2
  58. package/dist/transports/unix/types.d.ts.map +1 -1
  59. package/dist/transports/unix/types.js.map +1 -1
  60. package/dist/types.d.ts +40 -47
  61. package/dist/types.d.ts.map +1 -1
  62. package/dist/types.js.map +1 -1
  63. package/package.json +4 -3
  64. package/dist/protocol.d.ts +0 -22
  65. package/dist/protocol.d.ts.map +0 -1
  66. package/dist/protocol.js +0 -266
  67. package/dist/protocol.js.map +0 -1
  68. package/dist/sessions.d.ts +0 -2174
  69. package/dist/sessions.d.ts.map +0 -1
  70. package/dist/sessions.js +0 -305
  71. package/dist/sessions.js.map +0 -1
  72. package/dist/snapshots.d.ts +0 -24
  73. package/dist/snapshots.d.ts.map +0 -1
  74. package/dist/snapshots.js +0 -39
  75. package/dist/snapshots.js.map +0 -1
  76. package/dist/testing/service.d.ts +0 -59
  77. package/dist/testing/service.d.ts.map +0 -1
  78. package/dist/testing/service.js +0 -240
  79. package/dist/testing/service.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAmB,MAAM,6BAA6B,CAAC;AAC5G,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,KAAK,EAAwB,eAAe,EAA2C,MAAM,YAAY,CAAC;AAajH,UAAU,yBAAyB;IAClC,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,WAAW,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,eAAe,EAAE,CAAC,UAAU,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,UAAU,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3D,uBAAuB,EAAE,MAAM,IAAI,CAAC;IACpC,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAYD,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA2C;IAE3E,YAAY,OAAO,EAAE,yBAAyB,EAE7C;IAEK,cAAc,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyEjE;IAEK,UAAU,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAU3D;IAEK,YAAY,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC,CAgB/C;IAEK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB3B;YAEa,YAAY;YAeZ,OAAO;YAuBP,MAAM;IAuCpB,OAAO,CAAC,kBAAkB;YAiBZ,SAAS;YAWT,kBAAkB;IAahC,OAAO,CAAC,aAAa;YAIP,iBAAiB;YAOjB,MAAM;IASpB,OAAO,CAAC,eAAe;IAWvB,OAAO,CAAC,oBAAoB;YAId,YAAY;CAsB1B","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Command, EventEnvelope, SessionMetadata, SessionSnapshot } from \"@earendil-works/pi-protocol\";\nimport type { ByteConnection, ConnectionState } from \"./connection.ts\";\nimport { PiServerError } from \"./errors.ts\";\nimport type { CreateSessionOptions, PiServerService, PiSessionRuntime, PiSessionRuntimeEvent } from \"./types.ts\";\n\ninterface LiveSession {\n\tid: string;\n\truntime: PiSessionRuntime;\n\tconnections: Set<ConnectionState>;\n\tunsubscribe: () => void;\n\toperationCount: number;\n\tready: boolean;\n\tterminal: boolean;\n\tdisposing?: Promise<void>;\n}\n\ninterface LiveSessionManagerOptions {\n\tservice: PiServerService;\n\tisClosing: () => boolean;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\tcloseConnection: (connection: ByteConnection) => Promise<void>;\n\tdisconnect: (connection: ConnectionState) => Promise<void>;\n\tbroadcastServerSnapshot: () => void;\n\treportError: (error: unknown) => void;\n}\n\nfunction toMetadata(snapshot: SessionSnapshot): SessionMetadata {\n\treturn {\n\t\tid: snapshot.id,\n\t\tcreatedAt: snapshot.createdAt,\n\t\tupdatedAt: snapshot.updatedAt,\n\t\tsessionName: snapshot.name,\n\t\tcwd: snapshot.cwd,\n\t};\n}\n\nexport class LiveSessionManager {\n\tprivate readonly options: LiveSessionManagerOptions;\n\tprivate readonly liveSessions = new Map<string, LiveSession>();\n\tprivate readonly openingSessions = new Map<string, Promise<LiveSession>>();\n\n\tconstructor(options: LiveSessionManagerOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync executeCommand(connection: ConnectionState, command: Command) {\n\t\tswitch (command.command) {\n\t\t\tcase \"list\":\n\t\t\t\treturn { command: \"list\" as const, sessions: await this.listMetadata() };\n\t\t\tcase \"create\": {\n\t\t\t\tconst id = randomUUID();\n\t\t\t\tconst options: CreateSessionOptions = {\n\t\t\t\t\tid,\n\t\t\t\t\tcwd: command.cwd,\n\t\t\t\t\tname: command.name,\n\t\t\t\t\tmodel: command.model,\n\t\t\t\t\tthinkingLevel: command.thinkingLevel,\n\t\t\t\t};\n\t\t\t\tconst live = await this.acquire(id, () => this.options.service.createSession(options));\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"create\" as const, session };\n\t\t\t}\n\t\t\tcase \"attach\": {\n\t\t\t\tconst live = await this.acquire(command.sessionId, () =>\n\t\t\t\t\tthis.options.service.openSession(command.sessionId),\n\t\t\t\t);\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"attach\" as const, session };\n\t\t\t}\n\t\t\tcase \"detach\": {\n\t\t\t\tconst live = this.liveSessions.get(command.sessionId);\n\t\t\t\tif (connection.sessionIds.has(command.sessionId)) {\n\t\t\t\t\tconnection.sessionIds.delete(command.sessionId);\n\t\t\t\t\tif (live) {\n\t\t\t\t\t\tlive.connections.delete(connection);\n\t\t\t\t\t\tif (live.connections.size > 0 && !live.terminal && !live.disposing) {\n\t\t\t\t\t\t\tawait this.broadcastSnapshot(live);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait this.maybeDispose(live);\n\t\t\t\t\t}\n\t\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\t}\n\t\t\t\treturn { command: \"detach\" as const, sessionId: command.sessionId };\n\t\t\t}\n\t\t\tcase \"prompt\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.prompt({ text: command.text }),\n\t\t\t\t);\n\t\t\t\treturn { command: \"prompt\" as const, session };\n\t\t\t}\n\t\t\tcase \"steer\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.steer({ text: command.text }));\n\t\t\t\treturn { command: \"steer\" as const, session };\n\t\t\t}\n\t\t\tcase \"abort\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.abort());\n\t\t\t\treturn { command: \"abort\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_model\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.setModel(command.model));\n\t\t\t\treturn { command: \"set_model\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_thinking\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.setThinking(command.thinkingLevel),\n\t\t\t\t);\n\t\t\t\treturn { command: \"set_thinking\" as const, session };\n\t\t\t}\n\t\t}\n\t}\n\n\tasync disconnect(connection: ConnectionState): Promise<void> {\n\t\tconst sessions = [...connection.sessionIds]\n\t\t\t.map((id) => this.liveSessions.get(id))\n\t\t\t.filter((live): live is LiveSession => live !== undefined);\n\t\tconnection.sessionIds.clear();\n\t\tfor (const live of sessions) live.connections.delete(connection);\n\t\tconst results = await Promise.allSettled(sessions.map((live) => this.maybeDispose(live)));\n\t\tfor (const result of results) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t}\n\n\tasync listMetadata(): Promise<SessionMetadata[]> {\n\t\tconst stored = await this.options.service.listSessions();\n\t\tconst liveSnapshots = await Promise.all(\n\t\t\t[...this.liveSessions.values()]\n\t\t\t\t.filter((live) => !live.disposing)\n\t\t\t\t.map(async (live) => [live.id, await this.normalizedSnapshot(live)] as const),\n\t\t);\n\t\tconst liveById = new Map(liveSnapshots);\n\t\tconst metadata = stored.map((item) => {\n\t\t\tconst snapshot = liveById.get(item.id);\n\t\t\tif (!snapshot) return item;\n\t\t\tliveById.delete(item.id);\n\t\t\treturn { ...item, ...toMetadata(snapshot) };\n\t\t});\n\t\tfor (const snapshot of liveById.values()) metadata.push(toMetadata(snapshot));\n\t\treturn metadata;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tconst openingResults = await Promise.allSettled([...this.openingSessions.values()]);\n\t\tfor (const result of openingResults) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t\tconst sessions = [...this.liveSessions.values()];\n\t\tthis.liveSessions.clear();\n\t\tawait Promise.all(\n\t\t\tsessions.map(async (live) => {\n\t\t\t\tif (live.disposing) {\n\t\t\t\t\tawait live.disposing;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlive.unsubscribe();\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate async runOperation(\n\t\tconnection: ConnectionState,\n\t\tlive: LiveSession,\n\t\toperation: () => Promise<void>,\n\t): Promise<SessionSnapshot> {\n\t\tlive.operationCount += 1;\n\t\ttry {\n\t\t\tawait operation();\n\t\t\treturn this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t} finally {\n\t\t\tlive.operationCount -= 1;\n\t\t\tthis.scheduleMaybeDispose(live);\n\t\t}\n\t}\n\n\tprivate async acquire(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tfor (;;) {\n\t\t\tconst existing = this.liveSessions.get(id);\n\t\t\tif (existing) {\n\t\t\t\tif (existing.terminal) throw new PiServerError(\"session_locked\", `Session runtime is terminating: ${id}`);\n\t\t\t\tif (existing.disposing) {\n\t\t\t\t\tawait existing.disposing;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn existing;\n\t\t\t}\n\t\t\tconst opening = this.openingSessions.get(id);\n\t\t\tif (opening) return opening;\n\t\t\tconst pending = this.create(id, acquireRuntime);\n\t\t\tthis.openingSessions.set(id, pending);\n\t\t\ttry {\n\t\t\t\treturn await pending;\n\t\t\t} finally {\n\t\t\t\tif (this.openingSessions.get(id) === pending) this.openingSessions.delete(id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async create(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tconst runtime = await acquireRuntime();\n\t\tif (this.options.isClosing()) {\n\t\t\tawait runtime.dispose();\n\t\t\tthrow new Error(\"PiServer closed while acquiring a session runtime\");\n\t\t}\n\t\tlet live: LiveSession | undefined;\n\t\ttry {\n\t\t\tconst snapshot = await runtime.snapshot();\n\t\t\tif (snapshot.id !== id) {\n\t\t\t\tthrow new PiServerError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Service returned session ${snapshot.id} for server-assigned session ${id}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tlive = {\n\t\t\t\tid,\n\t\t\t\truntime,\n\t\t\t\tconnections: new Set(),\n\t\t\t\tunsubscribe: () => {},\n\t\t\t\toperationCount: 0,\n\t\t\t\tready: false,\n\t\t\t\tterminal: false,\n\t\t\t};\n\t\t\tlive.unsubscribe = runtime.subscribe((event) => this.handleRuntimeEvent(live!, event));\n\t\t\tthis.liveSessions.set(id, live);\n\t\t\tlive.ready = true;\n\t\t\treturn live;\n\t\t} catch (error) {\n\t\t\tif (live) live.unsubscribe();\n\t\t\ttry {\n\t\t\t\tawait runtime.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tthis.options.reportError(disposeError);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate handleRuntimeEvent(live: LiveSession, event: PiSessionRuntimeEvent): void {\n\t\tif (event.type === \"error\") {\n\t\t\tvoid this.terminate(live, event.error).catch((error: unknown) => this.options.reportError(error));\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"progress\") {\n\t\t\tconst envelope: EventEnvelope = {\n\t\t\t\ttype: \"event\",\n\t\t\t\tevent: { type: \"session_progress\", sessionId: live.id, progress: event.progress },\n\t\t\t};\n\t\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\t} else {\n\t\t\tvoid this.broadcastSnapshot(live).catch((error: unknown) => this.options.reportError(error));\n\t\t}\n\t\tthis.scheduleMaybeDispose(live);\n\t}\n\n\tprivate async terminate(live: LiveSession, error: PiServerError): Promise<void> {\n\t\tif (live.terminal) return;\n\t\tlive.terminal = true;\n\t\tthis.options.reportError(error);\n\t\tlive.unsubscribe();\n\t\tconst connections = [...live.connections];\n\t\tawait Promise.all(connections.map((connection) => this.options.closeConnection(connection.connection)));\n\t\tawait Promise.all(connections.map((connection) => this.options.disconnect(connection)));\n\t\tawait this.maybeDispose(live);\n\t}\n\n\tprivate async normalizedSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await live.runtime.snapshot();\n\t\tif (snapshot.id !== live.id) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Runtime session ID changed from ${live.id} to ${snapshot.id}`);\n\t\t}\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tphase: live.runtime.getPhase(),\n\t\t\tattached: live.connections.size > 0,\n\t\t\tlocked: true,\n\t\t};\n\t}\n\n\tprivate forConnection(snapshot: SessionSnapshot, connection: ConnectionState): SessionSnapshot {\n\t\treturn { ...snapshot, attached: connection.sessionIds.has(snapshot.id) };\n\t}\n\n\tprivate async broadcastSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await this.normalizedSnapshot(live);\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"session_snapshot\", snapshot } };\n\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\treturn snapshot;\n\t}\n\n\tprivate async attach(connection: ConnectionState, live: LiveSession): Promise<void> {\n\t\tif (connection.disconnected || connection.stage !== \"ready\" || connection.connection.closed) {\n\t\t\tawait this.maybeDispose(live);\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Connection closed while attaching to a session\");\n\t\t}\n\t\tconnection.sessionIds.add(live.id);\n\t\tlive.connections.add(connection);\n\t}\n\n\tprivate requireAttached(connection: ConnectionState, sessionId: string): LiveSession {\n\t\tif (!connection.sessionIds.has(sessionId)) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Connection is not attached to session ${sessionId}`);\n\t\t}\n\t\tconst live = this.liveSessions.get(sessionId);\n\t\tif (!live || live.terminal || live.disposing) {\n\t\t\tthrow new PiServerError(\"not_found\", `Session is not live: ${sessionId}`);\n\t\t}\n\t\treturn live;\n\t}\n\n\tprivate scheduleMaybeDispose(live: LiveSession): void {\n\t\tvoid this.maybeDispose(live).catch((error: unknown) => this.options.reportError(error));\n\t}\n\n\tprivate async maybeDispose(live: LiveSession): Promise<void> {\n\t\tif (\n\t\t\tthis.options.isClosing() ||\n\t\t\t!live.ready ||\n\t\t\tlive.disposing ||\n\t\t\tlive.connections.size > 0 ||\n\t\t\tlive.operationCount > 0 ||\n\t\t\t(!live.terminal && live.runtime.getPhase() !== \"idle\")\n\t\t) {\n\t\t\treturn live.disposing;\n\t\t}\n\t\tlive.unsubscribe();\n\t\tlive.disposing = (async () => {\n\t\t\ttry {\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t} finally {\n\t\t\t\tif (this.liveSessions.get(live.id) === live) this.liveSessions.delete(live.id);\n\t\t\t}\n\t\t})();\n\t\tawait live.disposing;\n\t\tif (!this.options.isClosing()) this.options.broadcastServerSnapshot();\n\t}\n}\n"]}
package/dist/sessions.js DELETED
@@ -1,305 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { PiServerError } from "./errors.js";
3
- function toMetadata(snapshot) {
4
- return {
5
- id: snapshot.id,
6
- createdAt: snapshot.createdAt,
7
- updatedAt: snapshot.updatedAt,
8
- sessionName: snapshot.name,
9
- cwd: snapshot.cwd,
10
- };
11
- }
12
- export class LiveSessionManager {
13
- options;
14
- liveSessions = new Map();
15
- openingSessions = new Map();
16
- constructor(options) {
17
- this.options = options;
18
- }
19
- async executeCommand(connection, command) {
20
- switch (command.command) {
21
- case "list":
22
- return { command: "list", sessions: await this.listMetadata() };
23
- case "create": {
24
- const id = randomUUID();
25
- const options = {
26
- id,
27
- cwd: command.cwd,
28
- name: command.name,
29
- model: command.model,
30
- thinkingLevel: command.thinkingLevel,
31
- };
32
- const live = await this.acquire(id, () => this.options.service.createSession(options));
33
- await this.attach(connection, live);
34
- const session = this.forConnection(await this.broadcastSnapshot(live), connection);
35
- this.options.broadcastServerSnapshot();
36
- return { command: "create", session };
37
- }
38
- case "attach": {
39
- const live = await this.acquire(command.sessionId, () => this.options.service.openSession(command.sessionId));
40
- await this.attach(connection, live);
41
- const session = this.forConnection(await this.broadcastSnapshot(live), connection);
42
- this.options.broadcastServerSnapshot();
43
- return { command: "attach", session };
44
- }
45
- case "detach": {
46
- const live = this.liveSessions.get(command.sessionId);
47
- if (connection.sessionIds.has(command.sessionId)) {
48
- connection.sessionIds.delete(command.sessionId);
49
- if (live) {
50
- live.connections.delete(connection);
51
- if (live.connections.size > 0 && !live.terminal && !live.disposing) {
52
- await this.broadcastSnapshot(live);
53
- }
54
- await this.maybeDispose(live);
55
- }
56
- this.options.broadcastServerSnapshot();
57
- }
58
- return { command: "detach", sessionId: command.sessionId };
59
- }
60
- case "prompt": {
61
- const live = this.requireAttached(connection, command.sessionId);
62
- const session = await this.runOperation(connection, live, () => live.runtime.prompt({ text: command.text }));
63
- return { command: "prompt", session };
64
- }
65
- case "steer": {
66
- const live = this.requireAttached(connection, command.sessionId);
67
- const session = await this.runOperation(connection, live, () => live.runtime.steer({ text: command.text }));
68
- return { command: "steer", session };
69
- }
70
- case "abort": {
71
- const live = this.requireAttached(connection, command.sessionId);
72
- const session = await this.runOperation(connection, live, () => live.runtime.abort());
73
- return { command: "abort", session };
74
- }
75
- case "set_model": {
76
- const live = this.requireAttached(connection, command.sessionId);
77
- const session = await this.runOperation(connection, live, () => live.runtime.setModel(command.model));
78
- return { command: "set_model", session };
79
- }
80
- case "set_thinking": {
81
- const live = this.requireAttached(connection, command.sessionId);
82
- const session = await this.runOperation(connection, live, () => live.runtime.setThinking(command.thinkingLevel));
83
- return { command: "set_thinking", session };
84
- }
85
- }
86
- }
87
- async disconnect(connection) {
88
- const sessions = [...connection.sessionIds]
89
- .map((id) => this.liveSessions.get(id))
90
- .filter((live) => live !== undefined);
91
- connection.sessionIds.clear();
92
- for (const live of sessions)
93
- live.connections.delete(connection);
94
- const results = await Promise.allSettled(sessions.map((live) => this.maybeDispose(live)));
95
- for (const result of results) {
96
- if (result.status === "rejected")
97
- this.options.reportError(result.reason);
98
- }
99
- }
100
- async listMetadata() {
101
- const stored = await this.options.service.listSessions();
102
- const liveSnapshots = await Promise.all([...this.liveSessions.values()]
103
- .filter((live) => !live.disposing)
104
- .map(async (live) => [live.id, await this.normalizedSnapshot(live)]));
105
- const liveById = new Map(liveSnapshots);
106
- const metadata = stored.map((item) => {
107
- const snapshot = liveById.get(item.id);
108
- if (!snapshot)
109
- return item;
110
- liveById.delete(item.id);
111
- return { ...item, ...toMetadata(snapshot) };
112
- });
113
- for (const snapshot of liveById.values())
114
- metadata.push(toMetadata(snapshot));
115
- return metadata;
116
- }
117
- async close() {
118
- const openingResults = await Promise.allSettled([...this.openingSessions.values()]);
119
- for (const result of openingResults) {
120
- if (result.status === "rejected")
121
- this.options.reportError(result.reason);
122
- }
123
- const sessions = [...this.liveSessions.values()];
124
- this.liveSessions.clear();
125
- await Promise.all(sessions.map(async (live) => {
126
- if (live.disposing) {
127
- await live.disposing;
128
- return;
129
- }
130
- live.unsubscribe();
131
- await live.runtime.dispose();
132
- }));
133
- }
134
- async runOperation(connection, live, operation) {
135
- live.operationCount += 1;
136
- try {
137
- await operation();
138
- return this.forConnection(await this.broadcastSnapshot(live), connection);
139
- }
140
- finally {
141
- live.operationCount -= 1;
142
- this.scheduleMaybeDispose(live);
143
- }
144
- }
145
- async acquire(id, acquireRuntime) {
146
- for (;;) {
147
- const existing = this.liveSessions.get(id);
148
- if (existing) {
149
- if (existing.terminal)
150
- throw new PiServerError("session_locked", `Session runtime is terminating: ${id}`);
151
- if (existing.disposing) {
152
- await existing.disposing;
153
- continue;
154
- }
155
- return existing;
156
- }
157
- const opening = this.openingSessions.get(id);
158
- if (opening)
159
- return opening;
160
- const pending = this.create(id, acquireRuntime);
161
- this.openingSessions.set(id, pending);
162
- try {
163
- return await pending;
164
- }
165
- finally {
166
- if (this.openingSessions.get(id) === pending)
167
- this.openingSessions.delete(id);
168
- }
169
- }
170
- }
171
- async create(id, acquireRuntime) {
172
- const runtime = await acquireRuntime();
173
- if (this.options.isClosing()) {
174
- await runtime.dispose();
175
- throw new Error("PiServer closed while acquiring a session runtime");
176
- }
177
- let live;
178
- try {
179
- const snapshot = await runtime.snapshot();
180
- if (snapshot.id !== id) {
181
- throw new PiServerError("invalid_request", `Service returned session ${snapshot.id} for server-assigned session ${id}`);
182
- }
183
- live = {
184
- id,
185
- runtime,
186
- connections: new Set(),
187
- unsubscribe: () => { },
188
- operationCount: 0,
189
- ready: false,
190
- terminal: false,
191
- };
192
- live.unsubscribe = runtime.subscribe((event) => this.handleRuntimeEvent(live, event));
193
- this.liveSessions.set(id, live);
194
- live.ready = true;
195
- return live;
196
- }
197
- catch (error) {
198
- if (live)
199
- live.unsubscribe();
200
- try {
201
- await runtime.dispose();
202
- }
203
- catch (disposeError) {
204
- this.options.reportError(disposeError);
205
- }
206
- throw error;
207
- }
208
- }
209
- handleRuntimeEvent(live, event) {
210
- if (event.type === "error") {
211
- void this.terminate(live, event.error).catch((error) => this.options.reportError(error));
212
- return;
213
- }
214
- if (event.type === "progress") {
215
- const envelope = {
216
- type: "event",
217
- event: { type: "session_progress", sessionId: live.id, progress: event.progress },
218
- };
219
- for (const connection of live.connections)
220
- void this.options.sendMessage(connection, envelope);
221
- }
222
- else {
223
- void this.broadcastSnapshot(live).catch((error) => this.options.reportError(error));
224
- }
225
- this.scheduleMaybeDispose(live);
226
- }
227
- async terminate(live, error) {
228
- if (live.terminal)
229
- return;
230
- live.terminal = true;
231
- this.options.reportError(error);
232
- live.unsubscribe();
233
- const connections = [...live.connections];
234
- await Promise.all(connections.map((connection) => this.options.closeConnection(connection.connection)));
235
- await Promise.all(connections.map((connection) => this.options.disconnect(connection)));
236
- await this.maybeDispose(live);
237
- }
238
- async normalizedSnapshot(live) {
239
- const snapshot = await live.runtime.snapshot();
240
- if (snapshot.id !== live.id) {
241
- throw new PiServerError("invalid_request", `Runtime session ID changed from ${live.id} to ${snapshot.id}`);
242
- }
243
- return {
244
- ...snapshot,
245
- phase: live.runtime.getPhase(),
246
- attached: live.connections.size > 0,
247
- locked: true,
248
- };
249
- }
250
- forConnection(snapshot, connection) {
251
- return { ...snapshot, attached: connection.sessionIds.has(snapshot.id) };
252
- }
253
- async broadcastSnapshot(live) {
254
- const snapshot = await this.normalizedSnapshot(live);
255
- const envelope = { type: "event", event: { type: "session_snapshot", snapshot } };
256
- for (const connection of live.connections)
257
- void this.options.sendMessage(connection, envelope);
258
- return snapshot;
259
- }
260
- async attach(connection, live) {
261
- if (connection.disconnected || connection.stage !== "ready" || connection.connection.closed) {
262
- await this.maybeDispose(live);
263
- throw new PiServerError("invalid_request", "Connection closed while attaching to a session");
264
- }
265
- connection.sessionIds.add(live.id);
266
- live.connections.add(connection);
267
- }
268
- requireAttached(connection, sessionId) {
269
- if (!connection.sessionIds.has(sessionId)) {
270
- throw new PiServerError("invalid_request", `Connection is not attached to session ${sessionId}`);
271
- }
272
- const live = this.liveSessions.get(sessionId);
273
- if (!live || live.terminal || live.disposing) {
274
- throw new PiServerError("not_found", `Session is not live: ${sessionId}`);
275
- }
276
- return live;
277
- }
278
- scheduleMaybeDispose(live) {
279
- void this.maybeDispose(live).catch((error) => this.options.reportError(error));
280
- }
281
- async maybeDispose(live) {
282
- if (this.options.isClosing() ||
283
- !live.ready ||
284
- live.disposing ||
285
- live.connections.size > 0 ||
286
- live.operationCount > 0 ||
287
- (!live.terminal && live.runtime.getPhase() !== "idle")) {
288
- return live.disposing;
289
- }
290
- live.unsubscribe();
291
- live.disposing = (async () => {
292
- try {
293
- await live.runtime.dispose();
294
- }
295
- finally {
296
- if (this.liveSessions.get(live.id) === live)
297
- this.liveSessions.delete(live.id);
298
- }
299
- })();
300
- await live.disposing;
301
- if (!this.options.isClosing())
302
- this.options.broadcastServerSnapshot();
303
- }
304
- }
305
- //# sourceMappingURL=sessions.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Command, EventEnvelope, SessionMetadata, SessionSnapshot } from \"@earendil-works/pi-protocol\";\nimport type { ByteConnection, ConnectionState } from \"./connection.ts\";\nimport { PiServerError } from \"./errors.ts\";\nimport type { CreateSessionOptions, PiServerService, PiSessionRuntime, PiSessionRuntimeEvent } from \"./types.ts\";\n\ninterface LiveSession {\n\tid: string;\n\truntime: PiSessionRuntime;\n\tconnections: Set<ConnectionState>;\n\tunsubscribe: () => void;\n\toperationCount: number;\n\tready: boolean;\n\tterminal: boolean;\n\tdisposing?: Promise<void>;\n}\n\ninterface LiveSessionManagerOptions {\n\tservice: PiServerService;\n\tisClosing: () => boolean;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\tcloseConnection: (connection: ByteConnection) => Promise<void>;\n\tdisconnect: (connection: ConnectionState) => Promise<void>;\n\tbroadcastServerSnapshot: () => void;\n\treportError: (error: unknown) => void;\n}\n\nfunction toMetadata(snapshot: SessionSnapshot): SessionMetadata {\n\treturn {\n\t\tid: snapshot.id,\n\t\tcreatedAt: snapshot.createdAt,\n\t\tupdatedAt: snapshot.updatedAt,\n\t\tsessionName: snapshot.name,\n\t\tcwd: snapshot.cwd,\n\t};\n}\n\nexport class LiveSessionManager {\n\tprivate readonly options: LiveSessionManagerOptions;\n\tprivate readonly liveSessions = new Map<string, LiveSession>();\n\tprivate readonly openingSessions = new Map<string, Promise<LiveSession>>();\n\n\tconstructor(options: LiveSessionManagerOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync executeCommand(connection: ConnectionState, command: Command) {\n\t\tswitch (command.command) {\n\t\t\tcase \"list\":\n\t\t\t\treturn { command: \"list\" as const, sessions: await this.listMetadata() };\n\t\t\tcase \"create\": {\n\t\t\t\tconst id = randomUUID();\n\t\t\t\tconst options: CreateSessionOptions = {\n\t\t\t\t\tid,\n\t\t\t\t\tcwd: command.cwd,\n\t\t\t\t\tname: command.name,\n\t\t\t\t\tmodel: command.model,\n\t\t\t\t\tthinkingLevel: command.thinkingLevel,\n\t\t\t\t};\n\t\t\t\tconst live = await this.acquire(id, () => this.options.service.createSession(options));\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"create\" as const, session };\n\t\t\t}\n\t\t\tcase \"attach\": {\n\t\t\t\tconst live = await this.acquire(command.sessionId, () =>\n\t\t\t\t\tthis.options.service.openSession(command.sessionId),\n\t\t\t\t);\n\t\t\t\tawait this.attach(connection, live);\n\t\t\t\tconst session = this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\treturn { command: \"attach\" as const, session };\n\t\t\t}\n\t\t\tcase \"detach\": {\n\t\t\t\tconst live = this.liveSessions.get(command.sessionId);\n\t\t\t\tif (connection.sessionIds.has(command.sessionId)) {\n\t\t\t\t\tconnection.sessionIds.delete(command.sessionId);\n\t\t\t\t\tif (live) {\n\t\t\t\t\t\tlive.connections.delete(connection);\n\t\t\t\t\t\tif (live.connections.size > 0 && !live.terminal && !live.disposing) {\n\t\t\t\t\t\t\tawait this.broadcastSnapshot(live);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait this.maybeDispose(live);\n\t\t\t\t\t}\n\t\t\t\t\tthis.options.broadcastServerSnapshot();\n\t\t\t\t}\n\t\t\t\treturn { command: \"detach\" as const, sessionId: command.sessionId };\n\t\t\t}\n\t\t\tcase \"prompt\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.prompt({ text: command.text }),\n\t\t\t\t);\n\t\t\t\treturn { command: \"prompt\" as const, session };\n\t\t\t}\n\t\t\tcase \"steer\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.steer({ text: command.text }));\n\t\t\t\treturn { command: \"steer\" as const, session };\n\t\t\t}\n\t\t\tcase \"abort\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.abort());\n\t\t\t\treturn { command: \"abort\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_model\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () => live.runtime.setModel(command.model));\n\t\t\t\treturn { command: \"set_model\" as const, session };\n\t\t\t}\n\t\t\tcase \"set_thinking\": {\n\t\t\t\tconst live = this.requireAttached(connection, command.sessionId);\n\t\t\t\tconst session = await this.runOperation(connection, live, () =>\n\t\t\t\t\tlive.runtime.setThinking(command.thinkingLevel),\n\t\t\t\t);\n\t\t\t\treturn { command: \"set_thinking\" as const, session };\n\t\t\t}\n\t\t}\n\t}\n\n\tasync disconnect(connection: ConnectionState): Promise<void> {\n\t\tconst sessions = [...connection.sessionIds]\n\t\t\t.map((id) => this.liveSessions.get(id))\n\t\t\t.filter((live): live is LiveSession => live !== undefined);\n\t\tconnection.sessionIds.clear();\n\t\tfor (const live of sessions) live.connections.delete(connection);\n\t\tconst results = await Promise.allSettled(sessions.map((live) => this.maybeDispose(live)));\n\t\tfor (const result of results) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t}\n\n\tasync listMetadata(): Promise<SessionMetadata[]> {\n\t\tconst stored = await this.options.service.listSessions();\n\t\tconst liveSnapshots = await Promise.all(\n\t\t\t[...this.liveSessions.values()]\n\t\t\t\t.filter((live) => !live.disposing)\n\t\t\t\t.map(async (live) => [live.id, await this.normalizedSnapshot(live)] as const),\n\t\t);\n\t\tconst liveById = new Map(liveSnapshots);\n\t\tconst metadata = stored.map((item) => {\n\t\t\tconst snapshot = liveById.get(item.id);\n\t\t\tif (!snapshot) return item;\n\t\t\tliveById.delete(item.id);\n\t\t\treturn { ...item, ...toMetadata(snapshot) };\n\t\t});\n\t\tfor (const snapshot of liveById.values()) metadata.push(toMetadata(snapshot));\n\t\treturn metadata;\n\t}\n\n\tasync close(): Promise<void> {\n\t\tconst openingResults = await Promise.allSettled([...this.openingSessions.values()]);\n\t\tfor (const result of openingResults) {\n\t\t\tif (result.status === \"rejected\") this.options.reportError(result.reason);\n\t\t}\n\t\tconst sessions = [...this.liveSessions.values()];\n\t\tthis.liveSessions.clear();\n\t\tawait Promise.all(\n\t\t\tsessions.map(async (live) => {\n\t\t\t\tif (live.disposing) {\n\t\t\t\t\tawait live.disposing;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tlive.unsubscribe();\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t}),\n\t\t);\n\t}\n\n\tprivate async runOperation(\n\t\tconnection: ConnectionState,\n\t\tlive: LiveSession,\n\t\toperation: () => Promise<void>,\n\t): Promise<SessionSnapshot> {\n\t\tlive.operationCount += 1;\n\t\ttry {\n\t\t\tawait operation();\n\t\t\treturn this.forConnection(await this.broadcastSnapshot(live), connection);\n\t\t} finally {\n\t\t\tlive.operationCount -= 1;\n\t\t\tthis.scheduleMaybeDispose(live);\n\t\t}\n\t}\n\n\tprivate async acquire(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tfor (;;) {\n\t\t\tconst existing = this.liveSessions.get(id);\n\t\t\tif (existing) {\n\t\t\t\tif (existing.terminal) throw new PiServerError(\"session_locked\", `Session runtime is terminating: ${id}`);\n\t\t\t\tif (existing.disposing) {\n\t\t\t\t\tawait existing.disposing;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\treturn existing;\n\t\t\t}\n\t\t\tconst opening = this.openingSessions.get(id);\n\t\t\tif (opening) return opening;\n\t\t\tconst pending = this.create(id, acquireRuntime);\n\t\t\tthis.openingSessions.set(id, pending);\n\t\t\ttry {\n\t\t\t\treturn await pending;\n\t\t\t} finally {\n\t\t\t\tif (this.openingSessions.get(id) === pending) this.openingSessions.delete(id);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async create(id: string, acquireRuntime: () => Promise<PiSessionRuntime>): Promise<LiveSession> {\n\t\tconst runtime = await acquireRuntime();\n\t\tif (this.options.isClosing()) {\n\t\t\tawait runtime.dispose();\n\t\t\tthrow new Error(\"PiServer closed while acquiring a session runtime\");\n\t\t}\n\t\tlet live: LiveSession | undefined;\n\t\ttry {\n\t\t\tconst snapshot = await runtime.snapshot();\n\t\t\tif (snapshot.id !== id) {\n\t\t\t\tthrow new PiServerError(\n\t\t\t\t\t\"invalid_request\",\n\t\t\t\t\t`Service returned session ${snapshot.id} for server-assigned session ${id}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tlive = {\n\t\t\t\tid,\n\t\t\t\truntime,\n\t\t\t\tconnections: new Set(),\n\t\t\t\tunsubscribe: () => {},\n\t\t\t\toperationCount: 0,\n\t\t\t\tready: false,\n\t\t\t\tterminal: false,\n\t\t\t};\n\t\t\tlive.unsubscribe = runtime.subscribe((event) => this.handleRuntimeEvent(live!, event));\n\t\t\tthis.liveSessions.set(id, live);\n\t\t\tlive.ready = true;\n\t\t\treturn live;\n\t\t} catch (error) {\n\t\t\tif (live) live.unsubscribe();\n\t\t\ttry {\n\t\t\t\tawait runtime.dispose();\n\t\t\t} catch (disposeError) {\n\t\t\t\tthis.options.reportError(disposeError);\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate handleRuntimeEvent(live: LiveSession, event: PiSessionRuntimeEvent): void {\n\t\tif (event.type === \"error\") {\n\t\t\tvoid this.terminate(live, event.error).catch((error: unknown) => this.options.reportError(error));\n\t\t\treturn;\n\t\t}\n\t\tif (event.type === \"progress\") {\n\t\t\tconst envelope: EventEnvelope = {\n\t\t\t\ttype: \"event\",\n\t\t\t\tevent: { type: \"session_progress\", sessionId: live.id, progress: event.progress },\n\t\t\t};\n\t\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\t} else {\n\t\t\tvoid this.broadcastSnapshot(live).catch((error: unknown) => this.options.reportError(error));\n\t\t}\n\t\tthis.scheduleMaybeDispose(live);\n\t}\n\n\tprivate async terminate(live: LiveSession, error: PiServerError): Promise<void> {\n\t\tif (live.terminal) return;\n\t\tlive.terminal = true;\n\t\tthis.options.reportError(error);\n\t\tlive.unsubscribe();\n\t\tconst connections = [...live.connections];\n\t\tawait Promise.all(connections.map((connection) => this.options.closeConnection(connection.connection)));\n\t\tawait Promise.all(connections.map((connection) => this.options.disconnect(connection)));\n\t\tawait this.maybeDispose(live);\n\t}\n\n\tprivate async normalizedSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await live.runtime.snapshot();\n\t\tif (snapshot.id !== live.id) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Runtime session ID changed from ${live.id} to ${snapshot.id}`);\n\t\t}\n\t\treturn {\n\t\t\t...snapshot,\n\t\t\tphase: live.runtime.getPhase(),\n\t\t\tattached: live.connections.size > 0,\n\t\t\tlocked: true,\n\t\t};\n\t}\n\n\tprivate forConnection(snapshot: SessionSnapshot, connection: ConnectionState): SessionSnapshot {\n\t\treturn { ...snapshot, attached: connection.sessionIds.has(snapshot.id) };\n\t}\n\n\tprivate async broadcastSnapshot(live: LiveSession): Promise<SessionSnapshot> {\n\t\tconst snapshot = await this.normalizedSnapshot(live);\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"session_snapshot\", snapshot } };\n\t\tfor (const connection of live.connections) void this.options.sendMessage(connection, envelope);\n\t\treturn snapshot;\n\t}\n\n\tprivate async attach(connection: ConnectionState, live: LiveSession): Promise<void> {\n\t\tif (connection.disconnected || connection.stage !== \"ready\" || connection.connection.closed) {\n\t\t\tawait this.maybeDispose(live);\n\t\t\tthrow new PiServerError(\"invalid_request\", \"Connection closed while attaching to a session\");\n\t\t}\n\t\tconnection.sessionIds.add(live.id);\n\t\tlive.connections.add(connection);\n\t}\n\n\tprivate requireAttached(connection: ConnectionState, sessionId: string): LiveSession {\n\t\tif (!connection.sessionIds.has(sessionId)) {\n\t\t\tthrow new PiServerError(\"invalid_request\", `Connection is not attached to session ${sessionId}`);\n\t\t}\n\t\tconst live = this.liveSessions.get(sessionId);\n\t\tif (!live || live.terminal || live.disposing) {\n\t\t\tthrow new PiServerError(\"not_found\", `Session is not live: ${sessionId}`);\n\t\t}\n\t\treturn live;\n\t}\n\n\tprivate scheduleMaybeDispose(live: LiveSession): void {\n\t\tvoid this.maybeDispose(live).catch((error: unknown) => this.options.reportError(error));\n\t}\n\n\tprivate async maybeDispose(live: LiveSession): Promise<void> {\n\t\tif (\n\t\t\tthis.options.isClosing() ||\n\t\t\t!live.ready ||\n\t\t\tlive.disposing ||\n\t\t\tlive.connections.size > 0 ||\n\t\t\tlive.operationCount > 0 ||\n\t\t\t(!live.terminal && live.runtime.getPhase() !== \"idle\")\n\t\t) {\n\t\t\treturn live.disposing;\n\t\t}\n\t\tlive.unsubscribe();\n\t\tlive.disposing = (async () => {\n\t\t\ttry {\n\t\t\t\tawait live.runtime.dispose();\n\t\t\t} finally {\n\t\t\t\tif (this.liveSessions.get(live.id) === live) this.liveSessions.delete(live.id);\n\t\t\t}\n\t\t})();\n\t\tawait live.disposing;\n\t\tif (!this.options.isClosing()) this.options.broadcastServerSnapshot();\n\t}\n}\n"]}
@@ -1,24 +0,0 @@
1
- import { type EventEnvelope, type ModelMetadata, type ServerSnapshot, type SessionMetadata } from "@earendil-works/pi-protocol";
2
- import type { ConnectionState } from "./connection.ts";
3
- import type { PiServerService } from "./types.ts";
4
- interface ServerSnapshotPublisherOptions {
5
- serverId: string;
6
- service: PiServerService;
7
- connections: Set<ConnectionState>;
8
- isClosing: () => boolean;
9
- listSessions: () => Promise<SessionMetadata[]>;
10
- sendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;
11
- reportError: (error: unknown) => void;
12
- }
13
- export declare class ServerSnapshotPublisher {
14
- private readonly options;
15
- private revision;
16
- private broadcastQueue;
17
- constructor(options: ServerSnapshotPublisherOptions);
18
- get currentRevision(): number;
19
- get(models?: ModelMetadata[]): Promise<ServerSnapshot>;
20
- broadcast(): Promise<void>;
21
- private performBroadcast;
22
- }
23
- export {};
24
- //# sourceMappingURL=snapshots.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"snapshots.d.ts","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,aAAa,EAElB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,UAAU,8BAA8B;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,eAAe,CAAC;IACzB,WAAW,EAAE,GAAG,CAAC,eAAe,CAAC,CAAC;IAClC,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,YAAY,EAAE,MAAM,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IAC/C,WAAW,EAAE,CAAC,UAAU,EAAE,eAAe,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvF,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACtC;AAED,qBAAa,uBAAuB;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,cAAc,CAAoC;IAE1D,YAAY,OAAO,EAAE,8BAA8B,EAElD;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,GAAG,CAAC,MAAM,CAAC,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAQ3D;IAED,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAIzB;YAEa,gBAAgB;CAY9B","sourcesContent":["import {\n\ttype EventEnvelope,\n\ttype ModelMetadata,\n\tPROTOCOL_VERSION,\n\ttype ServerSnapshot,\n\ttype SessionMetadata,\n} from \"@earendil-works/pi-protocol\";\nimport type { ConnectionState } from \"./connection.ts\";\nimport type { PiServerService } from \"./types.ts\";\n\ninterface ServerSnapshotPublisherOptions {\n\tserverId: string;\n\tservice: PiServerService;\n\tconnections: Set<ConnectionState>;\n\tisClosing: () => boolean;\n\tlistSessions: () => Promise<SessionMetadata[]>;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\treportError: (error: unknown) => void;\n}\n\nexport class ServerSnapshotPublisher {\n\tprivate readonly options: ServerSnapshotPublisherOptions;\n\tprivate revision = 0;\n\tprivate broadcastQueue: Promise<void> = Promise.resolve();\n\n\tconstructor(options: ServerSnapshotPublisherOptions) {\n\t\tthis.options = options;\n\t}\n\n\tget currentRevision(): number {\n\t\treturn this.revision;\n\t}\n\n\tasync get(models?: ModelMetadata[]): Promise<ServerSnapshot> {\n\t\treturn {\n\t\t\tserverId: this.options.serverId,\n\t\t\tprotocolVersion: PROTOCOL_VERSION,\n\t\t\trevision: this.revision,\n\t\t\tsessions: await this.options.listSessions(),\n\t\t\tmodels: models ?? (await this.options.service.listModels()),\n\t\t};\n\t}\n\n\tbroadcast(): Promise<void> {\n\t\tconst broadcast = this.broadcastQueue.then(() => this.performBroadcast());\n\t\tthis.broadcastQueue = broadcast.catch((error: unknown) => this.options.reportError(error));\n\t\treturn broadcast;\n\t}\n\n\tprivate async performBroadcast(): Promise<void> {\n\t\tconst readyConnections = [...this.options.connections].filter(\n\t\t\t(connection) => connection.stage === \"ready\" && !connection.disconnected,\n\t\t);\n\t\tif (readyConnections.length === 0 || this.options.isClosing()) return;\n\t\tconst revision = ++this.revision;\n\t\tconst models = await this.options.service.listModels();\n\t\tconst current = await this.get(models);\n\t\tconst snapshot: ServerSnapshot = { ...current, revision };\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"server_snapshot\", snapshot } };\n\t\tfor (const connection of readyConnections) await this.options.sendMessage(connection, envelope);\n\t}\n}\n"]}
package/dist/snapshots.js DELETED
@@ -1,39 +0,0 @@
1
- import { PROTOCOL_VERSION, } from "@earendil-works/pi-protocol";
2
- export class ServerSnapshotPublisher {
3
- options;
4
- revision = 0;
5
- broadcastQueue = Promise.resolve();
6
- constructor(options) {
7
- this.options = options;
8
- }
9
- get currentRevision() {
10
- return this.revision;
11
- }
12
- async get(models) {
13
- return {
14
- serverId: this.options.serverId,
15
- protocolVersion: PROTOCOL_VERSION,
16
- revision: this.revision,
17
- sessions: await this.options.listSessions(),
18
- models: models ?? (await this.options.service.listModels()),
19
- };
20
- }
21
- broadcast() {
22
- const broadcast = this.broadcastQueue.then(() => this.performBroadcast());
23
- this.broadcastQueue = broadcast.catch((error) => this.options.reportError(error));
24
- return broadcast;
25
- }
26
- async performBroadcast() {
27
- const readyConnections = [...this.options.connections].filter((connection) => connection.stage === "ready" && !connection.disconnected);
28
- if (readyConnections.length === 0 || this.options.isClosing())
29
- return;
30
- const revision = ++this.revision;
31
- const models = await this.options.service.listModels();
32
- const current = await this.get(models);
33
- const snapshot = { ...current, revision };
34
- const envelope = { type: "event", event: { type: "server_snapshot", snapshot } };
35
- for (const connection of readyConnections)
36
- await this.options.sendMessage(connection, envelope);
37
- }
38
- }
39
- //# sourceMappingURL=snapshots.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"snapshots.js","sourceRoot":"","sources":["../src/snapshots.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,gBAAgB,GAGhB,MAAM,6BAA6B,CAAC;AAcrC,MAAM,OAAO,uBAAuB;IAClB,OAAO,CAAiC;IACjD,QAAQ,GAAG,CAAC,CAAC;IACb,cAAc,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAE1D,YAAY,OAAuC,EAAE;QACpD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;IAED,IAAI,eAAe,GAAW;QAC7B,OAAO,IAAI,CAAC,QAAQ,CAAC;IAAA,CACrB;IAED,KAAK,CAAC,GAAG,CAAC,MAAwB,EAA2B;QAC5D,OAAO;YACN,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,QAAQ;YAC/B,eAAe,EAAE,gBAAgB;YACjC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,QAAQ,EAAE,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;YAC3C,MAAM,EAAE,MAAM,IAAI,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;SAC3D,CAAC;IAAA,CACF;IAED,SAAS,GAAkB;QAC1B,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;QAC1E,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3F,OAAO,SAAS,CAAC;IAAA,CACjB;IAEO,KAAK,CAAC,gBAAgB,GAAkB;QAC/C,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,MAAM,CAC5D,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,KAAK,OAAO,IAAI,CAAC,UAAU,CAAC,YAAY,CACxE,CAAC;QACF,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE;YAAE,OAAO;QACtE,MAAM,QAAQ,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC;QACjC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;QACvD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,MAAM,QAAQ,GAAmB,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,CAAC;QAC1D,MAAM,QAAQ,GAAkB,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,QAAQ,EAAE,EAAE,CAAC;QAChG,KAAK,MAAM,UAAU,IAAI,gBAAgB;YAAE,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAAA,CAChG;CACD","sourcesContent":["import {\n\ttype EventEnvelope,\n\ttype ModelMetadata,\n\tPROTOCOL_VERSION,\n\ttype ServerSnapshot,\n\ttype SessionMetadata,\n} from \"@earendil-works/pi-protocol\";\nimport type { ConnectionState } from \"./connection.ts\";\nimport type { PiServerService } from \"./types.ts\";\n\ninterface ServerSnapshotPublisherOptions {\n\tserverId: string;\n\tservice: PiServerService;\n\tconnections: Set<ConnectionState>;\n\tisClosing: () => boolean;\n\tlistSessions: () => Promise<SessionMetadata[]>;\n\tsendMessage: (connection: ConnectionState, message: EventEnvelope) => Promise<boolean>;\n\treportError: (error: unknown) => void;\n}\n\nexport class ServerSnapshotPublisher {\n\tprivate readonly options: ServerSnapshotPublisherOptions;\n\tprivate revision = 0;\n\tprivate broadcastQueue: Promise<void> = Promise.resolve();\n\n\tconstructor(options: ServerSnapshotPublisherOptions) {\n\t\tthis.options = options;\n\t}\n\n\tget currentRevision(): number {\n\t\treturn this.revision;\n\t}\n\n\tasync get(models?: ModelMetadata[]): Promise<ServerSnapshot> {\n\t\treturn {\n\t\t\tserverId: this.options.serverId,\n\t\t\tprotocolVersion: PROTOCOL_VERSION,\n\t\t\trevision: this.revision,\n\t\t\tsessions: await this.options.listSessions(),\n\t\t\tmodels: models ?? (await this.options.service.listModels()),\n\t\t};\n\t}\n\n\tbroadcast(): Promise<void> {\n\t\tconst broadcast = this.broadcastQueue.then(() => this.performBroadcast());\n\t\tthis.broadcastQueue = broadcast.catch((error: unknown) => this.options.reportError(error));\n\t\treturn broadcast;\n\t}\n\n\tprivate async performBroadcast(): Promise<void> {\n\t\tconst readyConnections = [...this.options.connections].filter(\n\t\t\t(connection) => connection.stage === \"ready\" && !connection.disconnected,\n\t\t);\n\t\tif (readyConnections.length === 0 || this.options.isClosing()) return;\n\t\tconst revision = ++this.revision;\n\t\tconst models = await this.options.service.listModels();\n\t\tconst current = await this.get(models);\n\t\tconst snapshot: ServerSnapshot = { ...current, revision };\n\t\tconst envelope: EventEnvelope = { type: \"event\", event: { type: \"server_snapshot\", snapshot } };\n\t\tfor (const connection of readyConnections) await this.options.sendMessage(connection, envelope);\n\t}\n}\n"]}
@@ -1,59 +0,0 @@
1
- import type { ModelMetadata, ModelRef, SessionMetadata, SessionPhase, SessionSnapshot, ThinkingLevel, TranscriptProgress } from "@earendil-works/pi-protocol";
2
- import { PiServerError } from "../errors.ts";
3
- import type { CreateSessionOptions, PiServerService, PiSessionRuntime, PiSessionRuntimeEvent, PromptInput } from "../types.ts";
4
- export declare const TEST_MODEL: ModelMetadata;
5
- export declare class Deferred<T> {
6
- readonly promise: Promise<T>;
7
- private resolvePromise;
8
- constructor();
9
- resolve(value: T): void;
10
- }
11
- interface StoredSession {
12
- snapshot: SessionSnapshot;
13
- }
14
- export declare class TestSessionRuntime implements PiSessionRuntime {
15
- readonly disposed: Deferred<void>;
16
- disposeCount: number;
17
- readonly steers: PromptInput[];
18
- private readonly stored;
19
- private readonly onDispose;
20
- private readonly listeners;
21
- private pendingPrompt?;
22
- constructor(stored: StoredSession, onDispose: () => void);
23
- snapshot(): SessionSnapshot;
24
- getPhase(): SessionPhase;
25
- prompt(input: PromptInput): Promise<void>;
26
- steer(input: PromptInput): Promise<void>;
27
- abort(): Promise<void>;
28
- setModel(model: ModelRef): Promise<void>;
29
- setThinking(thinkingLevel: ThinkingLevel): Promise<void>;
30
- subscribe(listener: (event: PiSessionRuntimeEvent) => void): () => void;
31
- dispose(): Promise<void>;
32
- setPhase(phase: SessionPhase): void;
33
- finishPrompt(): void;
34
- emitProgress(progress: TranscriptProgress): void;
35
- emitError(error: PiServerError): void;
36
- emitSnapshot(): void;
37
- private update;
38
- }
39
- interface ListDelay {
40
- entered: Deferred<void>;
41
- release: Deferred<void>;
42
- }
43
- export declare class TestServerService implements PiServerService {
44
- readonly sessions: Map<string, StoredSession>;
45
- readonly runtimes: Map<string, TestSessionRuntime[]>;
46
- readonly locked: Set<string>;
47
- lastCreatedId?: string;
48
- private nextListDelay?;
49
- listSessions(): Promise<SessionMetadata[]>;
50
- listModels(): Promise<ModelMetadata[]>;
51
- createSession(options: CreateSessionOptions): Promise<PiSessionRuntime>;
52
- openSession(sessionId: string): Promise<PiSessionRuntime>;
53
- seed(id?: string, name?: string, cwd?: string, model?: ModelRef, thinkingLevel?: ThinkingLevel): void;
54
- delayNextList(): ListDelay;
55
- latestRuntime(id: string): TestSessionRuntime;
56
- private acquire;
57
- }
58
- export {};
59
- //# sourceMappingURL=service.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/testing/service.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tModelMetadata,\n\tModelRef,\n\tSessionMetadata,\n\tSessionPhase,\n\tSessionSnapshot,\n\tThinkingLevel,\n\tTranscriptProgress,\n} from \"@earendil-works/pi-protocol\";\nimport { PiServerError } from \"../errors.ts\";\nimport type {\n\tCreateSessionOptions,\n\tPiServerService,\n\tPiSessionRuntime,\n\tPiSessionRuntimeEvent,\n\tPromptInput,\n} from \"../types.ts\";\n\nexport const TEST_MODEL: ModelMetadata = {\n\tprovider: \"test\",\n\tid: \"small\",\n\tname: \"Test Small\",\n\tapi: \"test-api\",\n\treasoning: true,\n\tinput: [\"text\", \"image\"],\n\tcontextWindow: 16_000,\n\tmaxTokens: 2_000,\n\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },\n\tsupportedThinkingLevels: [\"off\", \"medium\", \"high\"],\n\tauthenticated: true,\n};\n\nexport class Deferred<T> {\n\treadonly promise: Promise<T>;\n\tprivate resolvePromise!: (value: T) => void;\n\n\tconstructor() {\n\t\tthis.promise = new Promise<T>((resolve) => {\n\t\t\tthis.resolvePromise = resolve;\n\t\t});\n\t}\n\n\tresolve(value: T): void {\n\t\tthis.resolvePromise(value);\n\t}\n}\n\ninterface StoredSession {\n\tsnapshot: SessionSnapshot;\n}\n\nexport class TestSessionRuntime implements PiSessionRuntime {\n\treadonly disposed = new Deferred<void>();\n\tdisposeCount = 0;\n\treadonly steers: PromptInput[] = [];\n\tprivate readonly stored: StoredSession;\n\tprivate readonly onDispose: () => void;\n\tprivate readonly listeners = new Set<(event: PiSessionRuntimeEvent) => void>();\n\tprivate pendingPrompt?: { input: PromptInput; done: Deferred<\"complete\" | \"aborted\"> };\n\n\tconstructor(stored: StoredSession, onDispose: () => void) {\n\t\tthis.stored = stored;\n\t\tthis.onDispose = onDispose;\n\t}\n\n\tsnapshot(): SessionSnapshot {\n\t\treturn structuredClone(this.stored.snapshot);\n\t}\n\n\tgetPhase(): SessionPhase {\n\t\treturn this.stored.snapshot.phase;\n\t}\n\n\tasync prompt(input: PromptInput): Promise<void> {\n\t\tif (this.getPhase() !== \"idle\") throw new PiServerError(\"busy\", \"A prompt is already running\");\n\t\tconst done = new Deferred<\"complete\" | \"aborted\">();\n\t\tthis.pendingPrompt = { input, done };\n\t\tthis.update({\n\t\t\tphase: \"turn\",\n\t\t\ttranscript: [\n\t\t\t\t...this.stored.snapshot.transcript,\n\t\t\t\t{\n\t\t\t\t\tid: `user-${this.stored.snapshot.revision + 1}`,\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text: input.text }],\n\t\t\t\t\ttimestamp: this.stored.snapshot.revision + 1,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t\tconst outcome = await done.promise;\n\t\tconst assistant =\n\t\t\toutcome === \"complete\"\n\t\t\t\t? {\n\t\t\t\t\t\tid: `assistant-${this.stored.snapshot.revision + 1}`,\n\t\t\t\t\t\trole: \"assistant\" as const,\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: `reply:${input.text}` }],\n\t\t\t\t\t\tstatus: \"complete\" as const,\n\t\t\t\t\t\tmodel: this.stored.snapshot.model,\n\t\t\t\t\t\tstopReason: \"stop\" as const,\n\t\t\t\t\t\ttimestamp: this.stored.snapshot.revision + 1,\n\t\t\t\t\t}\n\t\t\t\t: {\n\t\t\t\t\t\tid: `assistant-${this.stored.snapshot.revision + 1}`,\n\t\t\t\t\t\trole: \"assistant\" as const,\n\t\t\t\t\t\tcontent: [{ type: \"text\" as const, text: \"\" }],\n\t\t\t\t\t\tstatus: \"aborted\" as const,\n\t\t\t\t\t\tmodel: this.stored.snapshot.model,\n\t\t\t\t\t\tstopReason: \"aborted\" as const,\n\t\t\t\t\t\ttimestamp: this.stored.snapshot.revision + 1,\n\t\t\t\t\t};\n\t\tthis.update({\n\t\t\tphase: \"idle\",\n\t\t\ttranscript: [...this.stored.snapshot.transcript, assistant],\n\t\t});\n\t\tthis.pendingPrompt = undefined;\n\t}\n\n\tasync steer(input: PromptInput): Promise<void> {\n\t\tif (this.getPhase() === \"idle\") throw new PiServerError(\"busy\", \"There is no active prompt to steer\");\n\t\tthis.steers.push(input);\n\t\tthis.update({\n\t\t\tqueuedSteerCount: this.stored.snapshot.queuedSteerCount + 1,\n\t\t\tqueuedSteer: [\n\t\t\t\t...this.stored.snapshot.queuedSteer,\n\t\t\t\t{\n\t\t\t\t\tid: `steer-${this.stored.snapshot.revision + 1}`,\n\t\t\t\t\trole: \"user\",\n\t\t\t\t\tcontent: [{ type: \"text\", text: input.text }],\n\t\t\t\t\ttimestamp: this.stored.snapshot.revision + 1,\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t}\n\n\tasync abort(): Promise<void> {\n\t\tif (!this.pendingPrompt) throw new PiServerError(\"busy\", \"There is no active prompt to abort\");\n\t\tthis.pendingPrompt.done.resolve(\"aborted\");\n\t}\n\n\tasync setModel(model: ModelRef): Promise<void> {\n\t\tif (this.getPhase() !== \"idle\") throw new PiServerError(\"busy\", \"Session is busy\");\n\t\tthis.update({ model });\n\t}\n\n\tasync setThinking(thinkingLevel: ThinkingLevel): Promise<void> {\n\t\tif (this.getPhase() !== \"idle\") throw new PiServerError(\"busy\", \"Session is busy\");\n\t\tthis.update({ thinkingLevel });\n\t}\n\n\tsubscribe(listener: (event: PiSessionRuntimeEvent) => void): () => void {\n\t\tthis.listeners.add(listener);\n\t\treturn () => this.listeners.delete(listener);\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tthis.disposeCount += 1;\n\t\tthis.onDispose();\n\t\tthis.disposed.resolve(undefined);\n\t}\n\n\tsetPhase(phase: SessionPhase): void {\n\t\tthis.stored.snapshot = { ...this.stored.snapshot, phase };\n\t}\n\n\tfinishPrompt(): void {\n\t\tif (!this.pendingPrompt) throw new Error(\"No prompt is pending\");\n\t\tthis.pendingPrompt.done.resolve(\"complete\");\n\t}\n\n\temitProgress(progress: TranscriptProgress): void {\n\t\tfor (const listener of this.listeners) listener({ type: \"progress\", progress });\n\t}\n\n\temitError(error: PiServerError): void {\n\t\tfor (const listener of this.listeners) listener({ type: \"error\", error });\n\t}\n\n\temitSnapshot(): void {\n\t\tfor (const listener of this.listeners) listener({ type: \"snapshot\" });\n\t}\n\n\tprivate update(updates: Partial<SessionSnapshot>): void {\n\t\tthis.stored.snapshot = {\n\t\t\t...this.stored.snapshot,\n\t\t\t...updates,\n\t\t\trevision: this.stored.snapshot.revision + 1,\n\t\t\tupdatedAt: this.stored.snapshot.updatedAt + 1,\n\t\t};\n\t\tthis.emitSnapshot();\n\t}\n}\n\ninterface ListDelay {\n\tentered: Deferred<void>;\n\trelease: Deferred<void>;\n}\n\nexport class TestServerService implements PiServerService {\n\treadonly sessions = new Map<string, StoredSession>();\n\treadonly runtimes = new Map<string, TestSessionRuntime[]>();\n\treadonly locked = new Set<string>();\n\tlastCreatedId?: string;\n\tprivate nextListDelay?: ListDelay;\n\n\tasync listSessions(): Promise<SessionMetadata[]> {\n\t\tconst delay = this.nextListDelay;\n\t\tif (delay) {\n\t\t\tthis.nextListDelay = undefined;\n\t\t\tdelay.entered.resolve(undefined);\n\t\t\tawait delay.release.promise;\n\t\t}\n\t\treturn [...this.sessions.values()].map(({ snapshot }) => ({\n\t\t\tid: snapshot.id,\n\t\t\tcreatedAt: snapshot.createdAt,\n\t\t\tupdatedAt: snapshot.updatedAt,\n\t\t\tsessionName: snapshot.name,\n\t\t\tcwd: snapshot.cwd,\n\t\t}));\n\t}\n\n\tasync listModels(): Promise<ModelMetadata[]> {\n\t\treturn [TEST_MODEL];\n\t}\n\n\tasync createSession(options: CreateSessionOptions): Promise<PiSessionRuntime> {\n\t\tthis.lastCreatedId = options.id;\n\t\tif (this.sessions.has(options.id)) throw new PiServerError(\"session_locked\", \"Session already exists\");\n\t\tthis.seed(options.id, options.name, options.cwd, options.model, options.thinkingLevel);\n\t\treturn this.acquire(options.id);\n\t}\n\n\tasync openSession(sessionId: string): Promise<PiSessionRuntime> {\n\t\tif (!this.sessions.has(sessionId)) throw new PiServerError(\"not_found\", `Unknown session: ${sessionId}`);\n\t\tif (this.locked.has(sessionId)) throw new PiServerError(\"session_locked\", `Session is locked: ${sessionId}`);\n\t\treturn this.acquire(sessionId);\n\t}\n\n\tseed(\n\t\tid = \"session-1\",\n\t\tname = `Session ${id}`,\n\t\tcwd = \"/tmp/pi-server-conformance\",\n\t\tmodel: ModelRef = { provider: TEST_MODEL.provider, id: TEST_MODEL.id },\n\t\tthinkingLevel: ThinkingLevel = \"off\",\n\t): void {\n\t\tthis.sessions.set(id, {\n\t\t\tsnapshot: {\n\t\t\t\tid,\n\t\t\t\tname,\n\t\t\t\tcwd,\n\t\t\t\tcreatedAt: 1,\n\t\t\t\tupdatedAt: 1,\n\t\t\t\tphase: \"idle\",\n\t\t\t\tmodel,\n\t\t\t\tthinkingLevel,\n\t\t\t\tattached: false,\n\t\t\t\tlocked: false,\n\t\t\t\trevision: 0,\n\t\t\t\ttranscript: [],\n\t\t\t\tqueuedSteer: [],\n\t\t\t\tqueuedSteerCount: 0,\n\t\t\t},\n\t\t});\n\t}\n\n\tdelayNextList(): ListDelay {\n\t\tconst delay = { entered: new Deferred<void>(), release: new Deferred<void>() };\n\t\tthis.nextListDelay = delay;\n\t\treturn delay;\n\t}\n\n\tlatestRuntime(id: string): TestSessionRuntime {\n\t\tconst runtimes = this.runtimes.get(id);\n\t\tif (!runtimes?.length) throw new Error(`No runtime for ${id}`);\n\t\treturn runtimes.at(-1)!;\n\t}\n\n\tprivate acquire(id: string): TestSessionRuntime {\n\t\tconst stored = this.sessions.get(id);\n\t\tif (!stored) throw new Error(`Unknown session: ${id}`);\n\t\tthis.locked.add(id);\n\t\tconst runtime = new TestSessionRuntime(stored, () => this.locked.delete(id));\n\t\tconst runtimes = this.runtimes.get(id) ?? [];\n\t\truntimes.push(runtime);\n\t\tthis.runtimes.set(id, runtimes);\n\t\treturn runtime;\n\t}\n}\n"]}