@averyyy/pi-coding-agent 0.80.3-piclient.6 → 0.80.3-piclient.8

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.
@@ -27,6 +27,8 @@ export interface PiServerHistorySnapshot {
27
27
  }
28
28
  export interface PiServerStreamOptions extends SimpleStreamOptions {
29
29
  sessionTree?: PiServerTreeSnapshot;
30
+ ephemeralMessages?: Message[];
31
+ contextOverlay?: Message[];
30
32
  onHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;
31
33
  }
32
34
  interface PiServerSyncOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"pi-server-client.d.ts","sourceRoot":"","sources":["../../src/core/pi-server-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport type {\n\tCompactionPreparationOptions,\n\tCompactResult,\n\tProxyAssistantMessageEvent,\n\tSessionTreeEntry,\n} from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tEventStream,\n\ttype Message,\n\ttype Model,\n\tparseStreamingJson,\n\ttype SimpleStreamOptions,\n} from \"@earendil-works/pi-ai\";\nimport { ChunkRequest } from \"./pi-server-request.ts\";\n\nclass PiServerEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {\n\tconstructor() {\n\t\tsuper(\n\t\t\t(event) => event.type === \"done\" || event.type === \"error\",\n\t\t\t(event) => {\n\t\t\t\tif (event.type === \"done\") return event.message;\n\t\t\t\tif (event.type === \"error\") return event.error;\n\t\t\t\tthrow new Error(\"Unexpected event type\");\n\t\t\t},\n\t\t);\n\t}\n}\n\nfunction getServerUrl(): string {\n\treturn process.env.PI_SERVER_URL ?? \"http://127.0.0.1:4217\";\n}\n\nfunction getAuthToken(): string {\n\treturn process.env.PI_SERVER_AUTH_TOKEN ?? \"\";\n}\n\nfunction createPiServerRequest(signal?: AbortSignal): ChunkRequest {\n\treturn new ChunkRequest({\n\t\tserverUrl: getServerUrl(),\n\t\tauthToken: getAuthToken(),\n\t\tsignal,\n\t});\n}\n\nconst sessionStaticContextHashes = new Map<string, string>();\nconst sessionSyncedEntryIds = new Map<string, Set<string>>();\nconst sessionTreeHashes = new Map<string, string>();\nconst sessionTreeEntryCounts = new Map<string, number>();\nconst sessionTreeLeafIds = new Map<string, string | null>();\nconst sessionHasTemporaryTree = new Set<string>();\nconst RESPONSE_BODY_EXCERPT_CHARS = 500;\n\nexport function hashStaticContext(ctx: Context): string {\n\tconst parts: string[] = [];\n\tif (ctx.systemPrompt !== undefined) parts.push(`sp:${ctx.systemPrompt}`);\n\tif (ctx.tools) {\n\t\tparts.push(\n\t\t\t`t:${JSON.stringify(ctx.tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })))}`,\n\t\t);\n\t}\n\treturn parts.join(\"|\");\n}\n\nfunction hashEntries(entries: SessionTreeEntry[]): string {\n\treturn createHash(\"sha256\").update(JSON.stringify(entries)).digest(\"hex\");\n}\n\nfunction getLinearTreeFromMessages(messages: Message[]): { entries: SessionTreeEntry[]; leafId: string | null } {\n\tlet parentId: string | null = null;\n\tconst entries = messages.map((message, index): SessionTreeEntry => {\n\t\tconst id = `message-${index}`;\n\t\tconst entry: SessionTreeEntry = {\n\t\t\ttype: \"message\",\n\t\t\tid,\n\t\t\tparentId,\n\t\t\ttimestamp: new Date(message.timestamp).toISOString(),\n\t\t\tmessage,\n\t\t};\n\t\tparentId = id;\n\t\treturn entry;\n\t});\n\treturn { entries, leafId: parentId };\n}\n\nexport function resetSessionTracking(sessionId: string): void {\n\tsessionStaticContextHashes.delete(sessionId);\n\tsessionSyncedEntryIds.delete(sessionId);\n\tsessionTreeHashes.delete(sessionId);\n\tsessionTreeEntryCounts.delete(sessionId);\n\tsessionTreeLeafIds.delete(sessionId);\n\tsessionHasTemporaryTree.delete(sessionId);\n}\n\nexport function resetAllSessionTracking(): void {\n\tsessionStaticContextHashes.clear();\n\tsessionSyncedEntryIds.clear();\n\tsessionTreeHashes.clear();\n\tsessionTreeEntryCounts.clear();\n\tsessionTreeLeafIds.clear();\n\tsessionHasTemporaryTree.clear();\n}\n\ninterface SessionInitResponse {\n\tsessionId: string;\n\tstaticContextHash: string;\n\ttreeHash?: string;\n\tmessageCount: number;\n\tentryCount?: number;\n\tleafId?: string | null;\n\trevision?: number;\n\tfromCache?: boolean;\n}\n\nexport interface PiServerTreeSnapshot {\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\treplace?: boolean;\n}\n\nexport interface PiServerCompactionResult {\n\tcompaction: CompactResult;\n\tcompactionEntry: SessionTreeEntry;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tmessages: Message[];\n}\n\nexport interface PiServerHistorySnapshot {\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tmessages: Message[];\n}\n\nexport interface PiServerStreamOptions extends SimpleStreamOptions {\n\tsessionTree?: PiServerTreeSnapshot;\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;\n}\n\ninterface PiServerResponseFailure {\n\tdetails: string;\n\tmatchText: string;\n}\n\ninterface PiServerHistoryResponse extends Partial<PiServerHistorySnapshot> {\n\tsessionId: string;\n\ttreeHash?: string;\n\tentryCount?: number;\n\tleafId?: string | null;\n}\n\ninterface PiServerSyncOptions {\n\tsignal?: AbortSignal;\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;\n}\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction getResponseStatus(response: Response): string {\n\tconst status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);\n\treturn response.status >= 500 ? `${status} (server error)` : status;\n}\n\nfunction getResponseContentType(response: Response): string {\n\treturn response.headers.get(\"content-type\") ?? \"unknown\";\n}\n\nfunction getBodyExcerpt(text: string): string {\n\tconst trimmed = text.trim();\n\tif (!trimmed) return \"<empty>\";\n\tconst excerpt = trimmed.slice(0, RESPONSE_BODY_EXCERPT_CHARS);\n\treturn excerpt.length < trimmed.length ? `${excerpt}...` : excerpt;\n}\n\nfunction getJsonErrorText(text: string): string | undefined {\n\tif (!text) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(text) as unknown;\n\t\tif (!isObject(parsed)) return undefined;\n\t\tconst error = parsed.error;\n\t\treturn typeof error === \"string\" && error.length > 0 ? error : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction formatResponseDetails(response: Response, bodyText: string): string {\n\tconst body = getJsonErrorText(bodyText) ?? getBodyExcerpt(bodyText);\n\treturn `${getResponseStatus(response)}; content-type: ${getResponseContentType(response)}; body excerpt: ${body}`;\n}\n\nasync function readPiServerFailure(response: Response): Promise<PiServerResponseFailure> {\n\tconst bodyText = await response.text();\n\treturn {\n\t\tdetails: formatResponseDetails(response, bodyText),\n\t\tmatchText: getJsonErrorText(bodyText) ?? bodyText,\n\t};\n}\n\nasync function readPiServerJson<T>(response: Response, errorPrefix: string): Promise<T> {\n\tconst bodyText = await response.text();\n\tif (!response.ok) {\n\t\tthrow new Error(`${errorPrefix} (${formatResponseDetails(response, bodyText)})`);\n\t}\n\ttry {\n\t\treturn JSON.parse(bodyText) as T;\n\t} catch {\n\t\tthrow new Error(`${errorPrefix} (${formatResponseDetails(response, bodyText)}; expected JSON)`);\n\t}\n}\n\nasync function ensurePiServerEventStream(response: Response): Promise<void> {\n\tconst contentType = getResponseContentType(response);\n\tif (contentType.toLowerCase().split(\";\")[0]?.trim() === \"text/event-stream\") {\n\t\treturn;\n\t}\n\tconst bodyText = await response.text();\n\tthrow new Error(`pi-server error: ${formatResponseDetails(response, bodyText)}; expected text/event-stream`);\n}\n\nasync function ensureSessionInit(\n\tsessionId: string,\n\tcontext: Context,\n\trequest: ChunkRequest,\n): Promise<SessionInitResponse> {\n\tconst currentHash = hashStaticContext(context);\n\tconst previousHash = sessionStaticContextHashes.get(sessionId);\n\n\tif (previousHash === currentHash) {\n\t\treturn {\n\t\t\tsessionId,\n\t\t\tstaticContextHash: previousHash,\n\t\t\tmessageCount: 0,\n\t\t\tfromCache: true,\n\t\t};\n\t}\n\n\tconst staticContext = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools,\n\t};\n\n\tconst endpoint = previousHash === undefined ? \"/api/session/init\" : \"/api/session/update\";\n\n\tconst response = await request.postJson(endpoint, { sessionId, staticContext });\n\n\tconst result = await readPiServerJson<SessionInitResponse>(response, \"Session init failed\");\n\tsessionStaticContextHashes.set(sessionId, currentHash);\n\tif (result.treeHash !== undefined) {\n\t\tsessionTreeHashes.set(sessionId, result.treeHash);\n\t\tsessionTreeLeafIds.set(sessionId, result.leafId ?? null);\n\t\tif (result.entryCount !== undefined) {\n\t\t\tsessionTreeEntryCounts.set(sessionId, result.entryCount);\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction getStaticContext(context: Context) {\n\treturn {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools,\n\t};\n}\n\nfunction markTreeSynced(sessionId: string, tree: PiServerTreeSnapshot): void {\n\tsessionSyncedEntryIds.set(sessionId, new Set(tree.entries.map((entry) => entry.id)));\n\tsessionTreeHashes.set(sessionId, hashEntries(tree.entries));\n\tsessionTreeEntryCounts.set(sessionId, tree.entries.length);\n\tsessionTreeLeafIds.set(sessionId, tree.leafId);\n\tif (tree.replace) {\n\t\tsessionHasTemporaryTree.add(sessionId);\n\t} else {\n\t\tsessionHasTemporaryTree.delete(sessionId);\n\t}\n}\n\nasync function postTreeJson(\n\trequest: ChunkRequest,\n\tendpoint: string,\n\tbody: unknown,\n\terrorPrefix: string,\n): Promise<void> {\n\tconst response = await request.postJson(endpoint, body);\n\tawait readPiServerJson<unknown>(response, errorPrefix);\n}\n\nasync function fetchPiServerHistory(\n\tsessionId: string,\n\trequest: ChunkRequest,\n): Promise<PiServerHistorySnapshot | undefined> {\n\tconst response = await request.getJson(`/api/session/${encodeURIComponent(sessionId)}/history`);\n\tif (response.status === 404) {\n\t\treturn undefined;\n\t}\n\tconst history = await readPiServerJson<PiServerHistoryResponse>(response, \"Session history reconciliation failed\");\n\tif (!history.entries || history.leafId === undefined || !history.messages) {\n\t\tthrow new Error(\"Session history reconciliation failed (response did not include the session tree)\");\n\t}\n\treturn {\n\t\tentries: history.entries,\n\t\tleafId: history.leafId,\n\t\tmessages: history.messages,\n\t};\n}\n\nasync function applyPiServerHistory(\n\tsessionId: string,\n\tsnapshot: PiServerHistorySnapshot,\n\tonHistoryReconciled: ((snapshot: PiServerHistorySnapshot) => void | Promise<void>) | undefined,\n): Promise<void> {\n\tmarkTreeSynced(sessionId, snapshot);\n\tawait onHistoryReconciled?.(snapshot);\n}\n\nfunction getKnownServerPrefixIds(sessionId: string, entries: SessionTreeEntry[]): Set<string> | undefined {\n\tconst entryCount = sessionTreeEntryCounts.get(sessionId);\n\tconst treeHash = sessionTreeHashes.get(sessionId);\n\tif (entryCount === undefined || treeHash === undefined || entryCount > entries.length) {\n\t\treturn undefined;\n\t}\n\tconst prefix = entries.slice(0, entryCount);\n\tif (hashEntries(prefix) !== treeHash) {\n\t\treturn undefined;\n\t}\n\tconst ids = new Set(prefix.map((entry) => entry.id));\n\tsessionSyncedEntryIds.set(sessionId, ids);\n\treturn ids;\n}\n\nfunction isRecoverableTreeDivergenceError(error: unknown): boolean {\n\tconst message = error instanceof Error ? error.message : String(error);\n\treturn /parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(message);\n}\n\nfunction isRecoverableMissingServerState(response: Response, errorBody: string): boolean {\n\tif (response.status !== 400 && response.status !== 404) return false;\n\treturn /Session has no static context|session not found|parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(\n\t\terrorBody,\n\t);\n}\n\nasync function syncFullPiServerTree(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\trequest: ChunkRequest,\n): Promise<void> {\n\tawait postTreeJson(\n\t\trequest,\n\t\t\"/api/session/tree/sync\",\n\t\t{\n\t\t\tsessionId,\n\t\t\tentries: tree.entries,\n\t\t\tleafId: tree.leafId,\n\t\t\tstaticContext: getStaticContext(context),\n\t\t},\n\t\t\"Session tree sync failed\",\n\t);\n\tmarkTreeSynced(sessionId, tree);\n}\n\nfunction shouldUseServerHistory(sessionId: string, tree: PiServerTreeSnapshot): boolean {\n\treturn !tree.replace && !sessionHasTemporaryTree.has(sessionId) && (sessionTreeEntryCounts.get(sessionId) ?? 0) > 0;\n}\n\nasync function recoverPiServerTreeDivergence(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\trequest: ChunkRequest,\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>,\n): Promise<void> {\n\tif (shouldUseServerHistory(sessionId, tree)) {\n\t\tconst snapshot = await fetchPiServerHistory(sessionId, request);\n\t\tif (snapshot && snapshot.entries.length > 0) {\n\t\t\tawait applyPiServerHistory(sessionId, snapshot, onHistoryReconciled);\n\t\t\tthrow new Error(\n\t\t\t\t\"pi-server history differed from local history; local session was reconciled to server history\",\n\t\t\t);\n\t\t}\n\t}\n\n\tawait syncFullPiServerTree(sessionId, context, tree, request);\n}\n\nexport async function syncPiServerTree(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\toptions?: PiServerSyncOptions,\n): Promise<void> {\n\tconst request = createPiServerRequest(options?.signal);\n\tawait ensureSessionInit(sessionId, context, request);\n\tawait syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);\n}\n\nasync function syncPiServerTreeWithRequest(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\trequest: ChunkRequest,\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>,\n): Promise<void> {\n\tconst syncTree = tree;\n\tconst currentHash = hashEntries(syncTree.entries);\n\tconst previousHash = sessionTreeHashes.get(sessionId);\n\tconst previousLeafId = sessionTreeLeafIds.get(sessionId);\n\n\tif (!tree.replace && previousHash === currentHash) {\n\t\tif (previousLeafId !== syncTree.leafId) {\n\t\t\ttry {\n\t\t\t\tawait postTreeJson(\n\t\t\t\t\trequest,\n\t\t\t\t\t\"/api/session/tree/switch\",\n\t\t\t\t\t{ sessionId, leafId: syncTree.leafId },\n\t\t\t\t\t\"Session tree switch failed\",\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (!isRecoverableTreeDivergenceError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tawait recoverPiServerTreeDivergence(sessionId, context, syncTree, request, onHistoryReconciled);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsessionTreeLeafIds.set(sessionId, syncTree.leafId);\n\t\t}\n\t\tmarkTreeSynced(sessionId, syncTree);\n\t\treturn;\n\t}\n\n\tconst syncedIds =\n\t\t!tree.replace && !sessionHasTemporaryTree.has(sessionId)\n\t\t\t? (sessionSyncedEntryIds.get(sessionId) ?? getKnownServerPrefixIds(sessionId, syncTree.entries))\n\t\t\t: undefined;\n\tif (syncedIds) {\n\t\tconst deltaEntries = syncTree.entries.filter((entry) => !syncedIds.has(entry.id));\n\t\tif (deltaEntries.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait postTreeJson(\n\t\t\t\t\trequest,\n\t\t\t\t\t\"/api/session/tree/append\",\n\t\t\t\t\t{\n\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\tentries: deltaEntries,\n\t\t\t\t\t\tleafId: syncTree.leafId,\n\t\t\t\t\t\tstaticContext: getStaticContext(context),\n\t\t\t\t\t},\n\t\t\t\t\t\"Session tree append failed\",\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (!isRecoverableTreeDivergenceError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tawait recoverPiServerTreeDivergence(sessionId, context, syncTree, request, onHistoryReconciled);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tmarkTreeSynced(sessionId, syncTree);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tawait recoverPiServerTreeDivergence(sessionId, context, syncTree, request, onHistoryReconciled);\n}\n\nfunction serializeOptions(options: SimpleStreamOptions | undefined): SimpleStreamOptions {\n\treturn {\n\t\ttemperature: options?.temperature,\n\t\tmaxTokens: options?.maxTokens,\n\t\treasoning: options?.reasoning,\n\t\tcacheRetention: options?.cacheRetention,\n\t\tsessionId: options?.sessionId,\n\t\tapiKey: options?.apiKey,\n\t\theaders: options?.headers,\n\t\tmetadata: options?.metadata,\n\t\ttransport: options?.transport,\n\t\tthinkingBudgets: options?.thinkingBudgets,\n\t\ttimeoutMs: options?.timeoutMs,\n\t\twebsocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,\n\t\tmaxRetries: options?.maxRetries,\n\t\tmaxRetryDelayMs: options?.maxRetryDelayMs,\n\t};\n}\n\nexport interface PiServerCompactOptions extends SimpleStreamOptions {\n\tcustomInstructions?: string;\n\tsettings?: unknown;\n\tpreparation?: CompactionPreparationOptions;\n\tsessionTree?: PiServerTreeSnapshot;\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;\n}\n\nexport async function compactPiServer(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions?: PiServerCompactOptions,\n): Promise<PiServerCompactionResult> {\n\tconst sessionId = options?.sessionId ?? \"default\";\n\tconst request = createPiServerRequest(options?.signal);\n\n\tawait ensureSessionInit(sessionId, context, request);\n\tconst tree = options?.sessionTree ?? getLinearTreeFromMessages(context.messages as Message[]);\n\tawait syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);\n\n\tconst makeBody = () => ({\n\t\tsessionId,\n\t\tmodel,\n\t\toptions: serializeOptions(options),\n\t\tsettings: options?.settings,\n\t\tpreparation: options?.preparation,\n\t\tcustomInstructions: options?.customInstructions,\n\t});\n\tlet response = await request.postJson(\"/api/session/compact\", makeBody());\n\tif (!response.ok) {\n\t\tlet failure = await readPiServerFailure(response);\n\t\tif (!options?.signal?.aborted && isRecoverableMissingServerState(response, failure.matchText)) {\n\t\t\tresetSessionTracking(sessionId);\n\t\t\tawait ensureSessionInit(sessionId, context, request);\n\t\t\tawait syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);\n\t\t\tresponse = await request.postJson(\"/api/session/compact\", makeBody());\n\t\t\tif (response.ok) {\n\t\t\t\tfailure = { details: \"\", matchText: \"\" };\n\t\t\t} else {\n\t\t\t\tfailure = await readPiServerFailure(response);\n\t\t\t}\n\t\t}\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Server compaction failed (${failure.details})`);\n\t\t}\n\t}\n\tconst result = await readPiServerJson<Partial<PiServerCompactionResult>>(response, \"Server compaction failed\");\n\tif (!result.compaction) {\n\t\tthrow new Error(\"Server compaction response did not include a compaction result\");\n\t}\n\tif (!result.compactionEntry || !result.entries || result.leafId === undefined || !result.messages) {\n\t\tthrow new Error(\"Server compaction response did not include the updated session tree\");\n\t}\n\tmarkTreeSynced(sessionId, { entries: result.entries, leafId: result.leafId });\n\treturn {\n\t\tcompaction: result.compaction,\n\t\tcompactionEntry: result.compactionEntry,\n\t\tentries: result.entries,\n\t\tleafId: result.leafId,\n\t\tmessages: result.messages,\n\t};\n}\n\nexport async function dropLastPiServerAssistantError(sessionId: string): Promise<void> {\n\tconst request = createPiServerRequest();\n\tconst response = await request.postJson(\"/api/session/drop-last-assistant-error\", { sessionId });\n\tawait readPiServerJson<unknown>(response, \"Dropping server assistant error failed\");\n}\n\nexport async function streamPiServer(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions?: PiServerStreamOptions,\n): Promise<PiServerEventStream> {\n\tconst sessionId = options?.sessionId ?? randomUUID();\n\tconst isEphemeralSession = options?.sessionId === undefined;\n\tconst stream = new PiServerEventStream();\n\n\tconst partial: AssistantMessage = {\n\t\trole: \"assistant\",\n\t\tstopReason: \"stop\",\n\t\tcontent: [],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\ttimestamp: Date.now(),\n\t};\n\n\t(async () => {\n\t\ttry {\n\t\t\tconst request = createPiServerRequest(options?.signal);\n\t\t\tawait ensureSessionInit(sessionId, context, request);\n\t\t\tconst tree = options?.sessionTree ?? {\n\t\t\t\t...getLinearTreeFromMessages(context.messages as Message[]),\n\t\t\t\treplace: true,\n\t\t\t};\n\t\t\tconst syncTree = tree;\n\t\t\tawait syncPiServerTreeWithRequest(sessionId, context, syncTree, request, options?.onHistoryReconciled);\n\n\t\t\tconst makeBody = () => ({\n\t\t\t\tsessionId,\n\t\t\t\tmodel,\n\t\t\t\toptions: serializeOptions(options),\n\t\t\t});\n\t\t\tlet response = await request.postJson(\"/api/stream\", makeBody());\n\n\t\t\tif (!response.ok) {\n\t\t\t\tlet failure = await readPiServerFailure(response);\n\t\t\t\tif (!options?.signal?.aborted && isRecoverableMissingServerState(response, failure.matchText)) {\n\t\t\t\t\tresetSessionTracking(sessionId);\n\t\t\t\t\tawait ensureSessionInit(sessionId, context, request);\n\t\t\t\t\tawait syncPiServerTreeWithRequest(sessionId, context, syncTree, request, options?.onHistoryReconciled);\n\t\t\t\t\tresponse = await request.postJson(\"/api/stream\", makeBody());\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tfailure = { details: \"\", matchText: \"\" };\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailure = await readPiServerFailure(response);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`pi-server error: ${failure.details}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait ensurePiServerEventStream(response);\n\t\t\tconst reader = response.body!.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request aborted by user\");\n\t\t\t\t}\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\tconst data = line.slice(6).trim();\n\t\t\t\t\t\tif (!data) continue;\n\t\t\t\t\t\tconst proxyEvent = JSON.parse(data) as ProxyAssistantMessageEvent;\n\t\t\t\t\t\tconst event = processProxyEvent(proxyEvent, partial);\n\t\t\t\t\t\tif (event) {\n\t\t\t\t\t\t\tstream.push(event);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request aborted by user\");\n\t\t\t}\n\n\t\t\tif (isEphemeralSession) {\n\t\t\t\tresetSessionTracking(sessionId);\n\t\t\t}\n\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tconst reason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\tpartial.stopReason = reason;\n\t\t\tpartial.errorMessage = errorMessage;\n\t\t\tstream.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason,\n\t\t\t\terror: partial,\n\t\t\t});\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n}\n\nfunction processProxyEvent(\n\tproxyEvent: ProxyAssistantMessageEvent,\n\tpartial: AssistantMessage,\n): AssistantMessageEvent | undefined {\n\tswitch (proxyEvent.type) {\n\t\tcase \"start\":\n\t\t\treturn { type: \"start\", partial };\n\t\tcase \"text_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = { type: \"text\", text: \"\" };\n\t\t\treturn { type: \"text_start\", contentIndex: proxyEvent.contentIndex, partial };\n\t\tcase \"text_delta\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"text\") {\n\t\t\t\tcontent.text += proxyEvent.delta;\n\t\t\t\treturn { type: \"text_delta\", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received text_delta for non-text content\");\n\t\t}\n\t\tcase \"text_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"text\") {\n\t\t\t\tcontent.textSignature = proxyEvent.contentSignature;\n\t\t\t\treturn { type: \"text_end\", contentIndex: proxyEvent.contentIndex, content: content.text, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received text_end for non-text content\");\n\t\t}\n\t\tcase \"thinking_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = { type: \"thinking\", thinking: \"\" };\n\t\t\treturn { type: \"thinking_start\", contentIndex: proxyEvent.contentIndex, partial };\n\t\tcase \"thinking_delta\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"thinking\") {\n\t\t\t\tcontent.thinking += proxyEvent.delta;\n\t\t\t\treturn { type: \"thinking_delta\", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received thinking_delta for non-thinking content\");\n\t\t}\n\t\tcase \"thinking_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"thinking\") {\n\t\t\t\tcontent.thinkingSignature = proxyEvent.contentSignature;\n\t\t\t\treturn { type: \"thinking_end\", contentIndex: proxyEvent.contentIndex, content: content.thinking, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received thinking_end for non-thinking content\");\n\t\t}\n\t\tcase \"toolcall_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tid: proxyEvent.id,\n\t\t\t\tname: proxyEvent.toolName,\n\t\t\t\targuments: {},\n\t\t\t\tpartialJson: \"\",\n\t\t\t} as any;\n\t\t\treturn { type: \"toolcall_start\", contentIndex: proxyEvent.contentIndex, partial };\n\t\tcase \"toolcall_delta\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"toolCall\") {\n\t\t\t\t(content as any).partialJson += proxyEvent.delta;\n\t\t\t\tcontent.arguments = parseStreamingJson((content as any).partialJson) || {};\n\t\t\t\tpartial.content[proxyEvent.contentIndex] = { ...content };\n\t\t\t\treturn { type: \"toolcall_delta\", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received toolcall_delta for non-toolCall content\");\n\t\t}\n\t\tcase \"toolcall_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"toolCall\") {\n\t\t\t\tdelete (content as any).partialJson;\n\t\t\t\treturn { type: \"toolcall_end\", contentIndex: proxyEvent.contentIndex, toolCall: content, partial };\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\t\tcase \"done\":\n\t\t\tpartial.stopReason = proxyEvent.reason;\n\t\t\tpartial.usage = proxyEvent.usage;\n\t\t\treturn { type: \"done\", reason: proxyEvent.reason, message: partial };\n\t\tcase \"error\":\n\t\t\tpartial.stopReason = proxyEvent.reason;\n\t\t\tpartial.errorMessage = proxyEvent.errorMessage;\n\t\t\tpartial.usage = proxyEvent.usage;\n\t\t\treturn { type: \"error\", reason: proxyEvent.reason, error: partial };\n\t\tdefault: {\n\t\t\tconst _exhaustiveCheck: never = proxyEvent;\n\t\t\tconsole.warn(`Unhandled proxy event type: ${(proxyEvent as any).type}`);\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n\nexport { getServerUrl, getAuthToken };\n"]}
