@anureo/adapter-pi 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +119 -2
- package/package.json +64 -4
- package/protocol/1.5.2/run.json +792 -0
- package/src/adapter.d.ts +212 -0
- package/src/adapter.js +730 -0
- package/src/catalog.d.ts +17 -0
- package/src/catalog.js +23 -0
- package/src/host.d.ts +257 -0
- package/src/host.js +361 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +6 -0
- package/src/mapping.d.ts +104 -0
- package/src/mapping.js +324 -0
- package/src/options.d.ts +20 -0
- package/src/options.js +0 -0
- package/src/profile.d.ts +28 -0
- package/src/profile.js +43 -0
package/src/catalog.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type PiHostFactory, type PiHostModel } from "./host.js";
|
|
2
|
+
/** One Pi model with the picker metadata Anureo keeps. */
|
|
3
|
+
export type PiModel = PiHostModel;
|
|
4
|
+
/**
|
|
5
|
+
* Discover models available with the configured Pi credentials. The factory receives a request for
|
|
6
|
+
* a new in-memory host. cwd defaults to process.cwd(), and createHost defaults to createPiHost.
|
|
7
|
+
* A missing or empty agentDir lets the factory use its default. After acquisition, await host
|
|
8
|
+
* disposal even if listing rejects. If listing and disposal both fail, disposal wins. The caller
|
|
9
|
+
* receives models and has no host to release.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createPiCatalogSession(options?: {
|
|
12
|
+
cwd?: string;
|
|
13
|
+
agentDir?: string;
|
|
14
|
+
createHost?: PiHostFactory;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
models: PiModel[];
|
|
17
|
+
}>;
|
package/src/catalog.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createPiHost } from "./host.js";
|
|
2
|
+
/**
|
|
3
|
+
* Discover models available with the configured Pi credentials. The factory receives a request for
|
|
4
|
+
* a new in-memory host. cwd defaults to process.cwd(), and createHost defaults to createPiHost.
|
|
5
|
+
* A missing or empty agentDir lets the factory use its default. After acquisition, await host
|
|
6
|
+
* disposal even if listing rejects. If listing and disposal both fail, disposal wins. The caller
|
|
7
|
+
* receives models and has no host to release.
|
|
8
|
+
*/
|
|
9
|
+
export async function createPiCatalogSession(options = {}) {
|
|
10
|
+
const factory = options.createHost ?? createPiHost;
|
|
11
|
+
const host = await factory({
|
|
12
|
+
cwd: options.cwd ?? process.cwd(),
|
|
13
|
+
mode: 'new',
|
|
14
|
+
persist: false,
|
|
15
|
+
...(options.agentDir ? { agentDir: options.agentDir } : {}),
|
|
16
|
+
});
|
|
17
|
+
try {
|
|
18
|
+
return { models: await host.listModels() };
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await host.dispose();
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/host.d.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the adapter's native Pi SDK integration and normalization boundary. Adapter runtime code
|
|
3
|
+
* consumes PiSessionHost; injected hosts can provide the same contract without native SDK objects.
|
|
4
|
+
*/
|
|
5
|
+
import type { AgentSessionEvent } from '@earendil-works/pi-coding-agent';
|
|
6
|
+
import type { StructuredOutputToolDefinition } from '@anureo/sdk';
|
|
7
|
+
/** Normalized message blocks; tool arguments remain unredacted until adapter mapping. */
|
|
8
|
+
export type PiHostContent = {
|
|
9
|
+
type: 'text';
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'thinking';
|
|
13
|
+
thinking: string;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'image';
|
|
16
|
+
data?: string;
|
|
17
|
+
mimeType?: string;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'toolCall';
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
arguments: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
/** Native token counts, including separate cache reads and writes. Pi reports the total separately. */
|
|
25
|
+
export interface PiHostUsage {
|
|
26
|
+
input: number;
|
|
27
|
+
output: number;
|
|
28
|
+
cacheRead: number;
|
|
29
|
+
cacheWrite: number;
|
|
30
|
+
totalTokens: number;
|
|
31
|
+
}
|
|
32
|
+
/** Message snapshot for live publication and history mapping. Redact sensitive content before publishing it. */
|
|
33
|
+
export interface PiHostMessage {
|
|
34
|
+
role: string;
|
|
35
|
+
content: string | PiHostContent[];
|
|
36
|
+
/** On toolResult messages, identifies the earlier tool call whose history item is completed. */
|
|
37
|
+
toolCallId?: string;
|
|
38
|
+
toolName?: string;
|
|
39
|
+
isError?: boolean;
|
|
40
|
+
stopReason?: string;
|
|
41
|
+
/** Raw native error text. PiBackendSession publishes a sanitized error instead. */
|
|
42
|
+
errorMessage?: string;
|
|
43
|
+
usage?: PiHostUsage;
|
|
44
|
+
model?: string;
|
|
45
|
+
provider?: string;
|
|
46
|
+
/** Native timestamp in milliseconds since the Unix epoch. */
|
|
47
|
+
timestamp?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Ordered, unredacted notifications for PiBackendSession. It treats agent_end as the end of one
|
|
51
|
+
* attempt and uses settled to finish the SDK turn after retries and queued continuations.
|
|
52
|
+
*/
|
|
53
|
+
export type PiHostEvent = {
|
|
54
|
+
type: 'agent_start';
|
|
55
|
+
} | {
|
|
56
|
+
type: 'agent_end';
|
|
57
|
+
willRetry: boolean;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'settled';
|
|
60
|
+
aborted: boolean;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'message_start';
|
|
63
|
+
message: PiHostMessage;
|
|
64
|
+
} | {
|
|
65
|
+
type: 'message_update';
|
|
66
|
+
message: PiHostMessage;
|
|
67
|
+
update: {
|
|
68
|
+
kind: 'text' | 'thinking' | 'toolcall';
|
|
69
|
+
phase: 'start' | 'delta' | 'end';
|
|
70
|
+
index: number;
|
|
71
|
+
delta?: string;
|
|
72
|
+
};
|
|
73
|
+
} | {
|
|
74
|
+
type: 'message_end';
|
|
75
|
+
message: PiHostMessage;
|
|
76
|
+
} | {
|
|
77
|
+
type: 'tool_start';
|
|
78
|
+
id: string;
|
|
79
|
+
name: string;
|
|
80
|
+
args: unknown;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'tool_update';
|
|
83
|
+
id: string;
|
|
84
|
+
name: string;
|
|
85
|
+
partial: unknown;
|
|
86
|
+
} | {
|
|
87
|
+
type: 'tool_end';
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
result: unknown;
|
|
91
|
+
isError: boolean;
|
|
92
|
+
} | {
|
|
93
|
+
type: 'compaction_start';
|
|
94
|
+
reason: string;
|
|
95
|
+
} | {
|
|
96
|
+
type: 'compaction_end';
|
|
97
|
+
reason: string;
|
|
98
|
+
aborted: boolean;
|
|
99
|
+
summary?: string;
|
|
100
|
+
errorMessage?: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: 'retry';
|
|
103
|
+
phase: 'start' | 'end';
|
|
104
|
+
attempt: number;
|
|
105
|
+
success?: boolean;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'session_name';
|
|
108
|
+
name?: string;
|
|
109
|
+
} | {
|
|
110
|
+
type: 'error';
|
|
111
|
+
message: string;
|
|
112
|
+
}
|
|
113
|
+
/** The native source was lost. PiBackendSession fails observation without emitting a terminal turn. */
|
|
114
|
+
| {
|
|
115
|
+
type: 'failed';
|
|
116
|
+
code: string;
|
|
117
|
+
};
|
|
118
|
+
/** One active-branch transcript entry; parentId preserves ancestry for before/through forks. */
|
|
119
|
+
export interface PiHostEntry {
|
|
120
|
+
id: string;
|
|
121
|
+
/** Previous entry in this branch, or null at its root. */
|
|
122
|
+
parentId: string | null;
|
|
123
|
+
kind: 'message' | 'compaction' | 'other';
|
|
124
|
+
message?: PiHostMessage;
|
|
125
|
+
summary?: string;
|
|
126
|
+
/** Native timestamp in milliseconds since the Unix epoch. */
|
|
127
|
+
timestamp?: number;
|
|
128
|
+
}
|
|
129
|
+
/** Available provider model and picker/config metadata, without credentials. */
|
|
130
|
+
export interface PiHostModel {
|
|
131
|
+
/** `provider/id` reference accepted by setModel. */
|
|
132
|
+
ref: string;
|
|
133
|
+
name: string;
|
|
134
|
+
reasoning: boolean;
|
|
135
|
+
/** Levels accepted by the model picker; non-reasoning models expose only off. */
|
|
136
|
+
thinkingLevels: string[];
|
|
137
|
+
/** Native context capacity in tokens, when available. */
|
|
138
|
+
contextWindow?: number;
|
|
139
|
+
}
|
|
140
|
+
/** Queued input awaits execution; handled input was consumed without starting a model run. */
|
|
141
|
+
export type PiInputDisposition = 'queued' | 'handled';
|
|
142
|
+
/** Prompt acceptance distinguishes an immediate run from queueing or extension handling. */
|
|
143
|
+
export type PiPromptDisposition = PiInputDisposition | 'started';
|
|
144
|
+
/** Inline image bytes encoded as base64, paired with their MIME type. */
|
|
145
|
+
export interface PiHostImage {
|
|
146
|
+
data: string;
|
|
147
|
+
mimeType: string;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Implement this port to supply PiAdapter with an owned session through PiAdapterOptions.createHost.
|
|
151
|
+
* PiBackendSession observes and disposes each acquired host. Publish raw values for adapter redaction
|
|
152
|
+
* and use events to report execution separately from input acceptance. PiBackendSession maps native
|
|
153
|
+
* request failures to SDK errors.
|
|
154
|
+
*/
|
|
155
|
+
export interface PiSessionHost {
|
|
156
|
+
/** Native identity used for the SDK binding and persisted session lookup. */
|
|
157
|
+
readonly sessionId: string;
|
|
158
|
+
/** `provider/id` of the active model, if any. */
|
|
159
|
+
readonly model?: string;
|
|
160
|
+
/** Effective thinking level after applying the active model's constraints. */
|
|
161
|
+
readonly thinkingLevel: string;
|
|
162
|
+
/** True while the host has work in progress, including retries and queued continuations. */
|
|
163
|
+
readonly busy: boolean;
|
|
164
|
+
/** Observe future events in arrival order. Call the returned function to unsubscribe. Embedded hosts isolate listener exceptions. */
|
|
165
|
+
subscribe(listener: (event: PiHostEvent) => void): () => void;
|
|
166
|
+
/**
|
|
167
|
+
* Resolve on acceptance, before background completion. Preflight failures reject. The embedded
|
|
168
|
+
* host emits error for failures after acceptance. Subscribe first to observe synchronous events.
|
|
169
|
+
*/
|
|
170
|
+
prompt(text: string, images?: PiHostImage[]): Promise<PiPromptDisposition>;
|
|
171
|
+
/** Queue input before the next model call or report extension handling. Resolve on acceptance. */
|
|
172
|
+
steer(text: string, images?: PiHostImage[]): Promise<PiInputDisposition>;
|
|
173
|
+
/** Queue input after tool calls and steering drain or report extension handling. Resolve on acceptance. */
|
|
174
|
+
followUp(text: string, images?: PiHostImage[]): Promise<PiInputDisposition>;
|
|
175
|
+
/** Abort the current operation and wait for native idle; events independently describe the interrupted run. */
|
|
176
|
+
abort(): Promise<void>;
|
|
177
|
+
/** Resolve the models available under the host credentials; listing failures reject. */
|
|
178
|
+
listModels(): Promise<PiHostModel[]>;
|
|
179
|
+
/** Select a model by provider/id. The embedded host rejects MODEL_NOT_FOUND for an unknown reference and propagates native selection failures. */
|
|
180
|
+
setModel(ref: string): Promise<void>;
|
|
181
|
+
/** Return the active model's supported thinking levels; the embedded host returns a mutable copy. */
|
|
182
|
+
availableThinkingLevels(): string[];
|
|
183
|
+
/** Apply a level and return Pi's effective value, which may be clamped to model capabilities. */
|
|
184
|
+
setThinkingLevel(level: string): string;
|
|
185
|
+
/** Change the tools exposed to the model while retaining any configured output tool. Process rights are independent of this allowlist. */
|
|
186
|
+
setActiveTools(names: string[]): void;
|
|
187
|
+
/**
|
|
188
|
+
* Configure the optional tool for submitting structured results. OutputSession supplies the
|
|
189
|
+
* definition through PiBackendSession and awaits installation before dispatch. It passes
|
|
190
|
+
* undefined to disable the tool during terminal processing, rejection recovery or teardown.
|
|
191
|
+
* Pi awaits tool.execute. An accepted result requests termination, and the turn completes on
|
|
192
|
+
* settlement. The embedded host rejects installation with STRUCTURED_OUTPUT_BUSY while busy,
|
|
193
|
+
* permits disabling while busy and ignores calls after disposal.
|
|
194
|
+
*/
|
|
195
|
+
setOutputTool?(tool?: StructuredOutputToolDefinition): Promise<void>;
|
|
196
|
+
/** Entries on the active branch, oldest first. */
|
|
197
|
+
entries(): PiHostEntry[];
|
|
198
|
+
/**
|
|
199
|
+
* Abort current work before compacting. Optional instructions guide the summary.
|
|
200
|
+
* Resolve with the summary if present or reject a native failure.
|
|
201
|
+
*/
|
|
202
|
+
compact(instructions?: string): Promise<{
|
|
203
|
+
summary?: string;
|
|
204
|
+
}>;
|
|
205
|
+
/**
|
|
206
|
+
* Open a separately owned persisted branch through entryId; null starts an empty persisted session.
|
|
207
|
+
* Supply a verified branch boundary and dispose the returned host separately from the source.
|
|
208
|
+
* The embedded host can reject FORK_NEEDS_PERSISTENCE, FORK_FAILED or a native open or branch failure.
|
|
209
|
+
*/
|
|
210
|
+
fork(entryId: string | null): Promise<PiSessionHost>;
|
|
211
|
+
/** Whether sessions are persisted so resume and fork can work. */
|
|
212
|
+
readonly persistent: boolean;
|
|
213
|
+
/** Release observation and native resources once. Embedded disposal requests abort without awaiting idle. */
|
|
214
|
+
dispose(): Promise<void>;
|
|
215
|
+
}
|
|
216
|
+
/** Host acquisition policy supplied by PiBackendSession or a direct host consumer. */
|
|
217
|
+
export interface PiHostOpenOptions {
|
|
218
|
+
/** Working directory for resources and session discovery. */
|
|
219
|
+
cwd: string;
|
|
220
|
+
mode: 'new' | 'resume';
|
|
221
|
+
/** Required by the embedded host for persisted resume; ignored for new or in-memory sessions. */
|
|
222
|
+
sessionId?: string;
|
|
223
|
+
/** Nonempty provider/id. Omission or an empty string lets Pi restore a saved model or select a default. */
|
|
224
|
+
model?: string;
|
|
225
|
+
/** Initial thinking preference. Omission or an empty string lets Pi restore a saved level or select a default. */
|
|
226
|
+
thinkingLevel?: string;
|
|
227
|
+
/** Initial active Pi tool names. Omission keeps Pi defaults and `[]` supplies an empty allowlist. */
|
|
228
|
+
tools?: string[];
|
|
229
|
+
/** Nonempty text appended to the native system prompt. */
|
|
230
|
+
instructions?: string;
|
|
231
|
+
/** Global Pi configuration directory. Omission uses Pi's agent directory. */
|
|
232
|
+
agentDir?: string;
|
|
233
|
+
/** Session storage override. Omission uses Pi's cwd-derived session location for discovery and new sessions. */
|
|
234
|
+
sessionDir?: string;
|
|
235
|
+
/** False forces a new in-memory manager even in resume mode. True enables session files and forks. */
|
|
236
|
+
persist: boolean;
|
|
237
|
+
}
|
|
238
|
+
/** Open one owned host; callers must dispose successful acquisitions. Rejection returns no host to release. */
|
|
239
|
+
export type PiHostFactory = (options: PiHostOpenOptions) => Promise<PiSessionHost>;
|
|
240
|
+
/**
|
|
241
|
+
* Normalize a native message for toHostEvent and entryToHost. Unsupported metadata is dropped
|
|
242
|
+
* and unknown blocks become empty text. Tool arguments retain their object identity, and message
|
|
243
|
+
* content remains unredacted until adapter mapping.
|
|
244
|
+
*/
|
|
245
|
+
export declare function toHostMessage(value: unknown): PiHostMessage;
|
|
246
|
+
/**
|
|
247
|
+
* Translate a native notification for the embedded host's listener dispatch. Return undefined
|
|
248
|
+
* for events or update phases the host omits. Raw tool payloads retain identity for adapter mapping.
|
|
249
|
+
*/
|
|
250
|
+
export declare function toHostEvent(event: AgentSessionEvent): PiHostEvent | undefined;
|
|
251
|
+
/**
|
|
252
|
+
* Open an embedded Pi session with native resources and credentials. Persisted resume requires a
|
|
253
|
+
* discoverable sessionId or rejects SESSION_NOT_FOUND. An unknown nonempty model reference
|
|
254
|
+
* rejects MODEL_NOT_FOUND. Native loading and setup failures also reject. Await disposal of each
|
|
255
|
+
* returned host. Abort first when native idle completion is required before releasing observation.
|
|
256
|
+
*/
|
|
257
|
+
export declare const createPiHost: PiHostFactory;
|
package/src/host.js
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { Type } from 'typebox';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Normalize a native message for toHostEvent and entryToHost. Unsupported metadata is dropped
|
|
5
|
+
* and unknown blocks become empty text. Tool arguments retain their object identity, and message
|
|
6
|
+
* content remains unredacted until adapter mapping.
|
|
7
|
+
*/
|
|
8
|
+
export function toHostMessage(value) {
|
|
9
|
+
const message = record(value);
|
|
10
|
+
const content = message.content;
|
|
11
|
+
const usage = record(message.usage);
|
|
12
|
+
return {
|
|
13
|
+
role: typeof message.role === 'string' ? message.role : 'unknown',
|
|
14
|
+
content: typeof content === 'string' ? content : Array.isArray(content) ? content.map(toHostContent) : '',
|
|
15
|
+
...(typeof message.toolCallId === 'string' ? { toolCallId: message.toolCallId } : {}),
|
|
16
|
+
...(typeof message.toolName === 'string' ? { toolName: message.toolName } : {}),
|
|
17
|
+
...(typeof message.isError === 'boolean' ? { isError: message.isError } : {}),
|
|
18
|
+
...(typeof message.stopReason === 'string' ? { stopReason: message.stopReason } : {}),
|
|
19
|
+
...(typeof message.errorMessage === 'string' ? { errorMessage: message.errorMessage } : {}),
|
|
20
|
+
...(typeof message.model === 'string' ? { model: message.model } : {}),
|
|
21
|
+
...(typeof message.provider === 'string' ? { provider: message.provider } : {}),
|
|
22
|
+
...(typeof message.timestamp === 'number' ? { timestamp: message.timestamp } : {}),
|
|
23
|
+
...(typeof usage.input === 'number' ? {
|
|
24
|
+
usage: {
|
|
25
|
+
input: usage.input,
|
|
26
|
+
output: Number(usage.output ?? 0),
|
|
27
|
+
cacheRead: Number(usage.cacheRead ?? 0),
|
|
28
|
+
cacheWrite: Number(usage.cacheWrite ?? 0),
|
|
29
|
+
totalTokens: Number(usage.totalTokens ?? 0),
|
|
30
|
+
},
|
|
31
|
+
} : {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Translate a native notification for the embedded host's listener dispatch. Return undefined
|
|
36
|
+
* for events or update phases the host omits. Raw tool payloads retain identity for adapter mapping.
|
|
37
|
+
*/
|
|
38
|
+
export function toHostEvent(event) {
|
|
39
|
+
switch (event.type) {
|
|
40
|
+
case 'agent_start': return { type: 'agent_start' };
|
|
41
|
+
case 'agent_end': return { type: 'agent_end', willRetry: event.willRetry };
|
|
42
|
+
case 'agent_settled': return { type: 'settled', aborted: event.aborted };
|
|
43
|
+
case 'message_start': return { type: 'message_start', message: toHostMessage(event.message) };
|
|
44
|
+
case 'message_end': return { type: 'message_end', message: toHostMessage(event.message) };
|
|
45
|
+
case 'message_update': return messageUpdateToHost(event);
|
|
46
|
+
case 'tool_execution_start': return { type: 'tool_start', id: event.toolCallId, name: event.toolName, args: event.args };
|
|
47
|
+
case 'tool_execution_update': return { type: 'tool_update', id: event.toolCallId, name: event.toolName, partial: event.partialResult };
|
|
48
|
+
case 'tool_execution_end': return { type: 'tool_end', id: event.toolCallId, name: event.toolName, result: event.result, isError: event.isError };
|
|
49
|
+
case 'compaction_start': return { type: 'compaction_start', reason: event.reason };
|
|
50
|
+
case 'compaction_end': return {
|
|
51
|
+
type: 'compaction_end', reason: event.reason, aborted: event.aborted,
|
|
52
|
+
...(event.result && typeof record(event.result).summary === 'string' ? { summary: String(record(event.result).summary) } : {}),
|
|
53
|
+
...(event.errorMessage ? { errorMessage: event.errorMessage } : {}),
|
|
54
|
+
};
|
|
55
|
+
case 'auto_retry_start': return { type: 'retry', phase: 'start', attempt: event.attempt };
|
|
56
|
+
case 'auto_retry_end': return { type: 'retry', phase: 'end', attempt: event.attempt, success: event.success };
|
|
57
|
+
case 'session_info_changed': return { type: 'session_name', ...(event.name ? { name: event.name } : {}) };
|
|
58
|
+
default: return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Open an embedded Pi session with native resources and credentials. Persisted resume requires a
|
|
63
|
+
* discoverable sessionId or rejects SESSION_NOT_FOUND. An unknown nonempty model reference
|
|
64
|
+
* rejects MODEL_NOT_FOUND. Native loading and setup failures also reject. Await disposal of each
|
|
65
|
+
* returned host. Abort first when native idle completion is required before releasing observation.
|
|
66
|
+
*/
|
|
67
|
+
export const createPiHost = async (open) => {
|
|
68
|
+
const pi = await import('@earendil-works/pi-coding-agent');
|
|
69
|
+
const sessionManager = openSessionManager(pi, open);
|
|
70
|
+
return createHostFromManager({ pi, open, sessionManager, persistent: open.persist });
|
|
71
|
+
};
|
|
72
|
+
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
73
|
+
/** Preserve branch ancestry even for entries the adapter skips when mapping history. */
|
|
74
|
+
function entryToHost(entry) {
|
|
75
|
+
const value = record(entry);
|
|
76
|
+
const base = {
|
|
77
|
+
id: String(value.id ?? ''),
|
|
78
|
+
parentId: typeof value.parentId === 'string' ? value.parentId : null,
|
|
79
|
+
...(typeof value.timestamp === 'string' ? { timestamp: Date.parse(value.timestamp) } : {}),
|
|
80
|
+
};
|
|
81
|
+
if (value.type === 'message')
|
|
82
|
+
return { ...base, kind: 'message', message: toHostMessage(value.message) };
|
|
83
|
+
if (value.type === 'compaction')
|
|
84
|
+
return { ...base, kind: 'compaction', ...(typeof value.summary === 'string' ? { summary: value.summary } : {}) };
|
|
85
|
+
return { ...base, kind: 'other' };
|
|
86
|
+
}
|
|
87
|
+
/** Build block updates for toHostEvent. Pi's other assistant update kinds produce no host event. */
|
|
88
|
+
function messageUpdateToHost(event) {
|
|
89
|
+
const inner = event.assistantMessageEvent;
|
|
90
|
+
const match = /^(text|thinking|toolcall)_(start|delta|end)$/.exec(inner.type);
|
|
91
|
+
if (!match)
|
|
92
|
+
return undefined;
|
|
93
|
+
const kind = match[1];
|
|
94
|
+
const phase = match[2];
|
|
95
|
+
const index = 'contentIndex' in inner ? inner.contentIndex : 0;
|
|
96
|
+
return {
|
|
97
|
+
type: 'message_update',
|
|
98
|
+
message: toHostMessage(event.message),
|
|
99
|
+
update: { kind, phase, index, ...('delta' in inner ? { delta: inner.delta } : {}) },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Select a native manager before opening resources. Persisted resume throws if the ID cannot be found. */
|
|
103
|
+
function openSessionManager(pi, open) {
|
|
104
|
+
if (!open.persist)
|
|
105
|
+
return pi.SessionManager.inMemory(open.cwd);
|
|
106
|
+
if (open.mode !== 'resume')
|
|
107
|
+
return pi.SessionManager.create(open.cwd, open.sessionDir);
|
|
108
|
+
const file = open.sessionId ? pi.SessionManager.findById(open.cwd, open.sessionId, open.sessionDir) : undefined;
|
|
109
|
+
if (!file)
|
|
110
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
111
|
+
return pi.SessionManager.open(file, open.sessionDir);
|
|
112
|
+
}
|
|
113
|
+
/** Embedded implementation owned by the backend or direct factory caller; subscribes before any prompt can run. */
|
|
114
|
+
class DefaultPiHost {
|
|
115
|
+
/** Capture native dependencies and register normalization before exposing the host to its owner. */
|
|
116
|
+
constructor({ session, pi, open, persistent, extensions }) {
|
|
117
|
+
this.session = session;
|
|
118
|
+
this.pi = pi;
|
|
119
|
+
this.open = open;
|
|
120
|
+
this.persistent = persistent;
|
|
121
|
+
this.extensions = extensions;
|
|
122
|
+
this.unsubscribe = session.subscribe(event => this.onNativeEvent(event));
|
|
123
|
+
}
|
|
124
|
+
get sessionId() { return this.session.sessionId; }
|
|
125
|
+
get model() { const model = this.session.model; return model ? `${model.provider}/${model.id}` : undefined; }
|
|
126
|
+
get thinkingLevel() { return this.session.thinkingLevel; }
|
|
127
|
+
get busy() { return !this.session.isIdle; }
|
|
128
|
+
subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
|
|
129
|
+
/**
|
|
130
|
+
* PiBackendSession.submit awaits the native preflight disposition to return its receipt.
|
|
131
|
+
* A native prompt that resolves without a preflight callback is reported as handled. If Pi
|
|
132
|
+
* rejects after invoking the callback, the error goes to listeners as a run failure.
|
|
133
|
+
*/
|
|
134
|
+
prompt(text, images) {
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
let reportedPreflight = false;
|
|
137
|
+
const run = this.session.prompt(text, {
|
|
138
|
+
...(images ? { images: hostImages(images) } : {}),
|
|
139
|
+
source: 'rpc',
|
|
140
|
+
preflightResult: disposition => {
|
|
141
|
+
reportedPreflight = true;
|
|
142
|
+
resolve(disposition);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
run.then(() => { if (!reportedPreflight)
|
|
146
|
+
resolve('handled'); }, cause => {
|
|
147
|
+
if (!reportedPreflight)
|
|
148
|
+
reject(cause);
|
|
149
|
+
else
|
|
150
|
+
this.emit({ type: 'error', message: cause instanceof Error ? cause.message : 'Pi run failed' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
steer(text, images) { return this.session.steer(text, images ? hostImages(images) : undefined, { source: 'rpc' }); }
|
|
155
|
+
followUp(text, images) { return this.session.followUp(text, images ? hostImages(images) : undefined, { source: 'rpc' }); }
|
|
156
|
+
abort() { return this.session.abort(); }
|
|
157
|
+
/**
|
|
158
|
+
* Supply models to createPiCatalogSession and PiBackendSession.setConfig. For reasoning
|
|
159
|
+
* models, an explicit null in Pi's level map removes that level from the supported list.
|
|
160
|
+
*/
|
|
161
|
+
async listModels() {
|
|
162
|
+
const models = await this.session.modelRuntime.getAvailable();
|
|
163
|
+
return models.map(model => ({
|
|
164
|
+
ref: `${model.provider}/${model.id}`, name: model.name ?? model.id, reasoning: model.reasoning,
|
|
165
|
+
thinkingLevels: model.reasoning ? ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].filter(level => model.thinkingLevelMap?.[level] !== null) : ['off'],
|
|
166
|
+
contextWindow: model.contextWindow,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
/** Resolve the model before mutating the session. Pi checks credentials during selection. */
|
|
170
|
+
async setModel(ref) {
|
|
171
|
+
const model = resolveModel(this.session.modelRuntime, ref);
|
|
172
|
+
if (!model)
|
|
173
|
+
throw new Error('MODEL_NOT_FOUND');
|
|
174
|
+
await this.session.setModel(model);
|
|
175
|
+
}
|
|
176
|
+
availableThinkingLevels() { return [...this.session.getAvailableThinkingLevels()]; }
|
|
177
|
+
setThinkingLevel(level) {
|
|
178
|
+
this.session.setThinkingLevel(level);
|
|
179
|
+
return this.session.thinkingLevel;
|
|
180
|
+
}
|
|
181
|
+
/** Permission changes retain the current output tool so a structured submission can still finish. */
|
|
182
|
+
setActiveTools(names) { this.extensions.setActiveTools([...new Set([...names, ...(this.outputTool ? [this.outputTool] : [])])]); }
|
|
183
|
+
/** Replace or disable the submission tool while retaining permission tools; disabling is allowed during terminal cleanup. */
|
|
184
|
+
async setOutputTool(tool) {
|
|
185
|
+
if (this.disposed)
|
|
186
|
+
return;
|
|
187
|
+
if (tool && this.busy)
|
|
188
|
+
throw new Error('STRUCTURED_OUTPUT_BUSY');
|
|
189
|
+
this.outputCalls.clear();
|
|
190
|
+
const active = this.extensions.getActiveTools().filter(name => name !== this.outputTool);
|
|
191
|
+
if (!tool) {
|
|
192
|
+
this.extensions.setActiveTools(active);
|
|
193
|
+
this.outputTool = undefined;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const definition = createOutputTool(tool, this.outputCalls);
|
|
197
|
+
const name = definition.name;
|
|
198
|
+
this.extensions.registerTool(definition);
|
|
199
|
+
this.outputTool = name;
|
|
200
|
+
this.extensions.setActiveTools([...new Set([...active, name])]);
|
|
201
|
+
}
|
|
202
|
+
entries() { return this.session.sessionManager.getBranch().map(entryToHost); }
|
|
203
|
+
/** Await native manual compaction and expose only its optional textual summary. */
|
|
204
|
+
async compact(instructions) {
|
|
205
|
+
const result = await this.session.compact(instructions);
|
|
206
|
+
const summary = record(result).summary;
|
|
207
|
+
return typeof summary === 'string' ? { summary } : {};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* PiBackendSession.forkHost passes a verified leaf and uses the returned host for the new SDK
|
|
211
|
+
* session. Branch a separately opened manager so the source keeps its identity and active branch.
|
|
212
|
+
*/
|
|
213
|
+
async fork(entryId) {
|
|
214
|
+
if (!this.persistent)
|
|
215
|
+
throw new Error('FORK_NEEDS_PERSISTENCE');
|
|
216
|
+
const manager = this.session.sessionManager;
|
|
217
|
+
let forked;
|
|
218
|
+
if (entryId === null)
|
|
219
|
+
forked = this.pi.SessionManager.create(this.open.cwd, this.open.sessionDir);
|
|
220
|
+
else {
|
|
221
|
+
// Pi assigns a session-file path to persisted managers before flushing their transcript.
|
|
222
|
+
const source = this.pi.SessionManager.open(manager.getSessionFile(), this.open.sessionDir);
|
|
223
|
+
if (!source.createBranchedSession(entryId))
|
|
224
|
+
throw new Error('FORK_FAILED');
|
|
225
|
+
forked = source;
|
|
226
|
+
}
|
|
227
|
+
return createHostFromManager({ pi: this.pi, open: { ...this.open, mode: 'resume' }, sessionManager: forked, persistent: true });
|
|
228
|
+
}
|
|
229
|
+
/** Detach observers and clear cached arguments before Pi disposal. Repeated calls skip cleanup. */
|
|
230
|
+
async dispose() {
|
|
231
|
+
if (this.disposed)
|
|
232
|
+
return;
|
|
233
|
+
this.disposed = true;
|
|
234
|
+
this.unsubscribe?.();
|
|
235
|
+
this.unsubscribe = undefined;
|
|
236
|
+
this.listeners.clear();
|
|
237
|
+
this.outputCalls.clear();
|
|
238
|
+
this.session.dispose();
|
|
239
|
+
}
|
|
240
|
+
persistent;
|
|
241
|
+
session;
|
|
242
|
+
pi;
|
|
243
|
+
open;
|
|
244
|
+
extensions;
|
|
245
|
+
listeners = new Set();
|
|
246
|
+
unsubscribe;
|
|
247
|
+
disposed = false;
|
|
248
|
+
outputTool;
|
|
249
|
+
outputCalls = new Map();
|
|
250
|
+
/**
|
|
251
|
+
* Handle notifications from the Pi subscription registered by the constructor. Record raw output
|
|
252
|
+
* arguments before Pi coerces them, release them at tool end, and send supported events to listeners.
|
|
253
|
+
*/
|
|
254
|
+
onNativeEvent(event) {
|
|
255
|
+
if (event.type === 'tool_execution_start' && event.toolName === this.outputTool) {
|
|
256
|
+
this.outputCalls.set(event.toolCallId, structuredClone(event.args));
|
|
257
|
+
}
|
|
258
|
+
if (event.type === 'tool_execution_end')
|
|
259
|
+
this.outputCalls.delete(event.toolCallId);
|
|
260
|
+
const mapped = toHostEvent(event);
|
|
261
|
+
if (mapped)
|
|
262
|
+
this.emit(mapped);
|
|
263
|
+
}
|
|
264
|
+
/** Dispatch synchronously over a snapshot so subscription changes apply to subsequent notifications. */
|
|
265
|
+
emit(event) {
|
|
266
|
+
for (const listener of [...this.listeners]) {
|
|
267
|
+
try {
|
|
268
|
+
listener(event);
|
|
269
|
+
}
|
|
270
|
+
catch { /* a listener must not break Pi's loop */ }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Initialize resources for createPiHost and DefaultPiHost.fork, then open the native session.
|
|
276
|
+
* Pi's loader invokes the extension factory during reload to supply the API for tool registration.
|
|
277
|
+
* If capture fails, dispose the native session and reject before returning ownership.
|
|
278
|
+
*/
|
|
279
|
+
async function createHostFromManager({ pi, open, sessionManager, persistent }) {
|
|
280
|
+
const agentDir = open.agentDir ?? pi.getAgentDir();
|
|
281
|
+
const settingsManager = pi.SettingsManager.create(open.cwd, agentDir);
|
|
282
|
+
const modelRuntime = await pi.ModelRuntime.create({ authPath: join(agentDir, 'auth.json'), modelsPath: join(agentDir, 'models.json') });
|
|
283
|
+
const model = open.model ? resolveModel(modelRuntime, open.model) : undefined;
|
|
284
|
+
if (open.model && !model)
|
|
285
|
+
throw new Error('MODEL_NOT_FOUND');
|
|
286
|
+
let extensions;
|
|
287
|
+
const loader = new pi.DefaultResourceLoader({
|
|
288
|
+
cwd: open.cwd, agentDir, settingsManager,
|
|
289
|
+
disabledBuiltinExtensions: ['mcp'],
|
|
290
|
+
extensionFactories: [api => { extensions = api; }],
|
|
291
|
+
...(open.instructions ? { appendSystemPrompt: [open.instructions] } : {}),
|
|
292
|
+
});
|
|
293
|
+
await loader.reload();
|
|
294
|
+
const { session } = await pi.createAgentSession({
|
|
295
|
+
cwd: open.cwd, agentDir, sessionManager, settingsManager, modelRuntime, resourceLoader: loader,
|
|
296
|
+
...(model ? { model } : {}),
|
|
297
|
+
...(open.thinkingLevel ? { thinkingLevel: open.thinkingLevel } : {}),
|
|
298
|
+
});
|
|
299
|
+
if (!extensions) {
|
|
300
|
+
session.dispose();
|
|
301
|
+
throw new Error('PI_OUTPUT_TOOLS_UNAVAILABLE');
|
|
302
|
+
}
|
|
303
|
+
const host = new DefaultPiHost({ session, pi, open, persistent, extensions });
|
|
304
|
+
if (open.tools)
|
|
305
|
+
host.setActiveTools(open.tools);
|
|
306
|
+
return host;
|
|
307
|
+
}
|
|
308
|
+
/** Resolve provider/id at its first slash for open and setModel; missing separators or model matches return undefined. */
|
|
309
|
+
function resolveModel(runtime, ref) {
|
|
310
|
+
const slash = ref.indexOf('/');
|
|
311
|
+
return slash > 0 ? runtime.getModel(ref.slice(0, slash), ref.slice(slash + 1)) : undefined;
|
|
312
|
+
}
|
|
313
|
+
/** Add native image block tags without changing the base64 payloads. */
|
|
314
|
+
function hostImages(images) {
|
|
315
|
+
return images.map(image => ({ type: 'image', data: image.data, mimeType: image.mimeType }));
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Build the tool that DefaultPiHost.setOutputTool registers for an SDK submission. Pi invokes
|
|
319
|
+
* execute after coercive validation. Use the cached raw arguments when available so the SDK's
|
|
320
|
+
* callback validates the original input. An accepted result requests native termination, a
|
|
321
|
+
* declined result becomes tool error content, and callback exceptions propagate to Pi.
|
|
322
|
+
*/
|
|
323
|
+
function createOutputTool(tool, calls) {
|
|
324
|
+
const definition = structuredClone({ name: tool.name, description: tool.description, schema: tool.schema });
|
|
325
|
+
const { parameters, wrapped } = outputToolParameters(definition.schema);
|
|
326
|
+
const execute = tool.execute;
|
|
327
|
+
return {
|
|
328
|
+
name: definition.name, label: 'Submit result', description: definition.description,
|
|
329
|
+
parameters: Type.Unsafe(parameters),
|
|
330
|
+
async execute(id, args) {
|
|
331
|
+
const original = calls.has(id) ? calls.get(id) : args;
|
|
332
|
+
const result = await execute(wrapped ? record(original).value : original);
|
|
333
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: result,
|
|
334
|
+
...(result.accepted ? { terminate: true } : { isError: true }) };
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Use object schemas directly and wrap other roots in Pi's required object-shaped parameters. */
|
|
339
|
+
function outputToolParameters(schema) {
|
|
340
|
+
const object = typeof schema === 'object' && schema.type === 'object';
|
|
341
|
+
// A nested resource keeps local references rooted at the original schema for non-object values.
|
|
342
|
+
const valueSchema = typeof schema === 'boolean' ? schema : { ...schema, $id: schema.$id ?? 'urn:anureo:output-result' };
|
|
343
|
+
const parameters = object ? schema : {
|
|
344
|
+
...(typeof schema === 'object' && schema.$schema ? { $schema: schema.$schema } : {}),
|
|
345
|
+
type: 'object', properties: { value: valueSchema }, required: ['value'], additionalProperties: false,
|
|
346
|
+
};
|
|
347
|
+
return { parameters, wrapped: !object };
|
|
348
|
+
}
|
|
349
|
+
/** Normalize block variants for toHostMessage; preserve tool-argument identity and empty unknown blocks. */
|
|
350
|
+
function toHostContent(part) {
|
|
351
|
+
const block = record(part);
|
|
352
|
+
if (block.type === 'text')
|
|
353
|
+
return { type: 'text', text: String(block.text ?? '') };
|
|
354
|
+
if (block.type === 'thinking')
|
|
355
|
+
return { type: 'thinking', thinking: String(block.thinking ?? '') };
|
|
356
|
+
if (block.type === 'toolCall')
|
|
357
|
+
return { type: 'toolCall', id: String(block.id ?? ''), name: String(block.name ?? ''), arguments: record(block.arguments) };
|
|
358
|
+
if (block.type === 'image')
|
|
359
|
+
return { type: 'image', ...(typeof block.data === 'string' ? { data: block.data } : {}), ...(typeof block.mimeType === 'string' ? { mimeType: block.mimeType } : {}) };
|
|
360
|
+
return { type: 'text', text: '' };
|
|
361
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { PiAdapter, PiBackendSession, createPiAdapter } from "./adapter.js";
|
|
2
|
+
export type { PiAdapterOptions } from "./options.js";
|
|
3
|
+
export { VERIFIED_PI_VERSIONS, resolvePiProfile, type PiProfile } from "./profile.js";
|
|
4
|
+
export { createPiHost, type PiSessionHost, type PiHostEvent, type PiHostFactory, type PiHostOpenOptions, type PiHostMessage, type PiHostEntry, type PiHostModel } from "./host.js";
|
|
5
|
+
export { createRedactor, permissionTools, piTurnError, historyItems, usageOf } from "./mapping.js";
|
|
6
|
+
export { createPiCatalogSession, type PiModel } from "./catalog.js";
|
|
7
|
+
/** Host history entry shape, also available as PiHostEntry. */
|
|
8
|
+
export type { PiHostEntry as PiEntry } from "./host.js";
|