larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { isPiThinkingLevel } from "./pi-model-catalog.mjs";
|
|
7
|
+
import { PiRpcClient } from "./pi-rpc-client.mjs";
|
|
8
|
+
import { internalCommandSpec } from "../app/internal-command.mjs";
|
|
9
|
+
import { piAgentDirectory } from "./pi-provider-config.mjs";
|
|
10
|
+
import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
|
|
11
|
+
import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
|
|
12
|
+
import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
13
|
+
class EventSession {
|
|
14
|
+
listeners = new Set();
|
|
15
|
+
subscribe(listener) {
|
|
16
|
+
this.listeners.add(listener);
|
|
17
|
+
return () => this.listeners.delete(listener);
|
|
18
|
+
}
|
|
19
|
+
emit(event) {
|
|
20
|
+
for (const listener of this.listeners)
|
|
21
|
+
listener(event);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function textInput(input) {
|
|
25
|
+
return [{ type: "text", text: input.text }];
|
|
26
|
+
}
|
|
27
|
+
function writeLine(stdin, payload, inputId) {
|
|
28
|
+
if (!stdin || stdin.destroyed)
|
|
29
|
+
return Promise.resolve({ status: "rejected", inputId, retryable: true, reason: "runtime stdin is unavailable" });
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
let settled = false;
|
|
32
|
+
const finish = (result) => {
|
|
33
|
+
if (settled)
|
|
34
|
+
return;
|
|
35
|
+
settled = true;
|
|
36
|
+
resolve(result);
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
stdin.write(`${JSON.stringify(payload)}\n`, (error) => finish(error
|
|
40
|
+
? { status: "rejected", inputId, retryable: true, reason: error.message }
|
|
41
|
+
: { status: "accepted", inputId }));
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
finish({ status: "rejected", inputId, retryable: true, reason: error.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function parseJson(line) {
|
|
49
|
+
try {
|
|
50
|
+
const value = JSON.parse(line);
|
|
51
|
+
return value && typeof value === "object" ? value : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function attachLines(stream, listener) {
|
|
58
|
+
if (!stream)
|
|
59
|
+
return;
|
|
60
|
+
const lines = createInterface({ input: stream });
|
|
61
|
+
lines.on("line", (line) => {
|
|
62
|
+
const message = parseJson(line);
|
|
63
|
+
if (message)
|
|
64
|
+
listener(message);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
class CodexSession extends EventSession {
|
|
68
|
+
process;
|
|
69
|
+
config;
|
|
70
|
+
requestId = 0;
|
|
71
|
+
threadId = null;
|
|
72
|
+
activeTurnId = null;
|
|
73
|
+
turnOwnership = new Map();
|
|
74
|
+
unassignedTurnInputs = [];
|
|
75
|
+
recentTurnIds = [];
|
|
76
|
+
initialized = false;
|
|
77
|
+
freshThreadParams = null;
|
|
78
|
+
resumeFallbackStarted = false;
|
|
79
|
+
pending = new Map();
|
|
80
|
+
constructor(process, config) {
|
|
81
|
+
super();
|
|
82
|
+
this.process = process;
|
|
83
|
+
this.config = config;
|
|
84
|
+
attachLines(process.stdout, (message) => this.onMessage(message));
|
|
85
|
+
attachLines(process.stderr, (message) => this.onMessage(message));
|
|
86
|
+
process.once("exit", (code, signal) => this.emit({ type: "closed", code, signal }));
|
|
87
|
+
process.once("error", (error) => this.emit({ type: "error", message: `Codex process failed: ${error.message}` }));
|
|
88
|
+
this.request("initialize", { clientInfo: { name: "larkin-runtime", version: "1.0.0" }, capabilities: { experimentalApi: true } });
|
|
89
|
+
}
|
|
90
|
+
get sessionId() { return this.threadId; }
|
|
91
|
+
async prompt(input) {
|
|
92
|
+
if (!this.threadId)
|
|
93
|
+
return { status: "deferred", inputId: input.inputId, reason: "Codex thread is not initialized" };
|
|
94
|
+
return this.sendInput("turn/start", { threadId: this.threadId, input: textInput(input) }, input);
|
|
95
|
+
}
|
|
96
|
+
async busyInput(input) {
|
|
97
|
+
if (!this.threadId || !this.activeTurnId)
|
|
98
|
+
return { status: "deferred", inputId: input.inputId, reason: "Codex has no steerable active turn" };
|
|
99
|
+
return this.sendInput("turn/steer", {
|
|
100
|
+
threadId: this.threadId,
|
|
101
|
+
expectedTurnId: this.activeTurnId,
|
|
102
|
+
input: textInput(input),
|
|
103
|
+
}, input);
|
|
104
|
+
}
|
|
105
|
+
async cancel(_reason) {
|
|
106
|
+
if (this.threadId && this.activeTurnId)
|
|
107
|
+
this.request("turn/interrupt", { threadId: this.threadId, turnId: this.activeTurnId });
|
|
108
|
+
}
|
|
109
|
+
async close(_reason) { this.process.kill("SIGTERM"); }
|
|
110
|
+
request(method, params, inputId) {
|
|
111
|
+
const id = ++this.requestId;
|
|
112
|
+
this.pending.set(id, { method, ...(inputId ? { inputId } : {}) });
|
|
113
|
+
void writeLine(this.process.stdin, { jsonrpc: "2.0", id, method, params }, inputId ?? `request:${id}`);
|
|
114
|
+
return id;
|
|
115
|
+
}
|
|
116
|
+
sendInput(method, params, input) {
|
|
117
|
+
if (method === "turn/start")
|
|
118
|
+
this.unassignedTurnInputs.push(input.inputId);
|
|
119
|
+
const turnId = method === "turn/steer" ? String(params.expectedTurnId || "") || undefined : undefined;
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
const id = ++this.requestId;
|
|
122
|
+
this.pending.set(id, { method, inputId: input.inputId, ...(turnId ? { turnId } : {}), resolve });
|
|
123
|
+
void writeLine(this.process.stdin, { jsonrpc: "2.0", id, method, params }, input.inputId).then((result) => {
|
|
124
|
+
if (result.status !== "accepted") {
|
|
125
|
+
this.pending.delete(id);
|
|
126
|
+
if (method === "turn/start")
|
|
127
|
+
this.removeUnassignedInput(input.inputId);
|
|
128
|
+
resolve(result);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
onMessage(message) {
|
|
134
|
+
if (message.id != null && typeof message.method === "string" && !Object.hasOwn(message, "result") && !Object.hasOwn(message, "error")) {
|
|
135
|
+
void writeLine(this.process.stdin, {
|
|
136
|
+
jsonrpc: "2.0",
|
|
137
|
+
id: message.id,
|
|
138
|
+
error: { code: -32601, message: `Unsupported Codex app-server request: ${message.method}` },
|
|
139
|
+
}, `server-request:${message.id}`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (message.id != null && this.pending.has(Number(message.id))) {
|
|
143
|
+
const pending = this.pending.get(Number(message.id));
|
|
144
|
+
this.pending.delete(Number(message.id));
|
|
145
|
+
if (message.error) {
|
|
146
|
+
const errorMessage = String(message.error.message || `${pending.method} failed`);
|
|
147
|
+
const replaceableResumeFailure = pending.method === "thread/resume"
|
|
148
|
+
&& /(?:not found|missing|stale|does not exist|unknown (?:session|thread)|rollout)/i.test(errorMessage);
|
|
149
|
+
if (replaceableResumeFailure && this.freshThreadParams && !this.resumeFallbackStarted) {
|
|
150
|
+
this.resumeFallbackStarted = true;
|
|
151
|
+
this.request("thread/start", this.freshThreadParams);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (pending.method === "turn/steer")
|
|
155
|
+
this.activeTurnId = null;
|
|
156
|
+
if (pending.method === "turn/start" && pending.inputId)
|
|
157
|
+
this.removeUnassignedInput(pending.inputId);
|
|
158
|
+
pending.resolve?.({ status: "deferred", inputId: pending.inputId || `request:${message.id}`, reason: errorMessage });
|
|
159
|
+
if (pending.inputId)
|
|
160
|
+
this.emit({ type: "input-error", inputId: pending.inputId, retryable: true, message: errorMessage });
|
|
161
|
+
else
|
|
162
|
+
this.emit({ type: "error", message: errorMessage });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
let responseTurnId = pending.turnId;
|
|
166
|
+
if (pending.method === "turn/start" && pending.inputId) {
|
|
167
|
+
responseTurnId = String(message.result?.turn?.id || message.result?.turnId || "") || undefined;
|
|
168
|
+
if (responseTurnId)
|
|
169
|
+
this.assignTurnInput(responseTurnId, pending.inputId);
|
|
170
|
+
}
|
|
171
|
+
if (pending.method === "turn/steer" && pending.inputId && pending.turnId) {
|
|
172
|
+
this.assignTurnInput(pending.turnId, pending.inputId);
|
|
173
|
+
}
|
|
174
|
+
const responseOwnership = responseTurnId ? this.turnOwnership.get(responseTurnId) : undefined;
|
|
175
|
+
const completedOutcome = responseOwnership?.outcome;
|
|
176
|
+
const lateNormalSteer = pending.method === "turn/steer" && Boolean(responseOwnership?.completedAt) && !completedOutcome;
|
|
177
|
+
const lateSteerReason = "Codex turn ended before steer acceptance";
|
|
178
|
+
if (lateNormalSteer && pending.inputId && responseOwnership) {
|
|
179
|
+
responseOwnership.terminalInputIds.add(pending.inputId);
|
|
180
|
+
this.emit({ type: "input-error", inputId: pending.inputId, retryable: true,
|
|
181
|
+
willRetry: false, message: lateSteerReason });
|
|
182
|
+
}
|
|
183
|
+
if (pending.resolve)
|
|
184
|
+
pending.resolve(completedOutcome
|
|
185
|
+
? { status: "rejected", inputId: pending.inputId, retryable: completedOutcome.retryable, reason: completedOutcome.message }
|
|
186
|
+
: lateNormalSteer
|
|
187
|
+
? { status: "rejected", inputId: pending.inputId, retryable: true, reason: lateSteerReason }
|
|
188
|
+
: { status: "accepted", inputId: pending.inputId });
|
|
189
|
+
if (pending.method === "initialize" && !this.initialized) {
|
|
190
|
+
this.initialized = true;
|
|
191
|
+
void writeLine(this.process.stdin, { jsonrpc: "2.0", method: "initialized", params: {} }, "initialized");
|
|
192
|
+
const threadParams = {
|
|
193
|
+
cwd: this.config.workspaceDir,
|
|
194
|
+
approvalPolicy: "never",
|
|
195
|
+
sandbox: "danger-full-access",
|
|
196
|
+
developerInstructions: this.config.standingPrompt.content,
|
|
197
|
+
experimentalRawEvents: true,
|
|
198
|
+
...(configuredModel(this.config.model) ? { model: configuredModel(this.config.model) } : {}),
|
|
199
|
+
...(this.config.reasoningEffort ? { config: { model_reasoning_effort: this.config.reasoningEffort } } : {}),
|
|
200
|
+
};
|
|
201
|
+
this.freshThreadParams = threadParams;
|
|
202
|
+
this.request(this.config.resumeSessionId ? "thread/resume" : "thread/start", this.config.resumeSessionId
|
|
203
|
+
? { threadId: this.config.resumeSessionId, ...threadParams }
|
|
204
|
+
: threadParams);
|
|
205
|
+
}
|
|
206
|
+
const thread = message.result?.thread;
|
|
207
|
+
if (thread?.id)
|
|
208
|
+
this.adoptThread(String(thread.id));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (message.method === "thread/started" && message.params?.thread?.id)
|
|
212
|
+
this.adoptThread(String(message.params.thread.id));
|
|
213
|
+
if (message.method === "turn/started") {
|
|
214
|
+
this.activeTurnId = String(message.params?.turn?.id || "") || null;
|
|
215
|
+
if (this.activeTurnId && !this.turnOwnership.has(this.activeTurnId)) {
|
|
216
|
+
const inputId = this.unassignedTurnInputs.shift();
|
|
217
|
+
if (inputId)
|
|
218
|
+
this.assignTurnInput(this.activeTurnId, inputId);
|
|
219
|
+
}
|
|
220
|
+
this.emit({ type: "turn-start", ...(this.activeTurnId ? { turnId: this.activeTurnId } : {}) });
|
|
221
|
+
}
|
|
222
|
+
else if (message.method === "turn/completed") {
|
|
223
|
+
const turnId = String(message.params?.turn?.id || this.activeTurnId || "") || undefined;
|
|
224
|
+
const status = String(message.params?.turn?.status || "completed").toLowerCase();
|
|
225
|
+
const turnError = message.params?.turn?.error;
|
|
226
|
+
const errorMessage = String(turnError?.message || turnError || `Codex turn ${status}`);
|
|
227
|
+
if (turnId && (status === "failed" || status === "interrupted")) {
|
|
228
|
+
this.finishTurnInputs(turnId, errorMessage, status === "interrupted");
|
|
229
|
+
}
|
|
230
|
+
this.activeTurnId = null;
|
|
231
|
+
if (turnId)
|
|
232
|
+
this.completeTurn(turnId);
|
|
233
|
+
this.emit({ type: "turn-end", ...(this.threadId ? { sessionId: this.threadId } : {}), ...(turnId ? { turnId } : {}) });
|
|
234
|
+
}
|
|
235
|
+
else if (message.method === "item/agentMessage/delta" && message.params?.delta) {
|
|
236
|
+
this.emit({ type: "activity", activity: "text", text: String(message.params.delta) });
|
|
237
|
+
}
|
|
238
|
+
else if (message.method === "item/reasoning/summaryTextDelta" && message.params?.delta) {
|
|
239
|
+
this.emit({ type: "activity", activity: "thinking", text: String(message.params.delta) });
|
|
240
|
+
}
|
|
241
|
+
else if (message.method === "error" || message.method === "codex/event/error") {
|
|
242
|
+
const params = message.params ?? {};
|
|
243
|
+
const detail = params.error ?? {};
|
|
244
|
+
const errorMessage = String(detail.message || params.message || "Codex runtime error");
|
|
245
|
+
const willRetry = params.willRetry === true;
|
|
246
|
+
const errorTurnId = String(params.turnId || params.turn?.id || detail.turnId || this.activeTurnId || "") || null;
|
|
247
|
+
const fatal = params.fatal === true || detail.fatal === true
|
|
248
|
+
|| /^(?:transport|process|protocol)(?:_|-)error$/i.test(String(detail.type || detail.kind || ""));
|
|
249
|
+
if (fatal) {
|
|
250
|
+
this.emit({ type: "error", message: errorMessage });
|
|
251
|
+
}
|
|
252
|
+
else if (errorTurnId) {
|
|
253
|
+
if (willRetry)
|
|
254
|
+
this.emitTurnInputErrors(errorTurnId, errorMessage, true, true);
|
|
255
|
+
else
|
|
256
|
+
this.finishTurnInputs(errorTurnId, errorMessage, false);
|
|
257
|
+
}
|
|
258
|
+
else if (/model requires a newer version of Codex/i.test(errorMessage)) {
|
|
259
|
+
this.activeTurnId = null;
|
|
260
|
+
this.emit({ type: "configuration-error", message: errorMessage });
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
this.emit({ type: "input-error", retryable: willRetry, willRetry, message: errorMessage });
|
|
264
|
+
}
|
|
265
|
+
this.pruneTurnOwnership();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
adoptThread(id) {
|
|
269
|
+
if (this.threadId === id)
|
|
270
|
+
return;
|
|
271
|
+
this.threadId = id;
|
|
272
|
+
this.emit({ type: "session-init", sessionId: id });
|
|
273
|
+
}
|
|
274
|
+
assignTurnInput(turnId, inputId) {
|
|
275
|
+
const ownership = this.ownershipFor(turnId);
|
|
276
|
+
ownership.inputIds.add(inputId);
|
|
277
|
+
this.removeUnassignedInput(inputId);
|
|
278
|
+
if (ownership.outcome && !ownership.terminalInputIds.has(inputId)) {
|
|
279
|
+
if (ownership.outcome.configuration) {
|
|
280
|
+
ownership.terminalInputIds.add(inputId);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
ownership.terminalInputIds.add(inputId);
|
|
284
|
+
this.emit({ type: "input-error", inputId, retryable: ownership.outcome.retryable,
|
|
285
|
+
willRetry: false, message: ownership.outcome.message });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
removeUnassignedInput(inputId) {
|
|
290
|
+
const queued = this.unassignedTurnInputs.indexOf(inputId);
|
|
291
|
+
if (queued >= 0)
|
|
292
|
+
this.unassignedTurnInputs.splice(queued, 1);
|
|
293
|
+
}
|
|
294
|
+
ownershipFor(turnId) {
|
|
295
|
+
let ownership = this.turnOwnership.get(turnId);
|
|
296
|
+
if (!ownership) {
|
|
297
|
+
ownership = { inputIds: new Set(), terminalInputIds: new Set(), transientKeys: new Set(),
|
|
298
|
+
configurationTerminal: false, completedAt: null };
|
|
299
|
+
this.turnOwnership.set(turnId, ownership);
|
|
300
|
+
}
|
|
301
|
+
return ownership;
|
|
302
|
+
}
|
|
303
|
+
emitTurnInputErrors(turnId, message, retryable, willRetry) {
|
|
304
|
+
const ownership = this.turnOwnership.get(turnId);
|
|
305
|
+
if (!ownership)
|
|
306
|
+
return;
|
|
307
|
+
for (const inputId of ownership.inputIds) {
|
|
308
|
+
if (ownership.terminalInputIds.has(inputId))
|
|
309
|
+
continue;
|
|
310
|
+
const transientKey = `${inputId}\u0000${message}`;
|
|
311
|
+
if (willRetry && ownership.transientKeys.has(transientKey))
|
|
312
|
+
continue;
|
|
313
|
+
if (willRetry)
|
|
314
|
+
ownership.transientKeys.add(transientKey);
|
|
315
|
+
if (!willRetry)
|
|
316
|
+
ownership.terminalInputIds.add(inputId);
|
|
317
|
+
this.emit({ type: "input-error", inputId, retryable, willRetry, message });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
finishTurnInputs(turnId, message, retryable) {
|
|
321
|
+
const ownership = this.ownershipFor(turnId);
|
|
322
|
+
const configuration = /model requires a newer version of Codex/i.test(message);
|
|
323
|
+
ownership.outcome = { message, retryable, configuration };
|
|
324
|
+
if (configuration) {
|
|
325
|
+
for (const inputId of ownership.inputIds)
|
|
326
|
+
ownership.terminalInputIds.add(inputId);
|
|
327
|
+
if (!ownership.configurationTerminal) {
|
|
328
|
+
ownership.configurationTerminal = true;
|
|
329
|
+
this.activeTurnId = null;
|
|
330
|
+
this.emit({ type: "configuration-error", message });
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
this.emitTurnInputErrors(turnId, message, retryable, false);
|
|
335
|
+
}
|
|
336
|
+
completeTurn(turnId) {
|
|
337
|
+
const ownership = this.ownershipFor(turnId);
|
|
338
|
+
ownership.completedAt = Date.now();
|
|
339
|
+
if (!this.recentTurnIds.includes(turnId))
|
|
340
|
+
this.recentTurnIds.push(turnId);
|
|
341
|
+
this.pruneTurnOwnership();
|
|
342
|
+
}
|
|
343
|
+
pruneTurnOwnership() {
|
|
344
|
+
const cutoff = Date.now() - 5 * 60_000;
|
|
345
|
+
while (this.recentTurnIds.length > 64) {
|
|
346
|
+
const oldest = this.recentTurnIds.shift();
|
|
347
|
+
if (oldest)
|
|
348
|
+
this.turnOwnership.delete(oldest);
|
|
349
|
+
}
|
|
350
|
+
while (this.recentTurnIds.length) {
|
|
351
|
+
const oldest = this.recentTurnIds[0];
|
|
352
|
+
const ownership = this.turnOwnership.get(oldest);
|
|
353
|
+
if (!ownership?.completedAt || ownership.completedAt >= cutoff)
|
|
354
|
+
break;
|
|
355
|
+
this.recentTurnIds.shift();
|
|
356
|
+
this.turnOwnership.delete(oldest);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
class ClaudeSession extends EventSession {
|
|
361
|
+
process;
|
|
362
|
+
currentSessionId;
|
|
363
|
+
busy = false;
|
|
364
|
+
safeBoundary = true;
|
|
365
|
+
gated = [];
|
|
366
|
+
constructor(process, resumeSessionId) {
|
|
367
|
+
super();
|
|
368
|
+
this.process = process;
|
|
369
|
+
this.currentSessionId = resumeSessionId ?? null;
|
|
370
|
+
attachLines(process.stdout, (message) => this.onMessage(message));
|
|
371
|
+
process.once("exit", (code, signal) => this.emit({ type: "closed", code, signal }));
|
|
372
|
+
process.once("error", (error) => this.emit({ type: "error", message: `Claude process failed: ${error.message}` }));
|
|
373
|
+
}
|
|
374
|
+
get sessionId() { return this.currentSessionId; }
|
|
375
|
+
async prompt(input) {
|
|
376
|
+
this.busy = true;
|
|
377
|
+
this.safeBoundary = false;
|
|
378
|
+
return this.writeUser(input);
|
|
379
|
+
}
|
|
380
|
+
async busyInput(input) {
|
|
381
|
+
if (!this.busy || this.safeBoundary) {
|
|
382
|
+
this.safeBoundary = false;
|
|
383
|
+
return this.writeUser(input);
|
|
384
|
+
}
|
|
385
|
+
return new Promise((resolve) => this.gated.push({ input, resolve }));
|
|
386
|
+
}
|
|
387
|
+
async cancel(_reason) { this.process.kill("SIGINT"); }
|
|
388
|
+
async close(_reason) { this.process.stdin?.end?.(); this.process.kill("SIGTERM"); }
|
|
389
|
+
writeUser(input) {
|
|
390
|
+
return writeLine(this.process.stdin, {
|
|
391
|
+
type: "user",
|
|
392
|
+
message: { role: "user", content: [{ type: "text", text: input.text }] },
|
|
393
|
+
...(this.currentSessionId ? { session_id: this.currentSessionId } : {}),
|
|
394
|
+
}, input.inputId);
|
|
395
|
+
}
|
|
396
|
+
onMessage(message) {
|
|
397
|
+
const sessionId = typeof message.session_id === "string" ? message.session_id : null;
|
|
398
|
+
if (sessionId && sessionId !== this.currentSessionId) {
|
|
399
|
+
this.currentSessionId = sessionId;
|
|
400
|
+
this.emit({ type: "session-init", sessionId });
|
|
401
|
+
}
|
|
402
|
+
if (message.type === "assistant") {
|
|
403
|
+
this.safeBoundary = true;
|
|
404
|
+
for (const block of message.message?.content ?? []) {
|
|
405
|
+
if (block.type === "text" && block.text)
|
|
406
|
+
this.emit({ type: "activity", activity: "text", text: String(block.text) });
|
|
407
|
+
if (block.type === "thinking" && block.thinking)
|
|
408
|
+
this.emit({ type: "activity", activity: "thinking", text: String(block.thinking) });
|
|
409
|
+
if (block.type === "tool_use")
|
|
410
|
+
this.emit({ type: "activity", activity: "tool", name: String(block.name || "tool") });
|
|
411
|
+
}
|
|
412
|
+
const next = this.gated.shift();
|
|
413
|
+
if (next) {
|
|
414
|
+
this.safeBoundary = false;
|
|
415
|
+
void this.writeUser(next.input).then(next.resolve);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (message.type === "stream_event") {
|
|
419
|
+
this.safeBoundary = false;
|
|
420
|
+
}
|
|
421
|
+
else if (message.type === "result") {
|
|
422
|
+
this.busy = false;
|
|
423
|
+
this.safeBoundary = true;
|
|
424
|
+
if (message.is_error)
|
|
425
|
+
this.emit({ type: "error", message: String(message.result || message.subtype || "Claude runtime error") });
|
|
426
|
+
this.emit({ type: "turn-end", ...(this.currentSessionId ? { sessionId: this.currentSessionId } : {}) });
|
|
427
|
+
for (const next of this.gated.splice(0))
|
|
428
|
+
void this.writeUser(next.input).then(next.resolve);
|
|
429
|
+
}
|
|
430
|
+
else if (message.type === "system" && message.subtype === "init") {
|
|
431
|
+
this.emit({ type: "turn-start" });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
class PiSession extends EventSession {
|
|
436
|
+
sdk;
|
|
437
|
+
distribution;
|
|
438
|
+
unsubscribe;
|
|
439
|
+
ownedInputIds = new Set();
|
|
440
|
+
awaitingAcknowledgement = new Set();
|
|
441
|
+
finalAssistantError = null;
|
|
442
|
+
finalAssistantStopReason = null;
|
|
443
|
+
requestEpoch = 0;
|
|
444
|
+
activeEpoch = null;
|
|
445
|
+
settleArmedEpoch = null;
|
|
446
|
+
inputEpochs = new Map();
|
|
447
|
+
observedSubmitEpochs = new Set();
|
|
448
|
+
observedAcceptedEpochs = new Set();
|
|
449
|
+
observedCompletedEpochs = new Set();
|
|
450
|
+
firstOutputObserved = false;
|
|
451
|
+
toolCallOpen = false;
|
|
452
|
+
constructor(sdk, distribution) {
|
|
453
|
+
super();
|
|
454
|
+
this.sdk = sdk;
|
|
455
|
+
this.distribution = distribution;
|
|
456
|
+
const result = sdk.subscribe?.((event) => this.onEvent(event));
|
|
457
|
+
if (typeof result === "function")
|
|
458
|
+
this.unsubscribe = result;
|
|
459
|
+
if (sdk.sessionId)
|
|
460
|
+
setImmediate(() => this.emit({
|
|
461
|
+
type: "session-init",
|
|
462
|
+
sessionId: sdk.sessionId,
|
|
463
|
+
...(sdk.model ? { model: `${sdk.model.provider}/${sdk.model.id}` } : {}),
|
|
464
|
+
...(sdk.thinkingLevel ? { reasoningEffort: sdk.thinkingLevel } : {}),
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
get sessionId() { return this.sdk.sessionId ?? null; }
|
|
468
|
+
get effectiveModel() { return this.sdk.model ? `${this.sdk.model.provider}/${this.sdk.model.id}` : null; }
|
|
469
|
+
get effectiveReasoningEffort() { return this.sdk.thinkingLevel ?? null; }
|
|
470
|
+
async prompt(input) { return this.enqueue(input, () => this.sdk.prompt(input.text)); }
|
|
471
|
+
async busyInput(input) { return this.enqueue(input, () => this.sdk.steer(input.text)); }
|
|
472
|
+
async cancel(_reason) { await this.sdk.abort(); }
|
|
473
|
+
async close(_reason) { this.unsubscribe?.(); await this.sdk.dispose?.(); }
|
|
474
|
+
async enqueue(input, operation) {
|
|
475
|
+
if (this.ownedInputIds.size === 0)
|
|
476
|
+
this.requestEpoch += 1;
|
|
477
|
+
const epoch = this.requestEpoch;
|
|
478
|
+
this.ownedInputIds.add(input.inputId);
|
|
479
|
+
this.inputEpochs.set(input.inputId, epoch);
|
|
480
|
+
this.awaitingAcknowledgement.add(input.inputId);
|
|
481
|
+
const ownsRpcObservation = !this.observedSubmitEpochs.has(epoch);
|
|
482
|
+
if (ownsRpcObservation) {
|
|
483
|
+
this.observedSubmitEpochs.add(epoch);
|
|
484
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "rpc_submit" });
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
await operation();
|
|
488
|
+
this.awaitingAcknowledgement.delete(input.inputId);
|
|
489
|
+
if (ownsRpcObservation && !this.observedAcceptedEpochs.has(epoch)) {
|
|
490
|
+
this.observedAcceptedEpochs.add(epoch);
|
|
491
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "rpc_accepted" });
|
|
492
|
+
}
|
|
493
|
+
return { status: "accepted", inputId: input.inputId };
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
const wasAwaiting = this.awaitingAcknowledgement.has(input.inputId);
|
|
497
|
+
if (ownsRpcObservation && wasAwaiting)
|
|
498
|
+
this.emit({
|
|
499
|
+
type: "runtime-observation", runtime: "pi", distribution: this.distribution,
|
|
500
|
+
phase: /preflight timed out/i.test(error.message) ? "rpc_timeout" : "rpc_error",
|
|
501
|
+
});
|
|
502
|
+
this.awaitingAcknowledgement.delete(input.inputId);
|
|
503
|
+
this.ownedInputIds.delete(input.inputId);
|
|
504
|
+
this.inputEpochs.delete(input.inputId);
|
|
505
|
+
return { status: "rejected", inputId: input.inputId, retryable: true, reason: error.message };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
onEvent(raw) {
|
|
509
|
+
const event = raw;
|
|
510
|
+
if (event?.type === "larkin_rpc_failure") {
|
|
511
|
+
const message = String(event.message || "Pi RPC process failed");
|
|
512
|
+
if (this.awaitingAcknowledgement.size > 0)
|
|
513
|
+
this.emit({
|
|
514
|
+
type: "runtime-observation", runtime: "pi", distribution: this.distribution,
|
|
515
|
+
phase: /preflight timed out/i.test(message) ? "rpc_timeout" : "rpc_error",
|
|
516
|
+
});
|
|
517
|
+
for (const inputId of this.ownedInputIds) {
|
|
518
|
+
if (!this.awaitingAcknowledgement.has(inputId))
|
|
519
|
+
this.emit({ type: "input-error", inputId, retryable: true, willRetry: true, message });
|
|
520
|
+
}
|
|
521
|
+
this.awaitingAcknowledgement.clear();
|
|
522
|
+
this.ownedInputIds.clear();
|
|
523
|
+
this.inputEpochs.clear();
|
|
524
|
+
this.observedSubmitEpochs.clear();
|
|
525
|
+
this.observedAcceptedEpochs.clear();
|
|
526
|
+
this.observedCompletedEpochs.clear();
|
|
527
|
+
this.activeEpoch = null;
|
|
528
|
+
this.settleArmedEpoch = null;
|
|
529
|
+
this.emit({ type: "error", message });
|
|
530
|
+
}
|
|
531
|
+
else if (event?.type === "compaction_start" && this.awaitingAcknowledgement.size > 0) {
|
|
532
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "compaction_start" });
|
|
533
|
+
}
|
|
534
|
+
else if (event?.type === "compaction_end" && this.awaitingAcknowledgement.size > 0) {
|
|
535
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "compaction_end" });
|
|
536
|
+
}
|
|
537
|
+
else if ((event?.type === "auto_retry_start" || event?.type === "auto_retry_end"
|
|
538
|
+
|| String(event?.type || "").startsWith("summarization_retry_")) && this.awaitingAcknowledgement.size > 0) {
|
|
539
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "retry_progress" });
|
|
540
|
+
}
|
|
541
|
+
else if (event?.type === "turn_start") {
|
|
542
|
+
const epoch = this.oldestOwnedEpoch();
|
|
543
|
+
if (epoch === null || this.activeEpoch !== null)
|
|
544
|
+
return;
|
|
545
|
+
this.activeEpoch = epoch;
|
|
546
|
+
this.settleArmedEpoch = null;
|
|
547
|
+
this.firstOutputObserved = false;
|
|
548
|
+
this.toolCallOpen = false;
|
|
549
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "turn_start" });
|
|
550
|
+
this.emit({ type: "turn-start", ...(Number.isInteger(event.turnIndex) ? { turnId: `pi-${event.turnIndex}` } : {}) });
|
|
551
|
+
}
|
|
552
|
+
else if (event?.type === "agent_end") {
|
|
553
|
+
const assistant = [...(Array.isArray(event.messages) ? event.messages : [])]
|
|
554
|
+
.reverse().find((message) => message?.role === "assistant");
|
|
555
|
+
this.finalAssistantStopReason = typeof assistant?.stopReason === "string" ? assistant.stopReason : null;
|
|
556
|
+
this.finalAssistantError = assistant?.stopReason === "error"
|
|
557
|
+
? piAssistantProviderError(assistant)
|
|
558
|
+
: null;
|
|
559
|
+
this.settleArmedEpoch = this.activeEpoch;
|
|
560
|
+
if (event.willRetry !== true && this.activeEpoch !== null && !this.observedCompletedEpochs.has(this.activeEpoch)) {
|
|
561
|
+
this.observedCompletedEpochs.add(this.activeEpoch);
|
|
562
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "completed" });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else if (event?.type === "agent_settled") {
|
|
566
|
+
const epoch = this.activeEpoch;
|
|
567
|
+
if (epoch === null || this.settleArmedEpoch !== epoch)
|
|
568
|
+
return;
|
|
569
|
+
this.activeEpoch = null;
|
|
570
|
+
this.settleArmedEpoch = null;
|
|
571
|
+
const error = this.finalAssistantError;
|
|
572
|
+
const stopReason = this.finalAssistantStopReason;
|
|
573
|
+
this.finalAssistantError = null;
|
|
574
|
+
this.finalAssistantStopReason = null;
|
|
575
|
+
if (this.toolCallOpen) {
|
|
576
|
+
this.toolCallOpen = false;
|
|
577
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "tool_result" });
|
|
578
|
+
}
|
|
579
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "settled" });
|
|
580
|
+
const owned = [...this.ownedInputIds].filter((inputId) => this.inputEpochs.get(inputId) === epoch);
|
|
581
|
+
if (error) {
|
|
582
|
+
const classified = classifyPiProviderError(error);
|
|
583
|
+
const retryable = classified.category === "rate_limit" || isRetryablePiProviderError(error.upstream.message);
|
|
584
|
+
for (const inputId of owned)
|
|
585
|
+
this.emit({
|
|
586
|
+
type: "input-error", inputId, retryable, willRetry: false, message: classified.reason,
|
|
587
|
+
errorCategory: classified.category, nextAction: classified.nextAction, upstream: error.upstream,
|
|
588
|
+
});
|
|
589
|
+
this.emit({ type: "turn-end", ...(this.sessionId ? { sessionId: this.sessionId } : {}) });
|
|
590
|
+
}
|
|
591
|
+
else if (stopReason === "aborted") {
|
|
592
|
+
for (const inputId of owned)
|
|
593
|
+
this.emit({
|
|
594
|
+
type: "input-error", inputId, retryable: true, willRetry: false, message: "Pi assistant turn aborted",
|
|
595
|
+
});
|
|
596
|
+
this.emit({ type: "turn-end", ...(this.sessionId ? { sessionId: this.sessionId } : {}) });
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
this.emit({ type: "turn-end", ...(this.sessionId ? { sessionId: this.sessionId } : {}) });
|
|
600
|
+
}
|
|
601
|
+
for (const inputId of owned) {
|
|
602
|
+
this.ownedInputIds.delete(inputId);
|
|
603
|
+
this.inputEpochs.delete(inputId);
|
|
604
|
+
}
|
|
605
|
+
this.observedSubmitEpochs.delete(epoch);
|
|
606
|
+
this.observedAcceptedEpochs.delete(epoch);
|
|
607
|
+
this.observedCompletedEpochs.delete(epoch);
|
|
608
|
+
}
|
|
609
|
+
else if (event?.type === "tool_execution_start") {
|
|
610
|
+
if (!this.firstOutputObserved) {
|
|
611
|
+
this.firstOutputObserved = true;
|
|
612
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "first_output" });
|
|
613
|
+
}
|
|
614
|
+
if (!this.toolCallOpen) {
|
|
615
|
+
this.toolCallOpen = true;
|
|
616
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "tool_call" });
|
|
617
|
+
}
|
|
618
|
+
this.emit({ type: "activity", activity: "tool", name: String(event.toolName || "tool") });
|
|
619
|
+
}
|
|
620
|
+
else if (event?.type === "tool_execution_end") {
|
|
621
|
+
if (this.toolCallOpen) {
|
|
622
|
+
this.toolCallOpen = false;
|
|
623
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "tool_result" });
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else if (event?.type === "message_update" && event.assistantMessageEvent?.delta) {
|
|
627
|
+
if (!this.firstOutputObserved) {
|
|
628
|
+
this.firstOutputObserved = true;
|
|
629
|
+
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "first_output" });
|
|
630
|
+
}
|
|
631
|
+
const kind = event.assistantMessageEvent.type?.startsWith("thinking") ? "thinking" : "text";
|
|
632
|
+
this.emit({ type: "activity", activity: kind, text: String(event.assistantMessageEvent.delta) });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
oldestOwnedEpoch() {
|
|
636
|
+
for (const inputId of this.ownedInputIds) {
|
|
637
|
+
const epoch = this.inputEpochs.get(inputId);
|
|
638
|
+
if (epoch !== undefined)
|
|
639
|
+
return epoch;
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function safeProviderText(value, fallback) {
|
|
645
|
+
const source = typeof value === "string" ? value : fallback;
|
|
646
|
+
return source
|
|
647
|
+
.replace(/\b(?:authorization|proxy-authorization)\s*[:=]\s*(?:Bearer\s+)?[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
|
|
648
|
+
.replace(/\b(?:cookie|set-cookie|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|secret)\s*[:=]\s*[^\s,;]+/gi, (match) => `${match.split(/[:=]/, 1)[0]}=[redacted]`)
|
|
649
|
+
.replace(/(["'](?:authorization|cookie|api[_-]?key|access[_-]?token|refresh[_-]?token|token|secret)["']\s*:\s*)["'][^"']*["']/gi, "$1\"[redacted]\"")
|
|
650
|
+
.replace(/Bearer\s+[A-Za-z0-9._~+\/-]+/gi, "Bearer [redacted]")
|
|
651
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[redacted]")
|
|
652
|
+
.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 2_000) || fallback;
|
|
653
|
+
}
|
|
654
|
+
function piAssistantProviderError(message) {
|
|
655
|
+
const diagnostic = [...(Array.isArray(message.diagnostics) ? message.diagnostics : [])]
|
|
656
|
+
.reverse().find((item) => item && typeof item === "object");
|
|
657
|
+
const details = diagnostic?.details && typeof diagnostic.details === "object" ? diagnostic.details : {};
|
|
658
|
+
const bodyError = details.error && typeof details.error === "object" ? details.error : {};
|
|
659
|
+
const primary = bodyError.message ?? diagnostic?.error?.message ?? message.errorMessage;
|
|
660
|
+
const rawCode = bodyError.code ?? diagnostic?.error?.code;
|
|
661
|
+
const provider = typeof details.provider === "string" ? details.provider : typeof message.provider === "string" ? message.provider : undefined;
|
|
662
|
+
return {
|
|
663
|
+
upstream: {
|
|
664
|
+
...(provider ? { provider: safeProviderText(provider, "provider") } : {}),
|
|
665
|
+
...(typeof rawCode === "string" || typeof rawCode === "number"
|
|
666
|
+
? { code: typeof rawCode === "string" ? safeProviderText(rawCode, "unknown") : rawCode } : {}),
|
|
667
|
+
...(Number.isInteger(details.status) ? { status: Number(details.status) } : {}),
|
|
668
|
+
message: safeProviderText(primary, "Pi assistant turn failed"),
|
|
669
|
+
},
|
|
670
|
+
...(typeof bodyError.type === "string" ? { type: bodyError.type } : {}),
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
export function classifyPiProviderError(error) {
|
|
674
|
+
const upstream = "upstream" in error ? error.upstream : error;
|
|
675
|
+
const errorType = "type" in error ? error.type : undefined;
|
|
676
|
+
const reason = safeProviderText(upstream.message, "Pi provider request failed");
|
|
677
|
+
const signal = new Set([String(upstream.code ?? "").trim().toLowerCase(), String(errorType ?? "").trim().toLowerCase()].filter(Boolean));
|
|
678
|
+
if (upstream.status === 402 || [...signal].some((value) => ["payment_required", "billing_hard_limit_reached", "insufficient_balance"].includes(value)))
|
|
679
|
+
return {
|
|
680
|
+
category: "billing", reason, nextAction: "Update the provider billing method or balance, then retry.",
|
|
681
|
+
};
|
|
682
|
+
if ([...signal].some((value) => ["insufficient_quota", "quota_exceeded", "usage_limit_exceeded"].includes(value)))
|
|
683
|
+
return {
|
|
684
|
+
category: "quota", reason, nextAction: "Increase or reset the provider quota, then retry.",
|
|
685
|
+
};
|
|
686
|
+
if (upstream.status === 429 || [...signal].some((value) => ["rate_limit", "rate_limit_exceeded", "too_many_requests"].includes(value)))
|
|
687
|
+
return {
|
|
688
|
+
category: "rate_limit", reason, nextAction: "Wait for the provider rate-limit window, then retry.",
|
|
689
|
+
};
|
|
690
|
+
if (upstream.status === 401
|
|
691
|
+
|| [...signal].some((value) => ["authentication_error", "invalid_api_key", "invalid_token", "token_expired", "unauthorized", "provider_auth_error"].includes(value))
|
|
692
|
+
|| /\bAPI key auth failed\b|\bFailed to resolve API key\b/i.test(reason)) {
|
|
693
|
+
const readiness = providerAuthenticationFailureReadiness("pi", upstream.provider);
|
|
694
|
+
return { category: "auth", reason: readiness.reason, nextAction: readiness.nextAction };
|
|
695
|
+
}
|
|
696
|
+
return { category: "provider", reason, nextAction: "Inspect the provider status and request settings, then retry." };
|
|
697
|
+
}
|
|
698
|
+
function isRetryablePiProviderError(message) {
|
|
699
|
+
return /fetch failed|network|non-101|transport|timed? ?out|timeout|socket|connection|\b429\b|\b5\d\d\b|rate.?limit|temporar|overload|unavailable/i.test(message);
|
|
700
|
+
}
|
|
701
|
+
const CAPABILITIES = {
|
|
702
|
+
codex: { standingPrompt: "append", sessionResume: true, busyInput: "direct", cancel: true },
|
|
703
|
+
claude: { standingPrompt: "append", sessionResume: true, busyInput: "gated", cancel: true },
|
|
704
|
+
pi: { standingPrompt: "append", sessionResume: true, busyInput: "boundary", cancel: true },
|
|
705
|
+
};
|
|
706
|
+
function configuredModel(model) {
|
|
707
|
+
const value = model?.trim();
|
|
708
|
+
return value && value !== "default" ? value : undefined;
|
|
709
|
+
}
|
|
710
|
+
export function requirePiResumeSessionFile(sessionDir, sessionId) {
|
|
711
|
+
const sessionFile = fs.readdirSync(sessionDir).map((name) => path.join(sessionDir, name)).find((file) => {
|
|
712
|
+
try {
|
|
713
|
+
const first = fs.readFileSync(file, "utf8").split("\n", 1)[0];
|
|
714
|
+
return JSON.parse(first).id === sessionId;
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
return false;
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
if (!sessionFile)
|
|
721
|
+
throw new Error(`Pi resume session not found: ${sessionId}; refusing to silently create a fresh session`);
|
|
722
|
+
return sessionFile;
|
|
723
|
+
}
|
|
724
|
+
function isRecordedZeroTurnPiSession(stateDir, sessionId) {
|
|
725
|
+
try {
|
|
726
|
+
const status = JSON.parse(fs.readFileSync(path.join(stateDir, "status.json"), "utf8"));
|
|
727
|
+
return status?.session?.runtime === "pi" && status.session.id === sessionId && status.session.turns === 0;
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
export function createPiSessionManager(input) {
|
|
734
|
+
const sessionDir = path.join(input.stateDir, "runtime", "pi-sessions");
|
|
735
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
736
|
+
if (!input.resumeSessionId)
|
|
737
|
+
return { sessionDir, sessionFile: null, getSessionId: () => null };
|
|
738
|
+
try {
|
|
739
|
+
const sessionFile = requirePiResumeSessionFile(sessionDir, input.resumeSessionId);
|
|
740
|
+
return { sessionDir, sessionFile, getSessionId: () => input.resumeSessionId };
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
if (isRecordedZeroTurnPiSession(input.stateDir, input.resumeSessionId)) {
|
|
744
|
+
return { sessionDir, sessionFile: null, getSessionId: () => null };
|
|
745
|
+
}
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function writePrivateAtomic(file, content) {
|
|
750
|
+
try {
|
|
751
|
+
if (fs.lstatSync(file).isSymbolicLink())
|
|
752
|
+
throw new Error("standing prompt must not be a symlink");
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (error.code !== "ENOENT")
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
const temporary = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
759
|
+
try {
|
|
760
|
+
fs.writeFileSync(temporary, content, { mode: 0o600, flag: "wx" });
|
|
761
|
+
fs.renameSync(temporary, file);
|
|
762
|
+
fs.chmodSync(file, 0o600);
|
|
763
|
+
}
|
|
764
|
+
finally {
|
|
765
|
+
try {
|
|
766
|
+
fs.unlinkSync(temporary);
|
|
767
|
+
}
|
|
768
|
+
catch { /* renamed or absent */ }
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
class PiRpcBackend {
|
|
772
|
+
client;
|
|
773
|
+
sessionId;
|
|
774
|
+
model;
|
|
775
|
+
thinkingLevel;
|
|
776
|
+
constructor(client, state) {
|
|
777
|
+
this.client = client;
|
|
778
|
+
this.sessionId = state.sessionId ?? null;
|
|
779
|
+
if (state.model?.provider && state.model.id)
|
|
780
|
+
this.model = {
|
|
781
|
+
provider: state.model.provider, id: state.model.id,
|
|
782
|
+
...(state.model.reasoning !== undefined ? { reasoning: state.model.reasoning } : {}),
|
|
783
|
+
...(state.model.thinkingLevelMap ? { thinkingLevelMap: state.model.thinkingLevelMap } : {}),
|
|
784
|
+
};
|
|
785
|
+
this.thinkingLevel = state.thinkingLevel;
|
|
786
|
+
}
|
|
787
|
+
prompt(text) { return this.client.request("prompt", { message: text }); }
|
|
788
|
+
steer(text) { return this.client.request("steer", { message: text }); }
|
|
789
|
+
abort() { return this.client.request("abort"); }
|
|
790
|
+
dispose() { return this.client.close(); }
|
|
791
|
+
subscribe(listener) {
|
|
792
|
+
const offEvent = this.client.subscribe(listener);
|
|
793
|
+
const offFailure = this.client.subscribeFailure((error) => listener({ type: "larkin_rpc_failure", message: error.message }));
|
|
794
|
+
return () => { offEvent(); offFailure(); };
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async function createPiRpcBackend(input, dependencies, spawn) {
|
|
798
|
+
const stateRoot = input.stateDir ?? path.join(input.workspaceDir, ".larkin");
|
|
799
|
+
const runtimeDir = path.join(stateRoot, "runtime");
|
|
800
|
+
fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
801
|
+
const promptFile = path.join(runtimeDir, "pi-standing-prompt.md");
|
|
802
|
+
writePrivateAtomic(promptFile, input.standingPrompt.content);
|
|
803
|
+
const session = createPiSessionManager({ workspaceDir: input.workspaceDir, stateDir: stateRoot, resumeSessionId: input.resumeSessionId });
|
|
804
|
+
const requestedModel = configuredModel(input.model);
|
|
805
|
+
const requestedEffort = input.reasoningEffort?.trim() || undefined;
|
|
806
|
+
if (requestedEffort && !isPiThinkingLevel(requestedEffort))
|
|
807
|
+
throw new Error(`Unknown Pi thinking level: ${requestedEffort}`);
|
|
808
|
+
const args = ["--mode", "rpc", "--session-dir", session.sessionDir, "--append-system-prompt", promptFile,
|
|
809
|
+
...(session.sessionFile ? ["--session", session.sessionFile] : []),
|
|
810
|
+
...(requestedModel ? ["--model", requestedModel] : []),
|
|
811
|
+
...(requestedEffort ? ["--thinking", requestedEffort] : [])];
|
|
812
|
+
const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
|
|
813
|
+
const builtin = mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin";
|
|
814
|
+
const builtinSpec = builtin ? internalCommandSpec("pi-rpc", [], mergedEnv) : null;
|
|
815
|
+
const command = builtinSpec?.command ?? dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
816
|
+
const commandArgs = [...(builtinSpec?.args ?? []), ...args];
|
|
817
|
+
if (builtin) {
|
|
818
|
+
if (!mergedEnv.LARKIN_CONFIG_DIR)
|
|
819
|
+
throw new Error("内置 Pi 缺少 LARKIN_CONFIG_DIR");
|
|
820
|
+
mergedEnv.PI_CODING_AGENT_DIR = piAgentDirectory(mergedEnv.LARKIN_CONFIG_DIR, input.agentId);
|
|
821
|
+
mergedEnv.PI_TELEMETRY = "0";
|
|
822
|
+
}
|
|
823
|
+
const subagentExtension = resolvePiSubagentExtensionArg({
|
|
824
|
+
distribution: builtin ? "builtin" : "external",
|
|
825
|
+
piCommand: command,
|
|
826
|
+
env: mergedEnv,
|
|
827
|
+
});
|
|
828
|
+
if (subagentExtension)
|
|
829
|
+
commandArgs.push("-e", subagentExtension);
|
|
830
|
+
// bash 60s 超时护栏(issue #55/#56):与 pi-subagents 一起注入,两者工具名不冲突。
|
|
831
|
+
const bashTimeoutExtension = resolvePiBashTimeoutExtensionArg({
|
|
832
|
+
distribution: builtin ? "builtin" : "external",
|
|
833
|
+
piCommand: command,
|
|
834
|
+
env: mergedEnv,
|
|
835
|
+
});
|
|
836
|
+
if (bashTimeoutExtension)
|
|
837
|
+
commandArgs.push("-e", bashTimeoutExtension);
|
|
838
|
+
const child = spawn(command, commandArgs, {
|
|
839
|
+
cwd: input.workspaceDir,
|
|
840
|
+
env: mergedEnv,
|
|
841
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
842
|
+
});
|
|
843
|
+
const client = new PiRpcClient(child, dependencies.piRpcClientOptions);
|
|
844
|
+
try {
|
|
845
|
+
const [state, available] = await Promise.all([
|
|
846
|
+
client.request("get_state"),
|
|
847
|
+
client.request("get_available_models"),
|
|
848
|
+
]);
|
|
849
|
+
if (!available?.models?.length)
|
|
850
|
+
throw new Error("Pi has no authenticated available models. Run the official `pi` login flow or configure provider credentials; Larkin will not create a fallback session.");
|
|
851
|
+
const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
|
|
852
|
+
if (requestedModel && effectiveModel !== requestedModel)
|
|
853
|
+
throw new Error(`Pi model fallback refused: requested ${requestedModel}, effective ${effectiveModel || "none"}`);
|
|
854
|
+
if (requestedEffort && state.thinkingLevel !== requestedEffort)
|
|
855
|
+
throw new Error(`Pi thinking level ${requestedEffort} was not accepted by effective model ${effectiveModel || "unknown"}`);
|
|
856
|
+
return new PiRpcBackend(client, state);
|
|
857
|
+
}
|
|
858
|
+
catch (error) {
|
|
859
|
+
await client.close();
|
|
860
|
+
if (error instanceof RuntimePrerequisiteError)
|
|
861
|
+
throw error;
|
|
862
|
+
throw new RuntimePrerequisiteError(classifyRuntimePrerequisite("pi", error, command));
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
export function createNativeRuntimeAdapter(id, dependencies = {}) {
|
|
866
|
+
if (id !== "codex" && id !== "claude" && id !== "pi")
|
|
867
|
+
throw new Error(`Unsupported native runtime: ${id}`);
|
|
868
|
+
const spawn = dependencies.spawn ?? ((command, args, options) => nodeSpawn(command, args, options));
|
|
869
|
+
let resolvedExecutable = null;
|
|
870
|
+
const configuredCommand = id === "pi"
|
|
871
|
+
? dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi"
|
|
872
|
+
: id === "codex"
|
|
873
|
+
? dependencies.codexCommand ?? dependencies.env?.LARKIN_CODEX_COMMAND ?? "codex"
|
|
874
|
+
: dependencies.claudeCommand ?? dependencies.env?.LARKIN_CLAUDE_COMMAND ?? "claude";
|
|
875
|
+
const codexCommand = dependencies.codexCommand ?? dependencies.env?.LARKIN_CODEX_COMMAND ?? "codex";
|
|
876
|
+
let codexUpdateAttempt = null;
|
|
877
|
+
let codexUpdateAttempted = false;
|
|
878
|
+
const runCodexUpdate = () => new Promise((resolve) => {
|
|
879
|
+
const spawnUpdate = dependencies.spawnCodexUpdate
|
|
880
|
+
?? ((command, args, options) => nodeSpawn(command, [...args], options));
|
|
881
|
+
let child;
|
|
882
|
+
try {
|
|
883
|
+
child = spawnUpdate(codexCommand, ["update"], {
|
|
884
|
+
env: { ...globalThis.process.env, ...dependencies.env, NO_COLOR: "1" },
|
|
885
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
catch (error) {
|
|
889
|
+
resolve({ recovered: false, reason: `Codex update failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const output = [];
|
|
893
|
+
let outputBytes = 0;
|
|
894
|
+
const capture = (chunk) => {
|
|
895
|
+
const text = String(chunk);
|
|
896
|
+
if (outputBytes >= 4 * 1024 * 1024)
|
|
897
|
+
return;
|
|
898
|
+
output.push(text.slice(0, 4 * 1024 * 1024 - outputBytes));
|
|
899
|
+
outputBytes += Buffer.byteLength(text);
|
|
900
|
+
};
|
|
901
|
+
child.stdout?.on("data", capture);
|
|
902
|
+
child.stderr?.on("data", capture);
|
|
903
|
+
let settled = false;
|
|
904
|
+
let timedOut = false;
|
|
905
|
+
let killTimer = null;
|
|
906
|
+
const finish = (result) => {
|
|
907
|
+
if (settled)
|
|
908
|
+
return;
|
|
909
|
+
settled = true;
|
|
910
|
+
clearTimeout(timeoutTimer);
|
|
911
|
+
if (killTimer)
|
|
912
|
+
clearTimeout(killTimer);
|
|
913
|
+
resolve(result);
|
|
914
|
+
};
|
|
915
|
+
const timeoutTimer = setTimeout(() => {
|
|
916
|
+
timedOut = true;
|
|
917
|
+
child.kill("SIGTERM");
|
|
918
|
+
killTimer = setTimeout(() => {
|
|
919
|
+
child.kill("SIGKILL");
|
|
920
|
+
finish({ recovered: false, reason: `Codex update timed out after ${dependencies.codexUpdateTimeoutMs ?? 180_000}ms` });
|
|
921
|
+
}, dependencies.codexUpdateKillGraceMs ?? 2_000);
|
|
922
|
+
}, dependencies.codexUpdateTimeoutMs ?? 180_000);
|
|
923
|
+
child.once("error", (error) => finish({ recovered: false, reason: `Codex update failed: ${error.message}` }));
|
|
924
|
+
child.once("exit", (code, signal) => {
|
|
925
|
+
const detail = output.join("").trim();
|
|
926
|
+
if (timedOut) {
|
|
927
|
+
finish({ recovered: false, reason: `Codex update timed out${detail ? `: ${detail}` : ""}` });
|
|
928
|
+
}
|
|
929
|
+
else if (code === 0) {
|
|
930
|
+
finish({ recovered: true, reason: `Codex update succeeded${detail ? `: ${detail}` : ""}` });
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
finish({ recovered: false, reason: `Codex update failed (${code ?? signal ?? "unknown"})${detail ? `: ${detail}` : ""}` });
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
const recoverCodexConfiguration = async (message) => {
|
|
938
|
+
if (!/model requires a newer version of Codex/i.test(message)) {
|
|
939
|
+
return { recovered: false, reason: `Codex configuration recovery is not applicable: ${message}` };
|
|
940
|
+
}
|
|
941
|
+
if (codexUpdateAttempt)
|
|
942
|
+
return codexUpdateAttempt;
|
|
943
|
+
if (codexUpdateAttempted)
|
|
944
|
+
return { recovered: false, reason: "Codex update was already attempted during this runtime startup" };
|
|
945
|
+
codexUpdateAttempted = true;
|
|
946
|
+
codexUpdateAttempt = runCodexUpdate().then((result) => result.recovered
|
|
947
|
+
? result
|
|
948
|
+
: { recovered: false, reason: `${message}\n${result.reason}` });
|
|
949
|
+
try {
|
|
950
|
+
return await codexUpdateAttempt;
|
|
951
|
+
}
|
|
952
|
+
finally {
|
|
953
|
+
codexUpdateAttempt = null;
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
const adapter = {
|
|
957
|
+
id,
|
|
958
|
+
capabilities: CAPABILITIES[id],
|
|
959
|
+
async probe(input) {
|
|
960
|
+
const readiness = await probeNativeRuntimeReadiness({ runtime: id, agentId: input.agentId, cwd: input.workspaceDir,
|
|
961
|
+
env: { ...dependencies.env, ...input.env }, command: configuredCommand });
|
|
962
|
+
resolvedExecutable = readiness.state === "ready" ? readiness.executable ?? null : null;
|
|
963
|
+
return readiness;
|
|
964
|
+
},
|
|
965
|
+
async createSession(input) {
|
|
966
|
+
const productionSpawn = !dependencies.spawn && !(id === "pi" && dependencies.createPiSession);
|
|
967
|
+
if (productionSpawn && !resolvedExecutable) {
|
|
968
|
+
const readiness = await adapter.probe(input);
|
|
969
|
+
if (readiness.state !== "ready")
|
|
970
|
+
throw new RuntimePrerequisiteError(readiness);
|
|
971
|
+
}
|
|
972
|
+
if (id === "pi") {
|
|
973
|
+
const distribution = ({ ...globalThis.process.env, ...dependencies.env, ...input.env }).LARKIN_PI_DISTRIBUTION === "builtin"
|
|
974
|
+
? "builtin" : "external";
|
|
975
|
+
return new PiSession(await (dependencies.createPiSession
|
|
976
|
+
? dependencies.createPiSession(input)
|
|
977
|
+
: createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn)), distribution);
|
|
978
|
+
}
|
|
979
|
+
if (id === "codex") {
|
|
980
|
+
const codexInput = dependencies.codexModelOverride?.trim()
|
|
981
|
+
? { ...input, model: dependencies.codexModelOverride.trim() }
|
|
982
|
+
: input;
|
|
983
|
+
const process = spawn(resolvedExecutable || codexCommand, ["app-server", "--listen", "stdio://"], {
|
|
984
|
+
cwd: input.workspaceDir,
|
|
985
|
+
env: { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" },
|
|
986
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
987
|
+
});
|
|
988
|
+
return new CodexSession(process, codexInput);
|
|
989
|
+
}
|
|
990
|
+
const runtimeDir = path.join(input.stateDir ?? path.join(input.workspaceDir, ".larkin"), "runtime");
|
|
991
|
+
(dependencies.mkdir ?? fs.mkdirSync)(runtimeDir, { recursive: true });
|
|
992
|
+
const promptFile = path.join(runtimeDir, "claude-system-prompt.md");
|
|
993
|
+
if (dependencies.writeFile)
|
|
994
|
+
dependencies.writeFile(promptFile, input.standingPrompt.content, { mode: 0o600 });
|
|
995
|
+
else
|
|
996
|
+
writePrivateAtomic(promptFile, input.standingPrompt.content);
|
|
997
|
+
const args = [
|
|
998
|
+
"--dangerously-skip-permissions", "--verbose",
|
|
999
|
+
"--permission-mode", "bypassPermissions",
|
|
1000
|
+
"--output-format", "stream-json",
|
|
1001
|
+
"--input-format", "stream-json",
|
|
1002
|
+
"--include-partial-messages",
|
|
1003
|
+
"--append-system-prompt-file", promptFile,
|
|
1004
|
+
...(configuredModel(input.model) ? ["--model", configuredModel(input.model)] : []),
|
|
1005
|
+
...(input.reasoningEffort ? ["--effort", input.reasoningEffort] : []),
|
|
1006
|
+
...(input.resumeSessionId ? ["--resume", input.resumeSessionId] : []),
|
|
1007
|
+
];
|
|
1008
|
+
const process = spawn(resolvedExecutable || dependencies.claudeCommand || "claude", args, {
|
|
1009
|
+
cwd: input.workspaceDir,
|
|
1010
|
+
env: { ...globalThis.process.env, ...dependencies.env, ...input.env, CLAUDECODE: undefined },
|
|
1011
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1012
|
+
});
|
|
1013
|
+
return new ClaudeSession(process, input.resumeSessionId ?? undefined);
|
|
1014
|
+
},
|
|
1015
|
+
};
|
|
1016
|
+
if (id === "codex")
|
|
1017
|
+
adapter.recoverConfigurationError = recoverCodexConfiguration;
|
|
1018
|
+
return adapter;
|
|
1019
|
+
}
|