@rivus/runtime 0.16.2
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 +21 -0
- package/README.md +5 -0
- package/dist/acp.d.ts +179 -0
- package/dist/acp.js +768 -0
- package/dist/chunks/agent-loop.d.ts +481 -0
- package/dist/chunks/agent-loop.js +164 -0
- package/dist/chunks/rivus-runtime-tool.d.ts +386 -0
- package/dist/chunks/tool-input-digest.js +119 -0
- package/dist/index.d.ts +1306 -0
- package/dist/index.js +4164 -0
- package/dist/pi.d.ts +840 -0
- package/dist/pi.js +2143 -0
- package/package.json +69 -0
package/dist/pi.js
ADDED
|
@@ -0,0 +1,2143 @@
|
|
|
1
|
+
import { a as requiresToolApproval, l as normalizeAgentInvocation, r as createToolInputDigest$1, s as createInvocationAuthority } from "./chunks/tool-input-digest.js";
|
|
2
|
+
import { _ as createAgentLoopToolExecutionStart, d as createAgentLoopModelExecutionStart, f as createAgentLoopSkillExecutionEnd, g as createAgentLoopToolExecutionEnd, h as createAgentLoopThinkingDelta, l as toEffectAgentLoopInput, m as createAgentLoopTextDelta, o as fromEffectAgentLoop, p as createAgentLoopSkillExecutionStart, s as toCompatibilityAgentLoopInput, u as createAgentLoopModelExecutionEnd, v as createAgentLoopToolExecutionUpdate } from "./chunks/agent-loop.js";
|
|
3
|
+
import { Effect, Stream } from "effect";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
6
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
7
|
+
import { writeAtomicTextFile } from "@rivus/platform/persistence";
|
|
8
|
+
import { createSha256Digest } from "@rivus/platform/identity";
|
|
9
|
+
import { Type, Unsafe } from "typebox";
|
|
10
|
+
import { DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, createAgentSession, createBashToolDefinition, createReadToolDefinition, defineTool, estimateTokens } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { realpathSync, statSync } from "node:fs";
|
|
12
|
+
//#region src/adapters/pi/config/pi-model-overrides.ts
|
|
13
|
+
async function mergePiProviderBaseUrlOverride(options) {
|
|
14
|
+
await updatePiProviderOverride(options.filePath, options.provider, (providerOverride) => ({
|
|
15
|
+
...providerOverride,
|
|
16
|
+
baseUrl: options.baseUrl
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Persist one complete custom model definition in models.json.
|
|
21
|
+
*
|
|
22
|
+
* Pi composes these definitions with its built-in provider metadata. Keeping the
|
|
23
|
+
* complete definition here is deliberate: a missing SDK catalog entry must be
|
|
24
|
+
* represented by its exact server id, never by a fuzzy or legacy alias.
|
|
25
|
+
*/
|
|
26
|
+
async function mergePiProviderModelDeclaration(options) {
|
|
27
|
+
await updatePiProviderOverride(options.filePath, options.provider, (providerOverride) => {
|
|
28
|
+
const models = providerOverride.models;
|
|
29
|
+
if (models !== void 0 && (!Array.isArray(models) || models.some((model) => !isJsonRecord(model)))) throw new Error(`${options.filePath} providers.${options.provider}.models must be a JSON array of objects`);
|
|
30
|
+
const existingModels = models ?? [];
|
|
31
|
+
const existingIndex = existingModels.findIndex((model) => model.id === options.model.id);
|
|
32
|
+
const nextModels = [...existingModels];
|
|
33
|
+
if (existingIndex >= 0) nextModels[existingIndex] = {
|
|
34
|
+
...nextModels[existingIndex],
|
|
35
|
+
...options.model
|
|
36
|
+
};
|
|
37
|
+
else nextModels.push({ ...options.model });
|
|
38
|
+
return {
|
|
39
|
+
...providerOverride,
|
|
40
|
+
models: nextModels
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function updatePiProviderOverride(filePath, provider, update) {
|
|
45
|
+
const overrides = await readPiModelOverrides(filePath);
|
|
46
|
+
const providers = readJsonRecord(overrides.providers, filePath, "providers");
|
|
47
|
+
const providerOverride = readJsonRecord(providers[provider], filePath, `providers.${provider}`);
|
|
48
|
+
await writePiModelOverrides(filePath, {
|
|
49
|
+
...overrides,
|
|
50
|
+
providers: {
|
|
51
|
+
...providers,
|
|
52
|
+
[provider]: update(providerOverride)
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function readPiModelOverrides(filePath) {
|
|
57
|
+
try {
|
|
58
|
+
return readJsonRecord(JSON.parse(await readFile(filePath, "utf8")), filePath, "root");
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (isFileNotFound(error)) return {};
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function writePiModelOverrides(filePath, overrides) {
|
|
65
|
+
await writeAtomicTextFile(filePath, `${JSON.stringify(overrides, null, 2)}\n`, {
|
|
66
|
+
durable: true,
|
|
67
|
+
mode: 384
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function readJsonRecord(value, filePath, path) {
|
|
71
|
+
if (value === void 0) return {};
|
|
72
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${filePath} ${path} must be a JSON object`);
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function isJsonRecord(value) {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
function isFileNotFound(error) {
|
|
79
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/adapters/pi/skills/pi-skill-tool.ts
|
|
83
|
+
const PI_SKILL_READER_TOOL_NAME = "rivus_read_skill";
|
|
84
|
+
function createPiSkillRuntime(skills) {
|
|
85
|
+
if (skills.length === 0) return Object.freeze({ prompt: "" });
|
|
86
|
+
const skillsById = new Map(skills.map((skill) => [skill.id, skill]));
|
|
87
|
+
const prompt = [
|
|
88
|
+
"Granted Skills are versioned instructions loaded on demand.",
|
|
89
|
+
`Before following a Skill, call ${PI_SKILL_READER_TOOL_NAME} with its exact ID and follow the returned content.`,
|
|
90
|
+
"Granted Skill catalog:",
|
|
91
|
+
...skills.map((skill) => `- ${skill.id} | ${skill.title} | v${skill.version} | ${skill.digest}`)
|
|
92
|
+
].join("\n");
|
|
93
|
+
const tool = {
|
|
94
|
+
description: "Read the full versioned instructions for one Skill granted to this Agent Runtime.",
|
|
95
|
+
execute: async (_callId, input) => {
|
|
96
|
+
const skillId = readSkillId$1(input);
|
|
97
|
+
const skill = skillsById.get(skillId);
|
|
98
|
+
if (!skill) throw new Error(`Skill is not granted: ${skillId}`);
|
|
99
|
+
const details = {
|
|
100
|
+
contentLength: skill.content.length,
|
|
101
|
+
digest: skill.digest,
|
|
102
|
+
skillId: skill.id,
|
|
103
|
+
title: skill.title,
|
|
104
|
+
version: skill.version
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
content: [{
|
|
108
|
+
text: skill.content,
|
|
109
|
+
type: "text"
|
|
110
|
+
}],
|
|
111
|
+
details
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
executionMode: "sequential",
|
|
115
|
+
label: "Read granted Skill",
|
|
116
|
+
name: PI_SKILL_READER_TOOL_NAME,
|
|
117
|
+
parameters: Unsafe({
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
properties: { skillId: {
|
|
120
|
+
description: "Exact Skill ID from the granted Skill catalog.",
|
|
121
|
+
type: "string"
|
|
122
|
+
} },
|
|
123
|
+
required: ["skillId"],
|
|
124
|
+
type: "object"
|
|
125
|
+
}),
|
|
126
|
+
promptSnippet: `${PI_SKILL_READER_TOOL_NAME}: read one granted Skill by exact ID`
|
|
127
|
+
};
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
prompt,
|
|
130
|
+
tool
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function readSkillId$1(input) {
|
|
134
|
+
if (input === null || typeof input !== "object" || Array.isArray(input) || typeof input.skillId !== "string") throw new Error("Skill reader input requires a string skillId");
|
|
135
|
+
return input.skillId;
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/adapters/pi/execution/pi-session-prompts.ts
|
|
139
|
+
/** One ordinary prompt writer, with ordered native steering while that prompt is streaming. */
|
|
140
|
+
async function runPiSessionPrompts(options) {
|
|
141
|
+
const { input, handle } = options;
|
|
142
|
+
const { session } = handle;
|
|
143
|
+
const prepare = (text) => handle.preparePrompt?.({
|
|
144
|
+
...input,
|
|
145
|
+
text
|
|
146
|
+
}) ?? text;
|
|
147
|
+
if (!options.supportsSteering) {
|
|
148
|
+
const text = await prepare(input.text);
|
|
149
|
+
if (!input.abortSignal.aborted) await session.prompt(text);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!session.steer || typeof session.isStreaming !== "boolean" || !session.clearQueue) throw new Error("Pi steering requires native steer, isStreaming and clearQueue capabilities");
|
|
153
|
+
const mailbox = input.steering;
|
|
154
|
+
const stop = new AbortController();
|
|
155
|
+
const onAbort = () => stop.abort();
|
|
156
|
+
input.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
157
|
+
if (input.abortSignal.aborted) stop.abort();
|
|
158
|
+
const close = () => Effect.runPromise(mailbox?.close?.() ?? Effect.succeed([]));
|
|
159
|
+
const take = () => Effect.runPromise(mailbox?.next() ?? Effect.never, { signal: stop.signal }).then((text) => ({
|
|
160
|
+
type: "steering",
|
|
161
|
+
text
|
|
162
|
+
}), (error) => {
|
|
163
|
+
if (stop.signal.aborted) return { type: "closed" };
|
|
164
|
+
throw error;
|
|
165
|
+
});
|
|
166
|
+
let prompt = Promise.resolve({ type: "completed" });
|
|
167
|
+
const startPrompt = (text) => {
|
|
168
|
+
prompt = session.prompt(text).then(() => ({ type: "completed" }), (error) => ({
|
|
169
|
+
type: "failed",
|
|
170
|
+
error
|
|
171
|
+
}));
|
|
172
|
+
};
|
|
173
|
+
const settlePrompt = async () => {
|
|
174
|
+
const outcome = await prompt;
|
|
175
|
+
if (outcome.type === "failed") throw outcome.error;
|
|
176
|
+
};
|
|
177
|
+
let next;
|
|
178
|
+
let succeeded = false;
|
|
179
|
+
try {
|
|
180
|
+
const text = await prepare(input.text);
|
|
181
|
+
if (input.abortSignal.aborted) return;
|
|
182
|
+
startPrompt(text);
|
|
183
|
+
next = take();
|
|
184
|
+
while (!input.abortSignal.aborted) {
|
|
185
|
+
const outcome = await Promise.race([next, prompt]);
|
|
186
|
+
if (outcome.type === "failed") throw outcome.error;
|
|
187
|
+
if (outcome.type === "closed") break;
|
|
188
|
+
if (outcome.type === "completed") {
|
|
189
|
+
const remaining = await close();
|
|
190
|
+
stop.abort();
|
|
191
|
+
const claimed = await next;
|
|
192
|
+
const accepted = claimed.type === "steering" ? [claimed.text, ...remaining] : remaining;
|
|
193
|
+
for (const acceptedText of accepted) {
|
|
194
|
+
const prepared = await prepare(acceptedText);
|
|
195
|
+
if (input.abortSignal.aborted) return;
|
|
196
|
+
startPrompt(prepared);
|
|
197
|
+
await settlePrompt();
|
|
198
|
+
}
|
|
199
|
+
succeeded = true;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const prepared = await prepare(outcome.text);
|
|
203
|
+
if (input.abortSignal.aborted) break;
|
|
204
|
+
if (session.isStreaming) await session.steer(prepared);
|
|
205
|
+
else {
|
|
206
|
+
await settlePrompt();
|
|
207
|
+
if (input.abortSignal.aborted) break;
|
|
208
|
+
startPrompt(prepared);
|
|
209
|
+
}
|
|
210
|
+
next = take();
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
input.abortSignal.removeEventListener("abort", onAbort);
|
|
214
|
+
await close();
|
|
215
|
+
stop.abort();
|
|
216
|
+
await next;
|
|
217
|
+
const cancelledOrFailed = !succeeded || input.abortSignal.aborted;
|
|
218
|
+
if (cancelledOrFailed) await options.abort();
|
|
219
|
+
await prompt;
|
|
220
|
+
if (cancelledOrFailed) session.clearQueue();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/adapters/pi/execution/pi-agent-loop.ts
|
|
225
|
+
function createPiAgentLoop$1(options) {
|
|
226
|
+
return {
|
|
227
|
+
...options.supportsSteering ? { supportsSteering: true } : {},
|
|
228
|
+
run: (input) => {
|
|
229
|
+
const stream = Stream.fromAsyncIterable(runPiSession(input, options), (error) => error);
|
|
230
|
+
if (!options.runBoundary) return stream;
|
|
231
|
+
return Stream.unwrapScoped(Effect.acquireRelease(options.runBoundary.acquireRun({ signal: input.abortSignal }), (lease) => Effect.sync(lease.release)).pipe(Effect.as(stream)));
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function createPiSdkAgentLoop$1(options) {
|
|
236
|
+
return createPiAgentLoop$1({
|
|
237
|
+
...options.supportsSteering ? { supportsSteering: true } : {},
|
|
238
|
+
resolveSession: async () => {
|
|
239
|
+
const result = await options.createAgentSession(options.sessionOptions);
|
|
240
|
+
return {
|
|
241
|
+
dispose: () => result.session.dispose?.(),
|
|
242
|
+
session: result.session
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function* runPiSession(input, options) {
|
|
248
|
+
const queue = createAsyncQueue();
|
|
249
|
+
let removeAbortListener;
|
|
250
|
+
let unsubscribe;
|
|
251
|
+
let dispose;
|
|
252
|
+
let handleForRun;
|
|
253
|
+
let activationAttempted = false;
|
|
254
|
+
let abortCompletion;
|
|
255
|
+
let failure;
|
|
256
|
+
let terminalErrorMessage;
|
|
257
|
+
let done = false;
|
|
258
|
+
const mappingState = {
|
|
259
|
+
activeSkillIds: /* @__PURE__ */ new Map(),
|
|
260
|
+
modelCallCount: 0
|
|
261
|
+
};
|
|
262
|
+
const complete = (error) => {
|
|
263
|
+
failure = error;
|
|
264
|
+
done = true;
|
|
265
|
+
queue.wake();
|
|
266
|
+
};
|
|
267
|
+
const sessionTask = (async () => {
|
|
268
|
+
try {
|
|
269
|
+
const handle = await options.resolveSession(input);
|
|
270
|
+
handleForRun = handle;
|
|
271
|
+
dispose = handle.dispose ?? (() => handle.session.dispose?.());
|
|
272
|
+
activationAttempted = true;
|
|
273
|
+
await handle.activate?.(input);
|
|
274
|
+
const abortSession = () => {
|
|
275
|
+
if (abortCompletion) return abortCompletion;
|
|
276
|
+
abortCompletion = Promise.resolve(handle.session.abort?.()).then(() => void 0, (error) => {
|
|
277
|
+
if (!input.abortSignal.aborted) throw error;
|
|
278
|
+
});
|
|
279
|
+
return abortCompletion;
|
|
280
|
+
};
|
|
281
|
+
const requestAbort = () => {
|
|
282
|
+
abortSession().then(() => complete(), (error) => complete(error));
|
|
283
|
+
};
|
|
284
|
+
input.abortSignal.addEventListener("abort", requestAbort, { once: true });
|
|
285
|
+
removeAbortListener = () => input.abortSignal.removeEventListener("abort", requestAbort);
|
|
286
|
+
if (input.abortSignal.aborted) {
|
|
287
|
+
await abortSession();
|
|
288
|
+
complete();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
unsubscribe = handle.session.subscribe((event) => {
|
|
292
|
+
if (event.type === "message_end") {
|
|
293
|
+
const message = readAssistantMessage(event.message);
|
|
294
|
+
if (message) terminalErrorMessage = message.stopReason === "error" ? message.errorMessage ?? "Pi agent failed" : void 0;
|
|
295
|
+
}
|
|
296
|
+
const mappedEvents = mapPiEvents(event, input.runId, mappingState, handle.resolveToolName);
|
|
297
|
+
for (const mapped of mappedEvents) {
|
|
298
|
+
observeModelContent(options.modelContentObserver, handle.session, event, input.runId, mapped);
|
|
299
|
+
queue.offer(mapped);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
await runPiSessionPrompts({
|
|
303
|
+
input,
|
|
304
|
+
handle,
|
|
305
|
+
supportsSteering: options.supportsSteering === true,
|
|
306
|
+
abort: abortSession
|
|
307
|
+
});
|
|
308
|
+
complete(input.abortSignal.aborted || terminalErrorMessage === void 0 ? void 0 : new Error(terminalErrorMessage));
|
|
309
|
+
} catch (error) {
|
|
310
|
+
complete(input.abortSignal.aborted ? void 0 : error);
|
|
311
|
+
}
|
|
312
|
+
})();
|
|
313
|
+
try {
|
|
314
|
+
while (!done || queue.hasItems()) {
|
|
315
|
+
const event = queue.poll();
|
|
316
|
+
if (event !== void 0) {
|
|
317
|
+
yield event;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
await queue.wait();
|
|
321
|
+
}
|
|
322
|
+
if (failure !== void 0) throw failure;
|
|
323
|
+
} finally {
|
|
324
|
+
removeAbortListener?.();
|
|
325
|
+
unsubscribe?.();
|
|
326
|
+
await Promise.all([sessionTask, ...abortCompletion ? [abortCompletion] : []]);
|
|
327
|
+
try {
|
|
328
|
+
if (activationAttempted) await handleForRun?.deactivate?.();
|
|
329
|
+
} finally {
|
|
330
|
+
if (options.disposeSessionAfterRun !== false) await dispose?.();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function observeModelContent(observer, session, event, runId, mapped) {
|
|
335
|
+
if (!observer) return;
|
|
336
|
+
if (mapped.type === "model_execution_start") {
|
|
337
|
+
const modelInput = snapshotModelInput(session.state);
|
|
338
|
+
if (modelInput !== void 0) observer.observeInput({
|
|
339
|
+
input: modelInput,
|
|
340
|
+
modelCallId: mapped.modelCallId,
|
|
341
|
+
runId
|
|
342
|
+
});
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (mapped.type !== "model_execution_end" || event.type !== "message_end") return;
|
|
346
|
+
const output = snapshotAssistantOutput(event.message);
|
|
347
|
+
if (output !== void 0) observer.observeOutput({
|
|
348
|
+
modelCallId: mapped.modelCallId,
|
|
349
|
+
output,
|
|
350
|
+
runId
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
function snapshotModelInput(value) {
|
|
354
|
+
if (!isRecord$4(value)) return void 0;
|
|
355
|
+
const messages = Array.isArray(value.messages) ? value.messages.map(snapshotMessage).filter(isDefined) : void 0;
|
|
356
|
+
const systemPrompt = typeof value.systemPrompt === "string" ? value.systemPrompt : void 0;
|
|
357
|
+
const tools = Array.isArray(value.tools) ? value.tools.map(snapshotTool).filter(isDefined) : void 0;
|
|
358
|
+
if (!messages && systemPrompt === void 0 && !tools) return void 0;
|
|
359
|
+
return {
|
|
360
|
+
...messages ? { messages } : {},
|
|
361
|
+
...systemPrompt === void 0 ? {} : { systemPrompt },
|
|
362
|
+
...tools ? { tools } : {}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function snapshotAssistantOutput(value) {
|
|
366
|
+
if (!isRecord$4(value) || value.role !== "assistant") return void 0;
|
|
367
|
+
return snapshotMessage(value);
|
|
368
|
+
}
|
|
369
|
+
function snapshotMessage(value) {
|
|
370
|
+
if (!isRecord$4(value) || typeof value.role !== "string") return void 0;
|
|
371
|
+
const content = sanitizeContent(value.content);
|
|
372
|
+
return {
|
|
373
|
+
...content === void 0 ? {} : { content },
|
|
374
|
+
role: value.role,
|
|
375
|
+
...typeof value.toolCallId === "string" ? { toolCallId: value.toolCallId } : {},
|
|
376
|
+
...typeof value.toolName === "string" ? { toolName: value.toolName } : {}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function snapshotTool(value) {
|
|
380
|
+
if (!isRecord$4(value) || typeof value.name !== "string") return void 0;
|
|
381
|
+
return {
|
|
382
|
+
...typeof value.description === "string" ? { description: value.description } : {},
|
|
383
|
+
name: value.name,
|
|
384
|
+
...value.parameters === void 0 ? {} : { parameters: sanitizeSerializable(value.parameters) }
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function sanitizeContent(value) {
|
|
388
|
+
if (!Array.isArray(value)) return sanitizeSerializable(value);
|
|
389
|
+
return value.filter((item) => !isPrivateThinkingBlock(item)).map(sanitizeSerializable).filter(isDefined);
|
|
390
|
+
}
|
|
391
|
+
function sanitizeSerializable(value) {
|
|
392
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
393
|
+
if (Array.isArray(value)) return value.map(sanitizeSerializable).filter(isDefined);
|
|
394
|
+
if (!isRecord$4(value)) return void 0;
|
|
395
|
+
return Object.fromEntries(Object.entries(value).filter(([, nested]) => typeof nested !== "function" && nested !== void 0).map(([key, nested]) => [key, sanitizeSerializable(nested)]).filter((entry) => entry[1] !== void 0));
|
|
396
|
+
}
|
|
397
|
+
function isPrivateThinkingBlock(value) {
|
|
398
|
+
return isRecord$4(value) && (value.type === "thinking" || value.type === "redacted_thinking");
|
|
399
|
+
}
|
|
400
|
+
function isDefined(value) {
|
|
401
|
+
return value !== void 0;
|
|
402
|
+
}
|
|
403
|
+
function mapPiEvents(event, runId, state, resolveToolName) {
|
|
404
|
+
if (event.type === "tool_execution_start") {
|
|
405
|
+
const toolEvent = event;
|
|
406
|
+
if (toolEvent.toolName === "rivus_read_skill") {
|
|
407
|
+
const skillId = readSkillId(getToolExecutionInput(toolEvent));
|
|
408
|
+
if (!skillId) return [];
|
|
409
|
+
state.activeSkillIds.set(toolEvent.toolCallId, skillId);
|
|
410
|
+
return [createAgentLoopSkillExecutionStart({
|
|
411
|
+
skillCallId: toolEvent.toolCallId,
|
|
412
|
+
skillId
|
|
413
|
+
})];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (event.type === "tool_execution_update") {
|
|
417
|
+
if (event.toolName === "rivus_read_skill") return [];
|
|
418
|
+
}
|
|
419
|
+
if (event.type === "tool_execution_end") {
|
|
420
|
+
const toolEvent = event;
|
|
421
|
+
if (toolEvent.toolName === "rivus_read_skill") {
|
|
422
|
+
const details = readSkillDetails(toolEvent.result);
|
|
423
|
+
const skillId = details?.skillId ?? state.activeSkillIds.get(toolEvent.toolCallId);
|
|
424
|
+
state.activeSkillIds.delete(toolEvent.toolCallId);
|
|
425
|
+
if (!skillId) return [];
|
|
426
|
+
if (toolEvent.isError) return [createAgentLoopSkillExecutionEnd({
|
|
427
|
+
isError: true,
|
|
428
|
+
skillCallId: toolEvent.toolCallId,
|
|
429
|
+
skillId
|
|
430
|
+
})];
|
|
431
|
+
if (!details) throw new Error(`Skill reader completed without valid metadata: ${skillId}`);
|
|
432
|
+
return [createAgentLoopSkillExecutionEnd({
|
|
433
|
+
contentLength: details.contentLength,
|
|
434
|
+
digest: details.digest,
|
|
435
|
+
isError: false,
|
|
436
|
+
skillCallId: toolEvent.toolCallId,
|
|
437
|
+
skillId,
|
|
438
|
+
title: details.title,
|
|
439
|
+
version: details.version
|
|
440
|
+
})];
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const mapped = mapPiEvent(event, runId, state, resolveToolName);
|
|
444
|
+
return mapped ? [mapped] : [];
|
|
445
|
+
}
|
|
446
|
+
function mapPiEvent(event, runId, state, resolveToolName) {
|
|
447
|
+
switch (event.type) {
|
|
448
|
+
case "message_start": {
|
|
449
|
+
const message = readAssistantMessage(event.message);
|
|
450
|
+
if (!message) return void 0;
|
|
451
|
+
state.modelCallCount += 1;
|
|
452
|
+
state.activeModelCallId = `${runId}:model:${state.modelCallCount}`;
|
|
453
|
+
return createAgentLoopModelExecutionStart({
|
|
454
|
+
api: message.api,
|
|
455
|
+
model: message.model,
|
|
456
|
+
modelCallId: state.activeModelCallId,
|
|
457
|
+
provider: message.provider
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
case "message_update":
|
|
461
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
462
|
+
if (typeof assistantEvent.delta !== "string") return;
|
|
463
|
+
if (assistantEvent.type === "thinking_delta") return createAgentLoopThinkingDelta(assistantEvent.delta);
|
|
464
|
+
if (assistantEvent.type !== "text_delta") return;
|
|
465
|
+
return createAgentLoopTextDelta(assistantEvent.delta);
|
|
466
|
+
case "message_end": {
|
|
467
|
+
const message = readAssistantMessage(event.message);
|
|
468
|
+
if (!message) return void 0;
|
|
469
|
+
if (!state.activeModelCallId) {
|
|
470
|
+
state.modelCallCount += 1;
|
|
471
|
+
state.activeModelCallId = `${runId}:model:${state.modelCallCount}`;
|
|
472
|
+
}
|
|
473
|
+
const modelCallId = state.activeModelCallId;
|
|
474
|
+
delete state.activeModelCallId;
|
|
475
|
+
return createAgentLoopModelExecutionEnd({
|
|
476
|
+
api: message.api,
|
|
477
|
+
...message.errorMessage ? { errorMessage: message.errorMessage } : {},
|
|
478
|
+
model: message.model,
|
|
479
|
+
modelCallId,
|
|
480
|
+
provider: message.provider,
|
|
481
|
+
...message.responseModel ? { responseModel: message.responseModel } : {},
|
|
482
|
+
stopReason: message.stopReason,
|
|
483
|
+
usage: {
|
|
484
|
+
cacheRead: message.usage.cacheRead,
|
|
485
|
+
cacheWrite: message.usage.cacheWrite,
|
|
486
|
+
cost: message.usage.cost.total,
|
|
487
|
+
input: message.usage.input,
|
|
488
|
+
output: message.usage.output,
|
|
489
|
+
...message.usage.reasoning === void 0 ? {} : { reasoning: message.usage.reasoning },
|
|
490
|
+
total: message.usage.totalTokens
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
case "tool_execution_start": {
|
|
495
|
+
const toolEvent = event;
|
|
496
|
+
return createAgentLoopToolExecutionStart({
|
|
497
|
+
input: getToolExecutionInput(toolEvent),
|
|
498
|
+
toolCallId: toolEvent.toolCallId,
|
|
499
|
+
toolName: resolveToolName?.(toolEvent.toolName) ?? toolEvent.toolName
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
case "tool_execution_update": {
|
|
503
|
+
const toolEvent = event;
|
|
504
|
+
return createAgentLoopToolExecutionUpdate({
|
|
505
|
+
input: getToolExecutionInput(toolEvent),
|
|
506
|
+
partialResult: toolEvent.partialResult,
|
|
507
|
+
toolCallId: toolEvent.toolCallId,
|
|
508
|
+
toolName: resolveToolName?.(toolEvent.toolName) ?? toolEvent.toolName
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
case "tool_execution_end": {
|
|
512
|
+
const toolEvent = event;
|
|
513
|
+
return createAgentLoopToolExecutionEnd({
|
|
514
|
+
isError: toolEvent.isError,
|
|
515
|
+
result: toolEvent.result,
|
|
516
|
+
toolCallId: toolEvent.toolCallId,
|
|
517
|
+
toolName: resolveToolName?.(toolEvent.toolName) ?? toolEvent.toolName
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
default: return;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function readAssistantMessage(value) {
|
|
524
|
+
if (!isRecord$4(value) || value.role !== "assistant" || !isRecord$4(value.usage) || !isRecord$4(value.usage.cost)) return;
|
|
525
|
+
if (typeof value.api !== "string" || typeof value.model !== "string" || typeof value.provider !== "string" || typeof value.stopReason !== "string" || typeof value.usage.cacheRead !== "number" || typeof value.usage.cacheWrite !== "number" || typeof value.usage.cost.total !== "number" || typeof value.usage.input !== "number" || typeof value.usage.output !== "number" || typeof value.usage.totalTokens !== "number") return;
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
function isRecord$4(value) {
|
|
529
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
530
|
+
}
|
|
531
|
+
function getToolExecutionInput(event) {
|
|
532
|
+
return Object.hasOwn(event, "args") ? event.args : event.input;
|
|
533
|
+
}
|
|
534
|
+
function readSkillId(value) {
|
|
535
|
+
return isRecord$4(value) && typeof value.skillId === "string" ? value.skillId : void 0;
|
|
536
|
+
}
|
|
537
|
+
function readSkillDetails(value) {
|
|
538
|
+
if (!isRecord$4(value) || !isRecord$4(value.details)) return void 0;
|
|
539
|
+
const details = value.details;
|
|
540
|
+
if (typeof details.contentLength !== "number" || typeof details.digest !== "string" || typeof details.skillId !== "string" || typeof details.title !== "string" || typeof details.version !== "string") return;
|
|
541
|
+
return details;
|
|
542
|
+
}
|
|
543
|
+
function createAsyncQueue() {
|
|
544
|
+
const items = [];
|
|
545
|
+
let resume;
|
|
546
|
+
return {
|
|
547
|
+
hasItems: () => items.length > 0,
|
|
548
|
+
offer: (item) => {
|
|
549
|
+
items.push(item);
|
|
550
|
+
resume?.();
|
|
551
|
+
resume = void 0;
|
|
552
|
+
},
|
|
553
|
+
poll: () => items.shift(),
|
|
554
|
+
wait: () => new Promise((resolve) => {
|
|
555
|
+
resume = resolve;
|
|
556
|
+
}),
|
|
557
|
+
wake: () => {
|
|
558
|
+
resume?.();
|
|
559
|
+
resume = void 0;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
//#endregion
|
|
564
|
+
//#region src/adapters/pi/execution/pi-session-registry.ts
|
|
565
|
+
function createPiSessionRegistry$1(options) {
|
|
566
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
567
|
+
const resolve = async (input) => {
|
|
568
|
+
const cached = sessions.get(input.sessionKey);
|
|
569
|
+
if (cached) return cached;
|
|
570
|
+
const created = Promise.resolve(options.createSession(input)).catch((error) => {
|
|
571
|
+
sessions.delete(input.sessionKey);
|
|
572
|
+
throw error;
|
|
573
|
+
});
|
|
574
|
+
sessions.set(input.sessionKey, created);
|
|
575
|
+
return created;
|
|
576
|
+
};
|
|
577
|
+
const dispose = async (sessionKey) => {
|
|
578
|
+
const session = sessions.get(sessionKey);
|
|
579
|
+
sessions.delete(sessionKey);
|
|
580
|
+
if (!session) return;
|
|
581
|
+
await (await session).dispose?.();
|
|
582
|
+
};
|
|
583
|
+
return {
|
|
584
|
+
dispose,
|
|
585
|
+
disposeAll: async () => {
|
|
586
|
+
await Promise.all([...sessions.keys()].map(dispose));
|
|
587
|
+
},
|
|
588
|
+
list: async () => Promise.all(sessions.values()),
|
|
589
|
+
resolve,
|
|
590
|
+
size: () => sessions.size
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/adapters/pi/model-management/pi-model-call-deadline.ts
|
|
595
|
+
var PiModelCallDeadlineExceeded = class extends Error {
|
|
596
|
+
name = "PiModelCallDeadlineExceeded";
|
|
597
|
+
};
|
|
598
|
+
/** Race a provider or SDK promise against the Run deadline without retaining a late result. */
|
|
599
|
+
async function awaitPiModelCall(promise, options = {}) {
|
|
600
|
+
const completion = Promise.resolve(promise);
|
|
601
|
+
const signal = options.signal;
|
|
602
|
+
if (!signal) return completion;
|
|
603
|
+
let settled = false;
|
|
604
|
+
const observed = completion.then((value) => {
|
|
605
|
+
settled = true;
|
|
606
|
+
return value;
|
|
607
|
+
}, (error) => {
|
|
608
|
+
settled = true;
|
|
609
|
+
throw error;
|
|
610
|
+
});
|
|
611
|
+
let removeAbortListener;
|
|
612
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
613
|
+
const handleAbort = () => {
|
|
614
|
+
if (settled) return;
|
|
615
|
+
options.onAbort?.();
|
|
616
|
+
reject(options.isDeadlineExceeded?.() ? new PiModelCallDeadlineExceeded(options.deadlineMessage ?? "Pi model call deadline elapsed") : signal.reason instanceof Error ? signal.reason : new Error(options.abortMessage ?? "Pi model call aborted"));
|
|
617
|
+
};
|
|
618
|
+
if (options.isDeadlineExceeded?.() || signal.aborted) handleAbort();
|
|
619
|
+
else {
|
|
620
|
+
signal.addEventListener("abort", handleAbort, { once: true });
|
|
621
|
+
removeAbortListener = () => signal.removeEventListener("abort", handleAbort);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
const value = await Promise.race([observed, aborted]);
|
|
626
|
+
if (options.isDeadlineExceeded?.()) throw new PiModelCallDeadlineExceeded(options.deadlineMessage ?? "Pi model call deadline elapsed");
|
|
627
|
+
return value;
|
|
628
|
+
} finally {
|
|
629
|
+
removeAbortListener?.();
|
|
630
|
+
if (!settled) observed.catch(() => void 0);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/adapters/pi/model-management/pi-model-probe.ts
|
|
635
|
+
const PI_MODEL_PROBE_TOOL_NAME = "rivus_model_management_probe";
|
|
636
|
+
const PI_MODEL_PROBE_TOOL_RESULT = "RIVUS_MODEL_PROBE_TOOL_OK";
|
|
637
|
+
const PI_MODEL_PROBE_FOLLOWUP_RESULT = "RIVUS_MODEL_PROBE_FOLLOWUP_OK";
|
|
638
|
+
const probeToolParameters = Type.Object({ probe: Type.String() });
|
|
639
|
+
var PiModelProbeError = class extends Error {
|
|
640
|
+
name = "PiModelProbeError";
|
|
641
|
+
code;
|
|
642
|
+
outcome;
|
|
643
|
+
constructor(message, outcome, options, code = "model_change_probe_failed") {
|
|
644
|
+
super(message, options);
|
|
645
|
+
this.code = code;
|
|
646
|
+
this.outcome = outcome;
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
/**
|
|
650
|
+
* Exercise a candidate through the same Pi model runtime used by business runs.
|
|
651
|
+
* The probe builds an in-memory synthetic context, performs an actual streamed
|
|
652
|
+
* tool call, feeds the real fixed tool result into a second streamed request, and
|
|
653
|
+
* then sends a separate follow-up request. It never loads a persisted transcript
|
|
654
|
+
* or exposes business tools.
|
|
655
|
+
*/
|
|
656
|
+
function probePiModel(options) {
|
|
657
|
+
return Effect.gen(function* () {
|
|
658
|
+
const context = {
|
|
659
|
+
messages: [{
|
|
660
|
+
content: "This is an isolated compatibility probe. Call the fixed probe Tool exactly once, then repeat the exact Tool result in your response.",
|
|
661
|
+
role: "user",
|
|
662
|
+
timestamp: Date.now()
|
|
663
|
+
}],
|
|
664
|
+
systemPrompt: "You are running an isolated model compatibility probe. You must call the rivus_model_management_probe Tool exactly once with a short probe value. After receiving its result, repeat its exact result text in your response. Do not call any other Tool.",
|
|
665
|
+
tools: [{
|
|
666
|
+
description: "A fixed no-side-effect probe Tool used only for model compatibility checks.",
|
|
667
|
+
name: PI_MODEL_PROBE_TOOL_NAME,
|
|
668
|
+
parameters: probeToolParameters
|
|
669
|
+
}]
|
|
670
|
+
};
|
|
671
|
+
const transcript = [];
|
|
672
|
+
const initial = yield* streamProbeTurn(options, context, "initial");
|
|
673
|
+
transcript.push(initial.turn);
|
|
674
|
+
const toolCall = findSingleProbeToolCall(initial.message);
|
|
675
|
+
if (!toolCall) return yield* Effect.fail(new PiModelProbeError("candidate did not request the fixed probe Tool in its first streamed response", "known"));
|
|
676
|
+
if (typeof toolCall.arguments.probe !== "string" || toolCall.arguments.probe.length === 0) return yield* Effect.fail(new PiModelProbeError("candidate supplied invalid arguments to the fixed probe Tool", "known"));
|
|
677
|
+
const toolResult = {
|
|
678
|
+
content: [{
|
|
679
|
+
text: `${PI_MODEL_PROBE_TOOL_RESULT}:${randomUUID()}`,
|
|
680
|
+
type: "text"
|
|
681
|
+
}],
|
|
682
|
+
details: { probe: true },
|
|
683
|
+
isError: false,
|
|
684
|
+
role: "toolResult",
|
|
685
|
+
timestamp: Date.now(),
|
|
686
|
+
toolCallId: toolCall.id,
|
|
687
|
+
toolName: PI_MODEL_PROBE_TOOL_NAME
|
|
688
|
+
};
|
|
689
|
+
context.messages.push(initial.message, toolResult);
|
|
690
|
+
const toolResultText = toolResult.content[0].text;
|
|
691
|
+
const afterTool = yield* streamProbeTurn(options, context, "tool-result");
|
|
692
|
+
transcript.push({
|
|
693
|
+
...afterTool.turn,
|
|
694
|
+
toolResult: {
|
|
695
|
+
content: toolResultText,
|
|
696
|
+
toolCallId: toolCall.id,
|
|
697
|
+
toolName: PI_MODEL_PROBE_TOOL_NAME
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
if (!readText(afterTool.message).includes(toolResultText)) return yield* Effect.fail(new PiModelProbeError("candidate did not consume the fixed probe Tool result in its next streamed response", "known"));
|
|
701
|
+
if (findToolCalls(afterTool.message).length > 0) return yield* Effect.fail(new PiModelProbeError("candidate requested an additional Tool during the probe", "known"));
|
|
702
|
+
context.messages.push(afterTool.message, {
|
|
703
|
+
content: "Follow up after the Tool roundtrip. Reply with RIVUS_MODEL_PROBE_FOLLOWUP_OK.",
|
|
704
|
+
role: "user",
|
|
705
|
+
timestamp: Date.now()
|
|
706
|
+
});
|
|
707
|
+
const followup = yield* streamProbeTurn(options, context, "follow-up");
|
|
708
|
+
transcript.push(followup.turn);
|
|
709
|
+
if (findToolCalls(followup.message).length > 0) return yield* Effect.fail(new PiModelProbeError("candidate requested a Tool during the probe follow-up", "known"));
|
|
710
|
+
const followupText = readText(followup.message);
|
|
711
|
+
if (!followupText.includes("RIVUS_MODEL_PROBE_FOLLOWUP_OK")) return yield* Effect.fail(new PiModelProbeError("candidate did not produce the fixed probe follow-up response", "known"));
|
|
712
|
+
const assistantThinkingBlocks = transcript.reduce((count, turn) => count + turn.assistant.content.filter((content) => isRecord$3(content) && content.type === "thinking").length, 0);
|
|
713
|
+
const outputTokens = transcript.reduce((sum, turn) => sum + readOutputTokens(turn.assistant.usage), 0);
|
|
714
|
+
return Object.freeze({
|
|
715
|
+
assistantThinkingBlocks,
|
|
716
|
+
followupText,
|
|
717
|
+
model: Object.freeze({
|
|
718
|
+
id: options.model.id,
|
|
719
|
+
provider: options.model.provider
|
|
720
|
+
}),
|
|
721
|
+
outputTokens,
|
|
722
|
+
passed: true,
|
|
723
|
+
thinkingLevel: options.thinkingLevel,
|
|
724
|
+
toolResultText,
|
|
725
|
+
toolCallId: toolCall.id,
|
|
726
|
+
toolName: PI_MODEL_PROBE_TOOL_NAME,
|
|
727
|
+
transcript: Object.freeze(transcript)
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
function streamProbeTurn(options, context, kind) {
|
|
732
|
+
return streamPaidProbeCall({
|
|
733
|
+
budget: options.budget,
|
|
734
|
+
context,
|
|
735
|
+
description: `Pi model probe ${kind}`,
|
|
736
|
+
kind: "validation",
|
|
737
|
+
model: options.model,
|
|
738
|
+
modelRuntime: options.modelRuntime,
|
|
739
|
+
...options.signal ? { signal: options.signal } : {},
|
|
740
|
+
thinkingLevel: options.thinkingLevel,
|
|
741
|
+
validate: (message) => assertProbeStopReason(message, kind)
|
|
742
|
+
}).pipe(Effect.map(({ message, reservation }) => ({
|
|
743
|
+
message,
|
|
744
|
+
turn: {
|
|
745
|
+
assistant: snapshotAssistant(message),
|
|
746
|
+
kind,
|
|
747
|
+
reservationId: reservation.id
|
|
748
|
+
}
|
|
749
|
+
})));
|
|
750
|
+
}
|
|
751
|
+
/** Run one bounded, synthetic request for activation/recovery canaries. */
|
|
752
|
+
function runPiModelCanary(options) {
|
|
753
|
+
return streamPaidProbeCall({
|
|
754
|
+
budget: options.budget,
|
|
755
|
+
context: {
|
|
756
|
+
messages: [{
|
|
757
|
+
content: "Reply with the exact text RIVUS_MODEL_CANARY_OK.",
|
|
758
|
+
role: "user",
|
|
759
|
+
timestamp: Date.now()
|
|
760
|
+
}],
|
|
761
|
+
systemPrompt: "This is an isolated model canary. Reply with RIVUS_MODEL_CANARY_OK and do not call tools."
|
|
762
|
+
},
|
|
763
|
+
description: `Pi model ${options.kind} canary`,
|
|
764
|
+
kind: options.kind,
|
|
765
|
+
model: options.model,
|
|
766
|
+
modelRuntime: options.modelRuntime,
|
|
767
|
+
...options.signal ? { signal: options.signal } : {},
|
|
768
|
+
thinkingLevel: options.thinkingLevel,
|
|
769
|
+
validate: (message) => {
|
|
770
|
+
assertCanaryStopReason(message);
|
|
771
|
+
if (!readText(message).includes("RIVUS_MODEL_CANARY_OK")) throw new PiModelProbeError("model canary did not produce the fixed response", "known");
|
|
772
|
+
}
|
|
773
|
+
}).pipe(Effect.map(({ message, outputTokens }) => Object.freeze({
|
|
774
|
+
assistant: snapshotAssistant(message),
|
|
775
|
+
outputTokens
|
|
776
|
+
})));
|
|
777
|
+
}
|
|
778
|
+
function streamPaidProbeCall(options) {
|
|
779
|
+
return Effect.gen(function* () {
|
|
780
|
+
const reservation = yield* options.budget.reservePaidCall({ kind: options.kind });
|
|
781
|
+
const callSignal = yield* Effect.sync(() => createPaidCallSignal(options.signal, options.budget.deadlineAt));
|
|
782
|
+
let settled = false;
|
|
783
|
+
const settle = (input) => options.budget.settlePaidCall({
|
|
784
|
+
...input,
|
|
785
|
+
reservation
|
|
786
|
+
}).pipe(Effect.tap(() => Effect.sync(() => {
|
|
787
|
+
settled = true;
|
|
788
|
+
})));
|
|
789
|
+
return yield* Effect.gen(function* () {
|
|
790
|
+
if (callSignal.expired) {
|
|
791
|
+
yield* settle({
|
|
792
|
+
outcome: "known",
|
|
793
|
+
outputTokens: 0
|
|
794
|
+
});
|
|
795
|
+
return yield* Effect.fail(new PiModelProbeError("model change deadline elapsed before the provider request", "known", void 0, "model_change_deadline_exceeded"));
|
|
796
|
+
}
|
|
797
|
+
const message = yield* Effect.tryPromise({
|
|
798
|
+
try: async () => {
|
|
799
|
+
const stream = options.modelRuntime.streamSimple(options.model, options.context, {
|
|
800
|
+
signal: callSignal.signal,
|
|
801
|
+
maxTokens: reservation.maxOutputTokens,
|
|
802
|
+
maxRetries: 0,
|
|
803
|
+
maxRetryDelayMs: 0,
|
|
804
|
+
...options.thinkingLevel === "off" ? {} : { reasoning: options.thinkingLevel }
|
|
805
|
+
});
|
|
806
|
+
return await awaitPiModelCall((async () => {
|
|
807
|
+
for await (const _event of stream) callSignal.signal.throwIfAborted();
|
|
808
|
+
callSignal.signal.throwIfAborted();
|
|
809
|
+
return await stream.result();
|
|
810
|
+
})(), {
|
|
811
|
+
abortMessage: "model probe aborted",
|
|
812
|
+
deadlineMessage: "model change deadline elapsed during provider streaming",
|
|
813
|
+
isDeadlineExceeded: callSignal.isDeadlineExceeded,
|
|
814
|
+
signal: callSignal.signal
|
|
815
|
+
});
|
|
816
|
+
},
|
|
817
|
+
catch: (error) => error
|
|
818
|
+
}).pipe(Effect.catchAll((error) => {
|
|
819
|
+
const deadlineExceeded = callSignal.isDeadlineExceeded() || error instanceof PiModelCallDeadlineExceeded;
|
|
820
|
+
const probeError = new PiModelProbeError(deadlineExceeded ? `${options.description} stream exceeded the model change deadline` : `${options.description} stream failed: ${error instanceof Error ? error.message : String(error)}`, "unknown", { cause: error }, deadlineExceeded ? "model_change_deadline_exceeded" : "model_change_probe_failed");
|
|
821
|
+
return settle({
|
|
822
|
+
outcome: "unknown",
|
|
823
|
+
outputTokens: reservation.maxOutputTokens
|
|
824
|
+
}).pipe(Effect.zipRight(Effect.fail(probeError)));
|
|
825
|
+
}));
|
|
826
|
+
const outputTokens = readOutputTokens(message.usage);
|
|
827
|
+
yield* settle({
|
|
828
|
+
outcome: "known",
|
|
829
|
+
outputTokens
|
|
830
|
+
});
|
|
831
|
+
yield* Effect.try({
|
|
832
|
+
try: () => options.validate(message),
|
|
833
|
+
catch: (error) => error
|
|
834
|
+
});
|
|
835
|
+
return {
|
|
836
|
+
message,
|
|
837
|
+
outputTokens,
|
|
838
|
+
reservation
|
|
839
|
+
};
|
|
840
|
+
}).pipe(Effect.ensuring(Effect.uninterruptible(Effect.suspend(() => settled ? Effect.void : settle({
|
|
841
|
+
outcome: "unknown",
|
|
842
|
+
outputTokens: reservation.maxOutputTokens
|
|
843
|
+
})).pipe(Effect.orDie))), Effect.ensuring(Effect.sync(callSignal.dispose)));
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
function createPaidCallSignal(parent, deadlineAt) {
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
const deadlineMs = Date.parse(deadlineAt);
|
|
849
|
+
const remainingMs = deadlineMs - Date.now();
|
|
850
|
+
let deadlineExceeded = !Number.isFinite(deadlineMs) || remainingMs <= 0;
|
|
851
|
+
let timer;
|
|
852
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
853
|
+
if (parent) if (parent.aborted) controller.abort(parent.reason);
|
|
854
|
+
else parent.addEventListener("abort", onParentAbort, { once: true });
|
|
855
|
+
if (remainingMs > 0 && !controller.signal.aborted) timer = setTimeout(() => {
|
|
856
|
+
deadlineExceeded = true;
|
|
857
|
+
controller.abort(/* @__PURE__ */ new Error("model change deadline elapsed"));
|
|
858
|
+
}, Math.min(remainingMs, 2147483647));
|
|
859
|
+
return {
|
|
860
|
+
expired: deadlineExceeded,
|
|
861
|
+
isDeadlineExceeded: () => deadlineExceeded || Number.isFinite(deadlineMs) && Date.now() >= deadlineMs,
|
|
862
|
+
signal: controller.signal,
|
|
863
|
+
dispose: () => {
|
|
864
|
+
if (timer) clearTimeout(timer);
|
|
865
|
+
parent?.removeEventListener("abort", onParentAbort);
|
|
866
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("model probe cancelled"));
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function assertProbeStopReason(message, kind) {
|
|
871
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") throw new PiModelProbeError(`candidate streamed an ${message.stopReason} response: ${message.errorMessage ?? "unknown provider error"}`, "known");
|
|
872
|
+
if (message.stopReason === "length" || message.stopReason === "deferred") throw new PiModelProbeError(`candidate response was truncated or deferred during ${kind}`, "known");
|
|
873
|
+
if (kind !== "initial" && message.stopReason !== "stop") throw new PiModelProbeError(`candidate used unsupported stop reason ${message.stopReason} during ${kind}`, "known");
|
|
874
|
+
}
|
|
875
|
+
function assertCanaryStopReason(message) {
|
|
876
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") throw new PiModelProbeError(`model canary streamed an ${message.stopReason} response: ${message.errorMessage ?? "unknown provider error"}`, "known");
|
|
877
|
+
if (message.stopReason === "length" || message.stopReason === "deferred" || message.stopReason !== "stop") throw new PiModelProbeError(`model canary used unsupported stop reason ${message.stopReason}`, "known");
|
|
878
|
+
}
|
|
879
|
+
function findSingleProbeToolCall(message) {
|
|
880
|
+
const calls = findToolCalls(message);
|
|
881
|
+
if (calls.length !== 1 || calls[0]?.name !== "rivus_model_management_probe") return void 0;
|
|
882
|
+
const call = calls[0];
|
|
883
|
+
return {
|
|
884
|
+
arguments: call.arguments,
|
|
885
|
+
id: call.id
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
function findToolCalls(message) {
|
|
889
|
+
return message.content.flatMap((content) => {
|
|
890
|
+
if (!isRecord$3(content) || content.type !== "toolCall") return [];
|
|
891
|
+
return [{
|
|
892
|
+
arguments: isRecord$3(content.arguments) ? content.arguments : {},
|
|
893
|
+
id: typeof content.id === "string" ? content.id : "",
|
|
894
|
+
name: typeof content.name === "string" ? content.name : ""
|
|
895
|
+
}];
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
function readText(message) {
|
|
899
|
+
return message.content.filter((content) => isRecord$3(content) && content.type === "text" && typeof content.text === "string").map((content) => content.text).join("");
|
|
900
|
+
}
|
|
901
|
+
function snapshotAssistant(message) {
|
|
902
|
+
return Object.freeze({
|
|
903
|
+
content: Object.freeze(message.content.map((content) => cloneSerializable(content))),
|
|
904
|
+
model: message.model,
|
|
905
|
+
provider: message.provider,
|
|
906
|
+
stopReason: message.stopReason,
|
|
907
|
+
usage: Object.freeze(cloneSerializable(message.usage))
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
function readOutputTokens(usage) {
|
|
911
|
+
if (isRecord$3(usage) && typeof usage.output === "number" && Number.isSafeInteger(usage.output) && usage.output >= 0) return usage.output;
|
|
912
|
+
return 0;
|
|
913
|
+
}
|
|
914
|
+
function cloneSerializable(value) {
|
|
915
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
916
|
+
if (Array.isArray(value)) return value.map(cloneSerializable);
|
|
917
|
+
if (!isRecord$3(value)) return void 0;
|
|
918
|
+
return Object.fromEntries(Object.entries(value).filter(([, nested]) => typeof nested !== "function" && nested !== void 0).map(([key, nested]) => [key, cloneSerializable(nested)]));
|
|
919
|
+
}
|
|
920
|
+
function isRecord$3(value) {
|
|
921
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
922
|
+
}
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region src/adapters/pi/model-management/pi-model-resolution.ts
|
|
925
|
+
var PiModelResolutionError = class extends Error {
|
|
926
|
+
name = "PiModelResolutionError";
|
|
927
|
+
code;
|
|
928
|
+
resolutionCode;
|
|
929
|
+
constructor(resolutionCode, message) {
|
|
930
|
+
super(message);
|
|
931
|
+
this.code = resolutionCode === "provider-unknown" ? "model_change_provider_unsupported" : "model_change_target_unsupported";
|
|
932
|
+
this.resolutionCode = resolutionCode;
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
/**
|
|
936
|
+
* Resolve one exact provider/model pair from the installed Pi model runtime.
|
|
937
|
+
*
|
|
938
|
+
* Model aliases and fuzzy matches are intentionally excluded here. Natural-language
|
|
939
|
+
* interpretation belongs to the application/Agent; the activation boundary must bind
|
|
940
|
+
* to the exact model that the SDK can actually construct.
|
|
941
|
+
*/
|
|
942
|
+
function resolvePiModel(options) {
|
|
943
|
+
const provider = options.target.provider.trim();
|
|
944
|
+
const modelId = options.target.model.trim();
|
|
945
|
+
if (!provider || !modelId || provider.includes("/") || modelId.includes("\n")) throw new PiModelResolutionError("invalid-reference", "Pi model target must contain a provider and model id without a provider separator in either component");
|
|
946
|
+
if (options.modelRuntime.getModels(provider).length === 0) {
|
|
947
|
+
if (!options.modelRuntime.getProvider(provider)) throw new PiModelResolutionError("provider-unknown", `Pi provider is not installed: ${provider}`);
|
|
948
|
+
throw new PiModelResolutionError("model-unknown", `Pi provider ${provider} has no known model metadata; configure a declarative model before activation`);
|
|
949
|
+
}
|
|
950
|
+
const model = options.modelRuntime.getModel(provider, modelId);
|
|
951
|
+
if (!model) throw new PiModelResolutionError("model-unknown", `Pi model ${provider}/${modelId} is not present in the installed model catalog`);
|
|
952
|
+
return model;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Ensure metadata for an exact model id that is absent from the installed SDK
|
|
956
|
+
* catalog, then recompose the provider from models.json. Phase 1 only ships the
|
|
957
|
+
* verified DeepSeek declaration required by the management contract; all other
|
|
958
|
+
* unknown ids fail closed and must be supplied by a future explicit catalog path.
|
|
959
|
+
*/
|
|
960
|
+
async function ensurePiModel(options) {
|
|
961
|
+
try {
|
|
962
|
+
return resolvePiModel(options);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
if (!(error instanceof PiModelResolutionError) || error.resolutionCode !== "model-unknown") throw error;
|
|
965
|
+
}
|
|
966
|
+
const definition = knownPiDeclarativeModel(options.modelRuntime, options.target);
|
|
967
|
+
if (!definition) throw new PiModelResolutionError("model-unknown", `Pi model ${options.target.provider}/${options.target.model} is not present in the installed model catalog and has no approved declarative metadata`);
|
|
968
|
+
await mergePiProviderModelDeclaration({
|
|
969
|
+
filePath: options.modelsPath,
|
|
970
|
+
model: definition,
|
|
971
|
+
provider: options.target.provider
|
|
972
|
+
});
|
|
973
|
+
const refreshError = (await refreshPiModelCatalog(options.modelRuntime, {
|
|
974
|
+
allowNetwork: false,
|
|
975
|
+
provider: options.target.provider,
|
|
976
|
+
...options.signal ? { signal: options.signal } : {}
|
|
977
|
+
})).errors.get(options.target.provider);
|
|
978
|
+
if (refreshError) throw new PiModelResolutionError("model-unknown", `Pi provider ${options.target.provider} could not load declarative model metadata: ${refreshError.message}`);
|
|
979
|
+
return resolvePiModel(options);
|
|
980
|
+
}
|
|
981
|
+
function resolvePiModelBinding(options) {
|
|
982
|
+
const model = resolvePiModel(options);
|
|
983
|
+
return Object.freeze({
|
|
984
|
+
binding: Object.freeze({
|
|
985
|
+
bindingRevision: options.bindingRevision,
|
|
986
|
+
model: model.id,
|
|
987
|
+
provider: model.provider
|
|
988
|
+
}),
|
|
989
|
+
model
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
/** Refresh installed SDK metadata, retaining the SDK's persisted declarative catalog. */
|
|
993
|
+
async function refreshPiModelCatalog(modelRuntime, options = {}) {
|
|
994
|
+
const providers = options.provider === void 0 ? void 0 : [options.provider];
|
|
995
|
+
const result = await modelRuntime.refresh({
|
|
996
|
+
allowNetwork: options.allowNetwork ?? false,
|
|
997
|
+
...providers ? { providers } : {},
|
|
998
|
+
...options.signal ? { signal: options.signal } : {}
|
|
999
|
+
});
|
|
1000
|
+
const refreshedProviders = providers ?? modelRuntime.getRegisteredProviderIds();
|
|
1001
|
+
return Object.freeze({
|
|
1002
|
+
errors: result.errors,
|
|
1003
|
+
refreshedProviders: Object.freeze([...refreshedProviders])
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
function samePiProvider(left, right) {
|
|
1007
|
+
return left.provider.trim() === right.provider.trim();
|
|
1008
|
+
}
|
|
1009
|
+
function knownPiDeclarativeModel(modelRuntime, target) {
|
|
1010
|
+
if (target.provider !== "deepseek" || target.model !== "deepseek-flash") return void 0;
|
|
1011
|
+
const source = modelRuntime.getModel("deepseek", "deepseek-v4-flash");
|
|
1012
|
+
if (!source) return void 0;
|
|
1013
|
+
return {
|
|
1014
|
+
api: source.api,
|
|
1015
|
+
baseUrl: source.baseUrl,
|
|
1016
|
+
...source.compat ? { compat: source.compat } : {},
|
|
1017
|
+
contextWindow: 1e6,
|
|
1018
|
+
cost: {
|
|
1019
|
+
cacheRead: .006,
|
|
1020
|
+
cacheWrite: 0,
|
|
1021
|
+
input: .3,
|
|
1022
|
+
output: 1.2
|
|
1023
|
+
},
|
|
1024
|
+
id: target.model,
|
|
1025
|
+
input: ["text", "image"],
|
|
1026
|
+
maxTokens: 384e3,
|
|
1027
|
+
name: "DeepSeek V4.1 Flash",
|
|
1028
|
+
reasoning: true,
|
|
1029
|
+
...source.samplingParams ? { samplingParams: source.samplingParams } : {},
|
|
1030
|
+
...source.thinkingLevelMap ? { thinkingLevelMap: source.thinkingLevelMap } : {}
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/adapters/pi/model-management/pi-history-compatibility.ts
|
|
1035
|
+
var PiHistoryCompatibilityError = class extends Error {
|
|
1036
|
+
name = "PiHistoryCompatibilityError";
|
|
1037
|
+
code = "model_change_history_incompatible";
|
|
1038
|
+
messageIndex;
|
|
1039
|
+
constructor(message, messageIndex) {
|
|
1040
|
+
super(message);
|
|
1041
|
+
this.messageIndex = messageIndex;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
/**
|
|
1045
|
+
* Check the structural parts of a persisted Pi transcript that providers need for
|
|
1046
|
+
* the next request. The transcript is never returned or copied; only counts are
|
|
1047
|
+
* exposed for diagnostics. This deliberately rejects orphaned tool calls/results
|
|
1048
|
+
* instead of deleting or rewriting history during a model change.
|
|
1049
|
+
*/
|
|
1050
|
+
function inspectPiSessionHistory(sessionManager) {
|
|
1051
|
+
const context = sessionManager.buildSessionContext();
|
|
1052
|
+
const pendingToolCalls = /* @__PURE__ */ new Map();
|
|
1053
|
+
let assistantThinkingBlocks = 0;
|
|
1054
|
+
let assistantToolCalls = 0;
|
|
1055
|
+
let imageBlocks = 0;
|
|
1056
|
+
let toolResults = 0;
|
|
1057
|
+
for (const [messageIndex, message] of context.messages.entries()) {
|
|
1058
|
+
if (!isRecord$2(message) || typeof message.role !== "string") throw new PiHistoryCompatibilityError("Pi session history contains a malformed message", messageIndex);
|
|
1059
|
+
if (message.role === "assistant") {
|
|
1060
|
+
const content = message.content;
|
|
1061
|
+
if (!Array.isArray(content)) throw new PiHistoryCompatibilityError("Pi assistant history message content must be an array", messageIndex);
|
|
1062
|
+
for (const block of content) {
|
|
1063
|
+
if (!isRecord$2(block) || typeof block.type !== "string") throw new PiHistoryCompatibilityError("Pi assistant history contains a malformed content block", messageIndex);
|
|
1064
|
+
if (block.type === "thinking") {
|
|
1065
|
+
if (typeof block.thinking !== "string") throw new PiHistoryCompatibilityError("Pi assistant thinking content is malformed", messageIndex);
|
|
1066
|
+
assistantThinkingBlocks += 1;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
if (block.type === "toolCall") {
|
|
1070
|
+
if (typeof block.id !== "string" || !block.id || typeof block.name !== "string" || !block.name) throw new PiHistoryCompatibilityError("Pi assistant tool call is malformed", messageIndex);
|
|
1071
|
+
if (!isRecord$2(block.arguments)) throw new PiHistoryCompatibilityError("Pi assistant tool call arguments are malformed", messageIndex);
|
|
1072
|
+
pendingToolCalls.set(block.id, block.name);
|
|
1073
|
+
assistantToolCalls += 1;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (block.type === "text") {
|
|
1077
|
+
if (typeof block.text !== "string") throw new PiHistoryCompatibilityError("Pi assistant text content is malformed", messageIndex);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (block.type === "image") {
|
|
1081
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") throw new PiHistoryCompatibilityError("Pi assistant image content is malformed", messageIndex);
|
|
1082
|
+
imageBlocks += 1;
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
throw new PiHistoryCompatibilityError(`Pi assistant content type is unsupported: ${String(block.type)}`, messageIndex);
|
|
1086
|
+
}
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
if (message.role === "toolResult") {
|
|
1090
|
+
const content = message.content;
|
|
1091
|
+
if (!Array.isArray(content)) throw new PiHistoryCompatibilityError("Pi tool result content must be an array", messageIndex);
|
|
1092
|
+
if (typeof message.toolCallId !== "string" || !pendingToolCalls.has(message.toolCallId)) throw new PiHistoryCompatibilityError("Pi tool result has no preceding assistant tool call", messageIndex);
|
|
1093
|
+
if (typeof message.toolName !== "string" || !message.toolName) throw new PiHistoryCompatibilityError("Pi tool result is missing its tool name", messageIndex);
|
|
1094
|
+
if (pendingToolCalls.get(message.toolCallId) !== message.toolName) throw new PiHistoryCompatibilityError("Pi tool result tool name does not match its assistant tool call", messageIndex);
|
|
1095
|
+
imageBlocks += countImageBlocks(content);
|
|
1096
|
+
validatePlainContent(content, messageIndex, "tool result");
|
|
1097
|
+
pendingToolCalls.delete(message.toolCallId);
|
|
1098
|
+
toolResults += 1;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
if (message.role === "user") {
|
|
1102
|
+
if (typeof message.content !== "string") {
|
|
1103
|
+
if (!Array.isArray(message.content)) throw new PiHistoryCompatibilityError("Pi user history content must be text or an array", messageIndex);
|
|
1104
|
+
imageBlocks += countImageBlocks(message.content);
|
|
1105
|
+
validatePlainContent(message.content, messageIndex, "user");
|
|
1106
|
+
}
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (message.role === "custom") {
|
|
1110
|
+
if (typeof message.customType !== "string" || typeof message.display !== "boolean") throw new PiHistoryCompatibilityError("Pi custom history message metadata is malformed", messageIndex);
|
|
1111
|
+
if (typeof message.content === "string") continue;
|
|
1112
|
+
if (!Array.isArray(message.content)) throw new PiHistoryCompatibilityError("Pi custom history content must be text or an array", messageIndex);
|
|
1113
|
+
imageBlocks += countImageBlocks(message.content);
|
|
1114
|
+
validatePlainContent(message.content, messageIndex, "custom");
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (message.role === "bashExecution") {
|
|
1118
|
+
if (typeof message.command !== "string" || typeof message.output !== "string" || typeof message.cancelled !== "boolean" || typeof message.truncated !== "boolean" || message.exitCode !== void 0 && message.exitCode !== null && typeof message.exitCode !== "number" || message.fullOutputPath !== void 0 && typeof message.fullOutputPath !== "string" || message.excludeFromContext !== void 0 && typeof message.excludeFromContext !== "boolean") throw new PiHistoryCompatibilityError("Pi bash history message is malformed", messageIndex);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
if (message.role === "branchSummary") {
|
|
1122
|
+
if (typeof message.summary !== "string" || typeof message.fromId !== "string") throw new PiHistoryCompatibilityError("Pi branch summary history message is malformed", messageIndex);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (message.role === "compactionSummary") {
|
|
1126
|
+
if (typeof message.summary !== "string" || typeof message.tokensBefore !== "number" || !Number.isFinite(message.tokensBefore) || message.tokensBefore < 0) throw new PiHistoryCompatibilityError("Pi compaction summary history message is malformed", messageIndex);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
throw new PiHistoryCompatibilityError(`Pi history role is unsupported: ${String(message.role)}`, messageIndex);
|
|
1130
|
+
}
|
|
1131
|
+
if (pendingToolCalls.size > 0) throw new PiHistoryCompatibilityError("Pi session history contains an assistant tool call without a result", context.messages.length);
|
|
1132
|
+
return Object.freeze({
|
|
1133
|
+
assistantThinkingBlocks,
|
|
1134
|
+
assistantToolCalls,
|
|
1135
|
+
compatible: true,
|
|
1136
|
+
imageBlocks,
|
|
1137
|
+
messageCount: context.messages.length,
|
|
1138
|
+
toolResults
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
function countImageBlocks(content) {
|
|
1142
|
+
return content.filter((block) => isRecord$2(block) && block.type === "image").length;
|
|
1143
|
+
}
|
|
1144
|
+
function assertPiSessionHistoryCompatible(sessionManager) {
|
|
1145
|
+
return inspectPiSessionHistory(sessionManager);
|
|
1146
|
+
}
|
|
1147
|
+
function validatePlainContent(content, messageIndex, role) {
|
|
1148
|
+
for (const block of content) {
|
|
1149
|
+
if (!isRecord$2(block) || typeof block.type !== "string") throw new PiHistoryCompatibilityError(`Pi ${role} history contains a malformed content block`, messageIndex);
|
|
1150
|
+
if (block.type === "text") {
|
|
1151
|
+
if (typeof block.text !== "string") throw new PiHistoryCompatibilityError(`Pi ${role} text content is malformed`, messageIndex);
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
if (block.type === "image") {
|
|
1155
|
+
if (typeof block.data !== "string" || typeof block.mimeType !== "string") throw new PiHistoryCompatibilityError(`Pi ${role} image content is malformed`, messageIndex);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
throw new PiHistoryCompatibilityError(`Pi ${role} content type is unsupported: ${block.type}`, messageIndex);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
function isRecord$2(value) {
|
|
1162
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1163
|
+
}
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/adapters/pi/model-management/pi-model-runtime.ts
|
|
1166
|
+
var PiModelRuntimeMutationUnknownError = class extends Error {
|
|
1167
|
+
name = "PiModelRuntimeMutationUnknownError";
|
|
1168
|
+
outcome = "unknown";
|
|
1169
|
+
constructor(message, options) {
|
|
1170
|
+
super(message, options);
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
var PiModelTargetCompatibilityError = class extends Error {
|
|
1174
|
+
name = "PiModelTargetCompatibilityError";
|
|
1175
|
+
code = "model_change_target_unsupported";
|
|
1176
|
+
};
|
|
1177
|
+
/**
|
|
1178
|
+
* Adapter around Pi's live AgentSession instances.
|
|
1179
|
+
*
|
|
1180
|
+
* The application owns the safe Run boundary. Once that boundary is held, this
|
|
1181
|
+
* adapter waits for every cached session, validates its persisted context, and
|
|
1182
|
+
* calls AgentSession.setModel so the session manager and conversation history
|
|
1183
|
+
* survive the switch. Disposing cached sessions would silently create a new
|
|
1184
|
+
* transcript on the next Run and is therefore intentionally unsupported here.
|
|
1185
|
+
*/
|
|
1186
|
+
function createPiModelRuntime(options) {
|
|
1187
|
+
const registries = new Set(options.sessionRegistries ?? []);
|
|
1188
|
+
const unsettledMutations = /* @__PURE__ */ new Set();
|
|
1189
|
+
let active = options.initial;
|
|
1190
|
+
let selectionInitializationStarted = options.initial !== void 0;
|
|
1191
|
+
const resolveCandidate = async (context, operation) => {
|
|
1192
|
+
assertOperationActive(operation);
|
|
1193
|
+
const model = options.modelRuntime.getModel(context.target.provider, context.target.model);
|
|
1194
|
+
const resolved = model ? model : options.modelCatalogPath ? await ensurePiModel({
|
|
1195
|
+
modelRuntime: options.modelRuntime,
|
|
1196
|
+
modelsPath: options.modelCatalogPath,
|
|
1197
|
+
target: context.target,
|
|
1198
|
+
...operation?.signal ? { signal: operation.signal } : {}
|
|
1199
|
+
}) : resolvePiModel({
|
|
1200
|
+
modelRuntime: options.modelRuntime,
|
|
1201
|
+
target: context.target
|
|
1202
|
+
});
|
|
1203
|
+
assertOperationActive(operation);
|
|
1204
|
+
return Object.freeze({
|
|
1205
|
+
binding: Object.freeze({
|
|
1206
|
+
bindingRevision: context.bindingRevision,
|
|
1207
|
+
model: resolved.id,
|
|
1208
|
+
provider: resolved.provider
|
|
1209
|
+
}),
|
|
1210
|
+
model: resolved
|
|
1211
|
+
});
|
|
1212
|
+
};
|
|
1213
|
+
const listSessions = async () => {
|
|
1214
|
+
const handles = await Promise.all([...registries].map((registry) => registry.list()));
|
|
1215
|
+
const unique = [];
|
|
1216
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1217
|
+
for (const handle of handles.flat()) {
|
|
1218
|
+
if (seen.has(handle)) continue;
|
|
1219
|
+
seen.add(handle);
|
|
1220
|
+
unique.push(handle);
|
|
1221
|
+
}
|
|
1222
|
+
return unique;
|
|
1223
|
+
};
|
|
1224
|
+
const waitForIdleAndCheckHistory = async (model, operation) => {
|
|
1225
|
+
assertOperationActive(operation);
|
|
1226
|
+
const sessions = await listSessions();
|
|
1227
|
+
let abortPromise;
|
|
1228
|
+
const abortSessions = () => {
|
|
1229
|
+
if (abortPromise) return abortPromise;
|
|
1230
|
+
abortPromise = Promise.all(sessions.map((handle) => Promise.resolve().then(() => handle.session.abort?.()))).then(() => void 0);
|
|
1231
|
+
return abortPromise;
|
|
1232
|
+
};
|
|
1233
|
+
const onAbort = () => {
|
|
1234
|
+
abortSessions();
|
|
1235
|
+
};
|
|
1236
|
+
const signal = operation?.signal;
|
|
1237
|
+
if (signal) signal.addEventListener("abort", onAbort, { once: true });
|
|
1238
|
+
try {
|
|
1239
|
+
if (signal?.aborted) abortSessions();
|
|
1240
|
+
await awaitPiModelCall(Promise.all(sessions.map((handle) => Promise.resolve().then(() => handle.session.waitForIdle?.()))), {
|
|
1241
|
+
onAbort: abortSessions,
|
|
1242
|
+
...signal ? { signal } : {}
|
|
1243
|
+
});
|
|
1244
|
+
assertOperationActive(operation);
|
|
1245
|
+
for (const handle of sessions) {
|
|
1246
|
+
assertOperationActive(operation);
|
|
1247
|
+
const manager = handle.session.sessionManager;
|
|
1248
|
+
if (!manager) continue;
|
|
1249
|
+
const context = manager.buildSessionContext();
|
|
1250
|
+
const history = assertPiSessionHistoryCompatible(manager);
|
|
1251
|
+
if (model) {
|
|
1252
|
+
if (history.imageBlocks > 0 && !model.input.includes("image")) throw new PiModelTargetCompatibilityError(`Pi model ${model.provider}/${model.id} does not support ${history.imageBlocks} image history block(s)`);
|
|
1253
|
+
if (hasTextInput(context.messages) && !model.input.includes("text")) throw new PiModelTargetCompatibilityError(`Pi model ${model.provider}/${model.id} does not support text history input`);
|
|
1254
|
+
const estimatedTokens = context.messages.reduce((total, message) => total + estimateTokens(message), 0);
|
|
1255
|
+
if (!Number.isFinite(model.contextWindow) || estimatedTokens > model.contextWindow) throw new PiModelTargetCompatibilityError(`Pi model ${model.provider}/${model.id} context window ${model.contextWindow} is smaller than the estimated ${estimatedTokens}-token session history`);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
assertOperationActive(operation);
|
|
1259
|
+
} finally {
|
|
1260
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
1261
|
+
if (abortPromise) abortPromise.catch(() => void 0);
|
|
1262
|
+
}
|
|
1263
|
+
return sessions;
|
|
1264
|
+
};
|
|
1265
|
+
const setSessionsModel = async (sessions, model, thinkingLevel, operation) => {
|
|
1266
|
+
assertNoUnsettledMutations();
|
|
1267
|
+
for (const handle of sessions) {
|
|
1268
|
+
assertOperationActive(operation);
|
|
1269
|
+
if (!handle.session.setModel) throw new Error("Pi session does not expose live setModel; refusing to discard its history");
|
|
1270
|
+
await awaitModelMutation(Promise.resolve().then(() => {
|
|
1271
|
+
assertOperationActive(operation);
|
|
1272
|
+
return handle.session.setModel(model);
|
|
1273
|
+
}), operation);
|
|
1274
|
+
assertOperationActive(operation);
|
|
1275
|
+
handle.session.setThinkingLevel?.(thinkingLevel);
|
|
1276
|
+
assertOperationActive(operation);
|
|
1277
|
+
await awaitModelMutation(Promise.resolve().then(() => {
|
|
1278
|
+
assertOperationActive(operation);
|
|
1279
|
+
return handle.refreshResources?.();
|
|
1280
|
+
}), operation);
|
|
1281
|
+
assertOperationActive(operation);
|
|
1282
|
+
await awaitModelMutation(Promise.resolve().then(() => {
|
|
1283
|
+
assertOperationActive(operation);
|
|
1284
|
+
return handle.session.reload?.();
|
|
1285
|
+
}), operation);
|
|
1286
|
+
assertOperationActive(operation);
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const trackUnsettledMutation = (promise) => {
|
|
1290
|
+
const tracked = promise.then(() => void 0, () => void 0);
|
|
1291
|
+
unsettledMutations.add(tracked);
|
|
1292
|
+
tracked.then(() => unsettledMutations.delete(tracked), () => unsettledMutations.delete(tracked));
|
|
1293
|
+
};
|
|
1294
|
+
const awaitModelMutation = async (promise, operation) => {
|
|
1295
|
+
let timedOut = false;
|
|
1296
|
+
const completion = Promise.resolve(promise);
|
|
1297
|
+
try {
|
|
1298
|
+
if (!operation?.signal) return completion;
|
|
1299
|
+
return await awaitPiModelCall(completion, {
|
|
1300
|
+
onAbort: () => {
|
|
1301
|
+
timedOut = true;
|
|
1302
|
+
trackUnsettledMutation(completion);
|
|
1303
|
+
},
|
|
1304
|
+
signal: operation.signal
|
|
1305
|
+
});
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
if (timedOut) throw new PiModelRuntimeMutationUnknownError("Pi model session mutation did not settle before the model change deadline", { cause: error });
|
|
1308
|
+
throw error;
|
|
1309
|
+
}
|
|
1310
|
+
};
|
|
1311
|
+
const assertNoUnsettledMutations = () => {
|
|
1312
|
+
if (unsettledMutations.size > 0) throw new PiModelRuntimeMutationUnknownError("A previous Pi model session mutation is still settling; refusing a concurrent mutation");
|
|
1313
|
+
};
|
|
1314
|
+
const createProbeBudget = (budget) => {
|
|
1315
|
+
const settlements = /* @__PURE__ */ new Map();
|
|
1316
|
+
return {
|
|
1317
|
+
deadlineAt: budget.deadlineAt,
|
|
1318
|
+
reservePaidCall: ({ kind, maxOutputTokens }) => budget.reservePaidCall({
|
|
1319
|
+
kind,
|
|
1320
|
+
...maxOutputTokens === void 0 ? {} : { maxOutputTokens }
|
|
1321
|
+
}).pipe(Effect.tap((result) => Effect.sync(() => settlements.set(result.reservation.id, result.settle))), Effect.map((result) => result.reservation)),
|
|
1322
|
+
settlePaidCall: ({ outcome, outputTokens, reservation }) => Effect.suspend(() => {
|
|
1323
|
+
const settle = settlements.get(reservation.id);
|
|
1324
|
+
if (!settle) return Effect.fail(/* @__PURE__ */ new Error(`Pi probe attempted to settle unknown budget reservation: ${reservation.id}`));
|
|
1325
|
+
return settle({
|
|
1326
|
+
outcome,
|
|
1327
|
+
outputTokens
|
|
1328
|
+
}).pipe(Effect.tap(() => Effect.sync(() => settlements.delete(reservation.id))), Effect.asVoid);
|
|
1329
|
+
})
|
|
1330
|
+
};
|
|
1331
|
+
};
|
|
1332
|
+
const runCanary = (kind, model, budget, operation) => runPiModelCanary({
|
|
1333
|
+
budget: createProbeBudget(budget),
|
|
1334
|
+
kind,
|
|
1335
|
+
model,
|
|
1336
|
+
modelRuntime: options.modelRuntime,
|
|
1337
|
+
signal: operation.signal,
|
|
1338
|
+
thinkingLevel: currentThinkingLevel()
|
|
1339
|
+
});
|
|
1340
|
+
return {
|
|
1341
|
+
activate: (input) => Effect.gen(function* () {
|
|
1342
|
+
const operation = yield* Effect.sync(() => createOperationSignal(input.budget.deadlineAt));
|
|
1343
|
+
return yield* Effect.gen(function* () {
|
|
1344
|
+
yield* Effect.try({
|
|
1345
|
+
try: assertNoUnsettledMutations,
|
|
1346
|
+
catch: (error) => error
|
|
1347
|
+
});
|
|
1348
|
+
const candidate = yield* Effect.tryPromise({
|
|
1349
|
+
try: () => resolveCandidate(input.context, operation),
|
|
1350
|
+
catch: (error) => error
|
|
1351
|
+
});
|
|
1352
|
+
if (!sameTarget(candidate.binding, input.validation.model)) return yield* Effect.fail(/* @__PURE__ */ new Error("Pi activation candidate does not match validated model"));
|
|
1353
|
+
const canary = yield* runCanary("activation", candidate.model, input.budget, operation);
|
|
1354
|
+
const sessions = yield* Effect.tryPromise({
|
|
1355
|
+
try: () => waitForIdleAndCheckHistory(candidate.model, operation),
|
|
1356
|
+
catch: (error) => error
|
|
1357
|
+
});
|
|
1358
|
+
if (input.beforeApply) yield* input.beforeApply();
|
|
1359
|
+
yield* Effect.tryPromise({
|
|
1360
|
+
try: () => setSessionsModel(sessions, candidate.model, currentThinkingLevel(), operation),
|
|
1361
|
+
catch: (error) => error
|
|
1362
|
+
});
|
|
1363
|
+
active = {
|
|
1364
|
+
binding: candidate.binding,
|
|
1365
|
+
model: candidate.model,
|
|
1366
|
+
thinkingLevel: currentThinkingLevel()
|
|
1367
|
+
};
|
|
1368
|
+
return Object.freeze({
|
|
1369
|
+
evidence: Object.freeze({
|
|
1370
|
+
canary: canary.assistant,
|
|
1371
|
+
kind: "pi-model-activation-canary-v1",
|
|
1372
|
+
model: candidate.binding,
|
|
1373
|
+
sessionCount: sessions.length
|
|
1374
|
+
}),
|
|
1375
|
+
outputTokens: canary.outputTokens,
|
|
1376
|
+
switched: true
|
|
1377
|
+
});
|
|
1378
|
+
}).pipe(Effect.ensuring(Effect.sync(operation.dispose)));
|
|
1379
|
+
}),
|
|
1380
|
+
current: () => Effect.succeed(active?.binding),
|
|
1381
|
+
drain: (context) => Effect.gen(function* () {
|
|
1382
|
+
const operation = yield* Effect.sync(() => createOperationSignal(context.deadlineAt));
|
|
1383
|
+
yield* Effect.tryPromise({
|
|
1384
|
+
try: async () => {
|
|
1385
|
+
try {
|
|
1386
|
+
const candidate = await resolveCandidate(context, operation);
|
|
1387
|
+
await waitForIdleAndCheckHistory(candidate.model, operation);
|
|
1388
|
+
} finally {
|
|
1389
|
+
operation.dispose();
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
catch: (error) => error
|
|
1393
|
+
});
|
|
1394
|
+
}),
|
|
1395
|
+
refreshModelCatalog: (input = {}) => Effect.tryPromise({
|
|
1396
|
+
try: async () => {
|
|
1397
|
+
const operation = createOperationSignal(input.deadlineAt, input.signal);
|
|
1398
|
+
try {
|
|
1399
|
+
assertOperationActive({
|
|
1400
|
+
...input.deadlineAt === void 0 ? {} : { deadlineAt: input.deadlineAt },
|
|
1401
|
+
signal: operation.signal
|
|
1402
|
+
});
|
|
1403
|
+
const result = await options.modelRuntime.refresh({
|
|
1404
|
+
allowNetwork: input.allowNetwork ?? false,
|
|
1405
|
+
...input.provider ? { providers: [input.provider] } : {},
|
|
1406
|
+
signal: operation.signal
|
|
1407
|
+
});
|
|
1408
|
+
if (result.errors.size > 0) throw new Error([...result.errors.entries()].map(([provider, error]) => `${provider}: ${error.message}`).join("; "));
|
|
1409
|
+
assertOperationActive({
|
|
1410
|
+
...input.deadlineAt === void 0 ? {} : { deadlineAt: input.deadlineAt },
|
|
1411
|
+
signal: operation.signal
|
|
1412
|
+
});
|
|
1413
|
+
} finally {
|
|
1414
|
+
operation.dispose();
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
catch: (error) => error
|
|
1418
|
+
}),
|
|
1419
|
+
registerSessionRegistry: (registry) => {
|
|
1420
|
+
registries.add(registry);
|
|
1421
|
+
return () => registries.delete(registry);
|
|
1422
|
+
},
|
|
1423
|
+
initializeSelection: (input) => Effect.tryPromise({
|
|
1424
|
+
try: async () => {
|
|
1425
|
+
if (selectionInitializationStarted || active !== void 0) throw new Error("Pi model runtime selection has already been initialized");
|
|
1426
|
+
if (registries.size > 0) throw new Error("Pi model runtime selection must be initialized before any Pi session registry is attached");
|
|
1427
|
+
selectionInitializationStarted = true;
|
|
1428
|
+
try {
|
|
1429
|
+
const context = {
|
|
1430
|
+
baseline: input.binding,
|
|
1431
|
+
bindingRevision: input.binding.bindingRevision,
|
|
1432
|
+
target: input.binding
|
|
1433
|
+
};
|
|
1434
|
+
const candidate = await resolveCandidate(context);
|
|
1435
|
+
active = {
|
|
1436
|
+
binding: candidate.binding,
|
|
1437
|
+
model: candidate.model,
|
|
1438
|
+
thinkingLevel: input.thinkingLevel
|
|
1439
|
+
};
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
selectionInitializationStarted = false;
|
|
1442
|
+
throw error;
|
|
1443
|
+
}
|
|
1444
|
+
},
|
|
1445
|
+
catch: (error) => error
|
|
1446
|
+
}),
|
|
1447
|
+
getSessionOptions: () => Object.freeze({
|
|
1448
|
+
...active?.model ? { model: active.model } : {},
|
|
1449
|
+
...active?.thinkingLevel ? { thinkingLevel: active.thinkingLevel } : {}
|
|
1450
|
+
}),
|
|
1451
|
+
releasePrevious: (input) => Effect.try({
|
|
1452
|
+
try: () => {
|
|
1453
|
+
assertBeforeDeadline(input.deadlineAt);
|
|
1454
|
+
},
|
|
1455
|
+
catch: (error) => error
|
|
1456
|
+
}),
|
|
1457
|
+
restore: (input) => Effect.gen(function* () {
|
|
1458
|
+
const baselineContext = {
|
|
1459
|
+
...input.context,
|
|
1460
|
+
target: input.context.baseline
|
|
1461
|
+
};
|
|
1462
|
+
const operation = yield* Effect.sync(() => createOperationSignal(input.budget.deadlineAt));
|
|
1463
|
+
return yield* Effect.gen(function* () {
|
|
1464
|
+
yield* Effect.try({
|
|
1465
|
+
try: assertNoUnsettledMutations,
|
|
1466
|
+
catch: (error) => error
|
|
1467
|
+
});
|
|
1468
|
+
const baseline = yield* Effect.tryPromise({
|
|
1469
|
+
try: () => resolveCandidate(baselineContext, operation),
|
|
1470
|
+
catch: (error) => error
|
|
1471
|
+
});
|
|
1472
|
+
const canary = yield* runCanary("restore", baseline.model, input.budget, operation);
|
|
1473
|
+
const sessions = yield* Effect.tryPromise({
|
|
1474
|
+
try: () => waitForIdleAndCheckHistory(baseline.model, operation),
|
|
1475
|
+
catch: (error) => error
|
|
1476
|
+
});
|
|
1477
|
+
yield* Effect.tryPromise({
|
|
1478
|
+
try: () => setSessionsModel(sessions, baseline.model, currentThinkingLevel(), operation),
|
|
1479
|
+
catch: (error) => error
|
|
1480
|
+
});
|
|
1481
|
+
active = {
|
|
1482
|
+
binding: baseline.binding,
|
|
1483
|
+
model: baseline.model,
|
|
1484
|
+
thinkingLevel: currentThinkingLevel()
|
|
1485
|
+
};
|
|
1486
|
+
return Object.freeze({
|
|
1487
|
+
evidence: Object.freeze({
|
|
1488
|
+
canary: canary.assistant,
|
|
1489
|
+
kind: "pi-model-restore-canary-v1",
|
|
1490
|
+
model: baseline.binding,
|
|
1491
|
+
reason: input.reason,
|
|
1492
|
+
sessionCount: sessions.length
|
|
1493
|
+
}),
|
|
1494
|
+
restored: true
|
|
1495
|
+
});
|
|
1496
|
+
}).pipe(Effect.ensuring(Effect.sync(operation.dispose)));
|
|
1497
|
+
}),
|
|
1498
|
+
validate: (input) => Effect.gen(function* () {
|
|
1499
|
+
const operation = yield* Effect.sync(() => createOperationSignal(input.budget.deadlineAt));
|
|
1500
|
+
return yield* Effect.gen(function* () {
|
|
1501
|
+
const candidate = yield* Effect.tryPromise({
|
|
1502
|
+
try: () => resolveCandidate(input.context, operation),
|
|
1503
|
+
catch: (error) => error
|
|
1504
|
+
});
|
|
1505
|
+
const probe = yield* probePiModel({
|
|
1506
|
+
budget: createProbeBudget(input.budget),
|
|
1507
|
+
model: candidate.model,
|
|
1508
|
+
modelRuntime: options.modelRuntime,
|
|
1509
|
+
signal: operation.signal,
|
|
1510
|
+
thinkingLevel: currentThinkingLevel()
|
|
1511
|
+
});
|
|
1512
|
+
return Object.freeze({
|
|
1513
|
+
evidence: Object.freeze({
|
|
1514
|
+
assistantThinkingBlocks: probe.assistantThinkingBlocks,
|
|
1515
|
+
followupText: probe.followupText,
|
|
1516
|
+
kind: "pi-model-probe-v1",
|
|
1517
|
+
model: probe.model,
|
|
1518
|
+
outputTokens: probe.outputTokens,
|
|
1519
|
+
toolCallId: probe.toolCallId,
|
|
1520
|
+
toolName: probe.toolName,
|
|
1521
|
+
transcript: probe.transcript,
|
|
1522
|
+
toolRoundtrip: true
|
|
1523
|
+
}),
|
|
1524
|
+
model: candidate.binding,
|
|
1525
|
+
outputTokens: probe.outputTokens,
|
|
1526
|
+
passed: true
|
|
1527
|
+
});
|
|
1528
|
+
}).pipe(Effect.ensuring(Effect.sync(operation.dispose)));
|
|
1529
|
+
})
|
|
1530
|
+
};
|
|
1531
|
+
function currentThinkingLevel() {
|
|
1532
|
+
return active?.thinkingLevel ?? "medium";
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
function sameTarget(left, right) {
|
|
1536
|
+
return left.bindingRevision === right.bindingRevision && left.provider === right.provider && left.model === right.model;
|
|
1537
|
+
}
|
|
1538
|
+
function hasTextInput(messages) {
|
|
1539
|
+
return messages.some((message) => {
|
|
1540
|
+
if (!isRecord$1(message) || typeof message.role !== "string") return false;
|
|
1541
|
+
if (message.role === "user" || message.role === "custom" || message.role === "toolResult") return hasTextContent(message.content);
|
|
1542
|
+
if (message.role === "assistant") return Array.isArray(message.content) ? message.content.some((block) => isRecord$1(block) && (block.type === "text" && typeof block.text === "string" || block.type === "thinking" || block.type === "toolCall")) : false;
|
|
1543
|
+
return message.role === "bashExecution" || message.role === "branchSummary" || message.role === "compactionSummary";
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
function hasTextContent(content) {
|
|
1547
|
+
if (typeof content === "string") return content.length > 0;
|
|
1548
|
+
return Array.isArray(content) && content.some((block) => isRecord$1(block) && block.type === "text" && typeof block.text === "string");
|
|
1549
|
+
}
|
|
1550
|
+
function isRecord$1(value) {
|
|
1551
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1552
|
+
}
|
|
1553
|
+
function createOperationSignal(deadlineAt, parent) {
|
|
1554
|
+
const controller = new AbortController();
|
|
1555
|
+
let timer;
|
|
1556
|
+
const onParentAbort = () => controller.abort(parent?.reason);
|
|
1557
|
+
if (parent) if (parent.aborted) onParentAbort();
|
|
1558
|
+
else parent.addEventListener("abort", onParentAbort, { once: true });
|
|
1559
|
+
if (deadlineAt !== void 0) {
|
|
1560
|
+
const remaining = Date.parse(deadlineAt) - Date.now();
|
|
1561
|
+
if (!Number.isFinite(remaining) || remaining <= 0) controller.abort(/* @__PURE__ */ new Error("Pi model management deadline elapsed"));
|
|
1562
|
+
else timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("Pi model management deadline elapsed")), remaining);
|
|
1563
|
+
}
|
|
1564
|
+
return {
|
|
1565
|
+
signal: controller.signal,
|
|
1566
|
+
dispose: () => {
|
|
1567
|
+
if (timer) clearTimeout(timer);
|
|
1568
|
+
if (parent) parent.removeEventListener("abort", onParentAbort);
|
|
1569
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("Pi model operation disposed"));
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
function assertBeforeDeadline(deadlineAt) {
|
|
1574
|
+
if (deadlineAt === void 0) return;
|
|
1575
|
+
const deadline = Date.parse(deadlineAt);
|
|
1576
|
+
if (!Number.isFinite(deadline) || Date.now() >= deadline) throw new Error("Pi model management deadline elapsed");
|
|
1577
|
+
}
|
|
1578
|
+
function assertOperationActive(operation) {
|
|
1579
|
+
assertBeforeDeadline(operation?.deadlineAt);
|
|
1580
|
+
if (operation?.signal?.aborted) {
|
|
1581
|
+
const reason = operation.signal.reason;
|
|
1582
|
+
throw reason instanceof Error ? reason : /* @__PURE__ */ new Error("Pi model operation aborted");
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
//#endregion
|
|
1586
|
+
//#region src/adapters/pi/skills/pi-skill-catalog.ts
|
|
1587
|
+
var InvalidPiSkillCatalog = class extends Error {
|
|
1588
|
+
name = "InvalidPiSkillCatalog";
|
|
1589
|
+
};
|
|
1590
|
+
function validatePiSkillCatalog(input) {
|
|
1591
|
+
const fatal = input.diagnostics.find(({ type }) => type === "error" || type === "collision");
|
|
1592
|
+
if (fatal) throw new InvalidPiSkillCatalog(`Pi Skill discovery failed: ${fatal.message}${fatal.path ? ` (${fatal.path})` : ""}`);
|
|
1593
|
+
const names = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const { name } of input.skills) {
|
|
1595
|
+
validateSkillName(name);
|
|
1596
|
+
if (names.has(name)) throw new InvalidPiSkillCatalog(`Pi Skill discovery contains duplicate name: ${name}`);
|
|
1597
|
+
names.add(name);
|
|
1598
|
+
}
|
|
1599
|
+
return names;
|
|
1600
|
+
}
|
|
1601
|
+
function validateSkillName(name) {
|
|
1602
|
+
if (typeof name !== "string" || name.length === 0 || name.length > 64 || !/^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name) || name.includes("--")) throw new InvalidPiSkillCatalog(`Pi Skill discovery contains unsafe name: ${String(name)}`);
|
|
1603
|
+
}
|
|
1604
|
+
function validatePiSkillCommand(text, skillNames) {
|
|
1605
|
+
if (!text.startsWith("/skill:")) return;
|
|
1606
|
+
const name = text.slice(7).split(/\s/, 1)[0];
|
|
1607
|
+
if (!skillNames.has(name)) throw new InvalidPiSkillCatalog(`Unknown Pi Skill: ${name}`);
|
|
1608
|
+
}
|
|
1609
|
+
//#endregion
|
|
1610
|
+
//#region src/adapters/pi/skills/pi-skill-sources.ts
|
|
1611
|
+
var InvalidPiSkillSource = class extends Error {
|
|
1612
|
+
name = "InvalidPiSkillSource";
|
|
1613
|
+
};
|
|
1614
|
+
async function resolvePiSkillSources(options) {
|
|
1615
|
+
const roots = [];
|
|
1616
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1617
|
+
for (const path of [join(options.agentDir, "skills"), join(options.homeDirectory, ".agents", "skills")]) {
|
|
1618
|
+
const canonical = await optionalDirectory(path);
|
|
1619
|
+
if (canonical) appendUnique(roots, seen, canonical);
|
|
1620
|
+
}
|
|
1621
|
+
for (const path of options.projectSkillPaths ?? []) appendUnique(roots, seen, await requiredSkillSource(path));
|
|
1622
|
+
return Object.freeze(roots);
|
|
1623
|
+
}
|
|
1624
|
+
async function optionalDirectory(path) {
|
|
1625
|
+
let canonical;
|
|
1626
|
+
try {
|
|
1627
|
+
canonical = await realpath(path);
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
if (isMissingPath(error)) return void 0;
|
|
1630
|
+
throw new InvalidPiSkillSource(`failed to resolve optional user Skill directory: ${path}`, { cause: error });
|
|
1631
|
+
}
|
|
1632
|
+
if (!(await stat(canonical)).isDirectory()) throw new InvalidPiSkillSource(`user Skill source must be a directory: ${path}`);
|
|
1633
|
+
return canonical;
|
|
1634
|
+
}
|
|
1635
|
+
async function requiredSkillSource(path) {
|
|
1636
|
+
try {
|
|
1637
|
+
const canonical = await realpath(path);
|
|
1638
|
+
const metadata = await stat(canonical);
|
|
1639
|
+
if (!metadata.isDirectory() && !metadata.isFile()) throw new InvalidPiSkillSource(`Project Space Skill source must be a file or directory: ${path}`);
|
|
1640
|
+
return canonical;
|
|
1641
|
+
} catch (error) {
|
|
1642
|
+
if (error instanceof InvalidPiSkillSource) throw error;
|
|
1643
|
+
throw new InvalidPiSkillSource(`required Project Space Skill source is unavailable: ${path}`, { cause: error });
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
function appendUnique(roots, seen, path) {
|
|
1647
|
+
if (seen.has(path)) return;
|
|
1648
|
+
seen.add(path);
|
|
1649
|
+
roots.push(path);
|
|
1650
|
+
}
|
|
1651
|
+
function isMissingPath(error) {
|
|
1652
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1653
|
+
}
|
|
1654
|
+
//#endregion
|
|
1655
|
+
//#region src/adapters/pi/runtime/pi-session-resources.ts
|
|
1656
|
+
const PI_BEHAVIOR_SETTING_KEYS = Object.freeze([
|
|
1657
|
+
"lastChangelogVersion",
|
|
1658
|
+
"defaultProvider",
|
|
1659
|
+
"defaultModel",
|
|
1660
|
+
"defaultThinkingLevel",
|
|
1661
|
+
"transport",
|
|
1662
|
+
"steeringMode",
|
|
1663
|
+
"followUpMode",
|
|
1664
|
+
"theme",
|
|
1665
|
+
"compaction",
|
|
1666
|
+
"branchSummary",
|
|
1667
|
+
"retry",
|
|
1668
|
+
"hideThinkingBlock",
|
|
1669
|
+
"showCacheMissNotices",
|
|
1670
|
+
"externalEditor",
|
|
1671
|
+
"shellPath",
|
|
1672
|
+
"quietStartup",
|
|
1673
|
+
"shellCommandPrefix",
|
|
1674
|
+
"collapseChangelog",
|
|
1675
|
+
"enableSkillCommands",
|
|
1676
|
+
"terminal",
|
|
1677
|
+
"images",
|
|
1678
|
+
"enabledModels",
|
|
1679
|
+
"doubleEscapeAction",
|
|
1680
|
+
"treeFilterMode",
|
|
1681
|
+
"thinkingBudgets",
|
|
1682
|
+
"editorPaddingX",
|
|
1683
|
+
"outputPad",
|
|
1684
|
+
"autocompleteMaxVisible",
|
|
1685
|
+
"showHardwareCursor",
|
|
1686
|
+
"markdown",
|
|
1687
|
+
"warnings",
|
|
1688
|
+
"httpProxy",
|
|
1689
|
+
"httpIdleTimeoutMs",
|
|
1690
|
+
"websocketConnectTimeoutMs"
|
|
1691
|
+
]);
|
|
1692
|
+
async function createPiSessionResources(options) {
|
|
1693
|
+
const settingsManager = createSanitizedSettingsManager(options.cwd, options.agentDir, options.settingsOverrides);
|
|
1694
|
+
const commandPrefix = settingsManager.getShellCommandPrefix();
|
|
1695
|
+
const shellPath = settingsManager.getShellPath();
|
|
1696
|
+
const bashToolOptions = commandPrefix === void 0 && shellPath === void 0 ? void 0 : Object.freeze({
|
|
1697
|
+
...commandPrefix === void 0 ? {} : { commandPrefix },
|
|
1698
|
+
...shellPath === void 0 ? {} : { shellPath }
|
|
1699
|
+
});
|
|
1700
|
+
let skillPaths = await resolveSkillPaths(options);
|
|
1701
|
+
let currentResourceLoader = createResourceLoader(options, settingsManager, skillPaths);
|
|
1702
|
+
await currentResourceLoader.reload();
|
|
1703
|
+
let skillNames = validatePiSkillCatalog(currentResourceLoader.getSkills());
|
|
1704
|
+
const resourceLoader = createResourceLoaderFacade(() => currentResourceLoader, settingsManager);
|
|
1705
|
+
return {
|
|
1706
|
+
...bashToolOptions === void 0 ? {} : { bashToolOptions },
|
|
1707
|
+
get skillNames() {
|
|
1708
|
+
return skillNames;
|
|
1709
|
+
},
|
|
1710
|
+
get skillPaths() {
|
|
1711
|
+
return skillPaths;
|
|
1712
|
+
},
|
|
1713
|
+
refresh: async () => {
|
|
1714
|
+
const nextSkillPaths = await resolveSkillPaths(options);
|
|
1715
|
+
const nextResourceLoader = createResourceLoader(options, settingsManager, nextSkillPaths);
|
|
1716
|
+
await nextResourceLoader.reload();
|
|
1717
|
+
const nextSkillNames = validatePiSkillCatalog(nextResourceLoader.getSkills());
|
|
1718
|
+
skillPaths = nextSkillPaths;
|
|
1719
|
+
currentResourceLoader = nextResourceLoader;
|
|
1720
|
+
skillNames = nextSkillNames;
|
|
1721
|
+
},
|
|
1722
|
+
withSessionOptions: (sessionOptions) => Object.freeze({
|
|
1723
|
+
...sessionOptions,
|
|
1724
|
+
agentDir: options.agentDir,
|
|
1725
|
+
cwd: options.cwd,
|
|
1726
|
+
resourceLoader,
|
|
1727
|
+
settingsManager
|
|
1728
|
+
})
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
function createResourceLoaderFacade(getCurrent, settingsManager) {
|
|
1732
|
+
return {
|
|
1733
|
+
extendResources: (paths) => getCurrent().extendResources(paths),
|
|
1734
|
+
getAgentsFiles: () => getCurrent().getAgentsFiles(),
|
|
1735
|
+
getAppendSystemPrompt: () => getCurrent().getAppendSystemPrompt(),
|
|
1736
|
+
getAppendSystemPromptSources: () => getCurrent().getAppendSystemPromptSources(),
|
|
1737
|
+
getExtensions: () => getCurrent().getExtensions(),
|
|
1738
|
+
getPrompts: () => getCurrent().getPrompts(),
|
|
1739
|
+
getSkills: () => getCurrent().getSkills(),
|
|
1740
|
+
getSystemPrompt: () => getCurrent().getSystemPrompt(),
|
|
1741
|
+
getSystemPromptSource: () => getCurrent().getSystemPromptSource(),
|
|
1742
|
+
getThemes: () => getCurrent().getThemes(),
|
|
1743
|
+
reload: (reloadOptions) => getCurrent().reload(reloadOptions),
|
|
1744
|
+
settingsManager
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
async function resolveSkillPaths(options) {
|
|
1748
|
+
return resolvePiSkillSources({
|
|
1749
|
+
agentDir: options.agentDir,
|
|
1750
|
+
homeDirectory: options.homeDirectory,
|
|
1751
|
+
...options.projectSkillPaths ? { projectSkillPaths: options.projectSkillPaths } : {}
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
function createResourceLoader(options, settingsManager, skillPaths) {
|
|
1755
|
+
return new DefaultResourceLoader({
|
|
1756
|
+
agentDir: options.agentDir,
|
|
1757
|
+
additionalSkillPaths: [...skillPaths],
|
|
1758
|
+
...options.appendSystemPromptOverride ? { appendSystemPromptOverride: options.appendSystemPromptOverride } : {},
|
|
1759
|
+
cwd: options.cwd,
|
|
1760
|
+
noContextFiles: true,
|
|
1761
|
+
noExtensions: true,
|
|
1762
|
+
noPromptTemplates: true,
|
|
1763
|
+
noSkills: true,
|
|
1764
|
+
noThemes: true,
|
|
1765
|
+
settingsManager,
|
|
1766
|
+
...options.systemPromptOverride ? { systemPromptOverride: options.systemPromptOverride } : {}
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
function createSanitizedSettingsManager(cwd, agentDir, settingsOverrides) {
|
|
1770
|
+
const fileSettings = SettingsManager.create(cwd, agentDir, { projectTrusted: false }).getGlobalSettings();
|
|
1771
|
+
const behaviorSettings = Object.fromEntries(PI_BEHAVIOR_SETTING_KEYS.flatMap((key) => fileSettings[key] === void 0 ? [] : [[key, fileSettings[key]]]));
|
|
1772
|
+
for (const key of PI_BEHAVIOR_SETTING_KEYS) if (settingsOverrides?.[key] !== void 0) behaviorSettings[key] = settingsOverrides[key];
|
|
1773
|
+
return SettingsManager.inMemory(behaviorSettings, { projectTrusted: false });
|
|
1774
|
+
}
|
|
1775
|
+
//#endregion
|
|
1776
|
+
//#region src/adapters/pi/runtime/pi-assembly.ts
|
|
1777
|
+
async function createPiAssembly(options) {
|
|
1778
|
+
const nativeModelRuntime = await ModelRuntime.create({
|
|
1779
|
+
allowModelNetwork: options.allowModelNetwork ?? false,
|
|
1780
|
+
...options.authPath ? { authPath: options.authPath } : {},
|
|
1781
|
+
...options.modelsPath !== void 0 ? { modelsPath: options.modelsPath } : {}
|
|
1782
|
+
});
|
|
1783
|
+
const settingsManager = SettingsManager.create(options.cwd, options.agentDir, { projectTrusted: false });
|
|
1784
|
+
const createResources = () => createPiSessionResources({
|
|
1785
|
+
cwd: options.cwd,
|
|
1786
|
+
agentDir: options.agentDir,
|
|
1787
|
+
homeDirectory: options.homeDirectory,
|
|
1788
|
+
...options.settingsOverrides ? { settingsOverrides: options.settingsOverrides } : {},
|
|
1789
|
+
...options.resourceOptions
|
|
1790
|
+
});
|
|
1791
|
+
return {
|
|
1792
|
+
modelRuntime: nativeModelRuntime,
|
|
1793
|
+
createSessionResources: createResources,
|
|
1794
|
+
createSession: async (sessionOptions = {}) => {
|
|
1795
|
+
const { model, sessionCwd, sessionDirectory, thinkingLevel, resources, ...restOptions } = sessionOptions;
|
|
1796
|
+
const activeResources = resources ?? await createResources();
|
|
1797
|
+
const effectiveThinkingLevel = thinkingLevel ?? options.defaultThinkingLevel;
|
|
1798
|
+
const nativeSessionManager = sessionDirectory ? SessionManager.create(sessionCwd ?? options.cwd, sessionDirectory) : void 0;
|
|
1799
|
+
const sessionResult = await createAgentSession(activeResources.withSessionOptions({
|
|
1800
|
+
modelRuntime: nativeModelRuntime,
|
|
1801
|
+
...restOptions.noTools === void 0 ? {} : { noTools: restOptions.noTools },
|
|
1802
|
+
...restOptions.tools === void 0 ? {} : { tools: [...restOptions.tools] },
|
|
1803
|
+
...restOptions.excludeTools === void 0 ? {} : { excludeTools: [...restOptions.excludeTools] },
|
|
1804
|
+
...restOptions.customTools === void 0 ? {} : { customTools: [...restOptions.customTools] },
|
|
1805
|
+
...restOptions.scopedModels === void 0 ? {} : { scopedModels: restOptions.scopedModels.map(({ model: scopedModel, thinkingLevel: scopedThinkingLevel }) => ({
|
|
1806
|
+
model: scopedModel,
|
|
1807
|
+
...scopedThinkingLevel === void 0 ? {} : { thinkingLevel: scopedThinkingLevel }
|
|
1808
|
+
})) },
|
|
1809
|
+
...model ? { model } : {},
|
|
1810
|
+
...effectiveThinkingLevel ? { thinkingLevel: effectiveThinkingLevel } : {},
|
|
1811
|
+
...nativeSessionManager ? { sessionManager: nativeSessionManager } : {}
|
|
1812
|
+
}));
|
|
1813
|
+
return {
|
|
1814
|
+
...sessionResult,
|
|
1815
|
+
session: sessionResult.session,
|
|
1816
|
+
dispose: () => sessionResult.session.dispose(),
|
|
1817
|
+
refreshResources: () => activeResources.refresh()
|
|
1818
|
+
};
|
|
1819
|
+
},
|
|
1820
|
+
resolveThinkingLevel: (provider, modelId) => {
|
|
1821
|
+
return (provider !== void 0 && modelId !== void 0 ? settingsManager.getModelThinkingLevel(provider, modelId) : void 0) ?? settingsManager.getDefaultThinkingLevel();
|
|
1822
|
+
},
|
|
1823
|
+
dispose: async () => {}
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
//#endregion
|
|
1827
|
+
//#region src/adapters/pi/skills/pi-skill-read-tool.ts
|
|
1828
|
+
var ProjectSkillReadDenied = class extends Error {
|
|
1829
|
+
name = "ProjectSkillReadDenied";
|
|
1830
|
+
};
|
|
1831
|
+
function createPiSkillReadTool(options) {
|
|
1832
|
+
if (options.skillPaths.length === 0) throw new ProjectSkillReadDenied("Pi Skill read requires a trusted Skill source");
|
|
1833
|
+
const allowedSources = options.skillPaths.map((source) => {
|
|
1834
|
+
const path = realpathSync(source);
|
|
1835
|
+
return Object.freeze({
|
|
1836
|
+
directory: statSync(path).isDirectory(),
|
|
1837
|
+
path
|
|
1838
|
+
});
|
|
1839
|
+
});
|
|
1840
|
+
return createReadToolDefinition(options.cwd, { operations: {
|
|
1841
|
+
access: async (absolutePath) => {
|
|
1842
|
+
await stat(await authorize(absolutePath, allowedSources));
|
|
1843
|
+
},
|
|
1844
|
+
readFile: async (absolutePath) => readFile(await authorize(absolutePath, allowedSources))
|
|
1845
|
+
} });
|
|
1846
|
+
}
|
|
1847
|
+
const createPiProjectSkillReadTool = createPiSkillReadTool;
|
|
1848
|
+
function createPiSkillReadTools(options) {
|
|
1849
|
+
if (options.skillPaths.length === 0 || options.runtimeToolIds.includes("read")) return [];
|
|
1850
|
+
return [createPiSkillReadTool(options)];
|
|
1851
|
+
}
|
|
1852
|
+
async function authorize(path, sources) {
|
|
1853
|
+
const candidate = await realpath(path);
|
|
1854
|
+
for (const source of sources) {
|
|
1855
|
+
if (!source.directory) {
|
|
1856
|
+
if (candidate === source.path) return candidate;
|
|
1857
|
+
continue;
|
|
1858
|
+
}
|
|
1859
|
+
const relation = relative(source.path, candidate);
|
|
1860
|
+
if (relation === "" || !relation.startsWith("..") && !isAbsolute(relation)) return candidate;
|
|
1861
|
+
}
|
|
1862
|
+
throw new ProjectSkillReadDenied("read is restricted to the bound Pi Skill sources");
|
|
1863
|
+
}
|
|
1864
|
+
//#endregion
|
|
1865
|
+
//#region src/adapters/pi/skills/project-skill-catalog.ts
|
|
1866
|
+
var InvalidProjectSkillCatalog = class extends Error {
|
|
1867
|
+
name = "InvalidProjectSkillCatalog";
|
|
1868
|
+
};
|
|
1869
|
+
function validateProjectSkillCatalog(input) {
|
|
1870
|
+
const diagnostic = input.diagnostics.find(({ type }) => type === "error" || type === "collision");
|
|
1871
|
+
if (diagnostic) throw new InvalidProjectSkillCatalog(`Project Skill discovery failed: ${diagnostic.message}${diagnostic.path ? ` (${diagnostic.path})` : ""}`);
|
|
1872
|
+
return new Set(input.skills.map(({ name }) => name));
|
|
1873
|
+
}
|
|
1874
|
+
function validateProjectSkillCommand(text, skillNames) {
|
|
1875
|
+
if (!text.startsWith("/skill:")) return;
|
|
1876
|
+
const name = text.slice(7).split(/\s/, 1)[0];
|
|
1877
|
+
if (!skillNames.has(name)) throw new InvalidProjectSkillCatalog(`Unknown or ungranted Project Skill: ${name}`);
|
|
1878
|
+
}
|
|
1879
|
+
//#endregion
|
|
1880
|
+
//#region src/adapters/pi/tool-execution/pi-bash-tool.ts
|
|
1881
|
+
const DEFAULT_TIMEOUT_SECONDS = 120;
|
|
1882
|
+
/** Keep Pi's native execution, rendering and abort cleanup with a bounded default. */
|
|
1883
|
+
function createPiBashTool(cwd, options) {
|
|
1884
|
+
const { commandPrefix, shellPath, spawnHook } = options ?? {};
|
|
1885
|
+
const tool = createBashToolDefinition(cwd, {
|
|
1886
|
+
...commandPrefix === void 0 ? {} : { commandPrefix },
|
|
1887
|
+
...shellPath === void 0 ? {} : { shellPath },
|
|
1888
|
+
...spawnHook === void 0 ? {} : { spawnHook }
|
|
1889
|
+
});
|
|
1890
|
+
return defineTool({
|
|
1891
|
+
...tool,
|
|
1892
|
+
description: `${tool.description} Defaults to ${DEFAULT_TIMEOUT_SECONDS} seconds when timeout is omitted; set an explicit positive timeout for longer commands.`,
|
|
1893
|
+
parameters: {
|
|
1894
|
+
...tool.parameters,
|
|
1895
|
+
properties: {
|
|
1896
|
+
...tool.parameters.properties,
|
|
1897
|
+
timeout: {
|
|
1898
|
+
...tool.parameters.properties.timeout,
|
|
1899
|
+
description: `Timeout in seconds (optional, defaults to ${DEFAULT_TIMEOUT_SECONDS})`
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
},
|
|
1903
|
+
execute: (id, args, signal, onUpdate, context) => tool.execute(id, {
|
|
1904
|
+
...args,
|
|
1905
|
+
timeout: args.timeout === void 0 ? DEFAULT_TIMEOUT_SECONDS : args.timeout
|
|
1906
|
+
}, signal, onUpdate, context)
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
//#endregion
|
|
1910
|
+
//#region src/adapters/pi/tool-execution/pi-tool-proxy.ts
|
|
1911
|
+
function createPiToolProxyDefinitions$1(options) {
|
|
1912
|
+
const names = /* @__PURE__ */ new Set();
|
|
1913
|
+
return options.tools.map((tool) => {
|
|
1914
|
+
const name = toPiToolName(tool.id);
|
|
1915
|
+
if (names.has(name)) throw new Error(`Pi tool name collision: ${name}`);
|
|
1916
|
+
names.add(name);
|
|
1917
|
+
return {
|
|
1918
|
+
description: tool.description,
|
|
1919
|
+
execute: async (callId, input, signal) => {
|
|
1920
|
+
const activeInput = options.getActiveInput();
|
|
1921
|
+
const origin = activeInput?.invocation;
|
|
1922
|
+
if (!activeInput || !origin) throw new Error(`tool ${tool.id} requires an active agent run with a trusted invocation`);
|
|
1923
|
+
const invocation = normalizeAgentInvocation(origin);
|
|
1924
|
+
if (!invocation.tenantKey) throw new Error(`tool ${tool.id} requires a trusted tenant identity`);
|
|
1925
|
+
throwIfAborted(signal ?? activeInput.abortSignal);
|
|
1926
|
+
const inputDigest = createToolInputDigest(input);
|
|
1927
|
+
const operationId = createBoundId("operation", {
|
|
1928
|
+
agentId: options.agentId,
|
|
1929
|
+
inputDigest,
|
|
1930
|
+
instanceId: options.instanceId,
|
|
1931
|
+
sourceMessageId: invocation.sourceMessageId,
|
|
1932
|
+
toolId: tool.id,
|
|
1933
|
+
toolVersion: tool.version
|
|
1934
|
+
});
|
|
1935
|
+
const approvalId = createBoundId("approval", {
|
|
1936
|
+
callId,
|
|
1937
|
+
operationId,
|
|
1938
|
+
runId: activeInput.runId
|
|
1939
|
+
});
|
|
1940
|
+
if (requiresToolApproval(tool.risk)) {
|
|
1941
|
+
if (invocation.allowedActorOpenIds.length === 0) throw new Error(`tool ${tool.id} requires at least one trusted approval actor`);
|
|
1942
|
+
await options.approvals.requestApproval({
|
|
1943
|
+
agentId: options.agentId,
|
|
1944
|
+
allowedActorOpenIds: invocation.allowedActorOpenIds,
|
|
1945
|
+
approvalId,
|
|
1946
|
+
callId,
|
|
1947
|
+
endpointId: invocation.endpointId,
|
|
1948
|
+
inputDigest,
|
|
1949
|
+
instanceId: options.instanceId,
|
|
1950
|
+
operationId,
|
|
1951
|
+
risk: tool.risk,
|
|
1952
|
+
runId: activeInput.runId,
|
|
1953
|
+
sessionKey: activeInput.sessionKey,
|
|
1954
|
+
signal: signal ?? activeInput.abortSignal,
|
|
1955
|
+
sourceMessageId: invocation.sourceMessageId,
|
|
1956
|
+
tenantKey: invocation.tenantKey,
|
|
1957
|
+
toolId: tool.id,
|
|
1958
|
+
toolVersion: tool.version
|
|
1959
|
+
});
|
|
1960
|
+
throwIfAborted(signal ?? activeInput.abortSignal);
|
|
1961
|
+
}
|
|
1962
|
+
const result = await options.broker.execute({
|
|
1963
|
+
authority: createInvocationAuthority({
|
|
1964
|
+
agentId: options.agentId,
|
|
1965
|
+
allowedActorOpenIds: invocation.allowedActorOpenIds,
|
|
1966
|
+
...invocation.conversationId ? { conversationId: invocation.conversationId } : {},
|
|
1967
|
+
endpointId: invocation.endpointId,
|
|
1968
|
+
instanceId: options.instanceId,
|
|
1969
|
+
...invocation.memory ? { memory: {
|
|
1970
|
+
...invocation.memory,
|
|
1971
|
+
scopes: options.memoryScopes ?? []
|
|
1972
|
+
} } : {},
|
|
1973
|
+
runId: activeInput.runId,
|
|
1974
|
+
sessionKey: activeInput.sessionKey,
|
|
1975
|
+
sourceMessageId: invocation.sourceMessageId,
|
|
1976
|
+
tenantKey: invocation.tenantKey,
|
|
1977
|
+
toolGrantSet: options.toolGrantSet
|
|
1978
|
+
}),
|
|
1979
|
+
callId,
|
|
1980
|
+
input,
|
|
1981
|
+
operationId,
|
|
1982
|
+
...requiresToolApproval(tool.risk) ? { approvalId } : {},
|
|
1983
|
+
toolId: tool.id,
|
|
1984
|
+
version: tool.version
|
|
1985
|
+
});
|
|
1986
|
+
return {
|
|
1987
|
+
content: [{
|
|
1988
|
+
text: stringifyToolResult(result),
|
|
1989
|
+
type: "text"
|
|
1990
|
+
}],
|
|
1991
|
+
details: result
|
|
1992
|
+
};
|
|
1993
|
+
},
|
|
1994
|
+
executionMode: "sequential",
|
|
1995
|
+
label: tool.id,
|
|
1996
|
+
name,
|
|
1997
|
+
parameters: Unsafe(tool.inputSchema),
|
|
1998
|
+
promptSnippet: `${name}: ${tool.description}`
|
|
1999
|
+
};
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
function createToolInputDigest(input) {
|
|
2003
|
+
return createToolInputDigest$1(input, createSha256Digest);
|
|
2004
|
+
}
|
|
2005
|
+
function createPiToolNameResolver$1(tools) {
|
|
2006
|
+
const toolIdsByPiName = new Map(tools.map((tool) => [toPiToolName(tool.id), tool.id]));
|
|
2007
|
+
return (toolName) => toolIdsByPiName.get(toolName) ?? toolName;
|
|
2008
|
+
}
|
|
2009
|
+
function toPiToolName(toolId) {
|
|
2010
|
+
return `rivus_${toolId.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
2011
|
+
}
|
|
2012
|
+
function createBoundId(kind, binding) {
|
|
2013
|
+
return `${kind}:${createHash("sha256").update(JSON.stringify(binding)).digest("hex")}`;
|
|
2014
|
+
}
|
|
2015
|
+
function throwIfAborted(signal) {
|
|
2016
|
+
if (signal.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("tool execution was aborted");
|
|
2017
|
+
}
|
|
2018
|
+
function stringifyToolResult(result) {
|
|
2019
|
+
if (typeof result === "string") return result;
|
|
2020
|
+
return JSON.stringify(result ?? null);
|
|
2021
|
+
}
|
|
2022
|
+
//#endregion
|
|
2023
|
+
//#region src/adapters/pi/tool-execution/pi-session-tools.ts
|
|
2024
|
+
function resolvePiSessionToolNames(runtimeToolIds, customTools) {
|
|
2025
|
+
const names = [];
|
|
2026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2027
|
+
for (const name of [...runtimeToolIds, ...customTools.map(({ name }) => name)]) {
|
|
2028
|
+
if (seen.has(name)) throw new Error(`Pi Session Tool name collision: ${name}`);
|
|
2029
|
+
seen.add(name);
|
|
2030
|
+
names.push(name);
|
|
2031
|
+
}
|
|
2032
|
+
return Object.freeze(names);
|
|
2033
|
+
}
|
|
2034
|
+
//#endregion
|
|
2035
|
+
//#region src/adapters/pi/tool-execution/pi-managed-run-binding.ts
|
|
2036
|
+
function createPiManagedRunBinding(options) {
|
|
2037
|
+
let current;
|
|
2038
|
+
const deactivate = () => {
|
|
2039
|
+
if (!current) return;
|
|
2040
|
+
current.cleanup();
|
|
2041
|
+
current = void 0;
|
|
2042
|
+
};
|
|
2043
|
+
const activate = (run) => {
|
|
2044
|
+
deactivate();
|
|
2045
|
+
current = options.provider(run);
|
|
2046
|
+
};
|
|
2047
|
+
const spawnHook = (spawn) => {
|
|
2048
|
+
if (!current) return spawn;
|
|
2049
|
+
return current.spawnHook(spawn);
|
|
2050
|
+
};
|
|
2051
|
+
const tool = createPiBashTool(options.cwd, {
|
|
2052
|
+
...options.bashToolOptions,
|
|
2053
|
+
spawnHook
|
|
2054
|
+
});
|
|
2055
|
+
return Object.freeze({
|
|
2056
|
+
activate,
|
|
2057
|
+
deactivate,
|
|
2058
|
+
tool
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
//#endregion
|
|
2062
|
+
//#region src/adapters/compatibility/agent-execution/pi/pi-agent-loop.ts
|
|
2063
|
+
function createPiAgentLoop(options) {
|
|
2064
|
+
return fromEffectAgentLoop(createPiAgentLoop$1({
|
|
2065
|
+
...options.supportsSteering ? { supportsSteering: true } : {},
|
|
2066
|
+
...options.disposeSessionAfterRun === void 0 ? {} : { disposeSessionAfterRun: options.disposeSessionAfterRun },
|
|
2067
|
+
...options.modelContentObserver ? { modelContentObserver: options.modelContentObserver } : {},
|
|
2068
|
+
...options.runBoundary ? { runBoundary: options.runBoundary } : {},
|
|
2069
|
+
resolveSession: async (input) => toEffectPiAgentSessionHandle(await options.resolveSession(toCompatibilityAgentLoopInput(input)))
|
|
2070
|
+
}));
|
|
2071
|
+
}
|
|
2072
|
+
function createPiSdkAgentLoop(options) {
|
|
2073
|
+
return fromEffectAgentLoop(createPiSdkAgentLoop$1(options));
|
|
2074
|
+
}
|
|
2075
|
+
function toEffectPiAgentSessionHandle(handle) {
|
|
2076
|
+
return {
|
|
2077
|
+
...handle.activate ? { activate: (input) => handle.activate(toCompatibilityAgentLoopInput(input)) } : {},
|
|
2078
|
+
...handle.dispose ? { dispose: handle.dispose } : {},
|
|
2079
|
+
...handle.deactivate ? { deactivate: handle.deactivate } : {},
|
|
2080
|
+
...handle.preparePrompt ? { preparePrompt: (input) => handle.preparePrompt(toCompatibilityAgentLoopInput(input)) } : {},
|
|
2081
|
+
...handle.resolveToolName ? { resolveToolName: handle.resolveToolName } : {},
|
|
2082
|
+
...handle.refreshResources ? { refreshResources: handle.refreshResources } : {},
|
|
2083
|
+
session: handle.session
|
|
2084
|
+
};
|
|
2085
|
+
}
|
|
2086
|
+
function fromEffectPiAgentSessionHandle(handle) {
|
|
2087
|
+
return {
|
|
2088
|
+
...handle.activate ? { activate: (input) => handle.activate(toEffectAgentLoopInput(input)) } : {},
|
|
2089
|
+
...handle.dispose ? { dispose: handle.dispose } : {},
|
|
2090
|
+
...handle.deactivate ? { deactivate: handle.deactivate } : {},
|
|
2091
|
+
...handle.preparePrompt ? { preparePrompt: (input) => handle.preparePrompt(toEffectAgentLoopInput(input)) } : {},
|
|
2092
|
+
...handle.resolveToolName ? { resolveToolName: handle.resolveToolName } : {},
|
|
2093
|
+
...handle.refreshResources ? { refreshResources: handle.refreshResources } : {},
|
|
2094
|
+
session: handle.session
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/adapters/compatibility/agent-execution/pi/pi-session-registry.ts
|
|
2099
|
+
function createPiSessionRegistry(options) {
|
|
2100
|
+
const compatibilityHandles = /* @__PURE__ */ new WeakMap();
|
|
2101
|
+
const registry = createPiSessionRegistry$1({ createSession: async (input) => {
|
|
2102
|
+
const handle = await options.createSession(toCompatibilityAgentLoopInput(input));
|
|
2103
|
+
const effectHandle = toEffectPiAgentSessionHandle(handle);
|
|
2104
|
+
compatibilityHandles.set(effectHandle, handle);
|
|
2105
|
+
return effectHandle;
|
|
2106
|
+
} });
|
|
2107
|
+
return {
|
|
2108
|
+
dispose: (sessionKey) => registry.dispose(sessionKey),
|
|
2109
|
+
disposeAll: () => registry.disposeAll(),
|
|
2110
|
+
list: async () => (await registry.list()).map((effectHandle) => {
|
|
2111
|
+
const cached = compatibilityHandles.get(effectHandle);
|
|
2112
|
+
if (cached) return cached;
|
|
2113
|
+
const handle = fromEffectPiAgentSessionHandle(effectHandle);
|
|
2114
|
+
compatibilityHandles.set(effectHandle, handle);
|
|
2115
|
+
return handle;
|
|
2116
|
+
}),
|
|
2117
|
+
resolve: async (input) => {
|
|
2118
|
+
const effectHandle = await registry.resolve(toEffectAgentLoopInput(input));
|
|
2119
|
+
const cached = compatibilityHandles.get(effectHandle);
|
|
2120
|
+
if (cached) return cached;
|
|
2121
|
+
const handle = fromEffectPiAgentSessionHandle(effectHandle);
|
|
2122
|
+
compatibilityHandles.set(effectHandle, handle);
|
|
2123
|
+
return handle;
|
|
2124
|
+
},
|
|
2125
|
+
size: () => registry.size()
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
//#endregion
|
|
2129
|
+
//#region src/adapters/compatibility/agent-execution/pi/pi-tool-proxy.ts
|
|
2130
|
+
function createPiToolProxyDefinitions(options) {
|
|
2131
|
+
return createPiToolProxyDefinitions$1({
|
|
2132
|
+
...options,
|
|
2133
|
+
getActiveInput: () => {
|
|
2134
|
+
const input = options.getActiveInput();
|
|
2135
|
+
return input ? toEffectAgentLoopInput(input) : void 0;
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
function createPiToolNameResolver(tools) {
|
|
2140
|
+
return createPiToolNameResolver$1(tools);
|
|
2141
|
+
}
|
|
2142
|
+
//#endregion
|
|
2143
|
+
export { InvalidPiSkillCatalog, InvalidPiSkillSource, InvalidProjectSkillCatalog, PI_MODEL_PROBE_FOLLOWUP_RESULT, PI_MODEL_PROBE_TOOL_NAME, PI_MODEL_PROBE_TOOL_RESULT, PI_SKILL_READER_TOOL_NAME, PiHistoryCompatibilityError, PiModelProbeError, PiModelResolutionError, PiModelRuntimeMutationUnknownError, PiModelTargetCompatibilityError, ProjectSkillReadDenied, assertPiSessionHistoryCompatible, createPiAgentLoop as createLegacyPiAgentLoop, createPiSdkAgentLoop as createLegacyPiSdkAgentLoop, createPiSessionRegistry as createLegacyPiSessionRegistry, createPiToolNameResolver as createLegacyPiToolNameResolver, createPiToolProxyDefinitions as createLegacyPiToolProxyDefinitions, createPiAgentLoop$1 as createPiAgentLoop, createPiAssembly, createPiBashTool, createPiManagedRunBinding, createPiModelRuntime, createPiProjectSkillReadTool, createPiSdkAgentLoop$1 as createPiSdkAgentLoop, createPiSessionRegistry$1 as createPiSessionRegistry, createPiSessionResources, createPiSkillReadTool, createPiSkillReadTools, createPiSkillRuntime, createPiToolNameResolver$1 as createPiToolNameResolver, createPiToolProxyDefinitions$1 as createPiToolProxyDefinitions, ensurePiModel, fromEffectPiAgentSessionHandle as fromEffectLegacyPiAgentSessionHandle, inspectPiSessionHistory, mergePiProviderBaseUrlOverride, mergePiProviderModelDeclaration, probePiModel, refreshPiModelCatalog, resolvePiModel, resolvePiModelBinding, resolvePiSessionToolNames, resolvePiSkillSources, runPiModelCanary, runPiSessionPrompts, samePiProvider, toEffectPiAgentSessionHandle as toEffectLegacyPiAgentSessionHandle, validatePiSkillCatalog, validatePiSkillCommand, validateProjectSkillCatalog, validateProjectSkillCommand };
|