@earendil-works/pi-server 0.84.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/CHANGELOG.md +45 -0
- package/README.md +50 -0
- package/dist/connection.d.ts +28 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +4 -0
- package/dist/connection.js.map +1 -0
- package/dist/errors.d.ts +27 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +45 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/listener.d.ts +10 -0
- package/dist/listener.d.ts.map +1 -0
- package/dist/listener.js +2 -0
- package/dist/listener.js.map +1 -0
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +266 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +36 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +365 -0
- package/dist/server.js.map +1 -0
- package/dist/sessions.d.ts +2174 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +305 -0
- package/dist/sessions.js.map +1 -0
- package/dist/snapshots.d.ts +24 -0
- package/dist/snapshots.d.ts.map +1 -0
- package/dist/snapshots.js +39 -0
- package/dist/snapshots.js.map +1 -0
- package/dist/testing/client.d.ts +31 -0
- package/dist/testing/client.d.ts.map +1 -0
- package/dist/testing/client.js +118 -0
- package/dist/testing/client.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/server.d.ts +12 -0
- package/dist/testing/server.d.ts.map +1 -0
- package/dist/testing/server.js +17 -0
- package/dist/testing/server.js.map +1 -0
- package/dist/testing/service.d.ts +59 -0
- package/dist/testing/service.d.ts.map +1 -0
- package/dist/testing/service.js +240 -0
- package/dist/testing/service.js.map +1 -0
- package/dist/transports/unix/index.d.ts +4 -0
- package/dist/transports/unix/index.d.ts.map +1 -0
- package/dist/transports/unix/index.js +3 -0
- package/dist/transports/unix/index.js.map +1 -0
- package/dist/transports/unix/listener.d.ts +25 -0
- package/dist/transports/unix/listener.d.ts.map +1 -0
- package/dist/transports/unix/listener.js +432 -0
- package/dist/transports/unix/listener.js.map +1 -0
- package/dist/transports/unix/preset.d.ts +6 -0
- package/dist/transports/unix/preset.d.ts.map +1 -0
- package/dist/transports/unix/preset.js +21 -0
- package/dist/transports/unix/preset.js.map +1 -0
- package/dist/transports/unix/types.d.ts +15 -0
- package/dist/transports/unix/types.d.ts.map +1 -0
- package/dist/transports/unix/types.js +2 -0
- package/dist/transports/unix/types.js.map +1 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.84.0] - 2026-08-06
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- Changed `toProtocolToolResultMessage()` to require the original `ToolCall` and verify tool result association.
|
|
10
|
+
- Changed `PiServerService.listSessions()` to return durable `SessionMetadata` instead of runtime `SessionSummary` values ([#7708](https://github.com/earendil-works/pi/pull/7708)).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Hardened protocol adapters against contradictory lifecycle states, invalid identifiers and timestamps, sparse execution arrays, and additive `pi-ai` contract drift.
|
|
15
|
+
- Sanitized service and runtime failures into stable `not_implemented` and `internal_error` responses without exposing private error details ([#7644](https://github.com/earendil-works/pi/pull/7644)).
|
|
16
|
+
|
|
17
|
+
## [0.83.0] - 2026-07-29
|
|
18
|
+
|
|
19
|
+
## [0.82.1] - 2026-07-25
|
|
20
|
+
|
|
21
|
+
## [0.82.0] - 2026-07-24
|
|
22
|
+
|
|
23
|
+
## [0.81.1] - 2026-07-21
|
|
24
|
+
|
|
25
|
+
## [0.81.0] - 2026-07-21
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Renamed the orchestrator workspace package and internal server references to server ([#6898](https://github.com/earendil-works/pi/pull/6898) by [@cristinaponcela](https://github.com/cristinaponcela)).
|
|
30
|
+
|
|
31
|
+
## [0.80.10] - 2026-07-16
|
|
32
|
+
|
|
33
|
+
## [0.80.9] - 2026-07-16
|
|
34
|
+
|
|
35
|
+
## [0.80.8] - 2026-07-16
|
|
36
|
+
|
|
37
|
+
## [0.80.7] - 2026-07-14
|
|
38
|
+
|
|
39
|
+
## [0.80.6] - 2026-07-09
|
|
40
|
+
|
|
41
|
+
## [0.80.5] - 2026-07-09
|
|
42
|
+
|
|
43
|
+
## [0.80.4] - 2026-07-09
|
|
44
|
+
|
|
45
|
+
## [0.80.3] - 2026-06-30
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @earendil-works/pi-server
|
|
2
|
+
|
|
3
|
+
Experimental. This package is under active development and may change or be removed without notice. Its APIs and behavior are not yet stable.
|
|
4
|
+
|
|
5
|
+
Server package for pi.
|
|
6
|
+
|
|
7
|
+
## Session server core
|
|
8
|
+
|
|
9
|
+
The package exports the `PiServer` session server.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import type { PiServerService } from "@earendil-works/pi-server";
|
|
13
|
+
import { createUnixServer } from "@earendil-works/pi-server/unix";
|
|
14
|
+
|
|
15
|
+
const service: PiServerService = {
|
|
16
|
+
async listSessions() {
|
|
17
|
+
return storage.listSessions();
|
|
18
|
+
},
|
|
19
|
+
async listModels() {
|
|
20
|
+
return modelRegistry.listModels();
|
|
21
|
+
},
|
|
22
|
+
async createSession(options) {
|
|
23
|
+
return storage.createAndOpen(options);
|
|
24
|
+
},
|
|
25
|
+
async openSession(sessionId) {
|
|
26
|
+
return storage.open(sessionId);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const server = createUnixServer(service, {
|
|
31
|
+
path: "/tmp/pi/server.sock",
|
|
32
|
+
});
|
|
33
|
+
await server.start();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`PiServer` composes transport listeners through the `PiServerListener` interface. Each listener must complete any transport-specific authentication and authorization before passing a connection to `PiServer`. For example, a WebSocket listener can validate credentials during the HTTP upgrade, while the Unix listener relies on socket filesystem permissions. The Unix submodule exports the `createUnixListener()` building block and `createUnixServer()` preset, keeping the common case concise without coupling the primary server to Unix sockets. The listener uses length-prefixed CBOR messages from `@earendil-works/pi-protocol`.
|
|
37
|
+
|
|
38
|
+
This package does not provide a standalone CLI or coding-agent service. Applications supply the `PiServerService` implementation.
|
|
39
|
+
|
|
40
|
+
`PiServerService.listSessions()` returns protocol `SessionMetadata`, not acquired runtime state. Services should map the durable fields their storage supports and may omit `updatedAt`, `parentSessionId`, `sessionName`, and `cwd`. `PiServer` refreshes available metadata from live snapshots without requiring stored sessions to fabricate phase, model, thinking-level, attachment, or lock values.
|
|
41
|
+
|
|
42
|
+
## Transport testing
|
|
43
|
+
|
|
44
|
+
Custom transports can use `@earendil-works/pi-server/testing` for deterministic protocol conformance tests. It exports `createTestServer()`, `TestServerService`, `ProtocolTestClient`, and the transport-neutral `WireChannel` contract. `connectUnixTestClient()` is provided for Unix transport tests.
|
|
45
|
+
|
|
46
|
+
## `pi-ai` protocol bridge
|
|
47
|
+
|
|
48
|
+
`@earendil-works/pi-ai` domain objects and `@earendil-works/pi-protocol` wire DTOs remain independent. This package owns their boundary and exports `toProtocolModelMetadata()`, `toProtocolAssistantMessage()`, `toProtocolUserMessage()`, and `toProtocolToolResultMessage()`.
|
|
49
|
+
|
|
50
|
+
The adapters reject invalid tool inputs, identifiers, timestamps, and mismatched tool results; `toProtocolToolResultMessage()` requires the original `ToolCall` so it can verify the association and convert its arguments itself. Diagnostic details are explicitly sanitized. Closed `pi-ai` unions are mapped exhaustively, and compile-time field manifests enumerate current `pi-ai` properties so additions require an explicit review. The protocol mirrors `pi-ai` vocabulary such as `toolCall` and `toolUse` where the semantics are identical. Protocol schemas enforce consistent lifecycle states, and tests encode adapter output through the runtime schemas so incompatible changes fail in the bridging package.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ClientMessageDecoder } from "@earendil-works/pi-protocol";
|
|
2
|
+
import type { MaybePromise } from "./types.ts";
|
|
3
|
+
/** An established, authorized ordered byte connection. */
|
|
4
|
+
export interface ByteConnection {
|
|
5
|
+
readonly closed: boolean;
|
|
6
|
+
send(chunk: Uint8Array): Promise<void>;
|
|
7
|
+
close(finalChunk?: Uint8Array): MaybePromise<void>;
|
|
8
|
+
}
|
|
9
|
+
export interface ByteConnectionHandler {
|
|
10
|
+
onData(chunk: Uint8Array): void;
|
|
11
|
+
onClose(): void;
|
|
12
|
+
onError(error: Error): void;
|
|
13
|
+
}
|
|
14
|
+
export type ByteConnectionAcceptor = (connection: ByteConnection) => ByteConnectionHandler;
|
|
15
|
+
export type ConnectionStage = "awaitingHello" | "handshaking" | "ready" | "closing" | "closed";
|
|
16
|
+
export interface ConnectionState {
|
|
17
|
+
id: string;
|
|
18
|
+
connection: ByteConnection;
|
|
19
|
+
decoder: ClientMessageDecoder;
|
|
20
|
+
sessionIds: Set<string>;
|
|
21
|
+
stage: ConnectionStage;
|
|
22
|
+
disconnected: boolean;
|
|
23
|
+
handshakeComplete: boolean;
|
|
24
|
+
handshake?: Promise<void>;
|
|
25
|
+
handshakeTimeout: NodeJS.Timeout;
|
|
26
|
+
}
|
|
27
|
+
export declare function isTerminalConnection(state: ConnectionState): boolean;
|
|
28
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,0DAA0D;AAC1D,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,KAAK,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,qBAAqB;IACrC,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAChC,OAAO,IAAI,IAAI,CAAC;IAChB,OAAO,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,MAAM,sBAAsB,GAAG,CAAC,UAAU,EAAE,cAAc,KAAK,qBAAqB,CAAC;AAE3F,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE/F,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACxB,KAAK,EAAE,eAAe,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC,OAAO,CAAC;CACjC;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAEpE","sourcesContent":["import type { ClientMessageDecoder } from \"@earendil-works/pi-protocol\";\n\nimport type { MaybePromise } from \"./types.ts\";\n\n/** An established, authorized ordered byte connection. */\nexport interface ByteConnection {\n\treadonly closed: boolean;\n\tsend(chunk: Uint8Array): Promise<void>;\n\tclose(finalChunk?: Uint8Array): MaybePromise<void>;\n}\n\nexport interface ByteConnectionHandler {\n\tonData(chunk: Uint8Array): void;\n\tonClose(): void;\n\tonError(error: Error): void;\n}\n\nexport type ByteConnectionAcceptor = (connection: ByteConnection) => ByteConnectionHandler;\n\nexport type ConnectionStage = \"awaitingHello\" | \"handshaking\" | \"ready\" | \"closing\" | \"closed\";\n\nexport interface ConnectionState {\n\tid: string;\n\tconnection: ByteConnection;\n\tdecoder: ClientMessageDecoder;\n\tsessionIds: Set<string>;\n\tstage: ConnectionStage;\n\tdisconnected: boolean;\n\thandshakeComplete: boolean;\n\thandshake?: Promise<void>;\n\thandshakeTimeout: NodeJS.Timeout;\n}\n\nexport function isTerminalConnection(state: ConnectionState): boolean {\n\treturn state.disconnected || state.stage === \"closing\" || state.stage === \"closed\";\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAiCA,MAAM,UAAU,oBAAoB,CAAC,KAAsB,EAAW;IACrE,OAAO,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,QAAQ,CAAC;AAAA,CACnF","sourcesContent":["import type { ClientMessageDecoder } from \"@earendil-works/pi-protocol\";\n\nimport type { MaybePromise } from \"./types.ts\";\n\n/** An established, authorized ordered byte connection. */\nexport interface ByteConnection {\n\treadonly closed: boolean;\n\tsend(chunk: Uint8Array): Promise<void>;\n\tclose(finalChunk?: Uint8Array): MaybePromise<void>;\n}\n\nexport interface ByteConnectionHandler {\n\tonData(chunk: Uint8Array): void;\n\tonClose(): void;\n\tonError(error: Error): void;\n}\n\nexport type ByteConnectionAcceptor = (connection: ByteConnection) => ByteConnectionHandler;\n\nexport type ConnectionStage = \"awaitingHello\" | \"handshaking\" | \"ready\" | \"closing\" | \"closed\";\n\nexport interface ConnectionState {\n\tid: string;\n\tconnection: ByteConnection;\n\tdecoder: ClientMessageDecoder;\n\tsessionIds: Set<string>;\n\tstage: ConnectionStage;\n\tdisconnected: boolean;\n\thandshakeComplete: boolean;\n\thandshake?: Promise<void>;\n\thandshakeTimeout: NodeJS.Timeout;\n}\n\nexport function isTerminalConnection(state: ConnectionState): boolean {\n\treturn state.disconnected || state.stage === \"closing\" || state.stage === \"closed\";\n}\n"]}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { JsonValue, ProtocolErrorCode } from "@earendil-works/pi-protocol";
|
|
2
|
+
export type PiServerOperationErrorCode = Extract<ProtocolErrorCode, "busy" | "session_locked" | "not_found" | "invalid_request" | "not_implemented">;
|
|
3
|
+
export declare const INTERNAL_SERVER_ERROR_MESSAGE = "Internal server error";
|
|
4
|
+
export declare const NOT_IMPLEMENTED_MESSAGE = "Operation is not implemented";
|
|
5
|
+
/** A service/runtime error that can safely cross the protocol boundary. */
|
|
6
|
+
export declare class PiServerError extends Error {
|
|
7
|
+
readonly code: PiServerOperationErrorCode;
|
|
8
|
+
readonly details: JsonValue | undefined;
|
|
9
|
+
constructor(code: PiServerOperationErrorCode, message: string, details?: JsonValue);
|
|
10
|
+
}
|
|
11
|
+
export declare class SessionBusyError extends PiServerError {
|
|
12
|
+
constructor(message?: string, details?: JsonValue);
|
|
13
|
+
}
|
|
14
|
+
export declare class SessionLockedError extends PiServerError {
|
|
15
|
+
constructor(message?: string, details?: JsonValue);
|
|
16
|
+
}
|
|
17
|
+
export declare class SessionNotFoundError extends PiServerError {
|
|
18
|
+
constructor(message?: string, details?: JsonValue);
|
|
19
|
+
}
|
|
20
|
+
export declare class NotImplementedError extends PiServerError {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
23
|
+
/** An unsafe failure whose cause is retained for reporting but never serialized. */
|
|
24
|
+
export declare class InternalServerError extends Error {
|
|
25
|
+
constructor(cause: unknown);
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhF,MAAM,MAAM,0BAA0B,GAAG,OAAO,CAC/C,iBAAiB,EACjB,MAAM,GAAG,gBAAgB,GAAG,WAAW,GAAG,iBAAiB,GAAG,iBAAiB,CAC/E,CAAC;AAEF,eAAO,MAAM,6BAA6B,0BAA0B,CAAC;AACrE,eAAO,MAAM,uBAAuB,iCAAiC,CAAC;AAEtE,2EAA2E;AAC3E,qBAAa,aAAc,SAAQ,KAAK;IACvC,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;IAC1C,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;IAExC,YAAY,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAKjF;CACD;AAED,qBAAa,gBAAiB,SAAQ,aAAa;IAClD,YAAY,OAAO,SAAoB,EAAE,OAAO,CAAC,EAAE,SAAS,EAG3D;CACD;AAED,qBAAa,kBAAmB,SAAQ,aAAa;IACpD,YAAY,OAAO,SAAsB,EAAE,OAAO,CAAC,EAAE,SAAS,EAG7D;CACD;AAED,qBAAa,oBAAqB,SAAQ,aAAa;IACtD,YAAY,OAAO,SAA0B,EAAE,OAAO,CAAC,EAAE,SAAS,EAGjE;CACD;AAED,qBAAa,mBAAoB,SAAQ,aAAa;IACrD,cAGC;CACD;AAED,oFAAoF;AACpF,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,YAAY,KAAK,EAAE,OAAO,EAGzB;CACD","sourcesContent":["import type { JsonValue, ProtocolErrorCode } from \"@earendil-works/pi-protocol\";\n\nexport type PiServerOperationErrorCode = Extract<\n\tProtocolErrorCode,\n\t\"busy\" | \"session_locked\" | \"not_found\" | \"invalid_request\" | \"not_implemented\"\n>;\n\nexport const INTERNAL_SERVER_ERROR_MESSAGE = \"Internal server error\";\nexport const NOT_IMPLEMENTED_MESSAGE = \"Operation is not implemented\";\n\n/** A service/runtime error that can safely cross the protocol boundary. */\nexport class PiServerError extends Error {\n\treadonly code: PiServerOperationErrorCode;\n\treadonly details: JsonValue | undefined;\n\n\tconstructor(code: PiServerOperationErrorCode, message: string, details?: JsonValue) {\n\t\tsuper(message);\n\t\tthis.name = \"PiServerError\";\n\t\tthis.code = code;\n\t\tthis.details = details;\n\t}\n}\n\nexport class SessionBusyError extends PiServerError {\n\tconstructor(message = \"Session is busy\", details?: JsonValue) {\n\t\tsuper(\"busy\", message, details);\n\t\tthis.name = \"SessionBusyError\";\n\t}\n}\n\nexport class SessionLockedError extends PiServerError {\n\tconstructor(message = \"Session is locked\", details?: JsonValue) {\n\t\tsuper(\"session_locked\", message, details);\n\t\tthis.name = \"SessionLockedError\";\n\t}\n}\n\nexport class SessionNotFoundError extends PiServerError {\n\tconstructor(message = \"Session was not found\", details?: JsonValue) {\n\t\tsuper(\"not_found\", message, details);\n\t\tthis.name = \"SessionNotFoundError\";\n\t}\n}\n\nexport class NotImplementedError extends PiServerError {\n\tconstructor() {\n\t\tsuper(\"not_implemented\", NOT_IMPLEMENTED_MESSAGE);\n\t\tthis.name = \"NotImplementedError\";\n\t}\n}\n\n/** An unsafe failure whose cause is retained for reporting but never serialized. */\nexport class InternalServerError extends Error {\n\tconstructor(cause: unknown) {\n\t\tsuper(INTERNAL_SERVER_ERROR_MESSAGE, { cause });\n\t\tthis.name = \"InternalServerError\";\n\t}\n}\n"]}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export const INTERNAL_SERVER_ERROR_MESSAGE = "Internal server error";
|
|
2
|
+
export const NOT_IMPLEMENTED_MESSAGE = "Operation is not implemented";
|
|
3
|
+
/** A service/runtime error that can safely cross the protocol boundary. */
|
|
4
|
+
export class PiServerError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
constructor(code, message, details) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "PiServerError";
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class SessionBusyError extends PiServerError {
|
|
15
|
+
constructor(message = "Session is busy", details) {
|
|
16
|
+
super("busy", message, details);
|
|
17
|
+
this.name = "SessionBusyError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class SessionLockedError extends PiServerError {
|
|
21
|
+
constructor(message = "Session is locked", details) {
|
|
22
|
+
super("session_locked", message, details);
|
|
23
|
+
this.name = "SessionLockedError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class SessionNotFoundError extends PiServerError {
|
|
27
|
+
constructor(message = "Session was not found", details) {
|
|
28
|
+
super("not_found", message, details);
|
|
29
|
+
this.name = "SessionNotFoundError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class NotImplementedError extends PiServerError {
|
|
33
|
+
constructor() {
|
|
34
|
+
super("not_implemented", NOT_IMPLEMENTED_MESSAGE);
|
|
35
|
+
this.name = "NotImplementedError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** An unsafe failure whose cause is retained for reporting but never serialized. */
|
|
39
|
+
export class InternalServerError extends Error {
|
|
40
|
+
constructor(cause) {
|
|
41
|
+
super(INTERNAL_SERVER_ERROR_MESSAGE, { cause });
|
|
42
|
+
this.name = "InternalServerError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,6BAA6B,GAAG,uBAAuB,CAAC;AACrE,MAAM,CAAC,MAAM,uBAAuB,GAAG,8BAA8B,CAAC;AAEtE,2EAA2E;AAC3E,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC9B,IAAI,CAA6B;IACjC,OAAO,CAAwB;IAExC,YAAY,IAAgC,EAAE,OAAe,EAAE,OAAmB,EAAE;QACnF,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAAA,CACvB;CACD;AAED,MAAM,OAAO,gBAAiB,SAAQ,aAAa;IAClD,YAAY,OAAO,GAAG,iBAAiB,EAAE,OAAmB,EAAE;QAC7D,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IAAA,CAC/B;CACD;AAED,MAAM,OAAO,kBAAmB,SAAQ,aAAa;IACpD,YAAY,OAAO,GAAG,mBAAmB,EAAE,OAAmB,EAAE;QAC/D,KAAK,CAAC,gBAAgB,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAAA,CACjC;CACD;AAED,MAAM,OAAO,oBAAqB,SAAQ,aAAa;IACtD,YAAY,OAAO,GAAG,uBAAuB,EAAE,OAAmB,EAAE;QACnE,KAAK,CAAC,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IAAA,CACnC;CACD;AAED,MAAM,OAAO,mBAAoB,SAAQ,aAAa;IACrD,cAAc;QACb,KAAK,CAAC,iBAAiB,EAAE,uBAAuB,CAAC,CAAC;QAClD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IAAA,CAClC;CACD;AAED,oFAAoF;AACpF,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC7C,YAAY,KAAc,EAAE;QAC3B,KAAK,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IAAA,CAClC;CACD","sourcesContent":["import type { JsonValue, ProtocolErrorCode } from \"@earendil-works/pi-protocol\";\n\nexport type PiServerOperationErrorCode = Extract<\n\tProtocolErrorCode,\n\t\"busy\" | \"session_locked\" | \"not_found\" | \"invalid_request\" | \"not_implemented\"\n>;\n\nexport const INTERNAL_SERVER_ERROR_MESSAGE = \"Internal server error\";\nexport const NOT_IMPLEMENTED_MESSAGE = \"Operation is not implemented\";\n\n/** A service/runtime error that can safely cross the protocol boundary. */\nexport class PiServerError extends Error {\n\treadonly code: PiServerOperationErrorCode;\n\treadonly details: JsonValue | undefined;\n\n\tconstructor(code: PiServerOperationErrorCode, message: string, details?: JsonValue) {\n\t\tsuper(message);\n\t\tthis.name = \"PiServerError\";\n\t\tthis.code = code;\n\t\tthis.details = details;\n\t}\n}\n\nexport class SessionBusyError extends PiServerError {\n\tconstructor(message = \"Session is busy\", details?: JsonValue) {\n\t\tsuper(\"busy\", message, details);\n\t\tthis.name = \"SessionBusyError\";\n\t}\n}\n\nexport class SessionLockedError extends PiServerError {\n\tconstructor(message = \"Session is locked\", details?: JsonValue) {\n\t\tsuper(\"session_locked\", message, details);\n\t\tthis.name = \"SessionLockedError\";\n\t}\n}\n\nexport class SessionNotFoundError extends PiServerError {\n\tconstructor(message = \"Session was not found\", details?: JsonValue) {\n\t\tsuper(\"not_found\", message, details);\n\t\tthis.name = \"SessionNotFoundError\";\n\t}\n}\n\nexport class NotImplementedError extends PiServerError {\n\tconstructor() {\n\t\tsuper(\"not_implemented\", NOT_IMPLEMENTED_MESSAGE);\n\t\tthis.name = \"NotImplementedError\";\n\t}\n}\n\n/** An unsafe failure whose cause is retained for reporting but never serialized. */\nexport class InternalServerError extends Error {\n\tconstructor(cause: unknown) {\n\t\tsuper(INTERNAL_SERVER_ERROR_MESSAGE, { cause });\n\t\tthis.name = \"InternalServerError\";\n\t}\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./errors.ts\";\nexport * from \"./listener.ts\";\nexport * from \"./protocol.ts\";\nexport * from \"./server.ts\";\nexport * from \"./types.ts\";\n"]}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC","sourcesContent":["export * from \"./errors.ts\";\nexport * from \"./listener.ts\";\nexport * from \"./protocol.ts\";\nexport * from \"./server.ts\";\nexport * from \"./types.ts\";\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ByteConnectionAcceptor } from "./connection.ts";
|
|
2
|
+
/** Supplies established byte connections after any required transport authentication. */
|
|
3
|
+
export interface PiServerListener {
|
|
4
|
+
/** Human-readable bound address after startup, when the transport has one. */
|
|
5
|
+
readonly address?: string;
|
|
6
|
+
/** Starts listening and passes authorized connections to accept. */
|
|
7
|
+
start(accept: ByteConnectionAcceptor): Promise<void>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=listener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.d.ts","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,yFAAyF;AACzF,MAAM,WAAW,gBAAgB;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,KAAK,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB","sourcesContent":["import type { ByteConnectionAcceptor } from \"./connection.ts\";\n\n/** Supplies established byte connections after any required transport authentication. */\nexport interface PiServerListener {\n\t/** Human-readable bound address after startup, when the transport has one. */\n\treadonly address?: string;\n\t/** Starts listening and passes authorized connections to accept. */\n\tstart(accept: ByteConnectionAcceptor): Promise<void>;\n\tclose(): Promise<void>;\n}\n"]}
|
package/dist/listener.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listener.js","sourceRoot":"","sources":["../src/listener.ts"],"names":[],"mappings":"","sourcesContent":["import type { ByteConnectionAcceptor } from \"./connection.ts\";\n\n/** Supplies established byte connections after any required transport authentication. */\nexport interface PiServerListener {\n\t/** Human-readable bound address after startup, when the transport has one. */\n\treadonly address?: string;\n\t/** Starts listening and passes authorized connections to accept. */\n\tstart(accept: ByteConnectionAcceptor): Promise<void>;\n\tclose(): Promise<void>;\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Usage as AiUsage, type Api, type AssistantMessage, type Model, type ToolCall, type ToolResultMessage, type UserMessage } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { AssistantTranscriptItem, JsonValue, ModelMetadata, ToolTranscriptItem, Usage, UserTranscriptItem } from "@earendil-works/pi-protocol";
|
|
3
|
+
export interface AssistantTranscriptOptions {
|
|
4
|
+
id: string;
|
|
5
|
+
}
|
|
6
|
+
export interface UserTranscriptOptions {
|
|
7
|
+
id: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ToolTranscriptOptions {
|
|
10
|
+
id: string;
|
|
11
|
+
call: ToolCall;
|
|
12
|
+
}
|
|
13
|
+
/** Validate and copy a value from an execution boundary into the protocol's JSON-compatible subset. */
|
|
14
|
+
export declare function toProtocolJsonValue(value: unknown, seen?: Set<object>): JsonValue;
|
|
15
|
+
/** Lossily sanitize diagnostic tool details that must not affect execution semantics. */
|
|
16
|
+
export declare function sanitizeProtocolDetails(value: unknown, seen?: Set<object>): JsonValue | undefined;
|
|
17
|
+
export declare function toProtocolUsage(usage: AiUsage | undefined): Usage | undefined;
|
|
18
|
+
export declare function toProtocolModelMetadata(model: Model<Api>, authenticated: boolean): ModelMetadata;
|
|
19
|
+
export declare function toProtocolUserMessage(message: UserMessage, options: UserTranscriptOptions): UserTranscriptItem;
|
|
20
|
+
export declare function toProtocolAssistantMessage(message: AssistantMessage, options: AssistantTranscriptOptions): AssistantTranscriptItem;
|
|
21
|
+
export declare function toProtocolToolResultMessage(message: ToolResultMessage, options: ToolTranscriptOptions): ToolTranscriptItem;
|
|
22
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAGN,KAAK,KAAK,IAAI,OAAO,EACrB,KAAK,GAAG,EACR,KAAK,gBAAgB,EAErB,KAAK,KAAK,EAEV,KAAK,QAAQ,EACb,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EACX,uBAAuB,EACvB,SAAS,EACT,aAAa,EAEb,kBAAkB,EAClB,KAAK,EACL,kBAAkB,EAClB,MAAM,6BAA6B,CAAC;AAqFrC,MAAM,WAAW,0BAA0B;IAC1C,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,CAAC;CACf;AAsBD,uGAAuG;AACvG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,cAAoB,GAAG,SAAS,CAqBvF;AAED,yFAAyF;AACzF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,cAAoB,GAAG,SAAS,GAAG,SAAS,CAoBvG;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,CAmB7E;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,OAAO,GAAG,aAAa,CAoBhG;AAkBD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,qBAAqB,GAAG,kBAAkB,CAQ9G;AA4BD,wBAAgB,0BAA0B,CACzC,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,0BAA0B,GACjC,uBAAuB,CAmDzB;AAiBD,wBAAgB,2BAA2B,CAC1C,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,qBAAqB,GAC5B,kBAAkB,CAyBpB","sourcesContent":["import {\n\ttype ImageContent as AiImageContent,\n\ttype TextContent as AiTextContent,\n\ttype Usage as AiUsage,\n\ttype Api,\n\ttype AssistantMessage,\n\tgetSupportedThinkingLevels,\n\ttype Model,\n\ttype ModelThinkingLevel,\n\ttype ToolCall,\n\ttype ToolResultMessage,\n\ttype UserMessage,\n} from \"@earendil-works/pi-ai\";\nimport type {\n\tAssistantTranscriptItem,\n\tJsonValue,\n\tModelMetadata,\n\tThinkingLevel,\n\tToolTranscriptItem,\n\tUsage,\n\tUserTranscriptItem,\n} from \"@earendil-works/pi-protocol\";\n\ntype Assert<T extends true> = T;\ntype ExactKeys<T, Keys extends keyof T> = keyof T extends Keys ? true : false;\ntype _AiThinkingLevelsFitProtocol = Assert<ModelThinkingLevel extends ThinkingLevel ? true : false>;\ntype _ProtocolThinkingLevelsFitAi = Assert<ThinkingLevel extends ModelThinkingLevel ? true : false>;\ntype AiModelInput = Model<Api>[\"input\"][number];\ntype ProtocolModelInput = ModelMetadata[\"input\"][number];\ntype _AiModelInputsFitProtocol = Assert<AiModelInput extends ProtocolModelInput ? true : false>;\ntype _ProtocolModelInputsFitAi = Assert<ProtocolModelInput extends AiModelInput ? true : false>;\n/**\n * Enumerate mapped and intentionally omitted pi-ai fields so additions fail compilation here.\n * Provider replay metadata, diagnostics, cache-write retention splits, model transport settings,\n * model sampling defaults, pricing tiers, and deferred-tool availability remain intentionally\n * server-side.\n */\ntype _AiTextContentFieldsAccountedFor = Assert<ExactKeys<AiTextContent, \"type\" | \"text\" | \"textSignature\">>;\ntype _AiThinkingContentFieldsAccountedFor = Assert<\n\tExactKeys<\n\t\tExtract<AssistantMessage[\"content\"][number], { type: \"thinking\" }>,\n\t\t\"type\" | \"thinking\" | \"thinkingSignature\" | \"redacted\"\n\t>\n>;\ntype _AiImageContentFieldsAccountedFor = Assert<ExactKeys<AiImageContent, \"type\" | \"data\" | \"mimeType\">>;\ntype _AiToolCallFieldsAccountedFor = Assert<\n\tExactKeys<ToolCall, \"type\" | \"id\" | \"name\" | \"arguments\" | \"thoughtSignature\">\n>;\ntype _AiUsageFieldsAccountedFor = Assert<\n\tExactKeys<\n\t\tAiUsage,\n\t\t\"input\" | \"output\" | \"cacheRead\" | \"cacheWrite\" | \"cacheWrite1h\" | \"reasoning\" | \"totalTokens\" | \"cost\"\n\t>\n>;\ntype _AiUsageCostFieldsAccountedFor = Assert<\n\tExactKeys<AiUsage[\"cost\"], \"input\" | \"output\" | \"cacheRead\" | \"cacheWrite\" | \"total\">\n>;\ntype _AiModelFieldsAccountedFor = Assert<\n\tExactKeys<\n\t\tModel<Api>,\n\t\t| \"id\"\n\t\t| \"name\"\n\t\t| \"api\"\n\t\t| \"provider\"\n\t\t| \"baseUrl\"\n\t\t| \"reasoning\"\n\t\t| \"thinkingLevelMap\"\n\t\t| \"input\"\n\t\t| \"cost\"\n\t\t| \"contextWindow\"\n\t\t| \"maxTokens\"\n\t\t| \"samplingParams\"\n\t\t| \"headers\"\n\t\t| \"compat\"\n\t>\n>;\ntype _AiModelCostFieldsAccountedFor = Assert<\n\tExactKeys<Model<Api>[\"cost\"], \"input\" | \"output\" | \"cacheRead\" | \"cacheWrite\" | \"tiers\">\n>;\ntype _AiUserMessageFieldsAccountedFor = Assert<ExactKeys<UserMessage, \"role\" | \"content\" | \"timestamp\">>;\ntype _AiAssistantMessageFieldsAccountedFor = Assert<\n\tExactKeys<\n\t\tAssistantMessage,\n\t\t| \"role\"\n\t\t| \"content\"\n\t\t| \"api\"\n\t\t| \"provider\"\n\t\t| \"model\"\n\t\t| \"responseModel\"\n\t\t| \"responseId\"\n\t\t| \"diagnostics\"\n\t\t| \"usage\"\n\t\t| \"stopReason\"\n\t\t| \"deferred\"\n\t\t| \"errorMessage\"\n\t\t| \"rawStopReason\"\n\t\t| \"timestamp\"\n\t>\n>;\ntype _AiToolResultMessageFieldsAccountedFor = Assert<\n\tExactKeys<\n\t\tToolResultMessage,\n\t\t\"role\" | \"toolCallId\" | \"toolName\" | \"content\" | \"details\" | \"usage\" | \"addedToolNames\" | \"isError\" | \"timestamp\"\n\t>\n>;\n\nexport interface AssistantTranscriptOptions {\n\tid: string;\n}\n\nexport interface UserTranscriptOptions {\n\tid: string;\n}\n\nexport interface ToolTranscriptOptions {\n\tid: string;\n\tcall: ToolCall;\n}\n\nfunction nonNegativeInteger(value: number | undefined): number | undefined {\n\tif (value === undefined || !Number.isFinite(value)) return undefined;\n\treturn Math.max(0, Math.floor(value));\n}\n\nfunction nonNegativeNumber(value: number): number {\n\treturn Number.isFinite(value) ? Math.max(0, value) : 0;\n}\n\nfunction identifier(value: string, label: string): string {\n\tif (typeof value !== \"string\" || value.length === 0) throw new TypeError(`${label} must be a non-empty string`);\n\treturn value;\n}\n\nfunction timestamp(value: number): number {\n\tif (!Number.isSafeInteger(value) || value < 0)\n\t\tthrow new TypeError(\"Protocol timestamps must be non-negative integers\");\n\treturn value;\n}\n\n/** Validate and copy a value from an execution boundary into the protocol's JSON-compatible subset. */\nexport function toProtocolJsonValue(value: unknown, seen = new Set<object>()): JsonValue {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n\tif (typeof value === \"number\") {\n\t\tif (!Number.isFinite(value)) throw new TypeError(\"Protocol JSON numbers must be finite\");\n\t\treturn value;\n\t}\n\tif (typeof value !== \"object\") throw new TypeError(`Unsupported protocol JSON value: ${typeof value}`);\n\tif (seen.has(value)) throw new TypeError(\"Protocol JSON values must not contain circular references\");\n\tconst prototype = Object.getPrototypeOf(value);\n\tif (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n\t\tthrow new TypeError(\"Protocol JSON objects must be plain objects\");\n\t}\n\tseen.add(value);\n\ttry {\n\t\tif (Array.isArray(value)) return Array.from(value, (entry) => toProtocolJsonValue(entry, seen));\n\t\tconst result: Record<string, JsonValue> = {};\n\t\tfor (const [key, entry] of Object.entries(value)) result[key] = toProtocolJsonValue(entry, seen);\n\t\treturn result;\n\t} finally {\n\t\tseen.delete(value);\n\t}\n}\n\n/** Lossily sanitize diagnostic tool details that must not affect execution semantics. */\nexport function sanitizeProtocolDetails(value: unknown, seen = new Set<object>()): JsonValue | undefined {\n\tif (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n\tif (typeof value === \"number\") return Number.isFinite(value) ? value : String(value);\n\tif (typeof value === \"bigint\") return value.toString();\n\tif (value === undefined || typeof value === \"function\" || typeof value === \"symbol\") return undefined;\n\tif (value instanceof Date) return value.toISOString();\n\tif (typeof value !== \"object\") return String(value);\n\tif (seen.has(value)) return \"[Circular]\";\n\tseen.add(value);\n\ttry {\n\t\tif (Array.isArray(value)) return Array.from(value, (entry) => sanitizeProtocolDetails(entry, seen) ?? null);\n\t\tconst result: Record<string, JsonValue> = {};\n\t\tfor (const [key, entry] of Object.entries(value)) {\n\t\t\tconst normalized = sanitizeProtocolDetails(entry, seen);\n\t\t\tif (normalized !== undefined) result[key] = normalized;\n\t\t}\n\t\treturn result;\n\t} finally {\n\t\tseen.delete(value);\n\t}\n}\n\nexport function toProtocolUsage(usage: AiUsage | undefined): Usage | undefined {\n\tif (!usage) return undefined;\n\tconst reasoning = nonNegativeInteger(usage.reasoning);\n\tconst result = {\n\t\tinput: nonNegativeInteger(usage.input) ?? 0,\n\t\toutput: nonNegativeInteger(usage.output) ?? 0,\n\t\tcacheRead: nonNegativeInteger(usage.cacheRead) ?? 0,\n\t\tcacheWrite: nonNegativeInteger(usage.cacheWrite) ?? 0,\n\t\t...(reasoning === undefined ? {} : { reasoning }),\n\t\ttotalTokens: nonNegativeInteger(usage.totalTokens) ?? 0,\n\t\tcost: {\n\t\t\tinput: nonNegativeNumber(usage.cost.input),\n\t\t\toutput: nonNegativeNumber(usage.cost.output),\n\t\t\tcacheRead: nonNegativeNumber(usage.cost.cacheRead),\n\t\t\tcacheWrite: nonNegativeNumber(usage.cost.cacheWrite),\n\t\t\ttotal: nonNegativeNumber(usage.cost.total),\n\t\t},\n\t} satisfies Usage;\n\treturn result;\n}\n\nexport function toProtocolModelMetadata(model: Model<Api>, authenticated: boolean): ModelMetadata {\n\tconst result = {\n\t\tprovider: identifier(model.provider, \"Model provider\"),\n\t\tid: identifier(model.id, \"Model id\"),\n\t\tname: identifier(model.name, \"Model name\"),\n\t\tapi: identifier(model.api, \"Model API\"),\n\t\treasoning: model.reasoning,\n\t\tinput: [...model.input],\n\t\tcontextWindow: Math.max(1, Math.floor(model.contextWindow)),\n\t\tmaxTokens: Math.max(1, Math.floor(model.maxTokens)),\n\t\tcost: {\n\t\t\tinput: nonNegativeNumber(model.cost.input),\n\t\t\toutput: nonNegativeNumber(model.cost.output),\n\t\t\tcacheRead: nonNegativeNumber(model.cost.cacheRead),\n\t\t\tcacheWrite: nonNegativeNumber(model.cost.cacheWrite),\n\t\t},\n\t\tsupportedThinkingLevels: getSupportedThinkingLevels(model),\n\t\tauthenticated,\n\t} satisfies ModelMetadata;\n\treturn result;\n}\n\nfunction toProtocolUserContent(content: UserMessage[\"content\"]): UserTranscriptItem[\"content\"] {\n\tif (typeof content === \"string\") return [{ type: \"text\", text: content }];\n\treturn content.map((part) => {\n\t\tswitch (part.type) {\n\t\t\tcase \"text\":\n\t\t\t\treturn { type: \"text\", text: part.text };\n\t\t\tcase \"image\":\n\t\t\t\treturn { type: \"image\", data: part.data, mimeType: part.mimeType };\n\t\t\tdefault: {\n\t\t\t\tconst exhaustive: never = part;\n\t\t\t\treturn exhaustive;\n\t\t\t}\n\t\t}\n\t});\n}\n\nexport function toProtocolUserMessage(message: UserMessage, options: UserTranscriptOptions): UserTranscriptItem {\n\tconst result = {\n\t\tid: identifier(options.id, \"Transcript item id\"),\n\t\trole: \"user\",\n\t\tcontent: toProtocolUserContent(message.content),\n\t\ttimestamp: timestamp(message.timestamp),\n\t} satisfies UserTranscriptItem;\n\treturn result;\n}\n\nfunction toProtocolAssistantContent(message: AssistantMessage): AssistantTranscriptItem[\"content\"] {\n\treturn message.content.map((part) => {\n\t\tswitch (part.type) {\n\t\t\tcase \"text\":\n\t\t\t\treturn { type: \"text\", text: part.text };\n\t\t\tcase \"thinking\":\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"thinking\",\n\t\t\t\t\tthinking: part.thinking,\n\t\t\t\t\t...(part.redacted === undefined ? {} : { redacted: part.redacted }),\n\t\t\t\t};\n\t\t\tcase \"toolCall\":\n\t\t\t\treturn {\n\t\t\t\t\ttype: \"toolCall\",\n\t\t\t\t\ttoolCallId: identifier(part.id, \"Tool call id\"),\n\t\t\t\t\ttoolName: identifier(part.name, \"Tool call name\"),\n\t\t\t\t\tinput: toProtocolJsonValue(part.arguments),\n\t\t\t\t};\n\t\t\tdefault: {\n\t\t\t\tconst exhaustive: never = part;\n\t\t\t\treturn exhaustive;\n\t\t\t}\n\t\t}\n\t});\n}\n\nexport function toProtocolAssistantMessage(\n\tmessage: AssistantMessage,\n\toptions: AssistantTranscriptOptions,\n): AssistantTranscriptItem {\n\tconst usage = toProtocolUsage(message.usage);\n\tconst common = {\n\t\tid: identifier(options.id, \"Transcript item id\"),\n\t\trole: \"assistant\",\n\t\tcontent: toProtocolAssistantContent(message),\n\t\tmodel: {\n\t\t\tprovider: identifier(message.provider, \"Assistant provider\"),\n\t\t\tid: identifier(message.model, \"Assistant model\"),\n\t\t},\n\t\t...(message.responseModel === undefined\n\t\t\t? {}\n\t\t\t: { responseModel: identifier(message.responseModel, \"Assistant response model\") }),\n\t\t...(usage ? { usage } : {}),\n\t\ttimestamp: timestamp(message.timestamp),\n\t} as const;\n\tswitch (message.stopReason) {\n\t\tcase \"pending\":\n\t\t\treturn { ...common, status: \"streaming\" } satisfies AssistantTranscriptItem;\n\t\tcase \"stop\":\n\t\tcase \"length\":\n\t\tcase \"toolUse\":\n\t\t\treturn {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"complete\",\n\t\t\t\tstopReason: message.stopReason,\n\t\t\t} satisfies AssistantTranscriptItem;\n\t\tcase \"deferred\":\n\t\t\tthrow new TypeError(\"Deferred assistant messages are not supported by protocol v1\");\n\t\tcase \"error\":\n\t\t\tif (message.errorMessage?.length === 0) {\n\t\t\t\tthrow new TypeError(\"Assistant error messages must not be empty\");\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"error\",\n\t\t\t\tstopReason: \"error\",\n\t\t\t\t...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }),\n\t\t\t} satisfies AssistantTranscriptItem;\n\t\tcase \"aborted\":\n\t\t\treturn {\n\t\t\t\t...common,\n\t\t\t\tstatus: \"aborted\",\n\t\t\t\tstopReason: \"aborted\",\n\t\t\t\t...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }),\n\t\t\t} satisfies AssistantTranscriptItem;\n\t\tdefault: {\n\t\t\tconst exhaustive: never = message.stopReason;\n\t\t\treturn exhaustive;\n\t\t}\n\t}\n}\n\nfunction toProtocolToolContent(content: Array<AiTextContent | AiImageContent>): ToolTranscriptItem[\"content\"] {\n\treturn content.map((part) => {\n\t\tswitch (part.type) {\n\t\t\tcase \"text\":\n\t\t\t\treturn { type: \"text\", text: part.text };\n\t\t\tcase \"image\":\n\t\t\t\treturn { type: \"image\", data: part.data, mimeType: part.mimeType };\n\t\t\tdefault: {\n\t\t\t\tconst exhaustive: never = part;\n\t\t\t\treturn exhaustive;\n\t\t\t}\n\t\t}\n\t});\n}\n\nexport function toProtocolToolResultMessage(\n\tmessage: ToolResultMessage,\n\toptions: ToolTranscriptOptions,\n): ToolTranscriptItem {\n\tconst callId = identifier(options.call.id, \"Tool call id\");\n\tconst callName = identifier(options.call.name, \"Tool call name\");\n\tif (identifier(message.toolCallId, \"Tool result call id\") !== callId) {\n\t\tthrow new TypeError(`Tool result ${message.toolCallId} does not match tool call ${callId}`);\n\t}\n\tif (identifier(message.toolName, \"Tool result name\") !== callName) {\n\t\tthrow new TypeError(`Tool result ${message.toolName} does not match tool call ${callName}`);\n\t}\n\tconst details = sanitizeProtocolDetails(message.details);\n\tconst usage = toProtocolUsage(message.usage);\n\tconst common = {\n\t\tid: identifier(options.id, \"Transcript item id\"),\n\t\trole: \"tool\",\n\t\ttoolCallId: callId,\n\t\ttoolName: callName,\n\t\tinput: toProtocolJsonValue(options.call.arguments),\n\t\tcontent: toProtocolToolContent(message.content),\n\t\t...(details === undefined ? {} : { details }),\n\t\t...(usage ? { usage } : {}),\n\t\ttimestamp: timestamp(message.timestamp),\n\t} as const;\n\treturn message.isError\n\t\t? ({ ...common, status: \"error\", isError: true } satisfies ToolTranscriptItem)\n\t\t: ({ ...common, status: \"complete\", isError: false } satisfies ToolTranscriptItem);\n}\n"]}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { getSupportedThinkingLevels, } from "@earendil-works/pi-ai";
|
|
2
|
+
function nonNegativeInteger(value) {
|
|
3
|
+
if (value === undefined || !Number.isFinite(value))
|
|
4
|
+
return undefined;
|
|
5
|
+
return Math.max(0, Math.floor(value));
|
|
6
|
+
}
|
|
7
|
+
function nonNegativeNumber(value) {
|
|
8
|
+
return Number.isFinite(value) ? Math.max(0, value) : 0;
|
|
9
|
+
}
|
|
10
|
+
function identifier(value, label) {
|
|
11
|
+
if (typeof value !== "string" || value.length === 0)
|
|
12
|
+
throw new TypeError(`${label} must be a non-empty string`);
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function timestamp(value) {
|
|
16
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
17
|
+
throw new TypeError("Protocol timestamps must be non-negative integers");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
/** Validate and copy a value from an execution boundary into the protocol's JSON-compatible subset. */
|
|
21
|
+
export function toProtocolJsonValue(value, seen = new Set()) {
|
|
22
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
23
|
+
return value;
|
|
24
|
+
if (typeof value === "number") {
|
|
25
|
+
if (!Number.isFinite(value))
|
|
26
|
+
throw new TypeError("Protocol JSON numbers must be finite");
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== "object")
|
|
30
|
+
throw new TypeError(`Unsupported protocol JSON value: ${typeof value}`);
|
|
31
|
+
if (seen.has(value))
|
|
32
|
+
throw new TypeError("Protocol JSON values must not contain circular references");
|
|
33
|
+
const prototype = Object.getPrototypeOf(value);
|
|
34
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
35
|
+
throw new TypeError("Protocol JSON objects must be plain objects");
|
|
36
|
+
}
|
|
37
|
+
seen.add(value);
|
|
38
|
+
try {
|
|
39
|
+
if (Array.isArray(value))
|
|
40
|
+
return Array.from(value, (entry) => toProtocolJsonValue(entry, seen));
|
|
41
|
+
const result = {};
|
|
42
|
+
for (const [key, entry] of Object.entries(value))
|
|
43
|
+
result[key] = toProtocolJsonValue(entry, seen);
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
seen.delete(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Lossily sanitize diagnostic tool details that must not affect execution semantics. */
|
|
51
|
+
export function sanitizeProtocolDetails(value, seen = new Set()) {
|
|
52
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
53
|
+
return value;
|
|
54
|
+
if (typeof value === "number")
|
|
55
|
+
return Number.isFinite(value) ? value : String(value);
|
|
56
|
+
if (typeof value === "bigint")
|
|
57
|
+
return value.toString();
|
|
58
|
+
if (value === undefined || typeof value === "function" || typeof value === "symbol")
|
|
59
|
+
return undefined;
|
|
60
|
+
if (value instanceof Date)
|
|
61
|
+
return value.toISOString();
|
|
62
|
+
if (typeof value !== "object")
|
|
63
|
+
return String(value);
|
|
64
|
+
if (seen.has(value))
|
|
65
|
+
return "[Circular]";
|
|
66
|
+
seen.add(value);
|
|
67
|
+
try {
|
|
68
|
+
if (Array.isArray(value))
|
|
69
|
+
return Array.from(value, (entry) => sanitizeProtocolDetails(entry, seen) ?? null);
|
|
70
|
+
const result = {};
|
|
71
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
72
|
+
const normalized = sanitizeProtocolDetails(entry, seen);
|
|
73
|
+
if (normalized !== undefined)
|
|
74
|
+
result[key] = normalized;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
seen.delete(value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function toProtocolUsage(usage) {
|
|
83
|
+
if (!usage)
|
|
84
|
+
return undefined;
|
|
85
|
+
const reasoning = nonNegativeInteger(usage.reasoning);
|
|
86
|
+
const result = {
|
|
87
|
+
input: nonNegativeInteger(usage.input) ?? 0,
|
|
88
|
+
output: nonNegativeInteger(usage.output) ?? 0,
|
|
89
|
+
cacheRead: nonNegativeInteger(usage.cacheRead) ?? 0,
|
|
90
|
+
cacheWrite: nonNegativeInteger(usage.cacheWrite) ?? 0,
|
|
91
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
92
|
+
totalTokens: nonNegativeInteger(usage.totalTokens) ?? 0,
|
|
93
|
+
cost: {
|
|
94
|
+
input: nonNegativeNumber(usage.cost.input),
|
|
95
|
+
output: nonNegativeNumber(usage.cost.output),
|
|
96
|
+
cacheRead: nonNegativeNumber(usage.cost.cacheRead),
|
|
97
|
+
cacheWrite: nonNegativeNumber(usage.cost.cacheWrite),
|
|
98
|
+
total: nonNegativeNumber(usage.cost.total),
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
export function toProtocolModelMetadata(model, authenticated) {
|
|
104
|
+
const result = {
|
|
105
|
+
provider: identifier(model.provider, "Model provider"),
|
|
106
|
+
id: identifier(model.id, "Model id"),
|
|
107
|
+
name: identifier(model.name, "Model name"),
|
|
108
|
+
api: identifier(model.api, "Model API"),
|
|
109
|
+
reasoning: model.reasoning,
|
|
110
|
+
input: [...model.input],
|
|
111
|
+
contextWindow: Math.max(1, Math.floor(model.contextWindow)),
|
|
112
|
+
maxTokens: Math.max(1, Math.floor(model.maxTokens)),
|
|
113
|
+
cost: {
|
|
114
|
+
input: nonNegativeNumber(model.cost.input),
|
|
115
|
+
output: nonNegativeNumber(model.cost.output),
|
|
116
|
+
cacheRead: nonNegativeNumber(model.cost.cacheRead),
|
|
117
|
+
cacheWrite: nonNegativeNumber(model.cost.cacheWrite),
|
|
118
|
+
},
|
|
119
|
+
supportedThinkingLevels: getSupportedThinkingLevels(model),
|
|
120
|
+
authenticated,
|
|
121
|
+
};
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
function toProtocolUserContent(content) {
|
|
125
|
+
if (typeof content === "string")
|
|
126
|
+
return [{ type: "text", text: content }];
|
|
127
|
+
return content.map((part) => {
|
|
128
|
+
switch (part.type) {
|
|
129
|
+
case "text":
|
|
130
|
+
return { type: "text", text: part.text };
|
|
131
|
+
case "image":
|
|
132
|
+
return { type: "image", data: part.data, mimeType: part.mimeType };
|
|
133
|
+
default: {
|
|
134
|
+
const exhaustive = part;
|
|
135
|
+
return exhaustive;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export function toProtocolUserMessage(message, options) {
|
|
141
|
+
const result = {
|
|
142
|
+
id: identifier(options.id, "Transcript item id"),
|
|
143
|
+
role: "user",
|
|
144
|
+
content: toProtocolUserContent(message.content),
|
|
145
|
+
timestamp: timestamp(message.timestamp),
|
|
146
|
+
};
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function toProtocolAssistantContent(message) {
|
|
150
|
+
return message.content.map((part) => {
|
|
151
|
+
switch (part.type) {
|
|
152
|
+
case "text":
|
|
153
|
+
return { type: "text", text: part.text };
|
|
154
|
+
case "thinking":
|
|
155
|
+
return {
|
|
156
|
+
type: "thinking",
|
|
157
|
+
thinking: part.thinking,
|
|
158
|
+
...(part.redacted === undefined ? {} : { redacted: part.redacted }),
|
|
159
|
+
};
|
|
160
|
+
case "toolCall":
|
|
161
|
+
return {
|
|
162
|
+
type: "toolCall",
|
|
163
|
+
toolCallId: identifier(part.id, "Tool call id"),
|
|
164
|
+
toolName: identifier(part.name, "Tool call name"),
|
|
165
|
+
input: toProtocolJsonValue(part.arguments),
|
|
166
|
+
};
|
|
167
|
+
default: {
|
|
168
|
+
const exhaustive = part;
|
|
169
|
+
return exhaustive;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export function toProtocolAssistantMessage(message, options) {
|
|
175
|
+
const usage = toProtocolUsage(message.usage);
|
|
176
|
+
const common = {
|
|
177
|
+
id: identifier(options.id, "Transcript item id"),
|
|
178
|
+
role: "assistant",
|
|
179
|
+
content: toProtocolAssistantContent(message),
|
|
180
|
+
model: {
|
|
181
|
+
provider: identifier(message.provider, "Assistant provider"),
|
|
182
|
+
id: identifier(message.model, "Assistant model"),
|
|
183
|
+
},
|
|
184
|
+
...(message.responseModel === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: { responseModel: identifier(message.responseModel, "Assistant response model") }),
|
|
187
|
+
...(usage ? { usage } : {}),
|
|
188
|
+
timestamp: timestamp(message.timestamp),
|
|
189
|
+
};
|
|
190
|
+
switch (message.stopReason) {
|
|
191
|
+
case "pending":
|
|
192
|
+
return { ...common, status: "streaming" };
|
|
193
|
+
case "stop":
|
|
194
|
+
case "length":
|
|
195
|
+
case "toolUse":
|
|
196
|
+
return {
|
|
197
|
+
...common,
|
|
198
|
+
status: "complete",
|
|
199
|
+
stopReason: message.stopReason,
|
|
200
|
+
};
|
|
201
|
+
case "deferred":
|
|
202
|
+
throw new TypeError("Deferred assistant messages are not supported by protocol v1");
|
|
203
|
+
case "error":
|
|
204
|
+
if (message.errorMessage?.length === 0) {
|
|
205
|
+
throw new TypeError("Assistant error messages must not be empty");
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
...common,
|
|
209
|
+
status: "error",
|
|
210
|
+
stopReason: "error",
|
|
211
|
+
...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }),
|
|
212
|
+
};
|
|
213
|
+
case "aborted":
|
|
214
|
+
return {
|
|
215
|
+
...common,
|
|
216
|
+
status: "aborted",
|
|
217
|
+
stopReason: "aborted",
|
|
218
|
+
...(message.errorMessage === undefined ? {} : { errorMessage: message.errorMessage }),
|
|
219
|
+
};
|
|
220
|
+
default: {
|
|
221
|
+
const exhaustive = message.stopReason;
|
|
222
|
+
return exhaustive;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function toProtocolToolContent(content) {
|
|
227
|
+
return content.map((part) => {
|
|
228
|
+
switch (part.type) {
|
|
229
|
+
case "text":
|
|
230
|
+
return { type: "text", text: part.text };
|
|
231
|
+
case "image":
|
|
232
|
+
return { type: "image", data: part.data, mimeType: part.mimeType };
|
|
233
|
+
default: {
|
|
234
|
+
const exhaustive = part;
|
|
235
|
+
return exhaustive;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
export function toProtocolToolResultMessage(message, options) {
|
|
241
|
+
const callId = identifier(options.call.id, "Tool call id");
|
|
242
|
+
const callName = identifier(options.call.name, "Tool call name");
|
|
243
|
+
if (identifier(message.toolCallId, "Tool result call id") !== callId) {
|
|
244
|
+
throw new TypeError(`Tool result ${message.toolCallId} does not match tool call ${callId}`);
|
|
245
|
+
}
|
|
246
|
+
if (identifier(message.toolName, "Tool result name") !== callName) {
|
|
247
|
+
throw new TypeError(`Tool result ${message.toolName} does not match tool call ${callName}`);
|
|
248
|
+
}
|
|
249
|
+
const details = sanitizeProtocolDetails(message.details);
|
|
250
|
+
const usage = toProtocolUsage(message.usage);
|
|
251
|
+
const common = {
|
|
252
|
+
id: identifier(options.id, "Transcript item id"),
|
|
253
|
+
role: "tool",
|
|
254
|
+
toolCallId: callId,
|
|
255
|
+
toolName: callName,
|
|
256
|
+
input: toProtocolJsonValue(options.call.arguments),
|
|
257
|
+
content: toProtocolToolContent(message.content),
|
|
258
|
+
...(details === undefined ? {} : { details }),
|
|
259
|
+
...(usage ? { usage } : {}),
|
|
260
|
+
timestamp: timestamp(message.timestamp),
|
|
261
|
+
};
|
|
262
|
+
return message.isError
|
|
263
|
+
? { ...common, status: "error", isError: true }
|
|
264
|
+
: { ...common, status: "complete", isError: false };
|
|
265
|
+
}
|
|
266
|
+
//# sourceMappingURL=protocol.js.map
|