1
+ {"version":3,"file":"pi-server-client.d.ts","sourceRoot":"","sources":["../../src/core/pi-server-client.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type {\n\tCompactionPreparationOptions,\n\tCompactResult,\n\tProxyAssistantMessageEvent,\n\tSessionTreeEntry,\n} from \"@earendil-works/pi-agent-core\";\nimport {\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tEventStream,\n\ttype Message,\n\ttype Model,\n\tparseStreamingJson,\n\ttype SimpleStreamOptions,\n} from \"@earendil-works/pi-ai\";\nimport {\n\tbuildPiServerTreePrefixHashes,\n\thashPiServerSessionEntries,\n\thashPiServerStaticContext,\n} from \"./pi-server-protocol.ts\";\nimport { ChunkRequest } from \"./pi-server-request.ts\";\n\nclass PiServerEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> {\n\tconstructor() {\n\t\tsuper(\n\t\t\t(event) => event.type === \"done\" || event.type === \"error\",\n\t\t\t(event) => {\n\t\t\t\tif (event.type === \"done\") return event.message;\n\t\t\t\tif (event.type === \"error\") return event.error;\n\t\t\t\tthrow new Error(\"Unexpected event type\");\n\t\t\t},\n\t\t);\n\t}\n}\n\nfunction getServerUrl(): string {\n\treturn process.env.PI_SERVER_URL ?? \"http://127.0.0.1:4217\";\n}\n\nfunction getAuthToken(): string {\n\treturn process.env.PI_SERVER_AUTH_TOKEN ?? \"\";\n}\n\nfunction createPiServerRequest(signal?: AbortSignal): ChunkRequest {\n\treturn new ChunkRequest({\n\t\tserverUrl: getServerUrl(),\n\t\tauthToken: getAuthToken(),\n\t\tsignal,\n\t});\n}\n\nconst sessionStaticContextHashes = new Map<string, string>();\nconst sessionSyncedEntryIds = new Map<string, Set<string>>();\nconst sessionTreeHashes = new Map<string, string>();\nconst sessionTreeEntryCounts = new Map<string, number>();\nconst sessionTreeLeafIds = new Map<string, string | null>();\nconst sessionHasTemporaryTree = new Set<string>();\nconst RESPONSE_BODY_EXCERPT_CHARS = 500;\n\nexport function hashStaticContext(ctx: Context): string {\n\treturn hashPiServerStaticContext(ctx);\n}\n\nfunction hashEntries(entries: SessionTreeEntry[]): string {\n\treturn hashPiServerSessionEntries(entries);\n}\n\nfunction getLinearTreeFromMessages(messages: Message[]): { entries: SessionTreeEntry[]; leafId: string | null } {\n\tlet parentId: string | null = null;\n\tconst entries = messages.map((message, index): SessionTreeEntry => {\n\t\tconst id = `message-${index}`;\n\t\tconst entry: SessionTreeEntry = {\n\t\t\ttype: \"message\",\n\t\t\tid,\n\t\t\tparentId,\n\t\t\ttimestamp: new Date(message.timestamp).toISOString(),\n\t\t\tmessage,\n\t\t};\n\t\tparentId = id;\n\t\treturn entry;\n\t});\n\treturn { entries, leafId: parentId };\n}\n\nexport function resetSessionTracking(sessionId: string): void {\n\tsessionStaticContextHashes.delete(sessionId);\n\tsessionSyncedEntryIds.delete(sessionId);\n\tsessionTreeHashes.delete(sessionId);\n\tsessionTreeEntryCounts.delete(sessionId);\n\tsessionTreeLeafIds.delete(sessionId);\n\tsessionHasTemporaryTree.delete(sessionId);\n}\n\nexport function resetAllSessionTracking(): void {\n\tsessionStaticContextHashes.clear();\n\tsessionSyncedEntryIds.clear();\n\tsessionTreeHashes.clear();\n\tsessionTreeEntryCounts.clear();\n\tsessionTreeLeafIds.clear();\n\tsessionHasTemporaryTree.clear();\n}\n\ninterface SessionInitResponse {\n\tsessionId: string;\n\tstaticContextHash: string;\n\ttreeHash?: string;\n\tmessageCount: number;\n\tentryCount?: number;\n\tleafId?: string | null;\n\trevision?: number;\n\tfromCache?: boolean;\n}\n\nexport interface PiServerTreeSnapshot {\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\treplace?: boolean;\n}\n\nexport interface PiServerCompactionResult {\n\tcompaction: CompactResult;\n\tcompactionEntry: SessionTreeEntry;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tmessages: Message[];\n}\n\nexport interface PiServerHistorySnapshot {\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\tmessages: Message[];\n}\n\nexport interface PiServerStreamOptions extends SimpleStreamOptions {\n\tsessionTree?: PiServerTreeSnapshot;\n\tephemeralMessages?: Message[];\n\tcontextOverlay?: Message[];\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;\n}\n\ninterface PiServerResponseFailure {\n\tdetails: string;\n\tmatchText: string;\n}\n\ninterface PiServerHistoryResponse extends Partial<PiServerHistorySnapshot> {\n\tsessionId: string;\n\ttreeHash?: string;\n\tentryCount?: number;\n\tleafId?: string | null;\n\ttreePatch?: PiServerTreePatch;\n}\n\ninterface PiServerTreePatch {\n\tentriesFrom: number;\n\tbaseRevision?: number;\n\tentries: SessionTreeEntry[];\n\tleafId: string | null;\n\trevision: number;\n}\n\ninterface PiServerCompactionResponse extends Partial<PiServerCompactionResult> {\n\ttreePatch?: PiServerTreePatch & { baseTreeHash?: string };\n}\n\ninterface PiServerRunResponse {\n\trunId: string;\n\tstatus: \"running\" | \"completed\" | \"failed\";\n\tmessage?: AssistantMessage;\n\terrorMessage?: string;\n}\n\ninterface PiServerSyncOptions {\n\tsignal?: AbortSignal;\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;\n}\n\ntype PiServerFailurePhase = \"session_init\" | \"tree_sync\" | \"provider_stream\" | \"history_reconcile\";\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction getResponseStatus(response: Response): string {\n\tconst status = response.statusText ? `${response.status} ${response.statusText}` : String(response.status);\n\treturn response.status >= 500 ? `${status} (server error)` : status;\n}\n\nfunction getResponseContentType(response: Response): string {\n\treturn response.headers.get(\"content-type\") ?? \"unknown\";\n}\n\nfunction getBodyExcerpt(text: string): string {\n\tconst trimmed = text.trim();\n\tif (!trimmed) return \"<empty>\";\n\tconst excerpt = trimmed.slice(0, RESPONSE_BODY_EXCERPT_CHARS);\n\treturn excerpt.length < trimmed.length ? `${excerpt}...` : excerpt;\n}\n\nfunction getJsonErrorText(text: string): string | undefined {\n\tif (!text) return undefined;\n\ttry {\n\t\tconst parsed = JSON.parse(text) as unknown;\n\t\tif (!isObject(parsed)) return undefined;\n\t\tconst error = parsed.error;\n\t\treturn typeof error === \"string\" && error.length > 0 ? error : undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction formatResponseDetails(response: Response, bodyText: string): string {\n\tconst body = getJsonErrorText(bodyText) ?? getBodyExcerpt(bodyText);\n\treturn `${getResponseStatus(response)}; content-type: ${getResponseContentType(response)}; body excerpt: ${body}`;\n}\n\nasync function readPiServerFailure(response: Response): Promise<PiServerResponseFailure> {\n\tconst bodyText = await response.text();\n\treturn {\n\t\tdetails: formatResponseDetails(response, bodyText),\n\t\tmatchText: getJsonErrorText(bodyText) ?? bodyText,\n\t};\n}\n\nasync function readPiServerJson<T>(response: Response, errorPrefix: string): Promise<T> {\n\tconst bodyText = await response.text();\n\tif (!response.ok) {\n\t\tthrow new Error(`${errorPrefix} (${formatResponseDetails(response, bodyText)})`);\n\t}\n\ttry {\n\t\treturn JSON.parse(bodyText) as T;\n\t} catch {\n\t\tthrow new Error(`${errorPrefix} (${formatResponseDetails(response, bodyText)}; expected JSON)`);\n\t}\n}\n\nasync function ensurePiServerEventStream(response: Response): Promise<void> {\n\tconst contentType = getResponseContentType(response);\n\tif (contentType.toLowerCase().split(\";\")[0]?.trim() === \"text/event-stream\") {\n\t\treturn;\n\t}\n\tconst bodyText = await response.text();\n\tthrow new Error(`pi-server error: ${formatResponseDetails(response, bodyText)}; expected text/event-stream`);\n}\n\nasync function ensureSessionInit(\n\tsessionId: string,\n\tcontext: Context,\n\trequest: ChunkRequest,\n): Promise<SessionInitResponse> {\n\tconst currentHash = hashStaticContext(context);\n\tconst previousHash = sessionStaticContextHashes.get(sessionId);\n\n\tif (previousHash === currentHash) {\n\t\treturn {\n\t\t\tsessionId,\n\t\t\tstaticContextHash: previousHash,\n\t\t\tmessageCount: 0,\n\t\t\tfromCache: true,\n\t\t};\n\t}\n\n\tconst staticContext = {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools,\n\t};\n\n\tconst endpoint = previousHash === undefined ? \"/api/session/init\" : \"/api/session/update\";\n\n\tconst response = await request.postJson(endpoint, { sessionId, staticContext });\n\n\tconst result = await readPiServerJson<SessionInitResponse>(response, \"Session init failed\");\n\tsessionStaticContextHashes.set(sessionId, currentHash);\n\tif (result.treeHash !== undefined) {\n\t\tsessionTreeHashes.set(sessionId, result.treeHash);\n\t\tsessionTreeLeafIds.set(sessionId, result.leafId ?? null);\n\t\tif (result.entryCount !== undefined) {\n\t\t\tsessionTreeEntryCounts.set(sessionId, result.entryCount);\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction getStaticContext(context: Context) {\n\treturn {\n\t\tsystemPrompt: context.systemPrompt,\n\t\ttools: context.tools,\n\t};\n}\n\nfunction markTreeSynced(sessionId: string, tree: PiServerTreeSnapshot): void {\n\tsessionSyncedEntryIds.set(sessionId, new Set(tree.entries.map((entry) => entry.id)));\n\tconst prefixHashes = buildPiServerTreePrefixHashes(tree.entries);\n\tsessionTreeHashes.set(sessionId, prefixHashes[prefixHashes.length - 1]);\n\tsessionTreeEntryCounts.set(sessionId, tree.entries.length);\n\tsessionTreeLeafIds.set(sessionId, tree.leafId);\n\tif (tree.replace) {\n\t\tsessionHasTemporaryTree.add(sessionId);\n\t} else {\n\t\tsessionHasTemporaryTree.delete(sessionId);\n\t}\n}\n\nasync function postTreeJson(\n\trequest: ChunkRequest,\n\tendpoint: string,\n\tbody: unknown,\n\terrorPrefix: string,\n): Promise<void> {\n\tconst response = await request.postJson(endpoint, body);\n\tawait readPiServerJson<unknown>(response, errorPrefix);\n}\n\nasync function fetchPiServerHistory(\n\tsessionId: string,\n\trequest: ChunkRequest,\n\tlocalTree?: PiServerTreeSnapshot,\n): Promise<PiServerHistorySnapshot | undefined> {\n\tconst prefixHashes = localTree ? buildPiServerTreePrefixHashes(localTree.entries) : undefined;\n\tconst historyUrl =\n\t\tlocalTree && prefixHashes\n\t\t\t? `/api/session/${encodeURIComponent(sessionId)}/history?entriesFrom=${localTree.entries.length}&baseTreeHash=${encodeURIComponent(prefixHashes[prefixHashes.length - 1])}`\n\t\t\t: `/api/session/${encodeURIComponent(sessionId)}/history`;\n\tconst response = await request.getJson(historyUrl);\n\tif (response.status === 404) {\n\t\treturn undefined;\n\t}\n\tconst history = await readPiServerJson<PiServerHistoryResponse>(response, \"Session history reconciliation failed\");\n\tif (history.treePatch && localTree) {\n\t\treturn {\n\t\t\tentries: [...localTree.entries.slice(0, history.treePatch.entriesFrom), ...history.treePatch.entries],\n\t\t\tleafId: history.treePatch.leafId,\n\t\t\tmessages: history.messages ?? [],\n\t\t};\n\t}\n\tif (!history.entries || history.leafId === undefined || !history.messages) {\n\t\tthrow new Error(\"Session history reconciliation failed (response did not include the session tree)\");\n\t}\n\treturn {\n\t\tentries: history.entries,\n\t\tleafId: history.leafId,\n\t\tmessages: history.messages,\n\t};\n}\n\nasync function applyPiServerHistory(\n\tsessionId: string,\n\tsnapshot: PiServerHistorySnapshot,\n\tonHistoryReconciled: ((snapshot: PiServerHistorySnapshot) => void | Promise<void>) | undefined,\n): Promise<void> {\n\tmarkTreeSynced(sessionId, snapshot);\n\tawait onHistoryReconciled?.(snapshot);\n}\n\nasync function fetchPiServerRun(\n\tsessionId: string,\n\trunId: string,\n\trequest: ChunkRequest,\n): Promise<PiServerRunResponse | undefined> {\n\tconst response = await request.getJson(\n\t\t`/api/session/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(runId)}`,\n\t);\n\tif (response.status === 404) {\n\t\treturn undefined;\n\t}\n\treturn readPiServerJson<PiServerRunResponse>(response, \"Session run recovery failed\");\n}\n\nfunction getKnownServerPrefixIds(sessionId: string, entries: SessionTreeEntry[]): Set<string> | undefined {\n\tconst entryCount = sessionTreeEntryCounts.get(sessionId);\n\tconst treeHash = sessionTreeHashes.get(sessionId);\n\tif (entryCount === undefined || treeHash === undefined || entryCount > entries.length) {\n\t\treturn undefined;\n\t}\n\tconst prefixHashes = buildPiServerTreePrefixHashes(entries);\n\tif (prefixHashes[entryCount] !== treeHash) {\n\t\treturn undefined;\n\t}\n\tconst ids = new Set(entries.slice(0, entryCount).map((entry) => entry.id));\n\tsessionSyncedEntryIds.set(sessionId, ids);\n\treturn ids;\n}\n\nfunction isRecoverableTreeDivergenceError(error: unknown): boolean {\n\tconst message = error instanceof Error ? error.message : String(error);\n\treturn /parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(message);\n}\n\nfunction isRecoverableMissingServerState(response: Response, errorBody: string): boolean {\n\tif (response.status !== 400 && response.status !== 404) return false;\n\treturn /Session has no static context|session not found|parent entry .* does not exist|leafId .* does not exist|entry .* already exists/i.test(\n\t\terrorBody,\n\t);\n}\n\nasync function syncFullPiServerTree(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\trequest: ChunkRequest,\n): Promise<void> {\n\tawait postTreeJson(\n\t\trequest,\n\t\t\"/api/session/tree/sync\",\n\t\t{\n\t\t\tsessionId,\n\t\t\tentries: tree.entries,\n\t\t\tleafId: tree.leafId,\n\t\t\tstaticContext: getStaticContext(context),\n\t\t},\n\t\t\"Session tree sync failed\",\n\t);\n\tmarkTreeSynced(sessionId, tree);\n}\n\nfunction shouldUseServerHistory(sessionId: string, tree: PiServerTreeSnapshot): boolean {\n\treturn !tree.replace && !sessionHasTemporaryTree.has(sessionId) && (sessionTreeEntryCounts.get(sessionId) ?? 0) > 0;\n}\n\nasync function recoverPiServerTreeDivergence(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\trequest: ChunkRequest,\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>,\n): Promise<void> {\n\tif (shouldUseServerHistory(sessionId, tree)) {\n\t\tconst snapshot = await fetchPiServerHistory(sessionId, request, tree);\n\t\tif (snapshot && snapshot.entries.length > 0) {\n\t\t\tawait applyPiServerHistory(sessionId, snapshot, onHistoryReconciled);\n\t\t\tthrow new Error(\n\t\t\t\t\"pi-server history differed from local history; local session was reconciled to server history\",\n\t\t\t);\n\t\t}\n\t}\n\n\tawait syncFullPiServerTree(sessionId, context, tree, request);\n}\n\nexport async function syncPiServerTree(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\toptions?: PiServerSyncOptions,\n): Promise<void> {\n\tconst request = createPiServerRequest(options?.signal);\n\tawait ensureSessionInit(sessionId, context, request);\n\tawait syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);\n}\n\nasync function syncPiServerTreeWithRequest(\n\tsessionId: string,\n\tcontext: Context,\n\ttree: PiServerTreeSnapshot,\n\trequest: ChunkRequest,\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>,\n): Promise<void> {\n\tconst syncTree = tree;\n\tconst currentHash = hashEntries(syncTree.entries);\n\tconst previousHash = sessionTreeHashes.get(sessionId);\n\tconst previousLeafId = sessionTreeLeafIds.get(sessionId);\n\n\tif (!tree.replace && previousHash === currentHash) {\n\t\tif (previousLeafId !== syncTree.leafId) {\n\t\t\ttry {\n\t\t\t\tawait postTreeJson(\n\t\t\t\t\trequest,\n\t\t\t\t\t\"/api/session/tree/switch\",\n\t\t\t\t\t{ sessionId, leafId: syncTree.leafId },\n\t\t\t\t\t\"Session tree switch failed\",\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (!isRecoverableTreeDivergenceError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tawait recoverPiServerTreeDivergence(sessionId, context, syncTree, request, onHistoryReconciled);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsessionTreeLeafIds.set(sessionId, syncTree.leafId);\n\t\t}\n\t\tmarkTreeSynced(sessionId, syncTree);\n\t\treturn;\n\t}\n\n\tconst syncedIds =\n\t\t!tree.replace && !sessionHasTemporaryTree.has(sessionId)\n\t\t\t? (sessionSyncedEntryIds.get(sessionId) ?? getKnownServerPrefixIds(sessionId, syncTree.entries))\n\t\t\t: undefined;\n\tif (syncedIds) {\n\t\tconst deltaEntries = syncTree.entries.filter((entry) => !syncedIds.has(entry.id));\n\t\tif (deltaEntries.length > 0) {\n\t\t\ttry {\n\t\t\t\tawait postTreeJson(\n\t\t\t\t\trequest,\n\t\t\t\t\t\"/api/session/tree/append\",\n\t\t\t\t\t{\n\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\tentries: deltaEntries,\n\t\t\t\t\t\tleafId: syncTree.leafId,\n\t\t\t\t\t\tstaticContext: getStaticContext(context),\n\t\t\t\t\t},\n\t\t\t\t\t\"Session tree append failed\",\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tif (!isRecoverableTreeDivergenceError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tawait recoverPiServerTreeDivergence(sessionId, context, syncTree, request, onHistoryReconciled);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tmarkTreeSynced(sessionId, syncTree);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tawait recoverPiServerTreeDivergence(sessionId, context, syncTree, request, onHistoryReconciled);\n}\n\nfunction serializeOptions(options: SimpleStreamOptions | undefined): SimpleStreamOptions {\n\treturn {\n\t\ttemperature: options?.temperature,\n\t\tmaxTokens: options?.maxTokens,\n\t\treasoning: options?.reasoning,\n\t\tcacheRetention: options?.cacheRetention,\n\t\tsessionId: options?.sessionId,\n\t\tapiKey: options?.apiKey,\n\t\theaders: options?.headers,\n\t\tmetadata: options?.metadata,\n\t\ttransport: options?.transport,\n\t\tthinkingBudgets: options?.thinkingBudgets,\n\t\ttimeoutMs: options?.timeoutMs,\n\t\twebsocketConnectTimeoutMs: options?.websocketConnectTimeoutMs,\n\t\tmaxRetries: options?.maxRetries,\n\t\tmaxRetryDelayMs: options?.maxRetryDelayMs,\n\t};\n}\n\nexport interface PiServerCompactOptions extends SimpleStreamOptions {\n\tcustomInstructions?: string;\n\tsettings?: unknown;\n\tpreparation?: CompactionPreparationOptions;\n\tsessionTree?: PiServerTreeSnapshot;\n\tonHistoryReconciled?: (snapshot: PiServerHistorySnapshot) => void | Promise<void>;\n}\n\nexport async function compactPiServer(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions?: PiServerCompactOptions,\n): Promise<PiServerCompactionResult> {\n\tconst sessionId = options?.sessionId ?? \"default\";\n\tconst request = createPiServerRequest(options?.signal);\n\n\tawait ensureSessionInit(sessionId, context, request);\n\tconst tree = options?.sessionTree ?? getLinearTreeFromMessages(context.messages as Message[]);\n\tawait syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);\n\n\tconst makeBody = () => ({\n\t\tsessionId,\n\t\tmodel,\n\t\toptions: serializeOptions(options),\n\t\tsettings: options?.settings,\n\t\tpreparation: options?.preparation,\n\t\tcustomInstructions: options?.customInstructions,\n\t\tbaseTreeHash: sessionTreeHashes.get(sessionId) ?? hashEntries(tree.entries),\n\t});\n\tlet response = await request.postJson(\"/api/session/compact\", makeBody());\n\tif (!response.ok) {\n\t\tlet failure = await readPiServerFailure(response);\n\t\tif (!options?.signal?.aborted && isRecoverableMissingServerState(response, failure.matchText)) {\n\t\t\tresetSessionTracking(sessionId);\n\t\t\tawait ensureSessionInit(sessionId, context, request);\n\t\t\tawait syncPiServerTreeWithRequest(sessionId, context, tree, request, options?.onHistoryReconciled);\n\t\t\tresponse = await request.postJson(\"/api/session/compact\", makeBody());\n\t\t\tif (response.ok) {\n\t\t\t\tfailure = { details: \"\", matchText: \"\" };\n\t\t\t} else {\n\t\t\t\tfailure = await readPiServerFailure(response);\n\t\t\t}\n\t\t}\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Server compaction failed (${failure.details})`);\n\t\t}\n\t}\n\tconst result = await readPiServerJson<PiServerCompactionResponse>(response, \"Server compaction failed\");\n\tif (!result.compaction) {\n\t\tthrow new Error(\"Server compaction response did not include a compaction result\");\n\t}\n\tconst resultEntries =\n\t\tresult.entries ??\n\t\t(result.treePatch\n\t\t\t? [...tree.entries.slice(0, result.treePatch.entriesFrom), ...result.treePatch.entries]\n\t\t\t: undefined);\n\tconst resultLeafId = result.leafId ?? result.treePatch?.leafId;\n\tif (!result.compactionEntry || !resultEntries || resultLeafId === undefined) {\n\t\tthrow new Error(\"Server compaction response did not include the updated session tree\");\n\t}\n\tmarkTreeSynced(sessionId, { entries: resultEntries, leafId: resultLeafId });\n\treturn {\n\t\tcompaction: result.compaction,\n\t\tcompactionEntry: result.compactionEntry,\n\t\tentries: resultEntries,\n\t\tleafId: resultLeafId,\n\t\tmessages: result.messages ?? [],\n\t};\n}\n\nexport async function dropLastPiServerAssistantError(sessionId: string): Promise<void> {\n\tconst request = createPiServerRequest();\n\tconst response = await request.postJson(\"/api/session/drop-last-assistant-error\", { sessionId });\n\tawait readPiServerJson<unknown>(response, \"Dropping server assistant error failed\");\n}\n\nexport async function streamPiServer(\n\tmodel: Model<any>,\n\tcontext: Context,\n\toptions?: PiServerStreamOptions,\n): Promise<PiServerEventStream> {\n\tconst sessionId = options?.sessionId ?? randomUUID();\n\tconst runId = randomUUID();\n\tconst isEphemeralSession = options?.sessionId === undefined;\n\tconst stream = new PiServerEventStream();\n\n\tconst partial: AssistantMessage = {\n\t\trole: \"assistant\",\n\t\tstopReason: \"stop\",\n\t\tcontent: [],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\ttimestamp: Date.now(),\n\t};\n\n\t(async () => {\n\t\tlet phase: PiServerFailurePhase = \"session_init\";\n\t\ttry {\n\t\t\tconst request = createPiServerRequest(options?.signal);\n\t\t\tawait ensureSessionInit(sessionId, context, request);\n\t\t\tconst tree = options?.sessionTree ?? {\n\t\t\t\t...getLinearTreeFromMessages(context.messages as Message[]),\n\t\t\t\treplace: true,\n\t\t\t};\n\t\t\tconst syncTree = tree;\n\t\t\tphase = \"tree_sync\";\n\t\t\tawait syncPiServerTreeWithRequest(sessionId, context, syncTree, request, options?.onHistoryReconciled);\n\n\t\t\tconst makeBody = () => ({\n\t\t\t\tsessionId,\n\t\t\t\trunId,\n\t\t\t\tmodel,\n\t\t\t\toptions: serializeOptions(options),\n\t\t\t\tephemeralMessages: options?.ephemeralMessages,\n\t\t\t\tcontextOverlay: options?.contextOverlay,\n\t\t\t});\n\t\t\tphase = \"provider_stream\";\n\t\t\tlet response = await request.postJson(\"/api/stream\", makeBody());\n\n\t\t\tif (!response.ok) {\n\t\t\t\tlet failure = await readPiServerFailure(response);\n\t\t\t\tif (!options?.signal?.aborted && isRecoverableMissingServerState(response, failure.matchText)) {\n\t\t\t\t\tresetSessionTracking(sessionId);\n\t\t\t\t\tawait ensureSessionInit(sessionId, context, request);\n\t\t\t\t\tawait syncPiServerTreeWithRequest(sessionId, context, syncTree, request, options?.onHistoryReconciled);\n\t\t\t\t\tresponse = await request.postJson(\"/api/stream\", makeBody());\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tfailure = { details: \"\", matchText: \"\" };\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfailure = await readPiServerFailure(response);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`pi-server error: ${failure.details}`);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tawait ensurePiServerEventStream(response);\n\t\t\tconst reader = response.body!.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await reader.read();\n\t\t\t\tif (done) break;\n\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request aborted by user\");\n\t\t\t\t}\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\tconst data = line.slice(6).trim();\n\t\t\t\t\t\tif (!data) continue;\n\t\t\t\t\t\tconst proxyEvent = JSON.parse(data) as ProxyAssistantMessageEvent;\n\t\t\t\t\t\tconst event = processProxyEvent(proxyEvent, partial);\n\t\t\t\t\t\tif (event) {\n\t\t\t\t\t\t\tstream.push(event);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request aborted by user\");\n\t\t\t}\n\n\t\t\tif (isEphemeralSession) {\n\t\t\t\tresetSessionTracking(sessionId);\n\t\t\t}\n\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\tconst errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tif (!options?.signal?.aborted && phase === \"provider_stream\") {\n\t\t\t\ttry {\n\t\t\t\t\tconst recoveredRun = await fetchPiServerRun(sessionId, runId, createPiServerRequest());\n\t\t\t\t\tif (recoveredRun?.status === \"completed\" && recoveredRun.message) {\n\t\t\t\t\t\tswitch (recoveredRun.message.stopReason) {\n\t\t\t\t\t\t\tcase \"stop\":\n\t\t\t\t\t\t\tcase \"length\":\n\t\t\t\t\t\t\tcase \"toolUse\":\n\t\t\t\t\t\t\t\tstream.push({\n\t\t\t\t\t\t\t\t\ttype: \"done\",\n\t\t\t\t\t\t\t\t\treason: recoveredRun.message.stopReason,\n\t\t\t\t\t\t\t\t\tmessage: recoveredRun.message,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tcase \"aborted\":\n\t\t\t\t\t\t\t\tstream.push({ type: \"error\", reason: \"aborted\", error: recoveredRun.message });\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\tcase \"error\":\n\t\t\t\t\t\t\t\tstream.push({ type: \"error\", reason: \"error\", error: recoveredRun.message });\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstream.end();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Keep the original stream error authoritative.\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst reason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\tpartial.stopReason = reason;\n\t\t\tpartial.errorMessage = errorMessage;\n\t\t\tpartial.diagnostics = [\n\t\t\t\t...(partial.diagnostics ?? []),\n\t\t\t\t{\n\t\t\t\t\ttype: \"pi_server_failure\",\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\terror: { name: error instanceof Error ? error.name : \"Error\", message: errorMessage },\n\t\t\t\t\tdetails: { phase, source: \"pi-server\", retryable: phase === \"provider_stream\" },\n\t\t\t\t},\n\t\t\t];\n\t\t\tstream.push({\n\t\t\t\ttype: \"error\",\n\t\t\t\treason,\n\t\t\t\terror: partial,\n\t\t\t});\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n}\n\nfunction processProxyEvent(\n\tproxyEvent: ProxyAssistantMessageEvent,\n\tpartial: AssistantMessage,\n): AssistantMessageEvent | undefined {\n\tswitch (proxyEvent.type) {\n\t\tcase \"start\":\n\t\t\treturn { type: \"start\", partial };\n\t\tcase \"text_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = { type: \"text\", text: \"\" };\n\t\t\treturn { type: \"text_start\", contentIndex: proxyEvent.contentIndex, partial };\n\t\tcase \"text_delta\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"text\") {\n\t\t\t\tcontent.text += proxyEvent.delta;\n\t\t\t\treturn { type: \"text_delta\", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received text_delta for non-text content\");\n\t\t}\n\t\tcase \"text_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"text\") {\n\t\t\t\tcontent.textSignature = proxyEvent.contentSignature;\n\t\t\t\treturn { type: \"text_end\", contentIndex: proxyEvent.contentIndex, content: content.text, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received text_end for non-text content\");\n\t\t}\n\t\tcase \"thinking_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = { type: \"thinking\", thinking: \"\" };\n\t\t\treturn { type: \"thinking_start\", contentIndex: proxyEvent.contentIndex, partial };\n\t\tcase \"thinking_delta\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"thinking\") {\n\t\t\t\tcontent.thinking += proxyEvent.delta;\n\t\t\t\treturn { type: \"thinking_delta\", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received thinking_delta for non-thinking content\");\n\t\t}\n\t\tcase \"thinking_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"thinking\") {\n\t\t\t\tcontent.thinkingSignature = proxyEvent.contentSignature;\n\t\t\t\treturn { type: \"thinking_end\", contentIndex: proxyEvent.contentIndex, content: content.thinking, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received thinking_end for non-thinking content\");\n\t\t}\n\t\tcase \"toolcall_start\":\n\t\t\tpartial.content[proxyEvent.contentIndex] = {\n\t\t\t\ttype: \"toolCall\",\n\t\t\t\tid: proxyEvent.id,\n\t\t\t\tname: proxyEvent.toolName,\n\t\t\t\targuments: {},\n\t\t\t\tpartialJson: \"\",\n\t\t\t} as any;\n\t\t\treturn { type: \"toolcall_start\", contentIndex: proxyEvent.contentIndex, partial };\n\t\tcase \"toolcall_delta\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"toolCall\") {\n\t\t\t\t(content as any).partialJson += proxyEvent.delta;\n\t\t\t\tcontent.arguments = parseStreamingJson((content as any).partialJson) || {};\n\t\t\t\tpartial.content[proxyEvent.contentIndex] = { ...content };\n\t\t\t\treturn { type: \"toolcall_delta\", contentIndex: proxyEvent.contentIndex, delta: proxyEvent.delta, partial };\n\t\t\t}\n\t\t\tthrow new Error(\"Received toolcall_delta for non-toolCall content\");\n\t\t}\n\t\tcase \"toolcall_end\": {\n\t\t\tconst content = partial.content[proxyEvent.contentIndex];\n\t\t\tif (content?.type === \"toolCall\") {\n\t\t\t\tdelete (content as any).partialJson;\n\t\t\t\treturn { type: \"toolcall_end\", contentIndex: proxyEvent.contentIndex, toolCall: content, partial };\n\t\t\t}\n\t\t\treturn undefined;\n\t\t}\n\t\tcase \"done\":\n\t\t\tpartial.stopReason = proxyEvent.reason;\n\t\t\tpartial.usage = proxyEvent.usage;\n\t\t\treturn { type: \"done\", reason: proxyEvent.reason, message: partial };\n\t\tcase \"error\":\n\t\t\tpartial.stopReason = proxyEvent.reason;\n\t\t\tpartial.errorMessage = proxyEvent.errorMessage;\n\t\t\tpartial.usage = proxyEvent.usage;\n\t\t\treturn { type: \"error\", reason: proxyEvent.reason, error: partial };\n\t\tdefault: {\n\t\t\tconst _exhaustiveCheck: never = proxyEvent;\n\t\t\tconsole.warn(`Unhandled proxy event type: ${(proxyEvent as any).type}`);\n\t\t\treturn undefined;\n\t\t}\n\t}\n}\n\nexport { getServerUrl, getAuthToken };\n"]}
@@ -1,5 +1,6 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { EventStream, parseStreamingJson, } from "@earendil-works/pi-ai";
3
+ import { buildPiServerTreePrefixHashes, hashPiServerSessionEntries, hashPiServerStaticContext, } from "./pi-server-protocol.js";
3
4
  import { ChunkRequest } from "./pi-server-request.js";
