@mono-agent/agent-runtime 0.20.11 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +50 -11
- package/MIGRATION.md +288 -26
- package/README.md +352 -477
- package/package.json +13 -44
- package/src/agent/tool-bloat.js +145 -9
- package/src/agent/tools/agent-tool.js +108 -9
- package/src/agent/tools/bash.js +11 -26
- package/src/agent/tools/codex-subscription-search.js +123 -29
- package/src/agent/tools/exec.js +10 -2
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +149 -0
- package/src/agent/tools/pi-bridge.js +123 -19
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +293 -0
- package/src/agent/tools/shared/path-resolver.js +25 -6
- package/src/agent/tools/shared/process-jobs.js +6 -1
- package/src/agent/tools/shared/process-runner.js +26 -6
- package/src/agent/tools/shared/tool-context.js +8 -0
- package/src/agent/tools/web-access-interstitial.js +70 -0
- package/src/agent/tools/web-browser-render.js +83 -58
- package/src/agent/tools/web-controller.js +112 -21
- package/src/agent/tools/web-document-extractor.js +379 -0
- package/src/agent/tools/web-fetch.js +271 -243
- package/src/agent/tools/web-request.js +65 -0
- package/src/agent/tools/web-search-output.js +165 -0
- package/src/agent/tools/web-search-state.js +75 -0
- package/src/agent/tools/web-search.js +532 -71
- package/src/ai/cost.js +13 -68
- package/src/ai/failure.js +3 -3
- package/src/ai/index.js +5 -17
- package/src/ai/observer.js +8 -0
- package/src/ai/pi-interop.js +221 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/provider-check.js +131 -0
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +94 -42
- package/src/ai/providers/pi-native/compaction-summary.js +140 -0
- package/src/ai/providers/pi-native/harness-adapter.js +376 -0
- package/src/ai/providers/pi-native/prompt-cache-diagnostics.js +103 -0
- package/src/ai/providers/pi-native/provider-attribution.js +102 -0
- package/src/ai/providers/pi-native/result-builder.js +38 -14
- package/src/ai/providers/pi-native/session-lifecycle.js +253 -55
- package/src/ai/providers/pi-native/stream-subscriber.js +52 -6
- package/src/ai/providers/pi-native/terminal-recovery.js +40 -0
- package/src/ai/providers/pi-native/turn-runner.js +279 -28
- package/src/ai/providers/pi-native.js +206 -61
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/live-input-events.js +250 -54
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +76 -417
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/tool-lifecycle.js +32 -18
- package/src/ai/types.js +37 -112
- package/src/index.js +0 -6
- package/src/runtime.js +29 -16
- package/types/agent/tool-bloat.d.ts +1 -1
- package/types/agent/tools/agent-tool.d.ts +4 -2
- package/types/agent/tools/bash.d.ts +5 -3
- package/types/agent/tools/codex-subscription-search.d.ts +7 -3
- package/types/agent/tools/exec.d.ts +5 -3
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +47 -0
- package/types/agent/tools/pi-bridge.d.ts +7 -4
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +98 -0
- package/types/agent/tools/shared/process-jobs.d.ts +5 -1
- package/types/agent/tools/shared/process-runner.d.ts +14 -4
- package/types/agent/tools/shared/tool-context.d.ts +2 -0
- package/types/agent/tools/web-access-interstitial.d.ts +23 -0
- package/types/agent/tools/web-browser-render.d.ts +4 -1
- package/types/agent/tools/web-controller.d.ts +4 -2
- package/types/agent/tools/web-document-extractor.d.ts +27 -0
- package/types/agent/tools/web-fetch.d.ts +19 -24
- package/types/agent/tools/web-request.d.ts +20 -0
- package/types/agent/tools/web-search-output.d.ts +31 -0
- package/types/agent/tools/web-search-state.d.ts +21 -0
- package/types/agent/tools/web-search.d.ts +10 -45
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +2 -4
- package/types/ai/observer.d.ts +6 -0
- package/types/ai/pi-interop.d.ts +81 -0
- package/types/ai/provider-check.d.ts +53 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/compaction-driver.d.ts +2 -1
- package/types/ai/providers/pi-native/compaction-summary.d.ts +19 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +58 -0
- package/types/ai/providers/pi-native/prompt-cache-diagnostics.d.ts +3 -0
- package/types/ai/providers/pi-native/provider-attribution.d.ts +26 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +14 -4
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +25 -6
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/terminal-recovery.d.ts +2 -0
- package/types/ai/providers/pi-native/turn-runner.d.ts +68 -10
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/live-input-events.d.ts +32 -8
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/tool-lifecycle.d.ts +4 -3
- package/types/ai/types.d.ts +162 -256
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- package/types/ai/streaming/opencode-events.d.ts +0 -42
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Compatibility boundary between mono-agent's Pi-native bridge and the
|
|
3
|
+
// lane-based AgentHarness API introduced in pi-agent-core 0.85.
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AgentHarness,
|
|
7
|
+
BACKGROUND_CONTEXT,
|
|
8
|
+
createBranchSummaryMessage,
|
|
9
|
+
createCompactionSummaryMessage,
|
|
10
|
+
getOrThrow,
|
|
11
|
+
} from "@earendil-works/pi-agent-core";
|
|
12
|
+
import { installPromptCacheDiagnostics, promptCacheRequest } from "./prompt-cache-diagnostics.js";
|
|
13
|
+
|
|
14
|
+
export const PI_CONTEXT = BACKGROUND_CONTEXT;
|
|
15
|
+
|
|
16
|
+
function operationError(record, fallback) {
|
|
17
|
+
const error = new Error(record?.error?.message || fallback);
|
|
18
|
+
if (record?.error?.code) /** @type {any} */ (error).code = record.error.code;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isContextMessage(message) {
|
|
23
|
+
return message?.role !== "assistant"
|
|
24
|
+
|| !["error", "aborted", "deferred"].includes(message.stopReason);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pi 0.85 no longer exports its session-context projector. Reproduce the
|
|
29
|
+
* public entry contract here so transcript accounting uses the same latest-
|
|
30
|
+
* compaction and failed-assistant filtering rules as the harness.
|
|
31
|
+
* @param {any[]} pathEntries
|
|
32
|
+
* @param {{includeFailed?: boolean}} [options]
|
|
33
|
+
*/
|
|
34
|
+
export function buildPiSessionContext(pathEntries, { includeFailed = false } = {}) {
|
|
35
|
+
let start = 0;
|
|
36
|
+
for (let index = pathEntries.length - 1; index >= 0; index -= 1) {
|
|
37
|
+
if (pathEntries[index]?.type === "compaction") {
|
|
38
|
+
start = index;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const entries = start > 0 ? pathEntries.slice(start) : pathEntries;
|
|
43
|
+
const messages = [];
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (entry?.type === "message") {
|
|
46
|
+
if (includeFailed || isContextMessage(entry.message)) messages.push(entry.message);
|
|
47
|
+
} else if (entry?.type === "compaction") {
|
|
48
|
+
messages.push(createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp));
|
|
49
|
+
messages.push(...(includeFailed
|
|
50
|
+
? (entry.retainedTail || [])
|
|
51
|
+
: (entry.retainedTail || []).filter(isContextMessage)));
|
|
52
|
+
} else if (entry?.type === "branch_summary" && entry.summary) {
|
|
53
|
+
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return messages;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Give the surrounding bridge the small session surface it used before Pi
|
|
61
|
+
* moved branch operations onto AgentLane. The raw session remains available to
|
|
62
|
+
* AgentHarness.create(), while all transcript reads and writes use the attached
|
|
63
|
+
* main lane once the harness exists.
|
|
64
|
+
* @param {any} rawSession
|
|
65
|
+
*/
|
|
66
|
+
export function createPiSessionAdapter(rawSession) {
|
|
67
|
+
let lane = null;
|
|
68
|
+
let harness = null;
|
|
69
|
+
let closePromise = null;
|
|
70
|
+
|
|
71
|
+
const requireLane = () => {
|
|
72
|
+
if (!lane) throw new Error("Pi session lane is not attached");
|
|
73
|
+
return lane;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
rawSession,
|
|
78
|
+
get metadata() { return rawSession.metadata; },
|
|
79
|
+
attach(nextHarness, nextLane) {
|
|
80
|
+
harness = nextHarness;
|
|
81
|
+
lane = nextLane;
|
|
82
|
+
},
|
|
83
|
+
async buildContext() {
|
|
84
|
+
const entries = await requireLane().findEntries({ order: "oldestFirst" }, PI_CONTEXT);
|
|
85
|
+
// This bridge-facing transcript includes terminal error/abort messages so
|
|
86
|
+
// result classification can observe them. Pi filters those only when it
|
|
87
|
+
// constructs the next provider request.
|
|
88
|
+
return { messages: buildPiSessionContext(entries, { includeFailed: true }) };
|
|
89
|
+
},
|
|
90
|
+
getEntries() {
|
|
91
|
+
return requireLane().findEntries({ order: "oldestFirst" }, PI_CONTEXT);
|
|
92
|
+
},
|
|
93
|
+
getLeafId() {
|
|
94
|
+
return requireLane().getTipId(PI_CONTEXT);
|
|
95
|
+
},
|
|
96
|
+
appendMessage(message) {
|
|
97
|
+
return requireLane().appendMessage(message, PI_CONTEXT);
|
|
98
|
+
},
|
|
99
|
+
async moveTo(targetId) {
|
|
100
|
+
const result = getOrThrow(await requireLane().navigateTree(
|
|
101
|
+
targetId,
|
|
102
|
+
{ summarize: false },
|
|
103
|
+
PI_CONTEXT,
|
|
104
|
+
));
|
|
105
|
+
const record = result.navigation;
|
|
106
|
+
if (record.status === "failed") throw operationError(record, "Pi session navigation failed");
|
|
107
|
+
if (record.status !== "completed") throw operationError(record, "Pi session navigation was not completed");
|
|
108
|
+
return record.tipId;
|
|
109
|
+
},
|
|
110
|
+
getMetadata() {
|
|
111
|
+
return Promise.resolve(rawSession.metadata);
|
|
112
|
+
},
|
|
113
|
+
close() {
|
|
114
|
+
if (!closePromise) {
|
|
115
|
+
closePromise = (harness
|
|
116
|
+
? harness.close(PI_CONTEXT)
|
|
117
|
+
: rawSession.close(PI_CONTEXT));
|
|
118
|
+
}
|
|
119
|
+
return closePromise;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function adaptTool(tool) {
|
|
125
|
+
return {
|
|
126
|
+
...tool,
|
|
127
|
+
async execute(toolCallId, params, onUpdate, _toolContext, _invocation, context) {
|
|
128
|
+
return tool.execute(toolCallId, params, context.abortSignal, onUpdate);
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const FORWARDED_EVENT_TYPES = [
|
|
134
|
+
"run_start",
|
|
135
|
+
"run_resume",
|
|
136
|
+
"run_suspend",
|
|
137
|
+
"operation_abort",
|
|
138
|
+
"run_end",
|
|
139
|
+
"fault",
|
|
140
|
+
"handler_error",
|
|
141
|
+
"turn_start",
|
|
142
|
+
"turn_end",
|
|
143
|
+
"retry_scheduled",
|
|
144
|
+
"retry_start",
|
|
145
|
+
"retry_end",
|
|
146
|
+
"message_start",
|
|
147
|
+
"message_update",
|
|
148
|
+
"message_end",
|
|
149
|
+
"tool_start",
|
|
150
|
+
"tool_update",
|
|
151
|
+
"tool_end",
|
|
152
|
+
"entry_added",
|
|
153
|
+
"queue_update",
|
|
154
|
+
"value_update",
|
|
155
|
+
"config_update",
|
|
156
|
+
"compaction_start",
|
|
157
|
+
"compaction_end",
|
|
158
|
+
"navigation_start",
|
|
159
|
+
"navigation_end",
|
|
160
|
+
"lane_created",
|
|
161
|
+
"usage",
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
function legacyEvent(event) {
|
|
165
|
+
// appendMessage() emits runless lifecycle events for transcript seeding.
|
|
166
|
+
// They are persistence notifications, not output from the active provider
|
|
167
|
+
// run, and forwarding them would manufacture assistant boundaries/usage for
|
|
168
|
+
// prior history. Real run-owned message events always carry runId in Pi 0.85.
|
|
169
|
+
if ((event.type === "message_start" || event.type === "message_end") && !event.runId) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
if (event.type === "message_update") {
|
|
173
|
+
return { ...event, assistantMessageEvent: event.event };
|
|
174
|
+
}
|
|
175
|
+
if (event.type === "tool_start") return { ...event, type: "tool_execution_start" };
|
|
176
|
+
if (event.type === "tool_update") return { ...event, type: "tool_execution_update" };
|
|
177
|
+
if (event.type === "tool_end") return { ...event, type: "tool_execution_end" };
|
|
178
|
+
return event;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Attach Pi 0.85's harness to one session and expose the intentionally small
|
|
183
|
+
* surface consumed by mono-agent's turn runner.
|
|
184
|
+
* @param {any} session
|
|
185
|
+
* @param {any} options
|
|
186
|
+
*/
|
|
187
|
+
export async function createPiHarnessAdapter(session, options) {
|
|
188
|
+
const originalTools = Array.isArray(options.tools) ? options.tools : [];
|
|
189
|
+
const adaptedTools = originalTools.map(adaptTool);
|
|
190
|
+
const activeToolNames = originalTools.map((tool) => tool.name);
|
|
191
|
+
// Pi 0.85 removed mixed per-tool scheduling from AgentHarness: its runner
|
|
192
|
+
// consults only this global setting. Preserve the safety contract by
|
|
193
|
+
// serializing the batch whenever any offered tool is stateful, mutating, or
|
|
194
|
+
// MCP-backed. Read-only-only tool sets can still overlap in safe-parallel.
|
|
195
|
+
const toolExecution = originalTools.some((tool) => tool?.executionMode === "sequential")
|
|
196
|
+
? "sequential"
|
|
197
|
+
: "parallel";
|
|
198
|
+
|
|
199
|
+
/** @type {any} */
|
|
200
|
+
let created;
|
|
201
|
+
/** @type {any} */
|
|
202
|
+
let rawHarness;
|
|
203
|
+
/** @type {any} */
|
|
204
|
+
let lane;
|
|
205
|
+
let removePromptCacheDiagnostics = () => {};
|
|
206
|
+
try {
|
|
207
|
+
created = await AgentHarness.create({
|
|
208
|
+
...options,
|
|
209
|
+
session: session.rawSession,
|
|
210
|
+
tools: adaptedTools,
|
|
211
|
+
activeToolNames,
|
|
212
|
+
toolExecution,
|
|
213
|
+
// mono-agent owns proactive/reactive compaction policy. The permanent hook
|
|
214
|
+
// below also declines Pi's overflow recovery so one bridge never runs two
|
|
215
|
+
// competing policies.
|
|
216
|
+
compaction: { enabled: false, reserveTokens: 16_384, keepRecentTokens: 20_000 },
|
|
217
|
+
}, PI_CONTEXT);
|
|
218
|
+
rawHarness = created.harness;
|
|
219
|
+
// Attach the harness before lane/configuration awaits so any later failure
|
|
220
|
+
// can close the partially constructed handle instead of wedging the repo.
|
|
221
|
+
session.attach(rawHarness, null);
|
|
222
|
+
lane = await rawHarness.lane("main", PI_CONTEXT);
|
|
223
|
+
session.attach(rawHarness, lane);
|
|
224
|
+
|
|
225
|
+
// A restored lane carries its prior configuration, so explicitly bind it to
|
|
226
|
+
// this run's model, effort, and available tools. Legacy v3 transcripts import
|
|
227
|
+
// with an empty active-tool list and are upgraded atomically by Pi on write.
|
|
228
|
+
await rawHarness.setTools(adaptedTools, PI_CONTEXT);
|
|
229
|
+
await lane.setModel({ provider: options.model.provider, modelId: options.model.id }, PI_CONTEXT);
|
|
230
|
+
await lane.setThinkingLevel(options.thinkingLevel ?? "off", PI_CONTEXT);
|
|
231
|
+
await lane.setActiveTools(activeToolNames, PI_CONTEXT);
|
|
232
|
+
|
|
233
|
+
rawHarness.hooks.on("before_compaction", (event) => (
|
|
234
|
+
event.reason === "manual" ? undefined : { decline: true }
|
|
235
|
+
), { id: "mono-agent-compaction-owner" });
|
|
236
|
+
removePromptCacheDiagnostics = installPromptCacheDiagnostics(rawHarness, options);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
try { await session.close(); } catch { /* preserve the construction error */ }
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let closed = false;
|
|
243
|
+
let currentModel = options.model;
|
|
244
|
+
let currentThinkingLevel = options.thinkingLevel ?? "off";
|
|
245
|
+
let currentActiveToolNames = [...activeToolNames];
|
|
246
|
+
const manuallyAppendedEntryIds = new Set();
|
|
247
|
+
|
|
248
|
+
const adapter = {
|
|
249
|
+
getPromptCacheRequest: () => promptCacheRequest(rawHarness),
|
|
250
|
+
models: options.models,
|
|
251
|
+
getModel: () => currentModel,
|
|
252
|
+
getThinkingLevel: () => currentThinkingLevel,
|
|
253
|
+
getActiveTools: () => originalTools.filter((tool) => currentActiveToolNames.includes(tool.name)),
|
|
254
|
+
async setActiveTools(names) {
|
|
255
|
+
await lane.setActiveTools(names, PI_CONTEXT);
|
|
256
|
+
currentActiveToolNames = [...names];
|
|
257
|
+
},
|
|
258
|
+
async setCompactionSettings(settings) {
|
|
259
|
+
await rawHarness.setCompactionSettings(settings, PI_CONTEXT);
|
|
260
|
+
},
|
|
261
|
+
async appendMessage(message) {
|
|
262
|
+
const entryId = await lane.appendMessage(message, PI_CONTEXT);
|
|
263
|
+
manuallyAppendedEntryIds.add(entryId);
|
|
264
|
+
return entryId;
|
|
265
|
+
},
|
|
266
|
+
// Mirror pi's own lane.prompt() (accept → drive) rather than calling it,
|
|
267
|
+
// so the operation id is known the moment Pi admits the run instead of
|
|
268
|
+
// only when it settles. The live-input epoch needs it up front: without
|
|
269
|
+
// it every steer consumed mid-run stays "pending" until the whole run
|
|
270
|
+
// ends and is only acknowledged in one batch at the end.
|
|
271
|
+
async prompt(text, promptOptions) {
|
|
272
|
+
const images = promptOptions?.images;
|
|
273
|
+
const admission = getOrThrow(await lane.accept({
|
|
274
|
+
kind: "prompt",
|
|
275
|
+
prompt: text,
|
|
276
|
+
...(Array.isArray(images) && images.length > 0 ? { images } : {}),
|
|
277
|
+
}, PI_CONTEXT));
|
|
278
|
+
const { operationId } = admission;
|
|
279
|
+
if (typeof operationId !== "string" || operationId.length === 0) {
|
|
280
|
+
throw new Error("Pi run was admitted without an operation id");
|
|
281
|
+
}
|
|
282
|
+
promptOptions?.onOperationAdmitted?.(operationId);
|
|
283
|
+
const driven = getOrThrow(await lane.drive({ operationId, waitForRetry: true }, PI_CONTEXT));
|
|
284
|
+
if (driven.kind === "settled") return driven.outcome;
|
|
285
|
+
if (driven.kind === "waiting" && driven.reason === "deferred") {
|
|
286
|
+
return { operationId, status: "suspended", deferred: driven.deferred };
|
|
287
|
+
}
|
|
288
|
+
throw new Error(`Pi run ${operationId} returned an unwaited retry`);
|
|
289
|
+
},
|
|
290
|
+
// Pi's QueueResult carries `{ entryId }`; the live-input runner keys prompt
|
|
291
|
+
// epoch registration, `cancelQueued` and `message_end` correlation on the
|
|
292
|
+
// bare entry id, so unwrap it here (as appendMessage does) rather than hand
|
|
293
|
+
// the runner an object it would settle as "uncertain" without ever
|
|
294
|
+
// registering the steer.
|
|
295
|
+
async steer(message) {
|
|
296
|
+
const { entryId } = getOrThrow(await lane.steer(message, undefined, PI_CONTEXT));
|
|
297
|
+
if (typeof entryId !== "string" || entryId.length === 0) {
|
|
298
|
+
throw new Error("Pi steer settled without a queue entry id");
|
|
299
|
+
}
|
|
300
|
+
return entryId;
|
|
301
|
+
},
|
|
302
|
+
async cancelQueued(entryId) {
|
|
303
|
+
return getOrThrow(await lane.cancelQueued(entryId, PI_CONTEXT));
|
|
304
|
+
},
|
|
305
|
+
async abort() {
|
|
306
|
+
const result = await lane.abort(PI_CONTEXT);
|
|
307
|
+
// Aborting an already-idle lane is a benign race with prompt settlement.
|
|
308
|
+
if (!result.ok) {
|
|
309
|
+
const error = /** @type {{error: any}} */ (result).error;
|
|
310
|
+
if (error?._tag !== "NoActiveOperation") throw error;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
waitForIdle() {
|
|
314
|
+
return lane.waitForIdle(PI_CONTEXT);
|
|
315
|
+
},
|
|
316
|
+
async compact() {
|
|
317
|
+
const value = getOrThrow(await lane.compact(undefined, PI_CONTEXT));
|
|
318
|
+
const record = value.compaction;
|
|
319
|
+
if (record.status === "failed") throw operationError(record, "Pi compaction failed");
|
|
320
|
+
if (record.status !== "completed") throw operationError(record, "Pi compaction cancelled");
|
|
321
|
+
const entry = record.tipId
|
|
322
|
+
? await session.rawSession.getEntry(record.tipId, PI_CONTEXT)
|
|
323
|
+
: undefined;
|
|
324
|
+
if (!entry || entry.type !== "compaction") {
|
|
325
|
+
throw new Error("Pi compaction completed without a compaction entry");
|
|
326
|
+
}
|
|
327
|
+
return entry;
|
|
328
|
+
},
|
|
329
|
+
on(type, handler) {
|
|
330
|
+
if (type === "tool_result") {
|
|
331
|
+
return rawHarness.hooks.on("after_tool", (event) => handler(event));
|
|
332
|
+
}
|
|
333
|
+
if (type === "session_before_compact") {
|
|
334
|
+
return rawHarness.hooks.on("before_compaction", async (event, context) => {
|
|
335
|
+
const branchEntries = await lane.findEntries({ order: "oldestFirst" }, context);
|
|
336
|
+
const result = await handler({
|
|
337
|
+
...event,
|
|
338
|
+
branchEntries,
|
|
339
|
+
signal: context.abortSignal,
|
|
340
|
+
context,
|
|
341
|
+
});
|
|
342
|
+
if (result?.cancel) return { decline: true };
|
|
343
|
+
return result?.compaction === undefined ? undefined : { compaction: result.compaction };
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
throw new Error(`Unsupported Pi harness hook: ${String(type)}`);
|
|
347
|
+
},
|
|
348
|
+
subscribe(listener) {
|
|
349
|
+
const removes = FORWARDED_EVENT_TYPES.map((type) => rawHarness.events.on(
|
|
350
|
+
/** @type {any} */ (type),
|
|
351
|
+
(event) => {
|
|
352
|
+
if (event.type === "message_end" && manuallyAppendedEntryIds.has(event.entryId)) return;
|
|
353
|
+
const converted = legacyEvent(event);
|
|
354
|
+
if (converted) listener(converted);
|
|
355
|
+
},
|
|
356
|
+
));
|
|
357
|
+
return () => removes.forEach((remove) => remove());
|
|
358
|
+
},
|
|
359
|
+
async abortOpenOperations() {
|
|
360
|
+
if (!created.open.some((operation) => operation.lane === "main")) return;
|
|
361
|
+
// mono-agent tools predate Pi's invocation memo/checkpoint API, so replaying
|
|
362
|
+
// an interrupted durable operation could repeat an external side effect.
|
|
363
|
+
// Fail closed by settling it as aborted before admitting a new prompt.
|
|
364
|
+
getOrThrow(await lane.abort(PI_CONTEXT));
|
|
365
|
+
await lane.waitForIdle(PI_CONTEXT);
|
|
366
|
+
},
|
|
367
|
+
async close() {
|
|
368
|
+
if (closed) return;
|
|
369
|
+
closed = true;
|
|
370
|
+
removePromptCacheDiagnostics();
|
|
371
|
+
await session.close();
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
return adapter;
|
|
376
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const activeRequests = new WeakMap();
|
|
4
|
+
export const promptCacheRequest = (harness) => activeRequests.get(harness);
|
|
5
|
+
|
|
6
|
+
const MAX_MESSAGE_FINGERPRINTS = 128;
|
|
7
|
+
const safeString = (value) => typeof value === "string" ? value.slice(0, 160) : undefined;
|
|
8
|
+
const serialized = (value) => JSON.stringify(value) ?? "null";
|
|
9
|
+
const fingerprint = (value) => createHash("sha256").update(serialized(value)).digest("hex").slice(0, 16);
|
|
10
|
+
const bytes = (value) => value == null ? 0 : Buffer.byteLength(typeof value === "string" ? value : serialized(value));
|
|
11
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
12
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
13
|
+
|
|
14
|
+
function normalizedPayload(event) {
|
|
15
|
+
const payload = record(event?.payload);
|
|
16
|
+
if (!payload) return { family: "unsupported", reason: "payload_not_object" };
|
|
17
|
+
const api = safeString(event?.model?.api) ?? "unknown";
|
|
18
|
+
if (api === "google-generative-ai" || api === "google-vertex") {
|
|
19
|
+
const config = record(payload.config) ?? {};
|
|
20
|
+
const tools = list(config.tools).flatMap((tool) => list(record(tool)?.functionDeclarations).length > 0
|
|
21
|
+
? list(record(tool)?.functionDeclarations) : [tool]);
|
|
22
|
+
return { family: "google", system: config.systemInstruction ?? null, tools, messages: list(payload.contents), payload };
|
|
23
|
+
}
|
|
24
|
+
if (api === "pi-messages") {
|
|
25
|
+
const context = record(payload.context);
|
|
26
|
+
if (context && ("messages" in context || "systemPrompt" in context || "tools" in context)) {
|
|
27
|
+
return { family: "pi-messages", system: context.systemPrompt ?? null, tools: list(context.tools), messages: list(context.messages), payload };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (api === "bedrock-converse-stream") {
|
|
31
|
+
return { family: "bedrock", system: payload.system ?? null, tools: list(record(payload.toolConfig)?.tools), messages: list(payload.messages), payload };
|
|
32
|
+
}
|
|
33
|
+
if (api === "anthropic-messages") {
|
|
34
|
+
return { family: "anthropic", system: payload.system ?? null, tools: list(payload.tools), messages: list(payload.messages), payload };
|
|
35
|
+
}
|
|
36
|
+
if (["openai-responses", "azure-openai-responses", "openai-codex-responses"].includes(api)) {
|
|
37
|
+
return { family: api === "openai-codex-responses" ? "openai-codex" : "openai-responses", system: payload.instructions ?? null, tools: list(payload.tools), messages: list(payload.input), payload };
|
|
38
|
+
}
|
|
39
|
+
return { family: "unsupported", reason: `unrecognized_api:${api}` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cacheMetadata(payload, family) {
|
|
43
|
+
const disabled = payload.prompt_cache_retention === "none" || record(payload.options)?.cacheRetention === "none";
|
|
44
|
+
const key = payload.prompt_cache_key ?? payload.cache_key ?? payload.cachedContent
|
|
45
|
+
?? (family === "pi-messages" ? record(payload.options)?.sessionId : undefined);
|
|
46
|
+
let explicit = false;
|
|
47
|
+
const pending = [{ value: payload, depth: 0 }];
|
|
48
|
+
let visited = 0;
|
|
49
|
+
while (pending.length > 0 && visited < 10_000) {
|
|
50
|
+
const { value, depth } = pending.pop();
|
|
51
|
+
visited += 1;
|
|
52
|
+
if (depth > 12 || value === null || typeof value !== "object") continue;
|
|
53
|
+
for (const [name, child] of Object.entries(value)) {
|
|
54
|
+
if (["cache_control", "cachePoint", "cache_point", "prompt_cache_retention", "prompt_cache_options"].includes(name) && child != null) explicit = true;
|
|
55
|
+
if (typeof child === "object" && child !== null) pending.push({ value: child, depth: depth + 1 });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
cacheMode: disabled ? "disabled" : [key === undefined ? "" : "keyed", explicit ? "explicit" : ""].filter(Boolean).join("+") || "provider-default",
|
|
60
|
+
...(disabled || key === undefined ? {} : { cacheKeyFingerprint: fingerprint(key) }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Install a metadata-only, request-lifecycle-bounded Pi payload diagnostic. */
|
|
65
|
+
export function installPromptCacheDiagnostics(harness, options) {
|
|
66
|
+
if (options.promptCacheDiagnostics !== true || typeof options.onEvent !== "function") return () => {};
|
|
67
|
+
let ordinal = 0;
|
|
68
|
+
const remove = harness.hooks.on("before_payload", (event) => {
|
|
69
|
+
const correlation = { phase: "assistant", requestId: randomUUID() };
|
|
70
|
+
activeRequests.set(harness, correlation);
|
|
71
|
+
const normalized = normalizedPayload(event);
|
|
72
|
+
const model = event?.model ?? options.model ?? {};
|
|
73
|
+
const base = {
|
|
74
|
+
type: "prompt_cache_diagnostic",
|
|
75
|
+
...correlation,
|
|
76
|
+
requestOrdinal: ++ordinal,
|
|
77
|
+
model: [safeString(model.provider), safeString(model.id)].filter(Boolean).join(":") || "unknown",
|
|
78
|
+
api: safeString(model.api) ?? "unknown",
|
|
79
|
+
payloadFamily: normalized.family,
|
|
80
|
+
};
|
|
81
|
+
if (normalized.family === "unsupported") {
|
|
82
|
+
options.onEvent({ ...base, supported: false, unsupportedReason: normalized.reason });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const messages = normalized.messages;
|
|
86
|
+
const logicalInputInterpretation = normalized.payload.previous_response_id === undefined ? "full" : "delta";
|
|
87
|
+
const codexWireUnknown = normalized.family === "openai-codex" && logicalInputInterpretation === "full";
|
|
88
|
+
options.onEvent({
|
|
89
|
+
...base,
|
|
90
|
+
supported: true,
|
|
91
|
+
systemBytes: bytes(normalized.system), systemFingerprint: fingerprint(normalized.system),
|
|
92
|
+
toolDefinitionCount: normalized.tools.length, toolDefinitionsFingerprint: fingerprint(normalized.tools),
|
|
93
|
+
messageCount: messages.length,
|
|
94
|
+
messageFingerprints: messages.slice(0, MAX_MESSAGE_FINGERPRINTS).map(fingerprint),
|
|
95
|
+
messageFingerprintsTruncated: messages.length > MAX_MESSAGE_FINGERPRINTS,
|
|
96
|
+
...cacheMetadata(normalized.payload, normalized.family),
|
|
97
|
+
logicalInputInterpretation,
|
|
98
|
+
inputInterpretation: codexWireUnknown ? "unavailable" : logicalInputInterpretation,
|
|
99
|
+
inputInterpretationSource: codexWireUnknown ? "pre_transport_payload" : "provider_payload",
|
|
100
|
+
});
|
|
101
|
+
}, { id: "mono-agent-prompt-cache-diagnostics" });
|
|
102
|
+
return () => { activeRequests.delete(harness); remove(); };
|
|
103
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const OPENCODE_HOST = "opencode.ai";
|
|
4
|
+
const OPENCODE_PROVIDERS = new Set(["opencode", "opencode-go"]);
|
|
5
|
+
const REQUEST_METHODS = new Set([
|
|
6
|
+
"stream",
|
|
7
|
+
"complete",
|
|
8
|
+
"streamSimple",
|
|
9
|
+
"completeSimple",
|
|
10
|
+
"streamDeferred",
|
|
11
|
+
"fetchDeferred",
|
|
12
|
+
"cancelDeferred",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/** @typedef {import("@earendil-works/pi-ai").Models} Models */
|
|
16
|
+
/** @typedef {import("@earendil-works/pi-ai").ProviderHeaders} ProviderHeaders */
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Match Pi's OpenCode attribution boundary without accepting deceptive suffixes
|
|
20
|
+
* or subdomains. Provider ids remain authoritative for callers that deliberately
|
|
21
|
+
* route OpenCode through a nonstandard endpoint.
|
|
22
|
+
*
|
|
23
|
+
* @param {{provider?: string, baseUrl?: string}} model
|
|
24
|
+
*/
|
|
25
|
+
export function isOpenCodeModel(model) {
|
|
26
|
+
if (OPENCODE_PROVIDERS.has(model?.provider)) return true;
|
|
27
|
+
if (typeof model?.baseUrl !== "string") return false;
|
|
28
|
+
try {
|
|
29
|
+
return new URL(model.baseUrl).hostname === OPENCODE_HOST;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Add one default without replacing an auth/model/request value, including a
|
|
37
|
+
* caller's case-insensitive null suppression.
|
|
38
|
+
*
|
|
39
|
+
* @param {ProviderHeaders} headers
|
|
40
|
+
* @param {string} name
|
|
41
|
+
* @param {string} value
|
|
42
|
+
* @returns {ProviderHeaders}
|
|
43
|
+
*/
|
|
44
|
+
function addDefaultHeader(headers, name, value) {
|
|
45
|
+
const lowerName = name.toLowerCase();
|
|
46
|
+
if (Object.keys(headers).some((existingName) => existingName.toLowerCase() === lowerName)) {
|
|
47
|
+
return headers;
|
|
48
|
+
}
|
|
49
|
+
return { ...headers, [name]: value };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Object<string, *>|undefined} options
|
|
54
|
+
* @param {string} sessionId
|
|
55
|
+
* @returns {Object<string, *>}
|
|
56
|
+
*/
|
|
57
|
+
function withOpenCodeHeaderTransform(options, sessionId) {
|
|
58
|
+
const previousTransform = options?.transformHeaders;
|
|
59
|
+
return {
|
|
60
|
+
...(options ?? {}),
|
|
61
|
+
transformHeaders: async (headers) => {
|
|
62
|
+
let attributed = addDefaultHeader(headers, "x-opencode-session", sessionId);
|
|
63
|
+
attributed = addDefaultHeader(attributed, "x-opencode-client", "mono-agent");
|
|
64
|
+
return typeof previousTransform === "function"
|
|
65
|
+
? await previousTransform(attributed)
|
|
66
|
+
: attributed;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decorate every Pi Models request path for one run. Non-request methods stay
|
|
73
|
+
* bound to the original Models instance because its state lives in private
|
|
74
|
+
* fields; matching is performed on the actual model dispatched by Pi, covering
|
|
75
|
+
* builtin, custom-provider, compaction, deferred, and advanced/test models.
|
|
76
|
+
*
|
|
77
|
+
* @param {Models} models
|
|
78
|
+
* @param {string} sessionId
|
|
79
|
+
* @returns {Models}
|
|
80
|
+
*/
|
|
81
|
+
export function withOpenCodeSessionHeaders(models, sessionId) {
|
|
82
|
+
const wrappers = new Map();
|
|
83
|
+
return /** @type {Models} */ (new Proxy(models, {
|
|
84
|
+
get(target, property) {
|
|
85
|
+
const value = Reflect.get(target, property, target);
|
|
86
|
+
if (typeof property !== "string" || typeof value !== "function") return value;
|
|
87
|
+
if (!REQUEST_METHODS.has(property)) return value.bind(target);
|
|
88
|
+
|
|
89
|
+
let wrapper = wrappers.get(property);
|
|
90
|
+
if (wrapper === undefined) {
|
|
91
|
+
wrapper = (model, input, options) => value.call(
|
|
92
|
+
target,
|
|
93
|
+
model,
|
|
94
|
+
input,
|
|
95
|
+
isOpenCodeModel(model) ? withOpenCodeHeaderTransform(options, sessionId) : options,
|
|
96
|
+
);
|
|
97
|
+
wrappers.set(property, wrapper);
|
|
98
|
+
}
|
|
99
|
+
return wrapper;
|
|
100
|
+
},
|
|
101
|
+
}));
|
|
102
|
+
}
|