@peakflo/agent-harness 0.2.9-rc.1 → 0.2.9-rc.3
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 +51 -5
- package/dist/adapter-resolver.d.ts +61 -0
- package/dist/adapter-resolver.js +243 -0
- package/dist/adapters/claude-code-adapter.js +5 -1
- package/dist/cli.js +26 -22
- package/dist/harness-discovery.d.ts +39 -8
- package/dist/harness-discovery.js +117 -32
- package/dist/harness.d.ts +54 -0
- package/dist/harness.js +173 -15
- package/dist/node-floor.d.ts +46 -0
- package/dist/node-floor.js +76 -0
- package/dist/runtime/session-runner.d.ts +27 -1
- package/dist/runtime/session-runner.js +66 -3
- package/dist/store/conversation-store.js +2 -0
- package/dist/store/sqlite-store.d.ts +17 -0
- package/dist/store/sqlite-store.js +26 -0
- package/dist/types.d.ts +14 -0
- package/dist/vendor-backends.d.ts +77 -0
- package/dist/vendor-backends.js +167 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE NODE.JS FLOOR, CHECKED AT START.
|
|
3
|
+
*
|
|
4
|
+
* `package.json` says `engines.node: ">=22.19"`. npm treats that as advice: on
|
|
5
|
+
* an older Node it prints EBADENGINE and installs anyway, and `npx` then starts
|
|
6
|
+
* the runner as if nothing was wrong. So the floor was a line in a manifest
|
|
7
|
+
* that nothing enforced.
|
|
8
|
+
*
|
|
9
|
+
* WHERE THE FLOOR COMES FROM. Not from this package's own code — that needs
|
|
10
|
+
* `node:sqlite` without a flag (Node 22.13). It comes from Pi, the one backend
|
|
11
|
+
* this runner runs turns with: `@earendil-works/pi-coding-agent` declares
|
|
12
|
+
* `engines.node >=22.19.0`, and Pi's launcher is `#!/usr/bin/env node`, so Pi
|
|
13
|
+
* normally runs on the same Node as the runner. Pi 1.1.0 was seen to start on
|
|
14
|
+
* 22.14 (`--version`, `--list-models`), and nobody has shown that a full turn
|
|
15
|
+
* works there. An upstream requirement that is not proven false is kept.
|
|
16
|
+
*
|
|
17
|
+
* WHAT HAPPENS BELOW THE FLOOR. A desktop or self-hosted runner refuses to
|
|
18
|
+
* start, with one line that says what to do. `PF_ALLOW_UNSUPPORTED_NODE=1`
|
|
19
|
+
* starts it anyway, with a one-line warning, for someone who knows their
|
|
20
|
+
* setup works. A SANDBOX only warns: its Node is whatever the image was built
|
|
21
|
+
* with, nobody is at a terminal to read the line, and a refusal there would
|
|
22
|
+
* take a cloud runner out of service for a requirement of a tool the image
|
|
23
|
+
* already carries.
|
|
24
|
+
*
|
|
25
|
+
* PURE: no process state is read here, so every branch can be tested.
|
|
26
|
+
*/
|
|
27
|
+
/** Parse `22.19`, `v22.19.0`, `>=22.19` into numbers. Null when it is none of those. */
|
|
28
|
+
export declare function parseNodeVersion(text: string): number[] | null;
|
|
29
|
+
export declare function isNodeVersionAtLeast(current: string, floor: string): boolean;
|
|
30
|
+
export type NodeFloorVerdict = {
|
|
31
|
+
action: 'start';
|
|
32
|
+
} | {
|
|
33
|
+
action: 'warn';
|
|
34
|
+
message: string;
|
|
35
|
+
} | {
|
|
36
|
+
action: 'refuse';
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
export declare function checkNodeFloor(input: {
|
|
40
|
+
/** `process.versions.node`, e.g. `22.14.0`. */
|
|
41
|
+
current: string;
|
|
42
|
+
/** `engines.node` of this package, e.g. `>=22.19`. Null when unreadable. */
|
|
43
|
+
floor: string | null;
|
|
44
|
+
runtime: string | undefined;
|
|
45
|
+
env: NodeJS.ProcessEnv;
|
|
46
|
+
}): NodeFloorVerdict;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* THE NODE.JS FLOOR, CHECKED AT START.
|
|
4
|
+
*
|
|
5
|
+
* `package.json` says `engines.node: ">=22.19"`. npm treats that as advice: on
|
|
6
|
+
* an older Node it prints EBADENGINE and installs anyway, and `npx` then starts
|
|
7
|
+
* the runner as if nothing was wrong. So the floor was a line in a manifest
|
|
8
|
+
* that nothing enforced.
|
|
9
|
+
*
|
|
10
|
+
* WHERE THE FLOOR COMES FROM. Not from this package's own code — that needs
|
|
11
|
+
* `node:sqlite` without a flag (Node 22.13). It comes from Pi, the one backend
|
|
12
|
+
* this runner runs turns with: `@earendil-works/pi-coding-agent` declares
|
|
13
|
+
* `engines.node >=22.19.0`, and Pi's launcher is `#!/usr/bin/env node`, so Pi
|
|
14
|
+
* normally runs on the same Node as the runner. Pi 1.1.0 was seen to start on
|
|
15
|
+
* 22.14 (`--version`, `--list-models`), and nobody has shown that a full turn
|
|
16
|
+
* works there. An upstream requirement that is not proven false is kept.
|
|
17
|
+
*
|
|
18
|
+
* WHAT HAPPENS BELOW THE FLOOR. A desktop or self-hosted runner refuses to
|
|
19
|
+
* start, with one line that says what to do. `PF_ALLOW_UNSUPPORTED_NODE=1`
|
|
20
|
+
* starts it anyway, with a one-line warning, for someone who knows their
|
|
21
|
+
* setup works. A SANDBOX only warns: its Node is whatever the image was built
|
|
22
|
+
* with, nobody is at a terminal to read the line, and a refusal there would
|
|
23
|
+
* take a cloud runner out of service for a requirement of a tool the image
|
|
24
|
+
* already carries.
|
|
25
|
+
*
|
|
26
|
+
* PURE: no process state is read here, so every branch can be tested.
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.parseNodeVersion = parseNodeVersion;
|
|
30
|
+
exports.isNodeVersionAtLeast = isNodeVersionAtLeast;
|
|
31
|
+
exports.checkNodeFloor = checkNodeFloor;
|
|
32
|
+
/** Parse `22.19`, `v22.19.0`, `>=22.19` into numbers. Null when it is none of those. */
|
|
33
|
+
function parseNodeVersion(text) {
|
|
34
|
+
const match = text.trim().match(/^(?:>=\s*)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
35
|
+
if (!match)
|
|
36
|
+
return null;
|
|
37
|
+
return [match[1], match[2] ?? '0', match[3] ?? '0'].map(Number);
|
|
38
|
+
}
|
|
39
|
+
function isNodeVersionAtLeast(current, floor) {
|
|
40
|
+
const have = parseNodeVersion(current);
|
|
41
|
+
const need = parseNodeVersion(floor);
|
|
42
|
+
// A floor or a version that cannot be read is not a reason to refuse.
|
|
43
|
+
if (!have || !need)
|
|
44
|
+
return true;
|
|
45
|
+
for (let index = 0; index < 3; index++) {
|
|
46
|
+
if (have[index] > need[index])
|
|
47
|
+
return true;
|
|
48
|
+
if (have[index] < need[index])
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function checkNodeFloor(input) {
|
|
54
|
+
if (!input.floor || isNodeVersionAtLeast(input.current, input.floor)) {
|
|
55
|
+
return { action: 'start' };
|
|
56
|
+
}
|
|
57
|
+
const need = (parseNodeVersion(input.floor) ?? []).slice(0, 2).join('.');
|
|
58
|
+
const problem = `pf-agent-harness needs Node.js ${need} or newer; this is Node.js ${input.current}.`;
|
|
59
|
+
const allowed = /^(1|true|yes|on)$/i.test((input.env.PF_ALLOW_UNSUPPORTED_NODE ?? '').trim());
|
|
60
|
+
if (allowed) {
|
|
61
|
+
return {
|
|
62
|
+
action: 'warn',
|
|
63
|
+
message: `${problem} Starting anyway because PF_ALLOW_UNSUPPORTED_NODE is set.`
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (input.runtime === 'sandbox') {
|
|
67
|
+
return {
|
|
68
|
+
action: 'warn',
|
|
69
|
+
message: `${problem} Starting anyway in a sandbox.`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
action: 'refuse',
|
|
74
|
+
message: `${problem} Switch to a newer Node.js (for example \`nvm install 22 && nvm use 22\`) and start again.`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -174,6 +174,32 @@ export declare function qualifiedModelId(model: {
|
|
|
174
174
|
* claiming otherwise would send the reader to the wrong place.
|
|
175
175
|
*/
|
|
176
176
|
export declare function describeProviderError(error: string, qualifiedModel: string | undefined): string;
|
|
177
|
+
/**
|
|
178
|
+
* A VENDOR HARNESS GETS NO GATEWAY AND NO GATEWAY KEY.
|
|
179
|
+
*
|
|
180
|
+
* Codex, Claude Code and Cursor run on the vendor sign-in that is already on
|
|
181
|
+
* the machine, as they do in 20x. The Peakflo model gateway has no part in
|
|
182
|
+
* such a turn, so everything about it is taken off the lease HERE, once,
|
|
183
|
+
* before anything reads it: no catalogue fetch with the key, no `peakflo`
|
|
184
|
+
* provider built, no gateway URL in the adapter's environment, and no key
|
|
185
|
+
* expiry watched.
|
|
186
|
+
*
|
|
187
|
+
* Done in the runner and not only on the control plane, because a control
|
|
188
|
+
* plane older than this change still sends both for every backend. The key
|
|
189
|
+
* must not reach a vendor CLI's process whatever the control plane's age.
|
|
190
|
+
*/
|
|
191
|
+
export declare function withoutGatewayForVendorBackend(lease: LeaseResponse): LeaseResponse;
|
|
192
|
+
/**
|
|
193
|
+
* The model id as THIS backend understands it: `provider/model` for Pi, the
|
|
194
|
+
* vendor's own bare id for a vendor harness (see `vendorModelId`).
|
|
195
|
+
*/
|
|
196
|
+
export declare function backendModelId(agent: {
|
|
197
|
+
backend: string;
|
|
198
|
+
model: {
|
|
199
|
+
provider?: string | null;
|
|
200
|
+
model?: string | null;
|
|
201
|
+
};
|
|
202
|
+
}): string | undefined;
|
|
177
203
|
export declare function modelGatewayRefusal(lease: {
|
|
178
204
|
modelToken?: string | null;
|
|
179
205
|
session: {
|
|
@@ -338,7 +364,7 @@ export declare class SessionRunner {
|
|
|
338
364
|
* control plane at all propagates, since that is the one thing the runner
|
|
339
365
|
* cannot report through the control plane.
|
|
340
366
|
*/
|
|
341
|
-
run(
|
|
367
|
+
run(leased: LeaseResponse, signal: AbortSignal): Promise<RunTurnResult>;
|
|
342
368
|
/**
|
|
343
369
|
* Does this polled part merely mirror the prompt the runner itself sent?
|
|
344
370
|
*
|
|
@@ -23,6 +23,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
23
23
|
exports.SessionRunner = void 0;
|
|
24
24
|
exports.qualifiedModelId = qualifiedModelId;
|
|
25
25
|
exports.describeProviderError = describeProviderError;
|
|
26
|
+
exports.withoutGatewayForVendorBackend = withoutGatewayForVendorBackend;
|
|
27
|
+
exports.backendModelId = backendModelId;
|
|
26
28
|
exports.modelGatewayRefusal = modelGatewayRefusal;
|
|
27
29
|
exports.permissionFingerprint = permissionFingerprint;
|
|
28
30
|
exports.decodeQuestionAnswers = decodeQuestionAnswers;
|
|
@@ -30,6 +32,7 @@ exports.isApprovingOptionId = isApprovingOptionId;
|
|
|
30
32
|
const node_crypto_1 = require("node:crypto");
|
|
31
33
|
const model_gateway_1 = require("../adapters/model-gateway");
|
|
32
34
|
const contract_1 = require("../contract");
|
|
35
|
+
const vendor_backends_1 = require("../vendor-backends");
|
|
33
36
|
const attachments_1 = require("./attachments");
|
|
34
37
|
const cold_start_steps_1 = require("./cold-start-steps");
|
|
35
38
|
const conversation_1 = require("./conversation");
|
|
@@ -156,6 +159,43 @@ function describeProviderError(error, qualifiedModel) {
|
|
|
156
159
|
`to ${provider}, or choose a ${model_gateway_1.ENTERPRISE_AI_GATEWAY_PROVIDER_ID}/ model, ` +
|
|
157
160
|
`which is credentialed for each turn.`);
|
|
158
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* A VENDOR HARNESS GETS NO GATEWAY AND NO GATEWAY KEY.
|
|
164
|
+
*
|
|
165
|
+
* Codex, Claude Code and Cursor run on the vendor sign-in that is already on
|
|
166
|
+
* the machine, as they do in 20x. The Peakflo model gateway has no part in
|
|
167
|
+
* such a turn, so everything about it is taken off the lease HERE, once,
|
|
168
|
+
* before anything reads it: no catalogue fetch with the key, no `peakflo`
|
|
169
|
+
* provider built, no gateway URL in the adapter's environment, and no key
|
|
170
|
+
* expiry watched.
|
|
171
|
+
*
|
|
172
|
+
* Done in the runner and not only on the control plane, because a control
|
|
173
|
+
* plane older than this change still sends both for every backend. The key
|
|
174
|
+
* must not reach a vendor CLI's process whatever the control plane's age.
|
|
175
|
+
*/
|
|
176
|
+
function withoutGatewayForVendorBackend(lease) {
|
|
177
|
+
const { agent } = lease.session;
|
|
178
|
+
if (!(0, vendor_backends_1.isVendorBackend)(agent.backend))
|
|
179
|
+
return lease;
|
|
180
|
+
return {
|
|
181
|
+
...lease,
|
|
182
|
+
modelToken: '',
|
|
183
|
+
modelTokenExpiresAt: undefined,
|
|
184
|
+
session: {
|
|
185
|
+
...lease.session,
|
|
186
|
+
agent: { ...agent, model: { ...agent.model, gatewayUrl: '' } }
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The model id as THIS backend understands it: `provider/model` for Pi, the
|
|
192
|
+
* vendor's own bare id for a vendor harness (see `vendorModelId`).
|
|
193
|
+
*/
|
|
194
|
+
function backendModelId(agent) {
|
|
195
|
+
return (0, vendor_backends_1.isVendorBackend)(agent.backend)
|
|
196
|
+
? (0, vendor_backends_1.vendorModelId)(agent.model, agent.backend)
|
|
197
|
+
: qualifiedModelId(agent.model);
|
|
198
|
+
}
|
|
159
199
|
function modelGatewayRefusal(lease) {
|
|
160
200
|
const gatewayUrl = (lease.session.agent.model.gatewayUrl ?? '').trim();
|
|
161
201
|
if (!gatewayUrl)
|
|
@@ -590,7 +630,8 @@ class SessionRunner {
|
|
|
590
630
|
* control plane at all propagates, since that is the one thing the runner
|
|
591
631
|
* cannot report through the control plane.
|
|
592
632
|
*/
|
|
593
|
-
async run(
|
|
633
|
+
async run(leased, signal) {
|
|
634
|
+
const lease = withoutGatewayForVendorBackend(leased);
|
|
594
635
|
const { session } = lease;
|
|
595
636
|
const logColdStart = (line) => {
|
|
596
637
|
this.logger.log(line);
|
|
@@ -1109,6 +1150,28 @@ class SessionRunner {
|
|
|
1109
1150
|
// THE PROMPT THIS RUNNER JUST SENT. Drain uses it to recognise the
|
|
1110
1151
|
// backend's own echo of it — see `drain`.
|
|
1111
1152
|
sentPromptTexts, () => endFirstOutput(), () => endFirstIngest());
|
|
1153
|
+
/*
|
|
1154
|
+
* THE BACKEND MAY ONLY NOW KNOW ITS OWN SESSION ID.
|
|
1155
|
+
*
|
|
1156
|
+
* Claude Code is given a placeholder by `createSession` and learns the
|
|
1157
|
+
* real session id from the first message of the stream; the adapter
|
|
1158
|
+
* reports it on a part as `realSessionId`, and 20x re-keys the session
|
|
1159
|
+
* when it sees one (`agent-manager.ts`, "REKEYED ... reason=
|
|
1160
|
+
* adapter_provided_real_id"). The checkpoint must be that real id: the
|
|
1161
|
+
* placeholder names no saved session, and resuming it fails with
|
|
1162
|
+
* "session file does not exist" on every later turn.
|
|
1163
|
+
*
|
|
1164
|
+
* Within this turn the adapter answers to both ids, so only what is
|
|
1165
|
+
* SAVED changes. A backend with nothing to say keeps the id it had.
|
|
1166
|
+
*/
|
|
1167
|
+
const realBackendSessionId = adapter.checkpointSessionId?.(backendSessionId);
|
|
1168
|
+
if (realBackendSessionId && realBackendSessionId !== backendSessionId) {
|
|
1169
|
+
backendSessionId = realBackendSessionId;
|
|
1170
|
+
this.deps.conversation?.setCheckpoint((0, conversation_1.conversationKey)(session), backendSessionId, session.agent.backend, {
|
|
1171
|
+
backendOwnerId: ownBackendInstanceId,
|
|
1172
|
+
permissionDecision: permissionFingerprint(session.agent.permissionMode)
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1112
1175
|
partsProduced = drained.parts;
|
|
1113
1176
|
if (signal.aborted) {
|
|
1114
1177
|
stopReason = 'aborted';
|
|
@@ -1157,7 +1220,7 @@ class SessionRunner {
|
|
|
1157
1220
|
{
|
|
1158
1221
|
id: `error-${(0, node_crypto_1.randomUUID)()}`,
|
|
1159
1222
|
role: 'system',
|
|
1160
|
-
content: describeProviderError(error ?? String(drained.providerError),
|
|
1223
|
+
content: describeProviderError(error ?? String(drained.providerError), backendModelId(session.agent)),
|
|
1161
1224
|
partType: 'error'
|
|
1162
1225
|
}
|
|
1163
1226
|
]);
|
|
@@ -2463,7 +2526,7 @@ class SessionRunner {
|
|
|
2463
2526
|
turnId: session.turnId,
|
|
2464
2527
|
workspaceDir,
|
|
2465
2528
|
devServerPolicy: session.agent.devServerPolicy,
|
|
2466
|
-
model:
|
|
2529
|
+
model: backendModelId(session.agent),
|
|
2467
2530
|
systemPrompt: session.agent.systemPrompt ?? undefined,
|
|
2468
2531
|
skills: session.agent.skills.map((skill) => ({
|
|
2469
2532
|
name: skill.name,
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
28
|
exports.MemoryConversationStore = exports.SqliteConversationStore = void 0;
|
|
29
29
|
exports.openConversationStore = openConversationStore;
|
|
30
|
+
const sqlite_store_1 = require("./sqlite-store");
|
|
30
31
|
const SCHEMA = `
|
|
31
32
|
CREATE TABLE IF NOT EXISTS model_messages (
|
|
32
33
|
session_id TEXT NOT NULL,
|
|
@@ -292,6 +293,7 @@ function openConversationStore(path) {
|
|
|
292
293
|
};
|
|
293
294
|
}
|
|
294
295
|
try {
|
|
296
|
+
(0, sqlite_store_1.ensureStoreDirectory)(path);
|
|
295
297
|
const db = new sqlite.DatabaseSync(path);
|
|
296
298
|
return { store: new SqliteConversationStore(db), durable: true };
|
|
297
299
|
}
|
|
@@ -106,6 +106,23 @@ export interface OpenStoreResult {
|
|
|
106
106
|
/** Why durability was unavailable. Reported so a degraded fleet is visible. */
|
|
107
107
|
reason?: string;
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Make the directory a store file goes in, when it is not there yet.
|
|
111
|
+
*
|
|
112
|
+
* sqlite creates the FILE and never the directory: a `--workspace-root` that
|
|
113
|
+
* does not exist yet made every open fail with "unable to open database
|
|
114
|
+
* file", and the runner then ran in memory and reported itself degraded —
|
|
115
|
+
* for a path it was about to create anyway on the first session. Private
|
|
116
|
+
* mode, because the stores hold transcripts and the model's conversation.
|
|
117
|
+
*
|
|
118
|
+
* A directory that ALREADY EXISTS is left exactly as it is, its mode included:
|
|
119
|
+
* `mkdir` with `recursive` does not touch one, and changing the permissions
|
|
120
|
+
* of a directory somebody chose is not this function's call.
|
|
121
|
+
*
|
|
122
|
+
* Throws when the directory cannot be made; the caller turns that into the
|
|
123
|
+
* same visible memory fallback as any other open failure.
|
|
124
|
+
*/
|
|
125
|
+
export declare function ensureStoreDirectory(path: string): void;
|
|
109
126
|
/**
|
|
110
127
|
* Open the durable store, or say plainly why it could not be opened.
|
|
111
128
|
*
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
*/
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
34
|
exports.MemoryTranscriptStore = exports.SqliteTranscriptStore = void 0;
|
|
35
|
+
exports.ensureStoreDirectory = ensureStoreDirectory;
|
|
35
36
|
exports.openTranscriptStore = openTranscriptStore;
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const node_path_1 = require("node:path");
|
|
36
39
|
const SCHEMA = `
|
|
37
40
|
CREATE TABLE IF NOT EXISTS transcript_parts (
|
|
38
41
|
session_id TEXT NOT NULL,
|
|
@@ -309,6 +312,28 @@ class MemoryTranscriptStore {
|
|
|
309
312
|
}
|
|
310
313
|
}
|
|
311
314
|
exports.MemoryTranscriptStore = MemoryTranscriptStore;
|
|
315
|
+
/**
|
|
316
|
+
* Make the directory a store file goes in, when it is not there yet.
|
|
317
|
+
*
|
|
318
|
+
* sqlite creates the FILE and never the directory: a `--workspace-root` that
|
|
319
|
+
* does not exist yet made every open fail with "unable to open database
|
|
320
|
+
* file", and the runner then ran in memory and reported itself degraded —
|
|
321
|
+
* for a path it was about to create anyway on the first session. Private
|
|
322
|
+
* mode, because the stores hold transcripts and the model's conversation.
|
|
323
|
+
*
|
|
324
|
+
* A directory that ALREADY EXISTS is left exactly as it is, its mode included:
|
|
325
|
+
* `mkdir` with `recursive` does not touch one, and changing the permissions
|
|
326
|
+
* of a directory somebody chose is not this function's call.
|
|
327
|
+
*
|
|
328
|
+
* Throws when the directory cannot be made; the caller turns that into the
|
|
329
|
+
* same visible memory fallback as any other open failure.
|
|
330
|
+
*/
|
|
331
|
+
function ensureStoreDirectory(path) {
|
|
332
|
+
// sqlite's in-memory and temporary databases have no directory.
|
|
333
|
+
if (path === ':memory:' || path === '')
|
|
334
|
+
return;
|
|
335
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
|
|
336
|
+
}
|
|
312
337
|
/**
|
|
313
338
|
* Open the durable store, or say plainly why it could not be opened.
|
|
314
339
|
*
|
|
@@ -333,6 +358,7 @@ function openTranscriptStore(path) {
|
|
|
333
358
|
};
|
|
334
359
|
}
|
|
335
360
|
try {
|
|
361
|
+
ensureStoreDirectory(path);
|
|
336
362
|
const db = new sqlite.DatabaseSync(path);
|
|
337
363
|
return { store: new SqliteTranscriptStore(db), durable: true };
|
|
338
364
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -283,8 +283,22 @@ export interface RunnerHarness {
|
|
|
283
283
|
* account name or a token.
|
|
284
284
|
*/
|
|
285
285
|
reason?: string;
|
|
286
|
+
/**
|
|
287
|
+
* The check of this harness DID NOT FINISH (it timed out), so `usable:
|
|
288
|
+
* false` here is not a statement about the harness. The runner checks again
|
|
289
|
+
* shortly. Only ever sent with `installed: true, usable: false`. A reader
|
|
290
|
+
* that does not know this field shows the entry as not usable with its
|
|
291
|
+
* reason, which says the same thing in words.
|
|
292
|
+
*/
|
|
293
|
+
transient?: boolean;
|
|
286
294
|
/** This runner can run a turn with it: it is also listed in `backends`. */
|
|
287
295
|
runnable: boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Why this runner cannot run a harness that is installed and signed in
|
|
298
|
+
* (for example the Claude Agent SDK is missing). A fixed sentence. Only
|
|
299
|
+
* sent with `usable: true, runnable: false`.
|
|
300
|
+
*/
|
|
301
|
+
notRunnableReason?: string;
|
|
288
302
|
models?: RunnerHarnessModel[];
|
|
289
303
|
modelSource?: RunnerHarnessModelSource;
|
|
290
304
|
/** ISO time of the discovery pass that first found this state. */
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE VENDOR HARNESSES THIS RUNNER CAN RUN: Codex, Claude Code, Cursor.
|
|
3
|
+
*
|
|
4
|
+
* "Vendor" because each one talks to its own vendor with the sign-in that is
|
|
5
|
+
* already on the machine — exactly as in 20x, where the same adapters run
|
|
6
|
+
* with no gateway and no key from the app. That is what separates them from
|
|
7
|
+
* Pi, which is credentialed for every turn from the lease:
|
|
8
|
+
*
|
|
9
|
+
* Pi Codex / Claude Code / Cursor
|
|
10
|
+
* model credential gateway key in the lease the machine's own sign-in
|
|
11
|
+
* model id `provider/model` the vendor's own bare id
|
|
12
|
+
* where it runs every runtime desktop and self-hosted only
|
|
13
|
+
*
|
|
14
|
+
* WHICH ADAPTER, AS IN 20x `agent-manager.ts` `getAdapter`:
|
|
15
|
+
* codex -> CodexAppServerAdapter (`codex app-server --stdio`)
|
|
16
|
+
* claude-code -> ClaudeCodeAdapter (`@anthropic-ai/claude-agent-sdk`)
|
|
17
|
+
* cursor -> AcpAdapter('cursor') (`cursor-agent acp`)
|
|
18
|
+
*
|
|
19
|
+
* NEVER IN A SANDBOX. A cloud sandbox has no vendor sign-in and must not be
|
|
20
|
+
* given one: no tenant vendor credential is placed on an untrusted host. It
|
|
21
|
+
* stays Pi-only.
|
|
22
|
+
*/
|
|
23
|
+
export declare const VENDOR_BACKENDS: readonly ["codex", "claude-code", "cursor"];
|
|
24
|
+
export type VendorBackend = (typeof VENDOR_BACKENDS)[number];
|
|
25
|
+
export declare function isVendorBackend(name: string): name is VendorBackend;
|
|
26
|
+
/**
|
|
27
|
+
* The model id a vendor adapter is given, or undefined for "the harness's own
|
|
28
|
+
* default".
|
|
29
|
+
*
|
|
30
|
+
* An agent's model is stored as `{ provider, model }`. For a vendor harness the
|
|
31
|
+
* picker stores the HARNESS NAME as the provider (`codex` / `gpt-6-sol`), so a
|
|
32
|
+
* model that belongs to this harness is recognised by `provider === backend`
|
|
33
|
+
* and its bare id is what the adapter gets.
|
|
34
|
+
*
|
|
35
|
+
* ANY OTHER PROVIDER IS NOT PASSED ON. An agent switched from Pi to Codex can
|
|
36
|
+
* still carry `peakflo/gpt-4.1`; handing `gpt-4.1` to Codex would be a guess
|
|
37
|
+
* that the two vendors mean the same thing by a name. The harness then runs
|
|
38
|
+
* its own default model, which is what its CLI would do.
|
|
39
|
+
*/
|
|
40
|
+
export declare function vendorModelId(model: {
|
|
41
|
+
provider?: string | null;
|
|
42
|
+
model?: string | null;
|
|
43
|
+
}, backend: string): string | undefined;
|
|
44
|
+
export declare const CLAUDE_SDK_PACKAGE = "@anthropic-ai/claude-agent-sdk";
|
|
45
|
+
/**
|
|
46
|
+
* Why a harness that is installed and signed in still cannot be run by this
|
|
47
|
+
* runner. Fixed sentences, like every other reason in the report.
|
|
48
|
+
*/
|
|
49
|
+
export declare const NOT_RUNNABLE_REASONS: {
|
|
50
|
+
readonly claudeSdkMissing: "The Claude Agent SDK is not installed. Run `npm install -g @anthropic-ai/claude-agent-sdk` on this machine.";
|
|
51
|
+
readonly sandbox: "A cloud sandbox runs Pi only.";
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Directories to look for the SDK in, besides this package's own
|
|
55
|
+
* `node_modules`.
|
|
56
|
+
*
|
|
57
|
+
* WHY IT IS NOT SIMPLY A DEPENDENCY. In 20x the SDK ships inside the app. Here
|
|
58
|
+
* that would add its platform package — about 190 MB, a copy of the Claude
|
|
59
|
+
* binary — to EVERY install of the runner, the cloud sandbox included, for a
|
|
60
|
+
* harness most installs never use. The adapter already points the SDK at the
|
|
61
|
+
* `claude` the user installed, so only the SDK's own JavaScript is needed, and
|
|
62
|
+
* a machine that wants Claude Code installs it once, globally.
|
|
63
|
+
*/
|
|
64
|
+
export declare function claudeSdkSearchRoots(env?: NodeJS.ProcessEnv, execPath?: string): string[];
|
|
65
|
+
/**
|
|
66
|
+
* Where the SDK can be loaded from, or null. Synchronous and cheap (a few
|
|
67
|
+
* file reads), so it can be asked on every capability report.
|
|
68
|
+
*
|
|
69
|
+
* `ownTree` is this package's own resolution, replaced in tests — this
|
|
70
|
+
* repository has the SDK as a dev dependency, so "not installed" could not be
|
|
71
|
+
* tested otherwise.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveClaudeSdk(roots?: readonly string[], ownTree?: () => string): string | null;
|
|
74
|
+
/** Import the SDK from wherever `resolveClaudeSdk` finds it. Throws when absent. */
|
|
75
|
+
export declare function importClaudeSdk(): Promise<unknown>;
|
|
76
|
+
/** Whether this runner can build an adapter for a vendor backend right now. */
|
|
77
|
+
export declare function vendorBackendBlocker(backend: string, runtime: string): string | null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* THE VENDOR HARNESSES THIS RUNNER CAN RUN: Codex, Claude Code, Cursor.
|
|
4
|
+
*
|
|
5
|
+
* "Vendor" because each one talks to its own vendor with the sign-in that is
|
|
6
|
+
* already on the machine — exactly as in 20x, where the same adapters run
|
|
7
|
+
* with no gateway and no key from the app. That is what separates them from
|
|
8
|
+
* Pi, which is credentialed for every turn from the lease:
|
|
9
|
+
*
|
|
10
|
+
* Pi Codex / Claude Code / Cursor
|
|
11
|
+
* model credential gateway key in the lease the machine's own sign-in
|
|
12
|
+
* model id `provider/model` the vendor's own bare id
|
|
13
|
+
* where it runs every runtime desktop and self-hosted only
|
|
14
|
+
*
|
|
15
|
+
* WHICH ADAPTER, AS IN 20x `agent-manager.ts` `getAdapter`:
|
|
16
|
+
* codex -> CodexAppServerAdapter (`codex app-server --stdio`)
|
|
17
|
+
* claude-code -> ClaudeCodeAdapter (`@anthropic-ai/claude-agent-sdk`)
|
|
18
|
+
* cursor -> AcpAdapter('cursor') (`cursor-agent acp`)
|
|
19
|
+
*
|
|
20
|
+
* NEVER IN A SANDBOX. A cloud sandbox has no vendor sign-in and must not be
|
|
21
|
+
* given one: no tenant vendor credential is placed on an untrusted host. It
|
|
22
|
+
* stays Pi-only.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.NOT_RUNNABLE_REASONS = exports.CLAUDE_SDK_PACKAGE = exports.VENDOR_BACKENDS = void 0;
|
|
26
|
+
exports.isVendorBackend = isVendorBackend;
|
|
27
|
+
exports.vendorModelId = vendorModelId;
|
|
28
|
+
exports.claudeSdkSearchRoots = claudeSdkSearchRoots;
|
|
29
|
+
exports.resolveClaudeSdk = resolveClaudeSdk;
|
|
30
|
+
exports.importClaudeSdk = importClaudeSdk;
|
|
31
|
+
exports.vendorBackendBlocker = vendorBackendBlocker;
|
|
32
|
+
const node_fs_1 = require("node:fs");
|
|
33
|
+
const node_module_1 = require("node:module");
|
|
34
|
+
const node_path_1 = require("node:path");
|
|
35
|
+
const node_url_1 = require("node:url");
|
|
36
|
+
exports.VENDOR_BACKENDS = ['codex', 'claude-code', 'cursor'];
|
|
37
|
+
function isVendorBackend(name) {
|
|
38
|
+
return exports.VENDOR_BACKENDS.includes(name);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The model id a vendor adapter is given, or undefined for "the harness's own
|
|
42
|
+
* default".
|
|
43
|
+
*
|
|
44
|
+
* An agent's model is stored as `{ provider, model }`. For a vendor harness the
|
|
45
|
+
* picker stores the HARNESS NAME as the provider (`codex` / `gpt-6-sol`), so a
|
|
46
|
+
* model that belongs to this harness is recognised by `provider === backend`
|
|
47
|
+
* and its bare id is what the adapter gets.
|
|
48
|
+
*
|
|
49
|
+
* ANY OTHER PROVIDER IS NOT PASSED ON. An agent switched from Pi to Codex can
|
|
50
|
+
* still carry `peakflo/gpt-4.1`; handing `gpt-4.1` to Codex would be a guess
|
|
51
|
+
* that the two vendors mean the same thing by a name. The harness then runs
|
|
52
|
+
* its own default model, which is what its CLI would do.
|
|
53
|
+
*/
|
|
54
|
+
function vendorModelId(model, backend) {
|
|
55
|
+
const name = (model.model ?? '').trim();
|
|
56
|
+
const provider = (model.provider ?? '').trim();
|
|
57
|
+
return name && provider === backend ? name : undefined;
|
|
58
|
+
}
|
|
59
|
+
/* -------------------------------------------------------------------------- */
|
|
60
|
+
/* The Claude Agent SDK */
|
|
61
|
+
/* -------------------------------------------------------------------------- */
|
|
62
|
+
exports.CLAUDE_SDK_PACKAGE = '@anthropic-ai/claude-agent-sdk';
|
|
63
|
+
/**
|
|
64
|
+
* Why a harness that is installed and signed in still cannot be run by this
|
|
65
|
+
* runner. Fixed sentences, like every other reason in the report.
|
|
66
|
+
*/
|
|
67
|
+
exports.NOT_RUNNABLE_REASONS = {
|
|
68
|
+
claudeSdkMissing: 'The Claude Agent SDK is not installed. Run `npm install -g @anthropic-ai/claude-agent-sdk` on this machine.',
|
|
69
|
+
sandbox: 'A cloud sandbox runs Pi only.'
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Directories to look for the SDK in, besides this package's own
|
|
73
|
+
* `node_modules`.
|
|
74
|
+
*
|
|
75
|
+
* WHY IT IS NOT SIMPLY A DEPENDENCY. In 20x the SDK ships inside the app. Here
|
|
76
|
+
* that would add its platform package — about 190 MB, a copy of the Claude
|
|
77
|
+
* binary — to EVERY install of the runner, the cloud sandbox included, for a
|
|
78
|
+
* harness most installs never use. The adapter already points the SDK at the
|
|
79
|
+
* `claude` the user installed, so only the SDK's own JavaScript is needed, and
|
|
80
|
+
* a machine that wants Claude Code installs it once, globally.
|
|
81
|
+
*/
|
|
82
|
+
function claudeSdkSearchRoots(env = process.env, execPath = process.execPath) {
|
|
83
|
+
const roots = [
|
|
84
|
+
env.PF_CLAUDE_AGENT_SDK_DIR,
|
|
85
|
+
// `npm install -g` beside the running node: <prefix>/lib/node_modules
|
|
86
|
+
(0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.dirname)(execPath)), 'lib', 'node_modules'),
|
|
87
|
+
// Windows: <prefix>\node_modules
|
|
88
|
+
(0, node_path_1.join)((0, node_path_1.dirname)(execPath), 'node_modules'),
|
|
89
|
+
'/opt/homebrew/lib/node_modules',
|
|
90
|
+
'/usr/local/lib/node_modules'
|
|
91
|
+
];
|
|
92
|
+
return [...new Set(roots.filter((root) => Boolean(root)))];
|
|
93
|
+
}
|
|
94
|
+
/** The file a package directory loads as, read from its own package.json. */
|
|
95
|
+
function packageEntry(directory) {
|
|
96
|
+
try {
|
|
97
|
+
const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(directory, 'package.json'), 'utf8'));
|
|
98
|
+
const root = manifest.exports && typeof manifest.exports === 'object'
|
|
99
|
+
? (manifest.exports['.'] ??
|
|
100
|
+
manifest.exports)
|
|
101
|
+
: manifest.exports;
|
|
102
|
+
const fromExports = typeof root === 'string'
|
|
103
|
+
? root
|
|
104
|
+
: root && typeof root === 'object'
|
|
105
|
+
? (root.import ??
|
|
106
|
+
root.default)
|
|
107
|
+
: undefined;
|
|
108
|
+
const entry = [
|
|
109
|
+
fromExports,
|
|
110
|
+
manifest.module,
|
|
111
|
+
manifest.main,
|
|
112
|
+
'index.js'
|
|
113
|
+
].find((candidate) => typeof candidate === 'string');
|
|
114
|
+
const file = entry ? (0, node_path_1.join)(directory, entry) : null;
|
|
115
|
+
return file && (0, node_fs_1.existsSync)(file) ? file : null;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Where the SDK can be loaded from, or null. Synchronous and cheap (a few
|
|
123
|
+
* file reads), so it can be asked on every capability report.
|
|
124
|
+
*
|
|
125
|
+
* `ownTree` is this package's own resolution, replaced in tests — this
|
|
126
|
+
* repository has the SDK as a dev dependency, so "not installed" could not be
|
|
127
|
+
* tested otherwise.
|
|
128
|
+
*/
|
|
129
|
+
function resolveClaudeSdk(roots = claudeSdkSearchRoots(), ownTree = () => (0, node_module_1.createRequire)(__filename).resolve(exports.CLAUDE_SDK_PACKAGE)) {
|
|
130
|
+
try {
|
|
131
|
+
return ownTree();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Not in this package's own tree.
|
|
135
|
+
}
|
|
136
|
+
for (const root of roots) {
|
|
137
|
+
const entry = packageEntry((0, node_path_1.join)(root, exports.CLAUDE_SDK_PACKAGE));
|
|
138
|
+
if (entry)
|
|
139
|
+
return entry;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
/** Import the SDK from wherever `resolveClaudeSdk` finds it. Throws when absent. */
|
|
144
|
+
async function importClaudeSdk() {
|
|
145
|
+
const resolved = resolveClaudeSdk();
|
|
146
|
+
if (!resolved) {
|
|
147
|
+
throw new Error(exports.NOT_RUNNABLE_REASONS.claudeSdkMissing);
|
|
148
|
+
}
|
|
149
|
+
/*
|
|
150
|
+
* `new Function` keeps this a REAL dynamic import in the compiled CommonJS
|
|
151
|
+
* output. Written as `import(x)`, TypeScript turns it into `require(x)`,
|
|
152
|
+
* and the SDK is an ES module that `require` cannot load on every Node.
|
|
153
|
+
*/
|
|
154
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
155
|
+
return dynamicImport((0, node_url_1.pathToFileURL)(resolved).href);
|
|
156
|
+
}
|
|
157
|
+
/** Whether this runner can build an adapter for a vendor backend right now. */
|
|
158
|
+
function vendorBackendBlocker(backend, runtime) {
|
|
159
|
+
if (!isVendorBackend(backend))
|
|
160
|
+
return null;
|
|
161
|
+
if (runtime === 'sandbox')
|
|
162
|
+
return exports.NOT_RUNNABLE_REASONS.sandbox;
|
|
163
|
+
if (backend === 'claude-code' && !resolveClaudeSdk()) {
|
|
164
|
+
return exports.NOT_RUNNABLE_REASONS.claudeSdkMissing;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The version of THIS package, as installed. */
|
|
2
2
|
export declare function getHarnessVersion(): string;
|
|
3
|
+
/** `engines.node` of THIS package (e.g. `>=22.19`), or null when unreadable. */
|
|
4
|
+
export declare function getNodeFloor(): string | null;
|
|
3
5
|
/**
|
|
4
6
|
* The version of the SANDBOX TOOLS layer (gh, @peculiar/x509, ...) currently
|
|
5
7
|
* installed, or null when nothing was ever synced — a self-hosted runner or
|