larkin 0.3.3 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -33
- package/assets/readme/larkin-hero.svg +111 -0
- package/dist/agent/agent-state-store.cjs +321 -22
- package/dist/agent/agent-state-store.mjs +320 -22
- package/dist/agent/agent-transport.cjs +6 -4
- package/dist/agent/agent-transport.mjs +6 -4
- package/dist/agent/host-reminder-orchestrator.cjs +9 -3
- package/dist/agent/host-reminder-orchestrator.mjs +9 -3
- package/dist/agent/inbox-projection.cjs +114 -23
- package/dist/agent/inbox-projection.mjs +112 -23
- package/dist/app/cli.mjs +3 -1
- package/dist/app/local-control.mjs +108 -2
- package/dist/app/runtime-process.mjs +4 -0
- package/dist/app/session-cli.mjs +55 -20
- package/dist/feishu/document-comment.cjs +2 -1
- package/dist/feishu/document-comment.mjs +2 -1
- package/dist/feishu/host-business-state.cjs +7 -2
- package/dist/feishu/host-business-state.mjs +7 -2
- package/dist/feishu/host-shell.cjs +138 -32
- package/dist/feishu/host-shell.mjs +138 -32
- package/dist/runtime/pi-compaction-recovery.cjs +623 -0
- package/dist/runtime/pi-compaction-recovery.mjs +602 -0
- package/dist/runtime/pi-provider-config.cjs +2 -2
- package/dist/runtime/pi-provider-config.mjs +2 -2
- package/dist/runtime/pi-rpc-client.cjs +39 -22
- package/dist/runtime/pi-rpc-client.mjs +39 -22
- package/dist/runtime/provider-error-classifier.cjs +39 -0
- package/dist/runtime/provider-error-classifier.mjs +34 -0
- package/dist/runtime/runtime-adapters.mjs +82 -19
- package/dist/runtime/runtime-host.cjs +663 -12
- package/dist/runtime/runtime-host.mjs +661 -11
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PiRpcClient = void 0;
|
|
4
|
-
/** A strict
|
|
4
|
+
/** A strict LF-delimited client for Pi's public RPC protocol. */
|
|
5
5
|
class PiRpcClient {
|
|
6
6
|
process;
|
|
7
7
|
pending = new Map();
|
|
@@ -11,9 +11,10 @@ class PiRpcClient {
|
|
|
11
11
|
inputTimeoutMs;
|
|
12
12
|
inputProgressTimeoutMs;
|
|
13
13
|
inputMaxTimeoutMs;
|
|
14
|
-
maxFrameBytes;
|
|
15
14
|
maxStderrBytes;
|
|
16
15
|
shutdownGraceMs;
|
|
16
|
+
compactTimeoutMs;
|
|
17
|
+
expiredRequestIds = new Set();
|
|
17
18
|
stdoutBuffer = Buffer.alloc(0);
|
|
18
19
|
stderrBuffer = Buffer.alloc(0);
|
|
19
20
|
exited = false;
|
|
@@ -29,9 +30,11 @@ class PiRpcClient {
|
|
|
29
30
|
if (this.requestTimeoutMs <= 0 || this.inputTimeoutMs <= 0 || this.inputProgressTimeoutMs <= 0
|
|
30
31
|
|| this.inputMaxTimeoutMs < this.inputTimeoutMs)
|
|
31
32
|
throw new Error("Pi RPC timeout options are invalid");
|
|
32
|
-
this.maxFrameBytes = options.maxFrameBytes ?? 1024 * 1024;
|
|
33
33
|
this.maxStderrBytes = options.maxStderrBytes ?? 64 * 1024;
|
|
34
34
|
this.shutdownGraceMs = options.shutdownGraceMs ?? 2_000;
|
|
35
|
+
this.compactTimeoutMs = options.compactTimeoutMs ?? 120_000;
|
|
36
|
+
if (this.compactTimeoutMs <= 0)
|
|
37
|
+
throw new Error("Pi compact timeout must be positive");
|
|
35
38
|
process.stdout?.on("data", (chunk) => this.consume(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
36
39
|
process.stdout?.on("end", () => {
|
|
37
40
|
if (this.stdoutBuffer.length)
|
|
@@ -61,19 +64,26 @@ class PiRpcClient {
|
|
|
61
64
|
return () => this.failureListeners.delete(listener);
|
|
62
65
|
}
|
|
63
66
|
request(command, fields = {}) {
|
|
67
|
+
const input = isInputCommand(command);
|
|
68
|
+
return this.requestInternal(command, fields, input ? this.inputTimeoutMs : this.requestTimeoutMs, input ? Date.now() + this.inputMaxTimeoutMs : undefined, true);
|
|
69
|
+
}
|
|
70
|
+
/** Send the documented compact RPC with a Larkin-owned absolute deadline. */
|
|
71
|
+
requestCompact(customInstructions) {
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
return this.requestInternal("compact", customInstructions === undefined ? {} : { customInstructions }, this.compactTimeoutMs, startedAt + this.compactTimeoutMs, false);
|
|
74
|
+
}
|
|
75
|
+
requestInternal(command, fields, timeoutMs, absoluteDeadlineAt, failClientOnTimeout) {
|
|
64
76
|
if (this.failed)
|
|
65
77
|
return Promise.reject(this.failed);
|
|
66
78
|
const id = `larkin-${++this.nextId}`;
|
|
67
79
|
return new Promise((resolve, reject) => {
|
|
68
80
|
const startedAt = Date.now();
|
|
69
|
-
const input = isInputCommand(command);
|
|
70
81
|
const pending = {
|
|
71
|
-
command, startedAt,
|
|
72
|
-
|
|
73
|
-
resolve: resolve, reject,
|
|
82
|
+
command, startedAt, ...(absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt } : {}),
|
|
83
|
+
failClientOnTimeout, resolve: resolve, reject,
|
|
74
84
|
};
|
|
75
85
|
this.pending.set(id, pending);
|
|
76
|
-
this.armTimeout(id, pending,
|
|
86
|
+
this.armTimeout(id, pending, timeoutMs);
|
|
77
87
|
try {
|
|
78
88
|
if (!this.process.stdin || this.process.stdin.destroyed)
|
|
79
89
|
throw new Error("stdin is unavailable");
|
|
@@ -103,15 +113,8 @@ class PiRpcClient {
|
|
|
103
113
|
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
104
114
|
while (true) {
|
|
105
115
|
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`);
|
|
116
|
+
if (newline < 0)
|
|
113
117
|
return;
|
|
114
|
-
}
|
|
115
118
|
let frame = this.stdoutBuffer.subarray(0, newline);
|
|
116
119
|
this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1);
|
|
117
120
|
if (frame.at(-1) === 0x0d)
|
|
@@ -140,6 +143,8 @@ class PiRpcClient {
|
|
|
140
143
|
if (message.type === "response" && typeof message.id === "string") {
|
|
141
144
|
const pending = this.pending.get(message.id);
|
|
142
145
|
if (!pending) {
|
|
146
|
+
if (this.expiredRequestIds.delete(message.id))
|
|
147
|
+
return;
|
|
143
148
|
this.protocolFailure(`unexpected response id ${message.id}`);
|
|
144
149
|
return;
|
|
145
150
|
}
|
|
@@ -181,12 +186,24 @@ class PiRpcClient {
|
|
|
181
186
|
return;
|
|
182
187
|
const elapsed = Date.now() - pending.startedAt;
|
|
183
188
|
const absolute = pending.absoluteDeadlineAt !== undefined && Date.now() >= pending.absoluteDeadlineAt;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
this.pending.delete(id);
|
|
190
|
+
this.expiredRequestIds.add(id);
|
|
191
|
+
while (this.expiredRequestIds.size > 256) {
|
|
192
|
+
const oldest = this.expiredRequestIds.values().next().value;
|
|
193
|
+
if (!oldest)
|
|
194
|
+
break;
|
|
195
|
+
this.expiredRequestIds.delete(oldest);
|
|
196
|
+
}
|
|
197
|
+
const error = pending.command === "compact"
|
|
198
|
+
? new Error(`Pi RPC compact timed out at absolute compact deadline ${elapsed}ms`)
|
|
199
|
+
: isInputCommand(pending.command)
|
|
200
|
+
? new Error(absolute
|
|
201
|
+
? `Pi RPC ${pending.command} preflight timed out at absolute ${this.inputMaxTimeoutMs}ms limit`
|
|
202
|
+
: `Pi RPC ${pending.command} preflight timed out after ${elapsed}ms without progress`)
|
|
203
|
+
: new Error(`Pi RPC ${pending.command} timed out after ${this.requestTimeoutMs}ms`);
|
|
204
|
+
pending.reject(error);
|
|
205
|
+
if (pending.failClientOnTimeout !== false)
|
|
206
|
+
this.fail(error, true);
|
|
190
207
|
}, delay);
|
|
191
208
|
pending.timer.unref?.();
|
|
192
209
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** A strict
|
|
1
|
+
/** A strict LF-delimited client for Pi's public RPC protocol. */
|
|
2
2
|
export class PiRpcClient {
|
|
3
3
|
process;
|
|
4
4
|
pending = new Map();
|
|
@@ -8,9 +8,10 @@ export class PiRpcClient {
|
|
|
8
8
|
inputTimeoutMs;
|
|
9
9
|
inputProgressTimeoutMs;
|
|
10
10
|
inputMaxTimeoutMs;
|
|
11
|
-
maxFrameBytes;
|
|
12
11
|
maxStderrBytes;
|
|
13
12
|
shutdownGraceMs;
|
|
13
|
+
compactTimeoutMs;
|
|
14
|
+
expiredRequestIds = new Set();
|
|
14
15
|
stdoutBuffer = Buffer.alloc(0);
|
|
15
16
|
stderrBuffer = Buffer.alloc(0);
|
|
16
17
|
exited = false;
|
|
@@ -26,9 +27,11 @@ export class PiRpcClient {
|
|
|
26
27
|
if (this.requestTimeoutMs <= 0 || this.inputTimeoutMs <= 0 || this.inputProgressTimeoutMs <= 0
|
|
27
28
|
|| this.inputMaxTimeoutMs < this.inputTimeoutMs)
|
|
28
29
|
throw new Error("Pi RPC timeout options are invalid");
|
|
29
|
-
this.maxFrameBytes = options.maxFrameBytes ?? 1024 * 1024;
|
|
30
30
|
this.maxStderrBytes = options.maxStderrBytes ?? 64 * 1024;
|
|
31
31
|
this.shutdownGraceMs = options.shutdownGraceMs ?? 2_000;
|
|
32
|
+
this.compactTimeoutMs = options.compactTimeoutMs ?? 120_000;
|
|
33
|
+
if (this.compactTimeoutMs <= 0)
|
|
34
|
+
throw new Error("Pi compact timeout must be positive");
|
|
32
35
|
process.stdout?.on("data", (chunk) => this.consume(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))));
|
|
33
36
|
process.stdout?.on("end", () => {
|
|
34
37
|
if (this.stdoutBuffer.length)
|
|
@@ -58,19 +61,26 @@ export class PiRpcClient {
|
|
|
58
61
|
return () => this.failureListeners.delete(listener);
|
|
59
62
|
}
|
|
60
63
|
request(command, fields = {}) {
|
|
64
|
+
const input = isInputCommand(command);
|
|
65
|
+
return this.requestInternal(command, fields, input ? this.inputTimeoutMs : this.requestTimeoutMs, input ? Date.now() + this.inputMaxTimeoutMs : undefined, true);
|
|
66
|
+
}
|
|
67
|
+
/** Send the documented compact RPC with a Larkin-owned absolute deadline. */
|
|
68
|
+
requestCompact(customInstructions) {
|
|
69
|
+
const startedAt = Date.now();
|
|
70
|
+
return this.requestInternal("compact", customInstructions === undefined ? {} : { customInstructions }, this.compactTimeoutMs, startedAt + this.compactTimeoutMs, false);
|
|
71
|
+
}
|
|
72
|
+
requestInternal(command, fields, timeoutMs, absoluteDeadlineAt, failClientOnTimeout) {
|
|
61
73
|
if (this.failed)
|
|
62
74
|
return Promise.reject(this.failed);
|
|
63
75
|
const id = `larkin-${++this.nextId}`;
|
|
64
76
|
return new Promise((resolve, reject) => {
|
|
65
77
|
const startedAt = Date.now();
|
|
66
|
-
const input = isInputCommand(command);
|
|
67
78
|
const pending = {
|
|
68
|
-
command, startedAt,
|
|
69
|
-
|
|
70
|
-
resolve: resolve, reject,
|
|
79
|
+
command, startedAt, ...(absoluteDeadlineAt !== undefined ? { absoluteDeadlineAt } : {}),
|
|
80
|
+
failClientOnTimeout, resolve: resolve, reject,
|
|
71
81
|
};
|
|
72
82
|
this.pending.set(id, pending);
|
|
73
|
-
this.armTimeout(id, pending,
|
|
83
|
+
this.armTimeout(id, pending, timeoutMs);
|
|
74
84
|
try {
|
|
75
85
|
if (!this.process.stdin || this.process.stdin.destroyed)
|
|
76
86
|
throw new Error("stdin is unavailable");
|
|
@@ -100,15 +110,8 @@ export class PiRpcClient {
|
|
|
100
110
|
this.stdoutBuffer = Buffer.concat([this.stdoutBuffer, chunk]);
|
|
101
111
|
while (true) {
|
|
102
112
|
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`);
|
|
113
|
+
if (newline < 0)
|
|
110
114
|
return;
|
|
111
|
-
}
|
|
112
115
|
let frame = this.stdoutBuffer.subarray(0, newline);
|
|
113
116
|
this.stdoutBuffer = this.stdoutBuffer.subarray(newline + 1);
|
|
114
117
|
if (frame.at(-1) === 0x0d)
|
|
@@ -137,6 +140,8 @@ export class PiRpcClient {
|
|
|
137
140
|
if (message.type === "response" && typeof message.id === "string") {
|
|
138
141
|
const pending = this.pending.get(message.id);
|
|
139
142
|
if (!pending) {
|
|
143
|
+
if (this.expiredRequestIds.delete(message.id))
|
|
144
|
+
return;
|
|
140
145
|
this.protocolFailure(`unexpected response id ${message.id}`);
|
|
141
146
|
return;
|
|
142
147
|
}
|
|
@@ -178,12 +183,24 @@ export class PiRpcClient {
|
|
|
178
183
|
return;
|
|
179
184
|
const elapsed = Date.now() - pending.startedAt;
|
|
180
185
|
const absolute = pending.absoluteDeadlineAt !== undefined && Date.now() >= pending.absoluteDeadlineAt;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
this.pending.delete(id);
|
|
187
|
+
this.expiredRequestIds.add(id);
|
|
188
|
+
while (this.expiredRequestIds.size > 256) {
|
|
189
|
+
const oldest = this.expiredRequestIds.values().next().value;
|
|
190
|
+
if (!oldest)
|
|
191
|
+
break;
|
|
192
|
+
this.expiredRequestIds.delete(oldest);
|
|
193
|
+
}
|
|
194
|
+
const error = pending.command === "compact"
|
|
195
|
+
? new Error(`Pi RPC compact timed out at absolute compact deadline ${elapsed}ms`)
|
|
196
|
+
: isInputCommand(pending.command)
|
|
197
|
+
? new Error(absolute
|
|
198
|
+
? `Pi RPC ${pending.command} preflight timed out at absolute ${this.inputMaxTimeoutMs}ms limit`
|
|
199
|
+
: `Pi RPC ${pending.command} preflight timed out after ${elapsed}ms without progress`)
|
|
200
|
+
: new Error(`Pi RPC ${pending.command} timed out after ${this.requestTimeoutMs}ms`);
|
|
201
|
+
pending.reject(error);
|
|
202
|
+
if (pending.failClientOnTimeout !== false)
|
|
203
|
+
this.fail(error, true);
|
|
187
204
|
}, delay);
|
|
188
205
|
pending.timer.unref?.();
|
|
189
206
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = exports.CANONICAL_CONTEXT_WINDOW_MESSAGE = exports.CODEX_ERROR_PREFIX = void 0;
|
|
4
|
+
exports.buildStrictProviderErrorInput = buildStrictProviderErrorInput;
|
|
5
|
+
exports.classifyStrictProviderError = classifyStrictProviderError;
|
|
6
|
+
exports.CODEX_ERROR_PREFIX = "Codex error: ";
|
|
7
|
+
exports.CANONICAL_CONTEXT_WINDOW_MESSAGE = "Your input exceeds the context window of this model. Please adjust your input and try again.";
|
|
8
|
+
exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = "provider rejected the input because the context window was exceeded";
|
|
9
|
+
/** Build the one structured provider-error shape shared by ingestion and recovery. */
|
|
10
|
+
function buildStrictProviderErrorInput(source) {
|
|
11
|
+
const upstream = source.upstream && typeof source.upstream === "object" && !Array.isArray(source.upstream)
|
|
12
|
+
? source.upstream : {};
|
|
13
|
+
return {
|
|
14
|
+
message: source.reason !== undefined ? source.reason : source.message,
|
|
15
|
+
code: source.code ?? upstream.code,
|
|
16
|
+
type: source.type ?? upstream.type,
|
|
17
|
+
errorCategory: source.errorCategory,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The recovery path intentionally recognizes only the complete canonical
|
|
22
|
+
* provider message, or the exact internal projection when its category is
|
|
23
|
+
* persisted alongside it. The Codex wrapper is the only normalization
|
|
24
|
+
* permitted for the provider message.
|
|
25
|
+
*/
|
|
26
|
+
function classifyStrictProviderError(error) {
|
|
27
|
+
if (typeof error.message !== "string")
|
|
28
|
+
return undefined;
|
|
29
|
+
if (error.errorCategory !== undefined && error.errorCategory !== "context_window")
|
|
30
|
+
return undefined;
|
|
31
|
+
const message = error.message.startsWith(exports.CODEX_ERROR_PREFIX)
|
|
32
|
+
? error.message.slice(exports.CODEX_ERROR_PREFIX.length) : error.message;
|
|
33
|
+
if (message === exports.CANONICAL_CONTEXT_WINDOW_MESSAGE)
|
|
34
|
+
return "context_window";
|
|
35
|
+
if (error.errorCategory === "context_window" && error.message === exports.INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON) {
|
|
36
|
+
return "context_window";
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const CODEX_ERROR_PREFIX = "Codex error: ";
|
|
2
|
+
export const CANONICAL_CONTEXT_WINDOW_MESSAGE = "Your input exceeds the context window of this model. Please adjust your input and try again.";
|
|
3
|
+
export const INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON = "provider rejected the input because the context window was exceeded";
|
|
4
|
+
/** Build the one structured provider-error shape shared by ingestion and recovery. */
|
|
5
|
+
export function buildStrictProviderErrorInput(source) {
|
|
6
|
+
const upstream = source.upstream && typeof source.upstream === "object" && !Array.isArray(source.upstream)
|
|
7
|
+
? source.upstream : {};
|
|
8
|
+
return {
|
|
9
|
+
message: source.reason !== undefined ? source.reason : source.message,
|
|
10
|
+
code: source.code ?? upstream.code,
|
|
11
|
+
type: source.type ?? upstream.type,
|
|
12
|
+
errorCategory: source.errorCategory,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The recovery path intentionally recognizes only the complete canonical
|
|
17
|
+
* provider message, or the exact internal projection when its category is
|
|
18
|
+
* persisted alongside it. The Codex wrapper is the only normalization
|
|
19
|
+
* permitted for the provider message.
|
|
20
|
+
*/
|
|
21
|
+
export function classifyStrictProviderError(error) {
|
|
22
|
+
if (typeof error.message !== "string")
|
|
23
|
+
return undefined;
|
|
24
|
+
if (error.errorCategory !== undefined && error.errorCategory !== "context_window")
|
|
25
|
+
return undefined;
|
|
26
|
+
const message = error.message.startsWith(CODEX_ERROR_PREFIX)
|
|
27
|
+
? error.message.slice(CODEX_ERROR_PREFIX.length) : error.message;
|
|
28
|
+
if (message === CANONICAL_CONTEXT_WINDOW_MESSAGE)
|
|
29
|
+
return "context_window";
|
|
30
|
+
if (error.errorCategory === "context_window" && error.message === INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON) {
|
|
31
|
+
return "context_window";
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
1
|
+
import { spawn as nodeSpawn, spawnSync } from "node:child_process";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
@@ -10,6 +10,8 @@ import { piAgentDirectory } from "./pi-provider-config.mjs";
|
|
|
10
10
|
import { resolvePiSubagentExtensionArg } from "./pi-subagent-injection.mjs";
|
|
11
11
|
import { resolvePiBashTimeoutExtensionArg } from "./pi-bash-timeout-injection.mjs";
|
|
12
12
|
import { classifyRuntimePrerequisite, probeNativeRuntimeReadiness, providerAuthenticationFailureReadiness, RuntimePrerequisiteError, } from "./runtime-readiness.mjs";
|
|
13
|
+
import { buildStrictProviderErrorInput, classifyStrictProviderError, INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON, } from "./provider-error-classifier.mjs";
|
|
14
|
+
import { assertEffectivePiCompactionSettings, assertNoProjectPiCompactionOverride, prepareOwnedPiDirectory, readOwnedPiSettings, verifyPiCapabilities, } from "./pi-compaction-recovery.mjs";
|
|
13
15
|
class EventSession {
|
|
14
16
|
listeners = new Set();
|
|
15
17
|
subscribe(listener) {
|
|
@@ -447,6 +449,7 @@ class PiSession extends EventSession {
|
|
|
447
449
|
observedSubmitEpochs = new Set();
|
|
448
450
|
observedAcceptedEpochs = new Set();
|
|
449
451
|
observedCompletedEpochs = new Set();
|
|
452
|
+
observedAgentEndEpochs = new Set();
|
|
450
453
|
firstOutputObserved = false;
|
|
451
454
|
toolCallOpen = false;
|
|
452
455
|
constructor(sdk, distribution) {
|
|
@@ -470,6 +473,11 @@ class PiSession extends EventSession {
|
|
|
470
473
|
async prompt(input) { return this.enqueue(input, () => this.sdk.prompt(input.text)); }
|
|
471
474
|
async busyInput(input) { return this.enqueue(input, () => this.sdk.steer(input.text)); }
|
|
472
475
|
async cancel(_reason) { await this.sdk.abort(); }
|
|
476
|
+
async compact(customInstructions) {
|
|
477
|
+
if (!this.sdk.compact)
|
|
478
|
+
throw new Error("Pi compact RPC is unavailable");
|
|
479
|
+
return this.sdk.compact(customInstructions);
|
|
480
|
+
}
|
|
473
481
|
async close(_reason) { this.unsubscribe?.(); await this.sdk.dispose?.(); }
|
|
474
482
|
async enqueue(input, operation) {
|
|
475
483
|
if (this.ownedInputIds.size === 0)
|
|
@@ -481,14 +489,14 @@ class PiSession extends EventSession {
|
|
|
481
489
|
const ownsRpcObservation = !this.observedSubmitEpochs.has(epoch);
|
|
482
490
|
if (ownsRpcObservation) {
|
|
483
491
|
this.observedSubmitEpochs.add(epoch);
|
|
484
|
-
this.
|
|
492
|
+
this.emitObservation("rpc_submit");
|
|
485
493
|
}
|
|
486
494
|
try {
|
|
487
495
|
await operation();
|
|
488
496
|
this.awaitingAcknowledgement.delete(input.inputId);
|
|
489
497
|
if (ownsRpcObservation && !this.observedAcceptedEpochs.has(epoch)) {
|
|
490
498
|
this.observedAcceptedEpochs.add(epoch);
|
|
491
|
-
this.
|
|
499
|
+
this.emitObservation("rpc_accepted");
|
|
492
500
|
}
|
|
493
501
|
return { status: "accepted", inputId: input.inputId };
|
|
494
502
|
}
|
|
@@ -524,19 +532,23 @@ class PiSession extends EventSession {
|
|
|
524
532
|
this.observedSubmitEpochs.clear();
|
|
525
533
|
this.observedAcceptedEpochs.clear();
|
|
526
534
|
this.observedCompletedEpochs.clear();
|
|
535
|
+
this.observedAgentEndEpochs.clear();
|
|
527
536
|
this.activeEpoch = null;
|
|
528
537
|
this.settleArmedEpoch = null;
|
|
529
538
|
this.emit({ type: "error", message });
|
|
530
539
|
}
|
|
531
|
-
else if (event?.type === "compaction_start"
|
|
532
|
-
this.
|
|
540
|
+
else if (event?.type === "compaction_start") {
|
|
541
|
+
this.emitObservation("compaction_start", event.reason === "manual" || event.reason === "threshold" || event.reason === "overflow" ? { reason: event.reason } : {});
|
|
533
542
|
}
|
|
534
|
-
else if (event?.type === "compaction_end"
|
|
535
|
-
this.
|
|
543
|
+
else if (event?.type === "compaction_end") {
|
|
544
|
+
this.emitObservation("compaction_end", {
|
|
545
|
+
...(event.reason === "manual" || event.reason === "threshold" || event.reason === "overflow" ? { reason: event.reason } : {}),
|
|
546
|
+
willRetry: event.willRetry === true, success: event.aborted !== true && event.result !== undefined,
|
|
547
|
+
});
|
|
536
548
|
}
|
|
537
549
|
else if ((event?.type === "auto_retry_start" || event?.type === "auto_retry_end"
|
|
538
550
|
|| String(event?.type || "").startsWith("summarization_retry_")) && this.awaitingAcknowledgement.size > 0) {
|
|
539
|
-
this.
|
|
551
|
+
this.emitObservation("retry_progress");
|
|
540
552
|
}
|
|
541
553
|
else if (event?.type === "turn_start") {
|
|
542
554
|
const epoch = this.oldestOwnedEpoch();
|
|
@@ -546,7 +558,7 @@ class PiSession extends EventSession {
|
|
|
546
558
|
this.settleArmedEpoch = null;
|
|
547
559
|
this.firstOutputObserved = false;
|
|
548
560
|
this.toolCallOpen = false;
|
|
549
|
-
this.
|
|
561
|
+
this.emitObservation("turn_start");
|
|
550
562
|
this.emit({ type: "turn-start", ...(Number.isInteger(event.turnIndex) ? { turnId: `pi-${event.turnIndex}` } : {}) });
|
|
551
563
|
}
|
|
552
564
|
else if (event?.type === "agent_end") {
|
|
@@ -557,9 +569,17 @@ class PiSession extends EventSession {
|
|
|
557
569
|
? piAssistantProviderError(assistant)
|
|
558
570
|
: null;
|
|
559
571
|
this.settleArmedEpoch = this.activeEpoch;
|
|
572
|
+
if (this.activeEpoch !== null && !this.observedAgentEndEpochs.has(this.activeEpoch)) {
|
|
573
|
+
this.observedAgentEndEpochs.add(this.activeEpoch);
|
|
574
|
+
this.emitObservation("agent_end", { willRetry: event.willRetry === true });
|
|
575
|
+
}
|
|
576
|
+
if (event.willRetry === true && this.finalAssistantError?.upstream.message === "Your input exceeds the context window of this model. Please adjust your input and try again.") {
|
|
577
|
+
this.emit({ type: "input-error", inputId: this.oldestOwnedInput(), retryable: true, willRetry: true,
|
|
578
|
+
message: "Pi owns the context-overflow retry", errorCategory: "context_window" });
|
|
579
|
+
}
|
|
560
580
|
if (event.willRetry !== true && this.activeEpoch !== null && !this.observedCompletedEpochs.has(this.activeEpoch)) {
|
|
561
581
|
this.observedCompletedEpochs.add(this.activeEpoch);
|
|
562
|
-
this.
|
|
582
|
+
this.emitObservation("completed");
|
|
563
583
|
}
|
|
564
584
|
}
|
|
565
585
|
else if (event?.type === "agent_settled") {
|
|
@@ -576,7 +596,7 @@ class PiSession extends EventSession {
|
|
|
576
596
|
this.toolCallOpen = false;
|
|
577
597
|
this.emit({ type: "runtime-observation", runtime: "pi", distribution: this.distribution, phase: "tool_result" });
|
|
578
598
|
}
|
|
579
|
-
this.
|
|
599
|
+
this.emitObservation("settled");
|
|
580
600
|
const owned = [...this.ownedInputIds].filter((inputId) => this.inputEpochs.get(inputId) === epoch);
|
|
581
601
|
if (error) {
|
|
582
602
|
const classified = classifyPiProviderError(error);
|
|
@@ -605,6 +625,7 @@ class PiSession extends EventSession {
|
|
|
605
625
|
this.observedSubmitEpochs.delete(epoch);
|
|
606
626
|
this.observedAcceptedEpochs.delete(epoch);
|
|
607
627
|
this.observedCompletedEpochs.delete(epoch);
|
|
628
|
+
this.observedAgentEndEpochs.delete(epoch);
|
|
608
629
|
}
|
|
609
630
|
else if (event?.type === "tool_execution_start") {
|
|
610
631
|
if (!this.firstOutputObserved) {
|
|
@@ -632,6 +653,20 @@ class PiSession extends EventSession {
|
|
|
632
653
|
this.emit({ type: "activity", activity: kind, text: String(event.assistantMessageEvent.delta) });
|
|
633
654
|
}
|
|
634
655
|
}
|
|
656
|
+
emitObservation(phase, fields = {}) {
|
|
657
|
+
const inputId = this.oldestOwnedInput();
|
|
658
|
+
const observation = { type: "runtime-observation", runtime: "pi",
|
|
659
|
+
distribution: this.distribution, phase, ...fields };
|
|
660
|
+
// Correlation is host-internal metadata, not telemetry payload.
|
|
661
|
+
if (this.sessionId)
|
|
662
|
+
Object.defineProperty(observation, "sessionId", { value: this.sessionId, enumerable: false });
|
|
663
|
+
if (inputId)
|
|
664
|
+
Object.defineProperty(observation, "inputId", { value: inputId, enumerable: false });
|
|
665
|
+
this.emit(observation);
|
|
666
|
+
}
|
|
667
|
+
oldestOwnedInput() {
|
|
668
|
+
return this.ownedInputIds.values().next().value;
|
|
669
|
+
}
|
|
635
670
|
oldestOwnedEpoch() {
|
|
636
671
|
for (const inputId of this.ownedInputIds) {
|
|
637
672
|
const epoch = this.inputEpochs.get(inputId);
|
|
@@ -675,6 +710,13 @@ export function classifyPiProviderError(error) {
|
|
|
675
710
|
const errorType = "type" in error ? error.type : undefined;
|
|
676
711
|
const reason = safeProviderText(upstream.message, "Pi provider request failed");
|
|
677
712
|
const signal = new Set([String(upstream.code ?? "").trim().toLowerCase(), String(errorType ?? "").trim().toLowerCase()].filter(Boolean));
|
|
713
|
+
if (classifyStrictProviderError(buildStrictProviderErrorInput({
|
|
714
|
+
message: reason, type: errorType, upstream,
|
|
715
|
+
})) === "context_window")
|
|
716
|
+
return {
|
|
717
|
+
category: "context_window", reason: INTERNAL_CONTEXT_WINDOW_PROJECTION_REASON,
|
|
718
|
+
nextAction: "Start a fresh Runtime session, then replay the retained Inbox deliveries.",
|
|
719
|
+
};
|
|
678
720
|
if (upstream.status === 402 || [...signal].some((value) => ["payment_required", "billing_hard_limit_reached", "insufficient_balance"].includes(value)))
|
|
679
721
|
return {
|
|
680
722
|
category: "billing", reason, nextAction: "Update the provider billing method or balance, then retry.",
|
|
@@ -787,6 +829,8 @@ class PiRpcBackend {
|
|
|
787
829
|
prompt(text) { return this.client.request("prompt", { message: text }); }
|
|
788
830
|
steer(text) { return this.client.request("steer", { message: text }); }
|
|
789
831
|
abort() { return this.client.request("abort"); }
|
|
832
|
+
compact(customInstructions) { return this.client.requestCompact(customInstructions); }
|
|
833
|
+
getState() { return this.client.request("get_state"); }
|
|
790
834
|
dispose() { return this.client.close(); }
|
|
791
835
|
subscribe(listener) {
|
|
792
836
|
const offEvent = this.client.subscribe(listener);
|
|
@@ -809,8 +853,14 @@ export function resolvePiProcessExtensionArgs(input, resolvers = {}) {
|
|
|
809
853
|
args.push("-e", bashTimeout);
|
|
810
854
|
return args;
|
|
811
855
|
}
|
|
812
|
-
async function createPiRpcBackend(input, dependencies, spawn) {
|
|
856
|
+
async function createPiRpcBackend(input, dependencies, spawn, productionSpawn = true) {
|
|
813
857
|
const stateRoot = input.stateDir ?? path.join(input.workspaceDir, ".larkin");
|
|
858
|
+
const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
|
|
859
|
+
assertNoProjectPiCompactionOverride(input.workspaceDir);
|
|
860
|
+
const ownedPiDirectory = mergedEnv.LARKIN_CONFIG_DIR
|
|
861
|
+
? piAgentDirectory(mergedEnv.LARKIN_CONFIG_DIR, input.agentId)
|
|
862
|
+
: path.join(stateRoot, "pi-agent");
|
|
863
|
+
prepareOwnedPiDirectory(ownedPiDirectory);
|
|
814
864
|
const runtimeDir = path.join(stateRoot, "runtime");
|
|
815
865
|
fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
|
816
866
|
const promptFile = path.join(runtimeDir, "pi-standing-prompt.md");
|
|
@@ -824,17 +874,16 @@ async function createPiRpcBackend(input, dependencies, spawn) {
|
|
|
824
874
|
...(session.sessionFile ? ["--session", session.sessionFile] : []),
|
|
825
875
|
...(requestedModel ? ["--model", requestedModel] : []),
|
|
826
876
|
...(requestedEffort ? ["--thinking", requestedEffort] : [])];
|
|
827
|
-
const mergedEnv = { ...globalThis.process.env, ...dependencies.env, ...input.env, NO_COLOR: "1" };
|
|
828
877
|
const builtin = mergedEnv.LARKIN_PI_DISTRIBUTION === "builtin";
|
|
829
878
|
const builtinSpec = builtin ? internalCommandSpec("pi-rpc", [], mergedEnv) : null;
|
|
830
879
|
const command = builtinSpec?.command ?? dependencies.piCommand ?? dependencies.env?.LARKIN_PI_COMMAND ?? process.env.LARKIN_PI_COMMAND ?? "pi";
|
|
831
880
|
const commandArgs = [...(builtinSpec?.args ?? []), ...args];
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
881
|
+
const reportedVersion = productionSpawn && !builtin
|
|
882
|
+
? String(spawnSync(command, ["--version"], { cwd: input.workspaceDir, env: mergedEnv, encoding: "utf8", timeout: 5_000 }).stdout || "").trim()
|
|
883
|
+
: "0.83.0";
|
|
884
|
+
mergedEnv.PI_CODING_AGENT_DIR = ownedPiDirectory;
|
|
885
|
+
if (builtin)
|
|
836
886
|
mergedEnv.PI_TELEMETRY = "0";
|
|
837
|
-
}
|
|
838
887
|
commandArgs.push(...(dependencies.resolvePiProcessExtensionArgs ?? resolvePiProcessExtensionArgs)({
|
|
839
888
|
distribution: builtin ? "builtin" : "external",
|
|
840
889
|
piCommand: command,
|
|
@@ -855,6 +904,20 @@ async function createPiRpcBackend(input, dependencies, spawn) {
|
|
|
855
904
|
if (!available?.models?.length)
|
|
856
905
|
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.");
|
|
857
906
|
const effectiveModel = state.model?.provider && state.model.id ? `${state.model.provider}/${state.model.id}` : null;
|
|
907
|
+
if (productionSpawn) {
|
|
908
|
+
const ownedSettings = readOwnedPiSettings(ownedPiDirectory);
|
|
909
|
+
assertEffectivePiCompactionSettings({ contextWindow: state.model?.contextWindow, compaction: {
|
|
910
|
+
enabled: state.autoCompactionEnabled,
|
|
911
|
+
reserveTokens: ownedSettings.compaction?.reserveTokens,
|
|
912
|
+
keepRecentTokens: ownedSettings.compaction?.keepRecentTokens,
|
|
913
|
+
} });
|
|
914
|
+
const handshake = state.compactionCapabilities;
|
|
915
|
+
verifyPiCapabilities({ distribution: builtin ? "builtin" : "external", version: reportedVersion,
|
|
916
|
+
contextWindow: state.model?.contextWindow, autoCompactionEnabled: state.autoCompactionEnabled,
|
|
917
|
+
reserveTokens: handshake?.reserveTokens, keepRecentTokens: handshake?.keepRecentTokens,
|
|
918
|
+
compactRpc: true, events: handshake?.events,
|
|
919
|
+
trustedProtocol: builtin });
|
|
920
|
+
}
|
|
858
921
|
// 严格对账(Owner 决策):模型必须来自 pi 的权威可用列表,不做后缀猜测——
|
|
859
922
|
// 配置的模型与 pi 可用列表不符即报错,并在错误里列出可用模型。
|
|
860
923
|
const availableEntries = (available.models ?? []).map((entry) => ({
|
|
@@ -988,7 +1051,7 @@ export function createNativeRuntimeAdapter(id, dependencies = {}) {
|
|
|
988
1051
|
? "builtin" : "external";
|
|
989
1052
|
return new PiSession(await (dependencies.createPiSession
|
|
990
1053
|
? dependencies.createPiSession(input)
|
|
991
|
-
: createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn)), distribution);
|
|
1054
|
+
: createPiRpcBackend(input, { ...dependencies, piCommand: resolvedExecutable }, spawn, productionSpawn)), distribution);
|
|
992
1055
|
}
|
|
993
1056
|
if (id === "codex") {
|
|
994
1057
|
const codexInput = dependencies.codexModelOverride?.trim()
|