4
5
  class PiServerEventStream extends EventStream {
5
6
  constructor() {
@@ -33,16 +34,10 @@ const sessionTreeLeafIds = new Map();
33
34
  const sessionHasTemporaryTree = new Set();
34
35
  const RESPONSE_BODY_EXCERPT_CHARS = 500;
35
36
  export function hashStaticContext(ctx) {
36
- const parts = [];
37
- if (ctx.systemPrompt !== undefined)
38
- parts.push(`sp:${ctx.systemPrompt}`);
39
- if (ctx.tools) {
40
- parts.push(`t:${JSON.stringify(ctx.tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })))}`);
41
- }
42
- return parts.join("|");
37
+ return hashPiServerStaticContext(ctx);
43
38
  }
44
39
  function hashEntries(entries) {
45
- return createHash("sha256").update(JSON.stringify(entries)).digest("hex");
40
+ return hashPiServerSessionEntries(entries);
46
41
  }
47
42
  function getLinearTreeFromMessages(messages) {
48
43
  let parentId = null;
@@ -174,7 +169,8 @@ function getStaticContext(context) {
174
169
  }
175
170
  function markTreeSynced(sessionId, tree) {
176
171
  sessionSyncedEntryIds.set(sessionId, new Set(tree.entries.map((entry) => entry.id)));
177
- sessionTreeHashes.set(sessionId, hashEntries(tree.entries));
172
+ const prefixHashes = buildPiServerTreePrefixHashes(tree.entries);
173
+ sessionTreeHashes.set(sessionId, prefixHashes[prefixHashes.length - 1]);
178
174
  sessionTreeEntryCounts.set(sessionId, tree.entries.length);
179
175
  sessionTreeLeafIds.set(sessionId, tree.leafId);
180
176
  if (tree.replace) {
@@ -188,12 +184,23 @@ async function postTreeJson(request, endpoint, body, errorPrefix) {
188
184
  const response = await request.postJson(endpoint, body);
189
185
  await readPiServerJson(response, errorPrefix);
190
186
  }
191
- async function fetchPiServerHistory(sessionId, request) {
192
- const response = await request.getJson(`/api/session/${encodeURIComponent(sessionId)}/history`);
187
+ async function fetchPiServerHistory(sessionId, request, localTree) {
188
+ const prefixHashes = localTree ? buildPiServerTreePrefixHashes(localTree.entries) : undefined;
189
+ const historyUrl = localTree && prefixHashes
190
+ ? `/api/session/${encodeURIComponent(sessionId)}/history?entriesFrom=${localTree.entries.length}&baseTreeHash=${encodeURIComponent(prefixHashes[prefixHashes.length - 1])}`
191
+ : `/api/session/${encodeURIComponent(sessionId)}/history`;
192
+ const response = await request.getJson(historyUrl);
193
193
  if (response.status === 404) {
194
194
  return undefined;
195
195
  }
196
196
  const history = await readPiServerJson(response, "Session history reconciliation failed");
197
+ if (history.treePatch && localTree) {
198
+ return {
199
+ entries: [...localTree.entries.slice(0, history.treePatch.entriesFrom), ...history.treePatch.entries],
200
+ leafId: history.treePatch.leafId,
201
+ messages: history.messages ?? [],
202
+ };
203
+ }
197
204
  if (!history.entries || history.leafId === undefined || !history.messages) {
198
205
  throw new Error("Session history reconciliation failed (response did not include the session tree)");
199
206
  }
@@ -207,17 +214,24 @@ async function applyPiServerHistory(sessionId, snapshot, onHistoryReconciled) {
207
214
  markTreeSynced(sessionId, snapshot);
208
215
  await onHistoryReconciled?.(snapshot);
209
216
  }
217
+ async function fetchPiServerRun(sessionId, runId, request) {
218
+ const response = await request.getJson(`/api/session/${encodeURIComponent(sessionId)}/runs/${encodeURIComponent(runId)}`);
219
+ if (response.status === 404) {
220
+ return undefined;
221
+ }
222
+ return readPiServerJson(response, "Session run recovery failed");
223
+ }
210
224
  function getKnownServerPrefixIds(sessionId, entries) {
211
225
  const entryCount = sessionTreeEntryCounts.get(sessionId);
212
226
  const treeHash = sessionTreeHashes.get(sessionId);
213
227
  if (entryCount === undefined || treeHash === undefined || entryCount > entries.length) {
214
228
  return undefined;
215
229
  }
216
- const prefix = entries.slice(0, entryCount);
217
- if (hashEntries(prefix) !== treeHash) {
230
+ const prefixHashes = buildPiServerTreePrefixHashes(entries);
231
+ if (prefixHashes[entryCount] !== treeHash) {
218
232
  return undefined;
219
233
  }
220
- const ids = new Set(prefix.map((entry) => entry.id));
234
+ const ids = new Set(entries.slice(0, entryCount).map((entry) => entry.id));
221
235
  sessionSyncedEntryIds.set(sessionId, ids);
222
236
  return ids;
223
237
  }
@@ -244,7 +258,7 @@ function shouldUseServerHistory(sessionId, tree) {
244
258
  }
245
259
  async function recoverPiServerTreeDivergence(sessionId, context, tree, request, onHistoryReconciled) {
246
260
  if (shouldUseServerHistory(sessionId, tree)) {
247
- const snapshot = await fetchPiServerHistory(sessionId, request);
261
+ const snapshot = await fetchPiServerHistory(sessionId, request, tree);
248
262
  if (snapshot && snapshot.entries.length > 0) {
249
263
  await applyPiServerHistory(sessionId, snapshot, onHistoryReconciled);
250
264
  throw new Error("pi-server history differed from local history; local session was reconciled to server history");
@@ -337,6 +351,7 @@ export async function compactPiServer(model, context, options) {
337
351
  settings: options?.settings,
338
352
  preparation: options?.preparation,
339
353
  customInstructions: options?.customInstructions,
354
+ baseTreeHash: sessionTreeHashes.get(sessionId) ?? hashEntries(tree.entries),
340
355
  });
341
356
  let response = await request.postJson("/api/session/compact", makeBody());
342
357
  if (!response.ok) {
@@ -361,16 +376,21 @@ export async function compactPiServer(model, context, options) {
361
376
  if (!result.compaction) {
362
377
  throw new Error("Server compaction response did not include a compaction result");
363
378
  }
364
- if (!result.compactionEntry || !result.entries || result.leafId === undefined || !result.messages) {
379
+ const resultEntries = result.entries ??
380
+ (result.treePatch
381
+ ? [...tree.entries.slice(0, result.treePatch.entriesFrom), ...result.treePatch.entries]
382
+ : undefined);
383
+ const resultLeafId = result.leafId ?? result.treePatch?.leafId;
384
+ if (!result.compactionEntry || !resultEntries || resultLeafId === undefined) {
365
385
  throw new Error("Server compaction response did not include the updated session tree");
366
386
  }
367
- markTreeSynced(sessionId, { entries: result.entries, leafId: result.leafId });
387
+ markTreeSynced(sessionId, { entries: resultEntries, leafId: resultLeafId });
368
388
  return {
369
389
  compaction: result.compaction,
370
390
  compactionEntry: result.compactionEntry,
371
- entries: result.entries,
372
- leafId: result.leafId,
373
- messages: result.messages,
391
+ entries: resultEntries,
392
+ leafId: resultLeafId,
393
+ messages: result.messages ?? [],
374
394
  };
375
395
  }
376
396
  export async function dropLastPiServerAssistantError(sessionId) {
@@ -380,6 +400,7 @@ export async function dropLastPiServerAssistantError(sessionId) {
380
400
  }
381
401
  export async function streamPiServer(model, context, options) {
382
402
  const sessionId = options?.sessionId ?? randomUUID();
403
+ const runId = randomUUID();
383
404
  const isEphemeralSession = options?.sessionId === undefined;
384
405
  const stream = new PiServerEventStream();
385
406
  const partial = {
@@ -400,6 +421,7 @@ export async function streamPiServer(model, context, options) {
400
421
  timestamp: Date.now(),
401
422
  };
402
423
  (async () => {
424
+ let phase = "session_init";
403
425
  try {
404
426
  const request = createPiServerRequest(options?.signal);
405
427
  await ensureSessionInit(sessionId, context, request);
@@ -408,12 +430,17 @@ export async function streamPiServer(model, context, options) {
408
430
  replace: true,
409
431
  };
410
432
  const syncTree = tree;
433
+ phase = "tree_sync";
411
434
  await syncPiServerTreeWithRequest(sessionId, context, syncTree, request, options?.onHistoryReconciled);
412
435
  const makeBody = () => ({
413
436
  sessionId,
437
+ runId,
414
438
  model,
415
439
  options: serializeOptions(options),
440
+ ephemeralMessages: options?.ephemeralMessages,
441
+ contextOverlay: options?.contextOverlay,
416
442
  });
443
+ phase = "provider_stream";
417
444
  let response = await request.postJson("/api/stream", makeBody());
418
445
  if (!response.ok) {
419
446
  let failure = await readPiServerFailure(response);
@@ -470,9 +497,47 @@ export async function streamPiServer(model, context, options) {
470
497
  }
471
498
  catch (error) {
472
499
  const errorMessage = error instanceof Error ? error.message : String(error);
500
+ if (!options?.signal?.aborted && phase === "provider_stream") {
501
+ try {
502
+ const recoveredRun = await fetchPiServerRun(sessionId, runId, createPiServerRequest());
503
+ if (recoveredRun?.status === "completed" && recoveredRun.message) {
504
+ switch (recoveredRun.message.stopReason) {
505
+ case "stop":
506
+ case "length":
507
+ case "toolUse":
508
+ stream.push({
509
+ type: "done",
510
+ reason: recoveredRun.message.stopReason,
511
+ message: recoveredRun.message,
512
+ });
513
+ break;
514
+ case "aborted":
515
+ stream.push({ type: "error", reason: "aborted", error: recoveredRun.message });
516
+ break;
517
+ case "error":
518
+ stream.push({ type: "error", reason: "error", error: recoveredRun.message });
519
+ break;
520
+ }
521
+ stream.end();
522
+ return;
523
+ }
524
+ }
525
+ catch {
526
+ // Keep the original stream error authoritative.
527
+ }
528
+ }
473
529
  const reason = options?.signal?.aborted ? "aborted" : "error";
474
530
  partial.stopReason = reason;
475
531
  partial.errorMessage = errorMessage;
532
+ partial.diagnostics = [
533
+ ...(partial.diagnostics ?? []),
534
+ {
535
+ type: "pi_server_failure",
536
+ timestamp: Date.now(),
537
+ error: { name: error instanceof Error ? error.name : "Error", message: errorMessage },
538
+ details: { phase, source: "pi-server", retryable: phase === "provider_stream" },
539
+ },
540
+ ];
476
541
  stream.push({
477
542
  type: "error",
478
543
  reason,