@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/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Publishes the Pi adapter, host integration contracts and mapping and catalog helpers.
|
|
2
|
+
export { PiAdapter, PiBackendSession, createPiAdapter } from "./adapter.js";
|
|
3
|
+
export { VERIFIED_PI_VERSIONS, resolvePiProfile } from "./profile.js";
|
|
4
|
+
export { createPiHost } from "./host.js";
|
|
5
|
+
export { createRedactor, permissionTools, piTurnError, historyItems, usageOf } from "./mapping.js";
|
|
6
|
+
export { createPiCatalogSession } from "./catalog.js";
|
package/src/mapping.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { FileChange, HarnessError, HistoryItem, InputBlock, Item, JsonValue, Usage } from '@anureo/protocol';
|
|
2
|
+
import type { PiHostContent, PiHostEntry, PiHostImage, PiHostMessage } from "./host.js";
|
|
3
|
+
/** Text and JSON projection shared by live item publication and history reconstruction. */
|
|
4
|
+
export type Redactor = ReturnType<typeof createRedactor>;
|
|
5
|
+
/**
|
|
6
|
+
* Replace literal secrets in mapped content with `[REDACTED]`. The list is captured at creation,
|
|
7
|
+
* sorted longest first, and defaults to empty. Empty entries are ignored.
|
|
8
|
+
*
|
|
9
|
+
* JSON projection preserves keys, omits undefined object properties, and replaces unsupported
|
|
10
|
+
* values and containers beyond depth 24 with null. Property access errors propagate to the caller.
|
|
11
|
+
* PiAdapter captures this redactor at construction and shares it with its backends.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createRedactor(secrets?: readonly string[]): {
|
|
14
|
+
text(value: string): string;
|
|
15
|
+
json(value: unknown): JsonValue;
|
|
16
|
+
};
|
|
17
|
+
/** Built-in tools selected for read-only sessions. The host retains its process rights. */
|
|
18
|
+
export declare const READ_ONLY_TOOLS: readonly ["read", "grep", "find", "ls"];
|
|
19
|
+
/** Built-in tool allowlist for agent-full-access sessions. */
|
|
20
|
+
export declare const FULL_ACCESS_TOOLS: readonly ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
21
|
+
/** Permission values advertised by the adapter's configuration schema. */
|
|
22
|
+
export declare const PERMISSION_MODES: readonly ["read-only", "agent-full-access"];
|
|
23
|
+
/** Supported tool-allowlist policy, with no approval interaction. */
|
|
24
|
+
export type PiPermissionMode = typeof PERMISSION_MODES[number];
|
|
25
|
+
/**
|
|
26
|
+
* Return a fresh Pi tool allowlist for a permission mode. Omission returns undefined so
|
|
27
|
+
* PiBackendSession.hostOptions can retain host defaults and setConfig can skip changing tools.
|
|
28
|
+
* Unsupported values throw INVALID_PARAMS before the allowlist is applied.
|
|
29
|
+
*/
|
|
30
|
+
export declare function permissionTools(mode: unknown): string[] | undefined;
|
|
31
|
+
/** Classify a Pi tool by its native name for AHP tool items. Unrecognized names map to `other`. */
|
|
32
|
+
export declare function toolKind(name: string): ToolCallItem['kind'];
|
|
33
|
+
/**
|
|
34
|
+
* Derive file-change evidence from tool arguments alone. Other tools return undefined and file
|
|
35
|
+
* tools without a string path return an empty list. A write has an unknown operation because
|
|
36
|
+
* arguments cannot tell whether the target already exists. Write evidence is always partial.
|
|
37
|
+
* An edit is complete when all supplied fragments have both texts and at least one fragment exists.
|
|
38
|
+
*/
|
|
39
|
+
export declare function piFileChanges(name: string, args: unknown): FileChange[] | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Build a tool-call item with redacted arguments and file evidence. PiBackendSession.startTool
|
|
42
|
+
* supplies running as the status, then stores and publishes the item on a tool-start event.
|
|
43
|
+
*/
|
|
44
|
+
export declare function toolItem(redact: Redactor, id: string, name: string, args: unknown, status: ItemStatus): ToolCallItem;
|
|
45
|
+
/**
|
|
46
|
+
* Extract a string content field or concatenate text blocks from a Pi result. Text block values
|
|
47
|
+
* are coerced to strings, with nullish text treated as empty. Missing or unsupported content
|
|
48
|
+
* returns an empty string. Structured details are omitted.
|
|
49
|
+
*/
|
|
50
|
+
export declare function toolOutputText(result: unknown): string;
|
|
51
|
+
/**
|
|
52
|
+
* Copy a tool item with redacted result text and a status of failed or completed according to
|
|
53
|
+
* the native error flag. A supplied stopped status takes precedence. PiBackendSession.endTool
|
|
54
|
+
* stores and publishes the copy for a matching live call.
|
|
55
|
+
*/
|
|
56
|
+
export declare function completeTool(redact: Redactor, previous: ToolCallItem, result: unknown, isError: boolean, stopped?: ItemStatus): ToolCallItem;
|
|
57
|
+
/** Pass through string content or join only the text blocks without separators. */
|
|
58
|
+
export declare function contentText(content: string | PiHostContent[]): string;
|
|
59
|
+
/**
|
|
60
|
+
* Convert submission input to Pi text and inline images, joining text blocks with blank lines.
|
|
61
|
+
* Unsupported blocks or images without nonempty data and MIME type throw CAPABILITY_UNSUPPORTED.
|
|
62
|
+
* Image data is passed through without checking base64 encoding. PiBackendSession.submit forwards
|
|
63
|
+
* the text and any images to the host after conversion succeeds.
|
|
64
|
+
*/
|
|
65
|
+
export declare function nativeInput(input: InputBlock[]): {
|
|
66
|
+
text: string;
|
|
67
|
+
images: PiHostImage[];
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Sum reported native token counts across the supplied messages, with zero totals when none
|
|
71
|
+
* report usage. Scope defaults to turn. PiBackendSession.finishTurn supplies assistant messages
|
|
72
|
+
* and publishes the result in the usage update and turn completion.
|
|
73
|
+
*/
|
|
74
|
+
export declare function usageOf(messages: readonly PiHostMessage[], scope?: Usage['scope']): Usage;
|
|
75
|
+
/**
|
|
76
|
+
* Return a sanitized failure for stopReason error, or undefined otherwise. The first recognized
|
|
77
|
+
* HTTP status number in the native message determines the code. Only rate limits and service
|
|
78
|
+
* failures are marked retryable. PiBackendSession.messageEnd saves the failure for finishTurn
|
|
79
|
+
* and clears it on a later stop or toolUse. An interruption takes precedence at completion.
|
|
80
|
+
*/
|
|
81
|
+
export declare function piTurnError(message: PiHostMessage | undefined): HarnessError | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Identify turn-start user entry ids in oldest-first branch history. Known live turn bindings
|
|
84
|
+
* take precedence. Otherwise, only a first message or a user message after an assistant message
|
|
85
|
+
* whose stop reason differs from toolUse starts a turn. PiBackendSession.forkHost rejects targets
|
|
86
|
+
* outside this set.
|
|
87
|
+
*/
|
|
88
|
+
export declare function turnStarts(entries: readonly PiHostEntry[], known: ReadonlyMap<string, string>): Set<string>;
|
|
89
|
+
/**
|
|
90
|
+
* Reconstruct redacted items in transcript order from oldest-first active-branch entries.
|
|
91
|
+
* `turns` maps user entry ids to live AHP turn ids. Unbound turns use native-prefixed ids.
|
|
92
|
+
* Items before the first recognized turn use `native:session`.
|
|
93
|
+
* A result updates the most recent preceding call with its id, even across a turn boundary.
|
|
94
|
+
* Calls without a result omit status. User items retain only text. Input entries are left intact.
|
|
95
|
+
*
|
|
96
|
+
* PiBackendSession.readHistory reconstructs the branch before capping and paging the snapshot,
|
|
97
|
+
* so a returned call can include its result even when the result came from a later turn.
|
|
98
|
+
*/
|
|
99
|
+
export declare function historyItems(redact: Redactor, entries: readonly PiHostEntry[], turns: ReadonlyMap<string, string>): HistoryItem[];
|
|
100
|
+
type ToolCallItem = Extract<Item, {
|
|
101
|
+
type: 'toolCall';
|
|
102
|
+
}>;
|
|
103
|
+
type ItemStatus = NonNullable<Item['status']>;
|
|
104
|
+
export {};
|
package/src/mapping.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { HarnessSdkError } from '@anureo/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Replace literal secrets in mapped content with `[REDACTED]`. The list is captured at creation,
|
|
4
|
+
* sorted longest first, and defaults to empty. Empty entries are ignored.
|
|
5
|
+
*
|
|
6
|
+
* JSON projection preserves keys, omits undefined object properties, and replaces unsupported
|
|
7
|
+
* values and containers beyond depth 24 with null. Property access errors propagate to the caller.
|
|
8
|
+
* PiAdapter captures this redactor at construction and shares it with its backends.
|
|
9
|
+
*/
|
|
10
|
+
export function createRedactor(secrets = []) {
|
|
11
|
+
const values = [...new Set(secrets.filter(secret => secret.length > 0))].sort((a, b) => b.length - a.length);
|
|
12
|
+
const text = (value) => values.reduce((out, secret) => out.split(secret).join('[REDACTED]'), value);
|
|
13
|
+
/** Bound container traversal so cyclic or excessively nested input cannot recurse indefinitely. */
|
|
14
|
+
const project = (value, depth) => {
|
|
15
|
+
if (typeof value === 'string')
|
|
16
|
+
return text(value);
|
|
17
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean')
|
|
18
|
+
return value;
|
|
19
|
+
if (depth > 24)
|
|
20
|
+
return null;
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.map(entry => project(entry, depth + 1));
|
|
23
|
+
if (value && typeof value === 'object') {
|
|
24
|
+
const defined = Object.entries(value).filter(([, entry]) => entry !== undefined);
|
|
25
|
+
return Object.fromEntries(defined.map(([key, entry]) => [key, project(entry, depth + 1)]));
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
return { text, json: value => project(value, 0) };
|
|
30
|
+
}
|
|
31
|
+
/** Built-in tools selected for read-only sessions. The host retains its process rights. */
|
|
32
|
+
export const READ_ONLY_TOOLS = ['read', 'grep', 'find', 'ls'];
|
|
33
|
+
/** Built-in tool allowlist for agent-full-access sessions. */
|
|
34
|
+
export const FULL_ACCESS_TOOLS = ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'];
|
|
35
|
+
/** Permission values advertised by the adapter's configuration schema. */
|
|
36
|
+
export const PERMISSION_MODES = ['read-only', 'agent-full-access'];
|
|
37
|
+
/**
|
|
38
|
+
* Return a fresh Pi tool allowlist for a permission mode. Omission returns undefined so
|
|
39
|
+
* PiBackendSession.hostOptions can retain host defaults and setConfig can skip changing tools.
|
|
40
|
+
* Unsupported values throw INVALID_PARAMS before the allowlist is applied.
|
|
41
|
+
*/
|
|
42
|
+
export function permissionTools(mode) {
|
|
43
|
+
if (mode === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
if (mode === 'read-only')
|
|
46
|
+
return [...READ_ONLY_TOOLS];
|
|
47
|
+
if (mode === 'agent-full-access')
|
|
48
|
+
return [...FULL_ACCESS_TOOLS];
|
|
49
|
+
throw new HarnessSdkError('INVALID_PARAMS', 'Unsupported Pi permission mode');
|
|
50
|
+
}
|
|
51
|
+
/** Classify a Pi tool by its native name for AHP tool items. Unrecognized names map to `other`. */
|
|
52
|
+
export function toolKind(name) {
|
|
53
|
+
if (name === 'bash' || name === 'powershell')
|
|
54
|
+
return 'command';
|
|
55
|
+
if (name === 'edit' || name === 'write')
|
|
56
|
+
return 'fileChange';
|
|
57
|
+
if (name.startsWith('mcp__'))
|
|
58
|
+
return 'mcp';
|
|
59
|
+
return 'other';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Derive file-change evidence from tool arguments alone. Other tools return undefined and file
|
|
63
|
+
* tools without a string path return an empty list. A write has an unknown operation because
|
|
64
|
+
* arguments cannot tell whether the target already exists. Write evidence is always partial.
|
|
65
|
+
* An edit is complete when all supplied fragments have both texts and at least one fragment exists.
|
|
66
|
+
*/
|
|
67
|
+
export function piFileChanges(name, args) {
|
|
68
|
+
if (name !== 'edit' && name !== 'write')
|
|
69
|
+
return undefined;
|
|
70
|
+
const input = record(args);
|
|
71
|
+
const path = input.path;
|
|
72
|
+
if (typeof path !== 'string')
|
|
73
|
+
return [];
|
|
74
|
+
return [name === 'write' ? writeFileChange(path, input) : editFileChange(path, input)];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build a tool-call item with redacted arguments and file evidence. PiBackendSession.startTool
|
|
78
|
+
* supplies running as the status, then stores and publishes the item on a tool-start event.
|
|
79
|
+
*/
|
|
80
|
+
export function toolItem(redact, id, name, args, status) {
|
|
81
|
+
return buildToolItem({ redact, id, name, args, status });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extract a string content field or concatenate text blocks from a Pi result. Text block values
|
|
85
|
+
* are coerced to strings, with nullish text treated as empty. Missing or unsupported content
|
|
86
|
+
* returns an empty string. Structured details are omitted.
|
|
87
|
+
*/
|
|
88
|
+
export function toolOutputText(result) {
|
|
89
|
+
const content = record(result).content;
|
|
90
|
+
if (typeof content === 'string')
|
|
91
|
+
return content;
|
|
92
|
+
if (!Array.isArray(content))
|
|
93
|
+
return '';
|
|
94
|
+
return content.map(record).filter(part => part.type === 'text').map(part => String(part.text ?? '')).join('');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Copy a tool item with redacted result text and a status of failed or completed according to
|
|
98
|
+
* the native error flag. A supplied stopped status takes precedence. PiBackendSession.endTool
|
|
99
|
+
* stores and publishes the copy for a matching live call.
|
|
100
|
+
*/
|
|
101
|
+
export function completeTool(redact, previous, result, isError, stopped) {
|
|
102
|
+
return { ...previous, status: stopped ?? (isError ? 'failed' : 'completed'), output: redact.text(toolOutputText(result)) };
|
|
103
|
+
}
|
|
104
|
+
/** Pass through string content or join only the text blocks without separators. */
|
|
105
|
+
export function contentText(content) {
|
|
106
|
+
if (typeof content === 'string')
|
|
107
|
+
return content;
|
|
108
|
+
return content.flatMap(part => part.type === 'text' ? [part.text] : []).join('');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Convert submission input to Pi text and inline images, joining text blocks with blank lines.
|
|
112
|
+
* Unsupported blocks or images without nonempty data and MIME type throw CAPABILITY_UNSUPPORTED.
|
|
113
|
+
* Image data is passed through without checking base64 encoding. PiBackendSession.submit forwards
|
|
114
|
+
* the text and any images to the host after conversion succeeds.
|
|
115
|
+
*/
|
|
116
|
+
export function nativeInput(input) {
|
|
117
|
+
const text = [];
|
|
118
|
+
const images = [];
|
|
119
|
+
for (const block of input) {
|
|
120
|
+
if (block.type === 'text')
|
|
121
|
+
text.push(block.text);
|
|
122
|
+
else if (block.type === 'image' && block.data && block.mimeType)
|
|
123
|
+
images.push({ data: block.data, mimeType: block.mimeType });
|
|
124
|
+
else
|
|
125
|
+
throw new HarnessSdkError('CAPABILITY_UNSUPPORTED', 'Input cannot be represented by the Pi SDK');
|
|
126
|
+
}
|
|
127
|
+
return { text: text.join('\n\n'), images };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Sum reported native token counts across the supplied messages, with zero totals when none
|
|
131
|
+
* report usage. Scope defaults to turn. PiBackendSession.finishTurn supplies assistant messages
|
|
132
|
+
* and publishes the result in the usage update and turn completion.
|
|
133
|
+
*/
|
|
134
|
+
export function usageOf(messages, scope = 'turn') {
|
|
135
|
+
const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
136
|
+
for (const message of messages) {
|
|
137
|
+
if (!message.usage)
|
|
138
|
+
continue;
|
|
139
|
+
total.input += message.usage.input;
|
|
140
|
+
total.output += message.usage.output;
|
|
141
|
+
total.cacheRead += message.usage.cacheRead;
|
|
142
|
+
total.cacheWrite += message.usage.cacheWrite;
|
|
143
|
+
total.total += message.usage.totalTokens;
|
|
144
|
+
}
|
|
145
|
+
return { provider: 'pi', scope, source: 'native', ...total };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Return a sanitized failure for stopReason error, or undefined otherwise. The first recognized
|
|
149
|
+
* HTTP status number in the native message determines the code. Only rate limits and service
|
|
150
|
+
* failures are marked retryable. PiBackendSession.messageEnd saves the failure for finishTurn
|
|
151
|
+
* and clears it on a later stop or toolUse. An interruption takes precedence at completion.
|
|
152
|
+
*/
|
|
153
|
+
export function piTurnError(message) {
|
|
154
|
+
if (!message || message.stopReason !== 'error')
|
|
155
|
+
return undefined;
|
|
156
|
+
const code = turnErrorCode(message.errorMessage ?? '');
|
|
157
|
+
return { code, message: 'Pi model request failed', retryable: code === 'RATE_LIMITED' || code === 'NATIVE_SERVICE_UNAVAILABLE', outcomeUnknown: false };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Identify turn-start user entry ids in oldest-first branch history. Known live turn bindings
|
|
161
|
+
* take precedence. Otherwise, only a first message or a user message after an assistant message
|
|
162
|
+
* whose stop reason differs from toolUse starts a turn. PiBackendSession.forkHost rejects targets
|
|
163
|
+
* outside this set.
|
|
164
|
+
*/
|
|
165
|
+
export function turnStarts(entries, known) {
|
|
166
|
+
const starts = new Set();
|
|
167
|
+
let previous;
|
|
168
|
+
for (const entry of entries) {
|
|
169
|
+
if (entry.kind !== 'message' || !entry.message)
|
|
170
|
+
continue;
|
|
171
|
+
if (entry.message.role === 'user' && (known.has(entry.id) || canStartTurnAfter(previous))) {
|
|
172
|
+
starts.add(entry.id);
|
|
173
|
+
}
|
|
174
|
+
previous = entry.message;
|
|
175
|
+
}
|
|
176
|
+
return starts;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Reconstruct redacted items in transcript order from oldest-first active-branch entries.
|
|
180
|
+
* `turns` maps user entry ids to live AHP turn ids. Unbound turns use native-prefixed ids.
|
|
181
|
+
* Items before the first recognized turn use `native:session`.
|
|
182
|
+
* A result updates the most recent preceding call with its id, even across a turn boundary.
|
|
183
|
+
* Calls without a result omit status. User items retain only text. Input entries are left intact.
|
|
184
|
+
*
|
|
185
|
+
* PiBackendSession.readHistory reconstructs the branch before capping and paging the snapshot,
|
|
186
|
+
* so a returned call can include its result even when the result came from a later turn.
|
|
187
|
+
*/
|
|
188
|
+
export function historyItems(redact, entries, turns) {
|
|
189
|
+
const starts = turnStarts(entries, turns);
|
|
190
|
+
const items = [];
|
|
191
|
+
let turnId = 'native:session';
|
|
192
|
+
let nativeTurnId;
|
|
193
|
+
const toolCalls = new Map();
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (starts.has(entry.id)) {
|
|
196
|
+
nativeTurnId = entry.id;
|
|
197
|
+
turnId = turns.get(entry.id) ?? `native:${entry.id}`;
|
|
198
|
+
}
|
|
199
|
+
const base = { turnId, ...(nativeTurnId ? { nativeTurnId } : {}), nativeItemId: entry.id };
|
|
200
|
+
for (const mapped of historyEntryItems({ redact, entry, base })) {
|
|
201
|
+
if (mapped.item.type === 'toolCall')
|
|
202
|
+
toolCalls.set(mapped.item.id, mapped);
|
|
203
|
+
items.push(mapped);
|
|
204
|
+
}
|
|
205
|
+
applyHistoryToolResult(redact, entry, toolCalls);
|
|
206
|
+
}
|
|
207
|
+
return items;
|
|
208
|
+
}
|
|
209
|
+
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
210
|
+
/** Use the supplied whole-file text as evidence. The arguments establish neither existence nor prior contents. */
|
|
211
|
+
function writeFileChange(path, input) {
|
|
212
|
+
const materials = typeof input.content === 'string'
|
|
213
|
+
? [{ format: 'text', scope: 'file', oldText: null, newText: input.content }]
|
|
214
|
+
: [{ format: 'unavailable', reason: 'notProvided' }];
|
|
215
|
+
return { path, operation: 'unknown', materials, completeness: 'partial' };
|
|
216
|
+
}
|
|
217
|
+
/** Retain usable edit fragments even when other supplied fragments are incomplete. */
|
|
218
|
+
function editFileChange(path, input) {
|
|
219
|
+
const edits = Array.isArray(input.edits) ? input.edits.map(record) : [];
|
|
220
|
+
const fragments = edits.flatMap((edit) => {
|
|
221
|
+
if (typeof edit.oldText !== 'string' || typeof edit.newText !== 'string')
|
|
222
|
+
return [];
|
|
223
|
+
return [{ format: 'text', scope: 'fragment', oldText: edit.oldText, newText: edit.newText }];
|
|
224
|
+
});
|
|
225
|
+
const complete = fragments.length > 0 && fragments.length === edits.length;
|
|
226
|
+
return {
|
|
227
|
+
path,
|
|
228
|
+
operation: 'modify',
|
|
229
|
+
materials: fragments.length ? fragments : [{ format: 'unavailable', reason: 'notProvided' }],
|
|
230
|
+
completeness: complete ? 'complete' : 'partial',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Apply the same content projection to live and historical calls. A transcript call alone leaves status unset. */
|
|
234
|
+
function buildToolItem(call) {
|
|
235
|
+
const { redact, id, name, args } = call;
|
|
236
|
+
const fileChanges = piFileChanges(name, args);
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
type: 'toolCall',
|
|
240
|
+
kind: toolKind(name),
|
|
241
|
+
title: name,
|
|
242
|
+
toolName: name,
|
|
243
|
+
nativeType: name,
|
|
244
|
+
input: redact.json(args),
|
|
245
|
+
...(Object.hasOwn(call, 'status') ? { status: call.status } : {}),
|
|
246
|
+
...(fileChanges ? { fileChanges: redact.json(fileChanges) } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Classify the first recognized status number without exposing the provider's error text. */
|
|
250
|
+
function turnErrorCode(errorMessage) {
|
|
251
|
+
const status = /\b(401|403|429|5\d\d)\b/.exec(errorMessage)?.[1];
|
|
252
|
+
switch (status) {
|
|
253
|
+
case '401': return 'AUTH_REQUIRED';
|
|
254
|
+
case '403': return 'AUTH_FORBIDDEN';
|
|
255
|
+
case '429': return 'RATE_LIMITED';
|
|
256
|
+
case undefined: return 'PI_TURN_FAILED';
|
|
257
|
+
default: return 'NATIVE_SERVICE_UNAVAILABLE';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Infer a boundary from the prior message when a user entry has no known live binding. */
|
|
261
|
+
function canStartTurnAfter(previous) {
|
|
262
|
+
return !previous || (previous.role === 'assistant' && previous.stopReason !== 'toolUse');
|
|
263
|
+
}
|
|
264
|
+
/** Complete the matched call at its original position in history. Unmatched results are discarded. */
|
|
265
|
+
function applyHistoryToolResult(redact, entry, toolCalls) {
|
|
266
|
+
const message = entry.message;
|
|
267
|
+
if (entry.kind !== 'message' || message?.role !== 'toolResult' || !message.toolCallId)
|
|
268
|
+
return;
|
|
269
|
+
const previous = toolCalls.get(message.toolCallId);
|
|
270
|
+
if (previous?.item.type !== 'toolCall')
|
|
271
|
+
return;
|
|
272
|
+
const result = { content: contentText(message.content) };
|
|
273
|
+
previous.item = completeTool(redact, previous.item, result, message.isError === true);
|
|
274
|
+
}
|
|
275
|
+
/** Project one entry using the turn metadata supplied by historyItems. Results update earlier items. */
|
|
276
|
+
function historyEntryItems({ redact, entry, base }) {
|
|
277
|
+
if (entry.kind === 'compaction') {
|
|
278
|
+
return [{
|
|
279
|
+
...base,
|
|
280
|
+
item: {
|
|
281
|
+
id: entry.id,
|
|
282
|
+
type: 'compaction',
|
|
283
|
+
status: 'completed',
|
|
284
|
+
...(entry.summary ? { summary: redact.text(entry.summary) } : {}),
|
|
285
|
+
},
|
|
286
|
+
}];
|
|
287
|
+
}
|
|
288
|
+
const message = entry.message;
|
|
289
|
+
if (entry.kind !== 'message' || !message)
|
|
290
|
+
return [];
|
|
291
|
+
if (message.role === 'user') {
|
|
292
|
+
return [{
|
|
293
|
+
...base,
|
|
294
|
+
item: {
|
|
295
|
+
id: entry.id,
|
|
296
|
+
type: 'userMessage',
|
|
297
|
+
status: 'completed',
|
|
298
|
+
content: [{ type: 'text', text: redact.text(contentText(message.content)) }],
|
|
299
|
+
},
|
|
300
|
+
}];
|
|
301
|
+
}
|
|
302
|
+
if (message.role !== 'assistant' || !Array.isArray(message.content))
|
|
303
|
+
return [];
|
|
304
|
+
return assistantHistoryItems({ redact, message, content: message.content, base });
|
|
305
|
+
}
|
|
306
|
+
/** Keep native block indexes in text and reasoning ids. Text is final only on stop with no tool calls. */
|
|
307
|
+
function assistantHistoryItems({ redact, message, content, base }) {
|
|
308
|
+
const hasTools = content.some(part => part.type === 'toolCall');
|
|
309
|
+
return content.flatMap((part, index) => {
|
|
310
|
+
const id = `${base.nativeItemId}:${index}`;
|
|
311
|
+
if (part.type === 'text') {
|
|
312
|
+
const text = redact.text(part.text);
|
|
313
|
+
const phase = !hasTools && message.stopReason === 'stop' ? 'final' : 'commentary';
|
|
314
|
+
return [{ ...base, item: { id, type: 'agentMessage', status: 'completed', text, phase } }];
|
|
315
|
+
}
|
|
316
|
+
if (part.type === 'thinking')
|
|
317
|
+
return [{ ...base, item: { id, type: 'reasoning', status: 'completed', text: redact.text(part.thinking) } }];
|
|
318
|
+
if (part.type === 'toolCall') {
|
|
319
|
+
const item = buildToolItem({ redact, id: part.id, name: part.name, args: part.arguments });
|
|
320
|
+
return [{ ...base, item }];
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
323
|
+
});
|
|
324
|
+
}
|
package/src/options.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PiHostFactory } from "./host.js";
|
|
2
|
+
/** Configure Pi host acquisition, persistence and history for createPiAdapter. */
|
|
3
|
+
export interface PiAdapterOptions {
|
|
4
|
+
/** Trusted Pi SDK version. Omit to report `unknown` and use the baseline profile for any host. */
|
|
5
|
+
version?: string;
|
|
6
|
+
/** Open an owned host for the adapter to dispose. Defaults to the in-process Pi host. */
|
|
7
|
+
createHost?: PiHostFactory;
|
|
8
|
+
/** Override Pi's global configuration directory. Omit or pass an empty string to use the host default. */
|
|
9
|
+
agentDir?: string;
|
|
10
|
+
/** Override the directory for persisted session files. Omitting it or passing an empty string leaves selection to the host. */
|
|
11
|
+
sessionDir?: string;
|
|
12
|
+
/** Allow disk persistence when the version profile supports resume. Defaults to true. */
|
|
13
|
+
persistSessions?: boolean;
|
|
14
|
+
/** Literal secrets to redact in mapped message and tool content and history. Empty strings are ignored. */
|
|
15
|
+
secrets?: string[];
|
|
16
|
+
/** Number of most recent history items retained per snapshot. Defaults to 5000. Zero retains all. */
|
|
17
|
+
maxHistoryItems?: number;
|
|
18
|
+
/** Currently unused; the adapter does not invoke this callback. */
|
|
19
|
+
diagnostic?: (code: string) => void;
|
|
20
|
+
}
|
package/src/options.js
ADDED
|
File without changes
|
package/src/profile.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Capabilities } from '@anureo/protocol';
|
|
2
|
+
/** Exact Pi release strings verified for resume, fork and compaction. */
|
|
3
|
+
export declare const VERIFIED_PI_VERSIONS: readonly ["1.5.2"];
|
|
4
|
+
/** Capabilities selected from a trusted Pi version, before persistence settings are applied. */
|
|
5
|
+
export interface PiProfile {
|
|
6
|
+
/** Minimum release for this capability set; `0.0.0` denotes the baseline. */
|
|
7
|
+
since: string;
|
|
8
|
+
/** Allows the adapter to open a previously persisted session. */
|
|
9
|
+
resume: boolean;
|
|
10
|
+
/** Allows the adapter to branch from a verified turn boundary when persistence is enabled. */
|
|
11
|
+
fork: boolean;
|
|
12
|
+
/** Allows the adapter to request manual compaction. */
|
|
13
|
+
compact: boolean;
|
|
14
|
+
/** True only for an exact version in VERIFIED_PI_VERSIONS. */
|
|
15
|
+
verified: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Select capabilities using the first numeric release triple in the trusted Pi version string.
|
|
19
|
+
* Pi 1.5.2 and later major-1 releases enable resume, fork and compaction. Only exact entries in
|
|
20
|
+
* VERIFIED_PI_VERSIONS are marked verified. Omission or any other version selects the baseline.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolvePiProfile(version?: string): PiProfile;
|
|
23
|
+
/**
|
|
24
|
+
* Derive the native capabilities that PiAdapter advertises during negotiation and passes to
|
|
25
|
+
* PiBackendSession. Resume and fork require persistence as well as version support. The SDK
|
|
26
|
+
* output wrapper can advertise structured output separately.
|
|
27
|
+
*/
|
|
28
|
+
export declare function capabilities(profile: PiProfile, persist: boolean): Capabilities;
|
package/src/profile.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { PERMISSION_MODES } from "./mapping.js";
|
|
2
|
+
/** Exact Pi release strings verified for resume, fork and compaction. */
|
|
3
|
+
export const VERIFIED_PI_VERSIONS = ['1.5.2'];
|
|
4
|
+
/**
|
|
5
|
+
* Select capabilities using the first numeric release triple in the trusted Pi version string.
|
|
6
|
+
* Pi 1.5.2 and later major-1 releases enable resume, fork and compaction. Only exact entries in
|
|
7
|
+
* VERIFIED_PI_VERSIONS are marked verified. Omission or any other version selects the baseline.
|
|
8
|
+
*/
|
|
9
|
+
export function resolvePiProfile(version) {
|
|
10
|
+
if (version && supportsResumeForkAndCompaction(version)) {
|
|
11
|
+
return { since: '1.5.2', resume: true, fork: true, compact: true, verified: VERIFIED_PI_VERSIONS.includes(version) };
|
|
12
|
+
}
|
|
13
|
+
return { since: '0.0.0', resume: false, fork: false, compact: false, verified: false };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Derive the native capabilities that PiAdapter advertises during negotiation and passes to
|
|
17
|
+
* PiBackendSession. Resume and fork require persistence as well as version support. The SDK
|
|
18
|
+
* output wrapper can advertise structured output separately.
|
|
19
|
+
*/
|
|
20
|
+
export function capabilities(profile, persist) {
|
|
21
|
+
const stored = persist && profile.resume;
|
|
22
|
+
return {
|
|
23
|
+
session: { resume: stored, fork: stored && profile.fork ? ['before', 'through'] : [], history: true },
|
|
24
|
+
input: { delivery: ['native', 'afterTurn'], interrupt: { targets: ['session'], reportsPendingInputs: false } },
|
|
25
|
+
turn: { outputSchema: false, overrides: [], inputModalities: ['text', 'image'] },
|
|
26
|
+
...(profile.compact ? { compact: true } : {}),
|
|
27
|
+
config: { options: [
|
|
28
|
+
{ id: 'model', title: 'Model', scope: 'session', schema: { type: 'string', minLength: 1 } },
|
|
29
|
+
{ id: 'thinkingLevel', title: 'Thinking level', scope: 'session', schema: { type: 'string', enum: [...THINKING_LEVELS] } },
|
|
30
|
+
{ id: 'permissionMode', title: 'Permissions', scope: 'session', schema: { type: 'string', enum: [...PERMISSION_MODES], default: 'agent-full-access' } },
|
|
31
|
+
] },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const THINKING_LEVELS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
35
|
+
/** Check the first numeric release triple, ignoring surrounding text and suffixes. */
|
|
36
|
+
function supportsResumeForkAndCompaction(version) {
|
|
37
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
38
|
+
if (!match)
|
|
39
|
+
return false;
|
|
40
|
+
const [, major, minor, patch] = match;
|
|
41
|
+
const minorVersion = Number(minor);
|
|
42
|
+
return Number(major) === 1 && (minorVersion > 5 || (minorVersion === 5 && Number(patch) >= 2));
|
|
43
|
+
}
|