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,255 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PiRpcClient = void 0;
|
|
4
|
+
/** A strict, bounded LF-delimited client for Pi's public RPC protocol. */
|
|
5
|
+
class PiRpcClient {
|
|
6
|
+
process;
|
|
7
|
+
pending = new Map();
|
|
8
|
+
eventListeners = new Set();
|
|
9
|
+
failureListeners = new Set();
|
|
10
|
+
requestTimeoutMs;
|
|
11
|
+
inputTimeoutMs;
|
|
12
|
+
inputProgressTimeoutMs;
|
|
13
|
+
inputMaxTimeoutMs;
|
|
14
|
+
maxFrameBytes;
|
|
15
|
+
maxStderrBytes;
|
|
16
|
+
shutdownGraceMs;
|
|
17
|
+
stdoutBuffer = Buffer.alloc(0);
|
|
18
|
+
stderrBuffer = Buffer.alloc(0);
|
|
19
|
+
exited = false;
|
|
20
|
+
nextId = 0;
|
|
21
|
+
failed = null;
|
|
22
|
+
shutdownPromise = null;
|
|
23
|
+
constructor(process, options = {}) {
|
|
24
|
+
this.process = process;
|
|
25
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
|
|
26
|
+
this.inputTimeoutMs = options.inputTimeoutMs ?? 120_000;
|
|
27
|
+
this.inputProgressTimeoutMs = options.inputProgressTimeoutMs ?? 300_000;
|
|
28
|
+
this.inputMaxTimeoutMs = options.inputMaxTimeoutMs ?? 600_000;
|
|
29
|
+
if (this.requestTimeoutMs <= 0 || this.inputTimeoutMs <= 0 || this.inputProgressTimeoutMs <= 0
|
|
30
|
+
|| this.inputMaxTimeoutMs < this.inputTimeoutMs)
|
|
31
|
+
throw new Error("Pi RPC timeout options are invalid");
|
|
32
|
+
this.maxFrameBytes = options.maxFrameBytes ?? 1024 * 1024;
|
|
33
|
+
this.maxStderrBytes = options.maxStderrBytes ?? 64 * 1024;
|
|
34
|
+
this.shutdownGraceMs = options.shutdownGraceMs ?? 2_000;
|
|
35
|
+
process.stdout?.on("data", (chunk) => this.consume(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
36
|
+
process.stdout?.on("end", () => {
|
|
37
|
+
if (this.stdoutBuffer.length)
|
|
38
|
+
this.protocolFailure("unterminated final frame");
|
|
39
|
+
});
|
|
40
|
+
process.stderr?.on("data", (chunk) => {
|
|
41
|
+
if (this.stderrBuffer.length >= this.maxStderrBytes)
|
|
42
|
+
return;
|
|
43
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
44
|
+
this.stderrBuffer = Buffer.concat([this.stderrBuffer, value.subarray(0, this.maxStderrBytes - this.stderrBuffer.length)]);
|
|
45
|
+
});
|
|
46
|
+
process.once("error", (error) => this.fail(new Error(`Pi RPC process failed: ${error.message}`), true));
|
|
47
|
+
process.once("exit", (code, signal) => {
|
|
48
|
+
this.exited = true;
|
|
49
|
+
const stderr = this.stderrBuffer.toString("utf8").trim().slice(0, 2_000);
|
|
50
|
+
this.fail(new Error(`Pi RPC process exited (${code ?? signal ?? "unknown"})${stderr ? `: ${stderr}` : ""}`), false);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
subscribe(listener) {
|
|
54
|
+
this.eventListeners.add(listener);
|
|
55
|
+
return () => this.eventListeners.delete(listener);
|
|
56
|
+
}
|
|
57
|
+
subscribeFailure(listener) {
|
|
58
|
+
this.failureListeners.add(listener);
|
|
59
|
+
if (this.failed)
|
|
60
|
+
queueMicrotask(() => listener(this.failed));
|
|
61
|
+
return () => this.failureListeners.delete(listener);
|
|
62
|
+
}
|
|
63
|
+
request(command, fields = {}) {
|
|
64
|
+
if (this.failed)
|
|
65
|
+
return Promise.reject(this.failed);
|
|
66
|
+
const id = `larkin-${++this.nextId}`;
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const startedAt = Date.now();
|
|
69
|
+
const input = isInputCommand(command);
|
|
70
|
+
const pending = {
|
|
71
|
+
command, startedAt,
|
|
72
|
+
...(input ? { absoluteDeadlineAt: startedAt + this.inputMaxTimeoutMs } : {}),
|
|
73
|
+
resolve: resolve, reject,
|
|
74
|
+
};
|
|
75
|
+
this.pending.set(id, pending);
|
|
76
|
+
this.armTimeout(id, pending, input ? this.inputTimeoutMs : this.requestTimeoutMs);
|
|
77
|
+
try {
|
|
78
|
+
if (!this.process.stdin || this.process.stdin.destroyed)
|
|
79
|
+
throw new Error("stdin is unavailable");
|
|
80
|
+
this.process.stdin.write(`${JSON.stringify({ id, type: command, ...fields })}\n`, (error) => {
|
|
81
|
+
if (error)
|
|
82
|
+
this.fail(new Error(`Pi RPC ${command} write failed: ${error.message}`), true);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.fail(new Error(`Pi RPC ${command} write failed: ${error instanceof Error ? error.message : String(error)}`), true);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
notify(command, fields = {}) {
|
|
91
|
+
if (this.failed || !this.process.stdin || this.process.stdin.destroyed)
|
|
92
|
+
return;
|
|
93
|
+
this.process.stdin.write(`${JSON.stringify({ type: command, ...fields })}\n`);
|
|
94
|
+
}
|
|
95
|
+
close() {
|
|
96
|
+
if (!this.failed)
|
|
97
|
+
this.fail(new Error("Pi RPC client closed"), true);
|
|
98
|
+
return this.beginShutdown();
|
|
99
|
+
}
|
|
100
|
+
consume(chunk) {
|
|
101
|
+
if (this.failed)
|
|
102
|
+
return;
|
|
103
|
+
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
104
|
+
while (true) {
|
|
105
|
+
const newline = this.stdoutBuffer.indexOf(0x0a);
|
|
106
|
+
if (newline < 0) {
|
|
107
|
+
if (this.stdoutBuffer.length > this.maxFrameBytes)
|
|
108
|
+
this.protocolFailure(`frame exceeds ${this.maxFrameBytes} bytes`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (newline > this.maxFrameBytes) {
|
|
112
|
+
this.protocolFailure(`frame exceeds ${this.maxFrameBytes} bytes`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let frame = this.stdoutBuffer.subarray(0, newline);
|
|
116
|
+
this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1);
|
|
117
|
+
if (frame.at(-1) === 0x0d)
|
|
118
|
+
frame = frame.subarray(0, -1);
|
|
119
|
+
if (!frame.length) {
|
|
120
|
+
this.protocolFailure("empty frame");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let message;
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(frame.toString("utf8"));
|
|
126
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
127
|
+
throw new Error("frame is not an object");
|
|
128
|
+
message = parsed;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
this.protocolFailure(`malformed JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this.dispatch(message);
|
|
135
|
+
if (this.failed)
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
dispatch(message) {
|
|
140
|
+
if (message.type === "response" && typeof message.id === "string") {
|
|
141
|
+
const pending = this.pending.get(message.id);
|
|
142
|
+
if (!pending) {
|
|
143
|
+
this.protocolFailure(`unexpected response id ${message.id}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.pending.delete(message.id);
|
|
147
|
+
if (pending.timer)
|
|
148
|
+
clearTimeout(pending.timer);
|
|
149
|
+
if (message.command !== pending.command) {
|
|
150
|
+
pending.reject(new Error(`Pi RPC protocol error: response command ${String(message.command)} does not match ${pending.command}`));
|
|
151
|
+
this.fail(new Error("Pi RPC protocol error: mismatched response command"), true);
|
|
152
|
+
}
|
|
153
|
+
else if (message.success === true)
|
|
154
|
+
pending.resolve(message.data);
|
|
155
|
+
else
|
|
156
|
+
pending.reject(new Error(`Pi RPC ${pending.command} failed: ${String(message.error || "unknown error")}`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (isInputProgress(message))
|
|
160
|
+
this.refreshInputDeadlines(message.type);
|
|
161
|
+
for (const listener of this.eventListeners)
|
|
162
|
+
listener(message);
|
|
163
|
+
}
|
|
164
|
+
refreshInputDeadlines(eventType) {
|
|
165
|
+
const graceMs = eventType === "compaction_start" || eventType.startsWith("summarization_retry_")
|
|
166
|
+
|| eventType === "auto_retry_start" ? this.inputProgressTimeoutMs : this.inputTimeoutMs;
|
|
167
|
+
for (const [id, pending] of this.pending) {
|
|
168
|
+
if (!isInputCommand(pending.command))
|
|
169
|
+
continue;
|
|
170
|
+
this.armTimeout(id, pending, graceMs);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
armTimeout(id, pending, requestedMs) {
|
|
174
|
+
if (pending.timer)
|
|
175
|
+
clearTimeout(pending.timer);
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const absoluteRemaining = pending.absoluteDeadlineAt === undefined ? requestedMs : pending.absoluteDeadlineAt - now;
|
|
178
|
+
const delay = Math.max(0, Math.min(requestedMs, absoluteRemaining));
|
|
179
|
+
pending.timer = setTimeout(() => {
|
|
180
|
+
if (!this.pending.has(id))
|
|
181
|
+
return;
|
|
182
|
+
const elapsed = Date.now() - pending.startedAt;
|
|
183
|
+
const absolute = pending.absoluteDeadlineAt !== undefined && Date.now() >= pending.absoluteDeadlineAt;
|
|
184
|
+
const error = isInputCommand(pending.command)
|
|
185
|
+
? new Error(absolute
|
|
186
|
+
? `Pi RPC ${pending.command} preflight timed out at absolute ${this.inputMaxTimeoutMs}ms limit`
|
|
187
|
+
: `Pi RPC ${pending.command} preflight timed out after ${elapsed}ms without progress`)
|
|
188
|
+
: new Error(`Pi RPC ${pending.command} timed out after ${this.requestTimeoutMs}ms`);
|
|
189
|
+
this.fail(error, true);
|
|
190
|
+
}, delay);
|
|
191
|
+
pending.timer.unref?.();
|
|
192
|
+
}
|
|
193
|
+
protocolFailure(detail) {
|
|
194
|
+
this.fail(new Error(`Pi RPC protocol error: ${detail}`), true);
|
|
195
|
+
}
|
|
196
|
+
fail(error, terminate) {
|
|
197
|
+
if (this.failed)
|
|
198
|
+
return;
|
|
199
|
+
this.failed = error;
|
|
200
|
+
for (const pending of this.pending.values()) {
|
|
201
|
+
if (pending.timer)
|
|
202
|
+
clearTimeout(pending.timer);
|
|
203
|
+
pending.reject(error);
|
|
204
|
+
}
|
|
205
|
+
this.pending.clear();
|
|
206
|
+
for (const listener of this.failureListeners)
|
|
207
|
+
listener(error);
|
|
208
|
+
if (terminate)
|
|
209
|
+
void this.beginShutdown();
|
|
210
|
+
}
|
|
211
|
+
beginShutdown() {
|
|
212
|
+
if (this.shutdownPromise)
|
|
213
|
+
return this.shutdownPromise;
|
|
214
|
+
if (this.exited)
|
|
215
|
+
return Promise.resolve();
|
|
216
|
+
this.shutdownPromise = new Promise((resolve) => {
|
|
217
|
+
let settled = false;
|
|
218
|
+
let forceTimer = null;
|
|
219
|
+
let hardTimer = null;
|
|
220
|
+
const finish = () => {
|
|
221
|
+
if (settled)
|
|
222
|
+
return;
|
|
223
|
+
settled = true;
|
|
224
|
+
if (forceTimer)
|
|
225
|
+
clearTimeout(forceTimer);
|
|
226
|
+
if (hardTimer)
|
|
227
|
+
clearTimeout(hardTimer);
|
|
228
|
+
resolve();
|
|
229
|
+
};
|
|
230
|
+
this.process.once("exit", finish);
|
|
231
|
+
this.process.kill("SIGTERM");
|
|
232
|
+
forceTimer = setTimeout(() => {
|
|
233
|
+
if (this.exited) {
|
|
234
|
+
finish();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.process.kill("SIGKILL");
|
|
238
|
+
// A real child emits exit after SIGKILL. Keep a final bound for broken
|
|
239
|
+
// process doubles/platform failures without creating a second shutdown path.
|
|
240
|
+
hardTimer = setTimeout(finish, this.shutdownGraceMs);
|
|
241
|
+
hardTimer.unref?.();
|
|
242
|
+
}, this.shutdownGraceMs);
|
|
243
|
+
forceTimer.unref?.();
|
|
244
|
+
});
|
|
245
|
+
return this.shutdownPromise;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
exports.PiRpcClient = PiRpcClient;
|
|
249
|
+
const INPUT_COMMANDS = new Set(["prompt", "steer", "follow_up"]);
|
|
250
|
+
const INPUT_PROGRESS_EVENTS = new Set([
|
|
251
|
+
"compaction_start", "compaction_end", "auto_retry_start", "auto_retry_end",
|
|
252
|
+
"summarization_retry_scheduled", "summarization_retry_attempt_start", "summarization_retry_finished",
|
|
253
|
+
]);
|
|
254
|
+
const isInputCommand = (command) => INPUT_COMMANDS.has(command);
|
|
255
|
+
const isInputProgress = (message) => typeof message.type === "string" && INPUT_PROGRESS_EVENTS.has(message.type);
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/** A strict, bounded LF-delimited client for Pi's public RPC protocol. */
|
|
2
|
+
export class PiRpcClient {
|
|
3
|
+
process;
|
|
4
|
+
pending = new Map();
|
|
5
|
+
eventListeners = new Set();
|
|
6
|
+
failureListeners = new Set();
|
|
7
|
+
requestTimeoutMs;
|
|
8
|
+
inputTimeoutMs;
|
|
9
|
+
inputProgressTimeoutMs;
|
|
10
|
+
inputMaxTimeoutMs;
|
|
11
|
+
maxFrameBytes;
|
|
12
|
+
maxStderrBytes;
|
|
13
|
+
shutdownGraceMs;
|
|
14
|
+
stdoutBuffer = Buffer.alloc(0);
|
|
15
|
+
stderrBuffer = Buffer.alloc(0);
|
|
16
|
+
exited = false;
|
|
17
|
+
nextId = 0;
|
|
18
|
+
failed = null;
|
|
19
|
+
shutdownPromise = null;
|
|
20
|
+
constructor(process, options = {}) {
|
|
21
|
+
this.process = process;
|
|
22
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
|
|
23
|
+
this.inputTimeoutMs = options.inputTimeoutMs ?? 120_000;
|
|
24
|
+
this.inputProgressTimeoutMs = options.inputProgressTimeoutMs ?? 300_000;
|
|
25
|
+
this.inputMaxTimeoutMs = options.inputMaxTimeoutMs ?? 600_000;
|
|
26
|
+
if (this.requestTimeoutMs <= 0 || this.inputTimeoutMs <= 0 || this.inputProgressTimeoutMs <= 0
|
|
27
|
+
|| this.inputMaxTimeoutMs < this.inputTimeoutMs)
|
|
28
|
+
throw new Error("Pi RPC timeout options are invalid");
|
|
29
|
+
this.maxFrameBytes = options.maxFrameBytes ?? 1024 * 1024;
|
|
30
|
+
this.maxStderrBytes = options.maxStderrBytes ?? 64 * 1024;
|
|
31
|
+
this.shutdownGraceMs = options.shutdownGraceMs ?? 2_000;
|
|
32
|
+
process.stdout?.on("data", (chunk) => this.consume(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
33
|
+
process.stdout?.on("end", () => {
|
|
34
|
+
if (this.stdoutBuffer.length)
|
|
35
|
+
this.protocolFailure("unterminated final frame");
|
|
36
|
+
});
|
|
37
|
+
process.stderr?.on("data", (chunk) => {
|
|
38
|
+
if (this.stderrBuffer.length >= this.maxStderrBytes)
|
|
39
|
+
return;
|
|
40
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
41
|
+
this.stderrBuffer = Buffer.concat([this.stderrBuffer, value.subarray(0, this.maxStderrBytes - this.stderrBuffer.length)]);
|
|
42
|
+
});
|
|
43
|
+
process.once("error", (error) => this.fail(new Error(`Pi RPC process failed: ${error.message}`), true));
|
|
44
|
+
process.once("exit", (code, signal) => {
|
|
45
|
+
this.exited = true;
|
|
46
|
+
const stderr = this.stderrBuffer.toString("utf8").trim().slice(0, 2_000);
|
|
47
|
+
this.fail(new Error(`Pi RPC process exited (${code ?? signal ?? "unknown"})${stderr ? `: ${stderr}` : ""}`), false);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
subscribe(listener) {
|
|
51
|
+
this.eventListeners.add(listener);
|
|
52
|
+
return () => this.eventListeners.delete(listener);
|
|
53
|
+
}
|
|
54
|
+
subscribeFailure(listener) {
|
|
55
|
+
this.failureListeners.add(listener);
|
|
56
|
+
if (this.failed)
|
|
57
|
+
queueMicrotask(() => listener(this.failed));
|
|
58
|
+
return () => this.failureListeners.delete(listener);
|
|
59
|
+
}
|
|
60
|
+
request(command, fields = {}) {
|
|
61
|
+
if (this.failed)
|
|
62
|
+
return Promise.reject(this.failed);
|
|
63
|
+
const id = `larkin-${++this.nextId}`;
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const startedAt = Date.now();
|
|
66
|
+
const input = isInputCommand(command);
|
|
67
|
+
const pending = {
|
|
68
|
+
command, startedAt,
|
|
69
|
+
...(input ? { absoluteDeadlineAt: startedAt + this.inputMaxTimeoutMs } : {}),
|
|
70
|
+
resolve: resolve, reject,
|
|
71
|
+
};
|
|
72
|
+
this.pending.set(id, pending);
|
|
73
|
+
this.armTimeout(id, pending, input ? this.inputTimeoutMs : this.requestTimeoutMs);
|
|
74
|
+
try {
|
|
75
|
+
if (!this.process.stdin || this.process.stdin.destroyed)
|
|
76
|
+
throw new Error("stdin is unavailable");
|
|
77
|
+
this.process.stdin.write(`${JSON.stringify({ id, type: command, ...fields })}\n`, (error) => {
|
|
78
|
+
if (error)
|
|
79
|
+
this.fail(new Error(`Pi RPC ${command} write failed: ${error.message}`), true);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.fail(new Error(`Pi RPC ${command} write failed: ${error instanceof Error ? error.message : String(error)}`), true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
notify(command, fields = {}) {
|
|
88
|
+
if (this.failed || !this.process.stdin || this.process.stdin.destroyed)
|
|
89
|
+
return;
|
|
90
|
+
this.process.stdin.write(`${JSON.stringify({ type: command, ...fields })}\n`);
|
|
91
|
+
}
|
|
92
|
+
close() {
|
|
93
|
+
if (!this.failed)
|
|
94
|
+
this.fail(new Error("Pi RPC client closed"), true);
|
|
95
|
+
return this.beginShutdown();
|
|
96
|
+
}
|
|
97
|
+
consume(chunk) {
|
|
98
|
+
if (this.failed)
|
|
99
|
+
return;
|
|
100
|
+
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
101
|
+
while (true) {
|
|
102
|
+
const newline = this.stdoutBuffer.indexOf(0x0a);
|
|
103
|
+
if (newline < 0) {
|
|
104
|
+
if (this.stdoutBuffer.length > this.maxFrameBytes)
|
|
105
|
+
this.protocolFailure(`frame exceeds ${this.maxFrameBytes} bytes`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (newline > this.maxFrameBytes) {
|
|
109
|
+
this.protocolFailure(`frame exceeds ${this.maxFrameBytes} bytes`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let frame = this.stdoutBuffer.subarray(0, newline);
|
|
113
|
+
this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1);
|
|
114
|
+
if (frame.at(-1) === 0x0d)
|
|
115
|
+
frame = frame.subarray(0, -1);
|
|
116
|
+
if (!frame.length) {
|
|
117
|
+
this.protocolFailure("empty frame");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let message;
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(frame.toString("utf8"));
|
|
123
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
124
|
+
throw new Error("frame is not an object");
|
|
125
|
+
message = parsed;
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
this.protocolFailure(`malformed JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.dispatch(message);
|
|
132
|
+
if (this.failed)
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
dispatch(message) {
|
|
137
|
+
if (message.type === "response" && typeof message.id === "string") {
|
|
138
|
+
const pending = this.pending.get(message.id);
|
|
139
|
+
if (!pending) {
|
|
140
|
+
this.protocolFailure(`unexpected response id ${message.id}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.pending.delete(message.id);
|
|
144
|
+
if (pending.timer)
|
|
145
|
+
clearTimeout(pending.timer);
|
|
146
|
+
if (message.command !== pending.command) {
|
|
147
|
+
pending.reject(new Error(`Pi RPC protocol error: response command ${String(message.command)} does not match ${pending.command}`));
|
|
148
|
+
this.fail(new Error("Pi RPC protocol error: mismatched response command"), true);
|
|
149
|
+
}
|
|
150
|
+
else if (message.success === true)
|
|
151
|
+
pending.resolve(message.data);
|
|
152
|
+
else
|
|
153
|
+
pending.reject(new Error(`Pi RPC ${pending.command} failed: ${String(message.error || "unknown error")}`));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (isInputProgress(message))
|
|
157
|
+
this.refreshInputDeadlines(message.type);
|
|
158
|
+
for (const listener of this.eventListeners)
|
|
159
|
+
listener(message);
|
|
160
|
+
}
|
|
161
|
+
refreshInputDeadlines(eventType) {
|
|
162
|
+
const graceMs = eventType === "compaction_start" || eventType.startsWith("summarization_retry_")
|
|
163
|
+
|| eventType === "auto_retry_start" ? this.inputProgressTimeoutMs : this.inputTimeoutMs;
|
|
164
|
+
for (const [id, pending] of this.pending) {
|
|
165
|
+
if (!isInputCommand(pending.command))
|
|
166
|
+
continue;
|
|
167
|
+
this.armTimeout(id, pending, graceMs);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
armTimeout(id, pending, requestedMs) {
|
|
171
|
+
if (pending.timer)
|
|
172
|
+
clearTimeout(pending.timer);
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const absoluteRemaining = pending.absoluteDeadlineAt === undefined ? requestedMs : pending.absoluteDeadlineAt - now;
|
|
175
|
+
const delay = Math.max(0, Math.min(requestedMs, absoluteRemaining));
|
|
176
|
+
pending.timer = setTimeout(() => {
|
|
177
|
+
if (!this.pending.has(id))
|
|
178
|
+
return;
|
|
179
|
+
const elapsed = Date.now() - pending.startedAt;
|
|
180
|
+
const absolute = pending.absoluteDeadlineAt !== undefined && Date.now() >= pending.absoluteDeadlineAt;
|
|
181
|
+
const error = isInputCommand(pending.command)
|
|
182
|
+
? new Error(absolute
|
|
183
|
+
? `Pi RPC ${pending.command} preflight timed out at absolute ${this.inputMaxTimeoutMs}ms limit`
|
|
184
|
+
: `Pi RPC ${pending.command} preflight timed out after ${elapsed}ms without progress`)
|
|
185
|
+
: new Error(`Pi RPC ${pending.command} timed out after ${this.requestTimeoutMs}ms`);
|
|
186
|
+
this.fail(error, true);
|
|
187
|
+
}, delay);
|
|
188
|
+
pending.timer.unref?.();
|
|
189
|
+
}
|
|
190
|
+
protocolFailure(detail) {
|
|
191
|
+
this.fail(new Error(`Pi RPC protocol error: ${detail}`), true);
|
|
192
|
+
}
|
|
193
|
+
fail(error, terminate) {
|
|
194
|
+
if (this.failed)
|
|
195
|
+
return;
|
|
196
|
+
this.failed = error;
|
|
197
|
+
for (const pending of this.pending.values()) {
|
|
198
|
+
if (pending.timer)
|
|
199
|
+
clearTimeout(pending.timer);
|
|
200
|
+
pending.reject(error);
|
|
201
|
+
}
|
|
202
|
+
this.pending.clear();
|
|
203
|
+
for (const listener of this.failureListeners)
|
|
204
|
+
listener(error);
|
|
205
|
+
if (terminate)
|
|
206
|
+
void this.beginShutdown();
|
|
207
|
+
}
|
|
208
|
+
beginShutdown() {
|
|
209
|
+
if (this.shutdownPromise)
|
|
210
|
+
return this.shutdownPromise;
|
|
211
|
+
if (this.exited)
|
|
212
|
+
return Promise.resolve();
|
|
213
|
+
this.shutdownPromise = new Promise((resolve) => {
|
|
214
|
+
let settled = false;
|
|
215
|
+
let forceTimer = null;
|
|
216
|
+
let hardTimer = null;
|
|
217
|
+
const finish = () => {
|
|
218
|
+
if (settled)
|
|
219
|
+
return;
|
|
220
|
+
settled = true;
|
|
221
|
+
if (forceTimer)
|
|
222
|
+
clearTimeout(forceTimer);
|
|
223
|
+
if (hardTimer)
|
|
224
|
+
clearTimeout(hardTimer);
|
|
225
|
+
resolve();
|
|
226
|
+
};
|
|
227
|
+
this.process.once("exit", finish);
|
|
228
|
+
this.process.kill("SIGTERM");
|
|
229
|
+
forceTimer = setTimeout(() => {
|
|
230
|
+
if (this.exited) {
|
|
231
|
+
finish();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.process.kill("SIGKILL");
|
|
235
|
+
// A real child emits exit after SIGKILL. Keep a final bound for broken
|
|
236
|
+
// process doubles/platform failures without creating a second shutdown path.
|
|
237
|
+
hardTimer = setTimeout(finish, this.shutdownGraceMs);
|
|
238
|
+
hardTimer.unref?.();
|
|
239
|
+
}, this.shutdownGraceMs);
|
|
240
|
+
forceTimer.unref?.();
|
|
241
|
+
});
|
|
242
|
+
return this.shutdownPromise;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const INPUT_COMMANDS = new Set(["prompt", "steer", "follow_up"]);
|
|
246
|
+
const INPUT_PROGRESS_EVENTS = new Set([
|
|
247
|
+
"compaction_start", "compaction_end", "auto_retry_start", "auto_retry_end",
|
|
248
|
+
"summarization_retry_scheduled", "summarization_retry_attempt_start", "summarization_retry_finished",
|
|
249
|
+
]);
|
|
250
|
+
const isInputCommand = (command) => INPUT_COMMANDS.has(command);
|
|
251
|
+
const isInputProgress = (message) => typeof message.type === "string" && INPUT_PROGRESS_EVENTS.has(message.type);
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { BUNDLED_PI_VERSION } from "./pi-provider-config.mjs";
|
|
6
|
+
/**
|
|
7
|
+
* pi-subagents 扩展注入。
|
|
8
|
+
*
|
|
9
|
+
* 分发:构建期把 @tintinweb/pi-subagents bundle 成单文件
|
|
10
|
+
* `dist/runtime/pi-subagents.bundle.js`(pi-* 包 external),运行时通过
|
|
11
|
+
* `pi --extension/-e` 显式注入 —— builtin(binary-entry pi-rpc)与 external
|
|
12
|
+
* (用户 pi CLI)走同一路径,不碰用户 ~/.pi 配置。
|
|
13
|
+
*
|
|
14
|
+
* 版本门槛:pi-subagents peerDependency 要求 @earendil-works/pi-* >= 0.80.0;
|
|
15
|
+
* external 的 pi 版本低于 0.80 时不注入(降级为无 subagent 能力)。
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* 把 embedded bundle 落盘到 <configDir>/providers/pi/extensions/pi-subagents.bundle.js(0700/0600)。
|
|
19
|
+
* 无 embedded 资产或 configDir 缺失时返回 null。
|
|
20
|
+
*/
|
|
21
|
+
export function materializeEmbeddedPiSubagentBundle(configDir) {
|
|
22
|
+
const embedded = globalThis.__LARKIN_EMBEDDED_PI_SUBAGENTS_BUNDLE__;
|
|
23
|
+
if (!embedded || !configDir)
|
|
24
|
+
return null;
|
|
25
|
+
const dir = path.join(path.resolve(configDir), "providers", "pi", "extensions");
|
|
26
|
+
const target = path.join(dir, "pi-subagents.bundle.js");
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
29
|
+
fs.chmodSync(dir, 0o700);
|
|
30
|
+
if (!fs.existsSync(target) || fs.readFileSync(target, "utf8") !== embedded) {
|
|
31
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
32
|
+
fs.writeFileSync(temporary, embedded, { mode: 0o600, flag: "wx" });
|
|
33
|
+
fs.renameSync(temporary, target);
|
|
34
|
+
fs.chmodSync(target, 0o600);
|
|
35
|
+
}
|
|
36
|
+
return target;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 构建产物单文件路径。优先返回 dist/runtime/pi-subagents.bundle.js(源码/编译形态);
|
|
44
|
+
* standalone 二进制没有该文件,回退到 embedded 资产落盘(<configDir>/providers/pi/extensions/)。
|
|
45
|
+
*/
|
|
46
|
+
export function bundledPiSubagentExtensionPath(configDir) {
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL("./pi-subagents.bundle.js", import.meta.url);
|
|
49
|
+
const resolved = fileURLToPath(url);
|
|
50
|
+
if (fs.existsSync(resolved))
|
|
51
|
+
return resolved;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* fall through to embedded */
|
|
55
|
+
}
|
|
56
|
+
return materializeEmbeddedPiSubagentBundle(configDir);
|
|
57
|
+
}
|
|
58
|
+
/** 解析 `pi --version` 输出中的主版本号,无法解析返回 null。 */
|
|
59
|
+
export function parsePiVersion(output) {
|
|
60
|
+
if (!output)
|
|
61
|
+
return null;
|
|
62
|
+
const match = output.match(/(\d+)\.(\d+)\.\d+/);
|
|
63
|
+
if (!match)
|
|
64
|
+
return null;
|
|
65
|
+
return { major: Number(match[1]), minor: Number(match[2]) };
|
|
66
|
+
}
|
|
67
|
+
/** 是否满足 pi-subagents 的 pi 版本门槛(>= 0.80.0)。 */
|
|
68
|
+
export function piVersionSupportsSubagents(version) {
|
|
69
|
+
if (!version)
|
|
70
|
+
return false;
|
|
71
|
+
return version.major > 0 || (version.major === 0 && version.minor >= 80);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 解析 `pi --version`(external 专用;builtin 用 BUNDLED_PI_VERSION 常量,无需探测)。
|
|
75
|
+
* 返回 null 表示无法确认版本(视为不支持,不注入)。
|
|
76
|
+
*/
|
|
77
|
+
export function probeExternalPiVersion(piCommand, env) {
|
|
78
|
+
const result = spawnSync(piCommand, ["--version"], { env, encoding: "utf8", timeout: 5_000, maxBuffer: 64 * 1024 });
|
|
79
|
+
if (result.error || result.status !== 0)
|
|
80
|
+
return null;
|
|
81
|
+
return parsePiVersion(String(result.stdout || result.stderr || ""));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 注入决策:builtin 恒注入(内嵌 pi 版本固定 BUNDLED_PI_VERSION);external 需探测版本。
|
|
85
|
+
* 返回 `-e` 参数值(扩展 bundle 路径),或 null(不注入:产物缺失或版本不达标)。
|
|
86
|
+
* `probeVersion` 仅供测试注入;缺省时 external 用 probeExternalPiVersion 探测。
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* 用户 pi 是否已自行安装 pi-subagents(settings.json packages 或包目录)。
|
|
90
|
+
* 已装时 Larkin 不再 -e 注入,避免同名工具(Agent/get_subagent_result/steer_subagent)
|
|
91
|
+
* 重复注册导致 pi 扩展加载 FATAL(pi 对 duplicate tool registration 是硬失败)。
|
|
92
|
+
*/
|
|
93
|
+
export function userPiAlreadyHasSubagentsExtension(env) {
|
|
94
|
+
const agentDir = env.PI_CODING_AGENT_DIR || path.join(env.HOME || process.env.HOME || "", ".pi", "agent");
|
|
95
|
+
try {
|
|
96
|
+
const settingsFile = path.join(agentDir, "settings.json");
|
|
97
|
+
if (fs.existsSync(settingsFile)) {
|
|
98
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, "utf8"));
|
|
99
|
+
const packages = settings?.packages;
|
|
100
|
+
if (Array.isArray(packages)) {
|
|
101
|
+
for (const entry of packages) {
|
|
102
|
+
if (typeof entry === "string" && /pi-subagents/i.test(entry))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// settings.json 可能未登记但包目录已存在(残留/手动安装)。
|
|
108
|
+
// Split token: repo contract forbids the literal package-manager word in sources.
|
|
109
|
+
const npmDir = path.join(agentDir, "n" + "pm", "node_modules", "@tintinweb");
|
|
110
|
+
if (fs.existsSync(npmDir) && fs.readdirSync(npmDir).some((name) => /pi-subagents/i.test(name)))
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* unreadable config: assume not installed, injection stays safe */
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
export function resolvePiSubagentExtensionArg(input, probeVersion = () => probeExternalPiVersion(input.piCommand, input.env), resolveBundle = () => bundledPiSubagentExtensionPath(input.env.LARKIN_CONFIG_DIR)) {
|
|
119
|
+
// resolveBundle is injectable so unit tests stay environment-independent
|
|
120
|
+
// (no dependency on build artifacts or the filesystem).
|
|
121
|
+
const bundle = resolveBundle();
|
|
122
|
+
if (!bundle)
|
|
123
|
+
return null;
|
|
124
|
+
// 用户已自行安装同款扩展时跳过注入,避免工具名重复注册冲突。
|
|
125
|
+
if (input.distribution === "external" && userPiAlreadyHasSubagentsExtension(input.env))
|
|
126
|
+
return null;
|
|
127
|
+
const version = input.distribution === "builtin" ? parsePiVersion(BUNDLED_PI_VERSION) : probeVersion();
|
|
128
|
+
return piVersionSupportsSubagents(version) ? bundle : null;
|
|
129
|
+
}
|