@cognisphere-sh/cognisphere-harness 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +66 -11
- package/LICENSE +21 -0
- package/dist-web/assets/{file-editor-CQSmlsy_.js → file-editor-CqGYKmT3.js} +1 -1
- package/dist-web/assets/{index-1Hg12QW0.js → index-CDE-jsH8.js} +3 -3
- package/dist-web/index.html +1 -1
- package/home-template/config.example +0 -6
- package/home-template/docs/base-harness/README.md +11 -4
- package/home-template/scripts/build.sh +0 -5
- package/home-template/scripts/lib/remote-bootstrap.sh +2 -6
- package/home-template/scripts/setup-server.sh +7 -39
- package/package.json +7 -5
- package/skills/cognisphere-upgrade/SKILL.md +1 -1
- package/skills/create-plugin/SKILL.md +2 -2
- package/src/agents/base-agent/bootstrap/bootstrap.sh +2 -18
- package/src/agents/base-agent/extensions/context-meta.ts +4 -1
- package/src/agents/base-agent/system_prompts/0-base_prompt.md +3 -0
- package/src/api/agents.ts +3 -13
- package/src/api/credentials.ts +0 -15
- package/src/api/models.ts +2 -8
- package/src/api/secrets.ts +3 -3
- package/src/cli/index.ts +1 -1
- package/src/cli/init.ts +0 -12
- package/src/core/agent-manager.ts +12 -47
- package/src/core/config.ts +6 -0
- package/src/core/main.ts +10 -2
- package/src/core/model-access.ts +55 -0
- package/src/core/models-catalog.ts +72 -40
- package/src/core/models-store.ts +3 -4
- package/src/core/oauth-logins.ts +7 -3
- package/src/core/pi-delivery-receipts.ts +69 -0
- package/src/core/pi-models-sync.ts +2 -2
- package/src/core/queue.ts +7 -25
- package/src/core/rpc.ts +55 -49
- package/src/core/runner.ts +137 -131
- package/src/core/secrets.ts +2 -3
- package/src/core/types.ts +1 -1
- package/src/plugins/artifacts/index.ts +1 -1
- package/src/agents/base-agent/extensions/harness-bridge.ts +0 -63
|
@@ -10,21 +10,21 @@ import type { CredField, ProviderCatalogEntry } from "./types.js";
|
|
|
10
10
|
* can be appended via the UI.
|
|
11
11
|
*
|
|
12
12
|
* OAuth subscription auth: entries with `oauth: true` (Anthropic
|
|
13
|
-
* Claude Pro/Max, OpenAI Codex) support browser sign-in from the Models
|
|
13
|
+
* Claude Pro/Max, Sign in with ChatGPT, OpenAI Codex) support browser sign-in from the Models
|
|
14
14
|
* page in addition to (or instead of) API keys — see `oauth-logins.ts`.
|
|
15
15
|
* GitHub Copilot OAuth exists in pi-ai but has no catalog entry yet.
|
|
16
16
|
* Bedrock / Vertex / Azure / Cloudflare are in scope as multi-cred
|
|
17
17
|
* providers.
|
|
18
18
|
*
|
|
19
19
|
* SOURCE OF TRUTH for provider IDs and env-var names:
|
|
20
|
-
* temp/pi
|
|
21
|
-
* temp/pi
|
|
20
|
+
* temp/pi/packages/ai/src/types.ts (KnownProvider union)
|
|
21
|
+
* temp/pi/packages/ai/src/env-api-keys.ts (envMap)
|
|
22
22
|
* Keep this file in sync on each pi-coding-agent upgrade — the
|
|
23
23
|
* `KnownProvider` union below is a copy of pi-ai's; entries declared
|
|
24
24
|
* here are typechecked against it.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
// Mirrored from pi-ai's `KnownProvider` (pi
|
|
27
|
+
// Mirrored from pi-ai's `KnownProvider` (temp/pi/packages/ai/src/types.ts).
|
|
28
28
|
// Only ids declared in the catalog below are typechecked against it, so a
|
|
29
29
|
// provider added upstream stays invisible here until it gets an entry.
|
|
30
30
|
export type KnownProvider =
|
|
@@ -37,6 +37,7 @@ export type KnownProvider =
|
|
|
37
37
|
| "azure-openai-responses"
|
|
38
38
|
| "openai-codex"
|
|
39
39
|
| "radius"
|
|
40
|
+
| "typesafe"
|
|
40
41
|
| "nvidia"
|
|
41
42
|
| "deepseek"
|
|
42
43
|
| "github-copilot"
|
|
@@ -59,6 +60,7 @@ export type KnownProvider =
|
|
|
59
60
|
| "opencode"
|
|
60
61
|
| "opencode-go"
|
|
61
62
|
| "kimi-coding"
|
|
63
|
+
| "meta"
|
|
62
64
|
| "cloudflare-workers-ai"
|
|
63
65
|
| "cloudflare-ai-gateway"
|
|
64
66
|
| "qwen-token-plan"
|
|
@@ -97,46 +99,52 @@ export const PROVIDER_CATALOG: CatalogEntry[] = [
|
|
|
97
99
|
notes:
|
|
98
100
|
"API key or Claude Pro/Max subscription OAuth. When both are set, pi prefers the OAuth credential.",
|
|
99
101
|
models: [
|
|
102
|
+
"claude-opus-5-5",
|
|
103
|
+
"claude-sonnet-5-5",
|
|
104
|
+
"claude-fable-5-1",
|
|
105
|
+
"claude-opus-5",
|
|
106
|
+
"claude-sonnet-5",
|
|
107
|
+
"claude-opus-4-8",
|
|
100
108
|
"claude-opus-4-7",
|
|
101
|
-
"claude-opus-4-6",
|
|
102
|
-
"claude-opus-4-5",
|
|
103
109
|
"claude-sonnet-4-6",
|
|
104
|
-
"claude-sonnet-4-5",
|
|
105
110
|
"claude-haiku-4-5",
|
|
106
|
-
"claude-3-7-sonnet-20250219",
|
|
107
|
-
"claude-3-5-sonnet-20241022",
|
|
108
|
-
"claude-3-5-haiku-20241022",
|
|
109
111
|
],
|
|
110
112
|
},
|
|
111
113
|
{
|
|
112
114
|
id: "openai",
|
|
113
115
|
displayName: "OpenAI",
|
|
114
116
|
credentials: apiKeyOnly("OPENAI_API_KEY"),
|
|
117
|
+
oauth: true,
|
|
118
|
+
notes:
|
|
119
|
+
"API key or Sign in with ChatGPT (uses a ChatGPT subscription with the OpenAI API).",
|
|
115
120
|
models: [
|
|
116
|
-
"gpt-
|
|
117
|
-
"gpt-
|
|
121
|
+
"gpt-6.1-sol",
|
|
122
|
+
"gpt-6-sol",
|
|
123
|
+
"gpt-6-luna",
|
|
124
|
+
"gpt-6-astra",
|
|
125
|
+
"gpt-5.5",
|
|
126
|
+
"gpt-5.4-mini",
|
|
118
127
|
"gpt-4.1",
|
|
119
|
-
"gpt-4o",
|
|
120
|
-
"gpt-4o-mini",
|
|
121
|
-
"o4-mini",
|
|
122
128
|
"o3",
|
|
123
|
-
"o3-mini",
|
|
124
129
|
],
|
|
125
130
|
},
|
|
126
131
|
{
|
|
127
132
|
id: "openai-codex",
|
|
128
|
-
displayName: "OpenAI Codex",
|
|
133
|
+
displayName: "OpenAI Codex (legacy)",
|
|
129
134
|
credentials: [],
|
|
130
135
|
oauth: true,
|
|
131
136
|
notes:
|
|
132
|
-
"ChatGPT Plus/Pro subscription via OAuth — no API key. Sign in
|
|
137
|
+
"ChatGPT Plus/Pro subscription via OAuth — no API key. Superseded by Sign in with ChatGPT on OpenAI.",
|
|
133
138
|
models: [
|
|
139
|
+
"gpt-6.1-sol",
|
|
140
|
+
"gpt-6-sol",
|
|
141
|
+
"gpt-6-luna",
|
|
142
|
+
"gpt-6-astra",
|
|
143
|
+
"gpt-5.6-sol",
|
|
144
|
+
"gpt-5.6-luna",
|
|
145
|
+
"gpt-5.6-terra",
|
|
134
146
|
"gpt-5.5",
|
|
135
|
-
"gpt-5.4",
|
|
136
|
-
"gpt-5.4-mini",
|
|
137
|
-
"gpt-5.3-codex",
|
|
138
147
|
"gpt-5.3-codex-spark",
|
|
139
|
-
"gpt-5.2",
|
|
140
148
|
],
|
|
141
149
|
},
|
|
142
150
|
{
|
|
@@ -144,17 +152,21 @@ export const PROVIDER_CATALOG: CatalogEntry[] = [
|
|
|
144
152
|
displayName: "Google Gemini",
|
|
145
153
|
credentials: apiKeyOnly("GEMINI_API_KEY"),
|
|
146
154
|
models: [
|
|
155
|
+
"gemini-3.8-flash",
|
|
156
|
+
"gemini-3.1-pro-preview",
|
|
157
|
+
"gemini-3.5-flash-lite",
|
|
147
158
|
"gemini-2.5-pro",
|
|
148
159
|
"gemini-2.5-flash",
|
|
149
|
-
"gemini-2.5-flash-lite",
|
|
150
|
-
"gemini-2.0-flash",
|
|
151
160
|
],
|
|
152
161
|
},
|
|
153
162
|
{
|
|
154
163
|
id: "deepseek",
|
|
155
164
|
displayName: "DeepSeek",
|
|
156
165
|
credentials: apiKeyOnly("DEEPSEEK_API_KEY"),
|
|
157
|
-
models: [
|
|
166
|
+
models: [
|
|
167
|
+
"deepseek-v4-pro",
|
|
168
|
+
"deepseek-flash",
|
|
169
|
+
],
|
|
158
170
|
},
|
|
159
171
|
{
|
|
160
172
|
id: "mistral",
|
|
@@ -174,30 +186,39 @@ export const PROVIDER_CATALOG: CatalogEntry[] = [
|
|
|
174
186
|
models: [
|
|
175
187
|
"llama-3.3-70b-versatile",
|
|
176
188
|
"llama-3.1-8b-instant",
|
|
177
|
-
"
|
|
189
|
+
"openai/gpt-oss-120b",
|
|
190
|
+
"qwen/qwen3.8-27b",
|
|
178
191
|
],
|
|
179
192
|
},
|
|
180
193
|
{
|
|
181
194
|
id: "cerebras",
|
|
182
195
|
displayName: "Cerebras",
|
|
183
196
|
credentials: apiKeyOnly("CEREBRAS_API_KEY"),
|
|
184
|
-
models: [
|
|
197
|
+
models: [
|
|
198
|
+
"gpt-oss-120b",
|
|
199
|
+
"qwen-3.8-27b",
|
|
200
|
+
],
|
|
185
201
|
},
|
|
186
202
|
{
|
|
187
203
|
id: "xai",
|
|
188
204
|
displayName: "xAI",
|
|
189
205
|
credentials: apiKeyOnly("XAI_API_KEY"),
|
|
190
|
-
models: [
|
|
206
|
+
models: [
|
|
207
|
+
"grok-4.7",
|
|
208
|
+
"grok-4.6",
|
|
209
|
+
"grok-4.5",
|
|
210
|
+
],
|
|
191
211
|
},
|
|
192
212
|
{
|
|
193
213
|
id: "openrouter",
|
|
194
214
|
displayName: "OpenRouter",
|
|
195
215
|
credentials: apiKeyOnly("OPENROUTER_API_KEY"),
|
|
196
216
|
models: [
|
|
197
|
-
"anthropic/claude-sonnet-
|
|
198
|
-
"
|
|
199
|
-
"
|
|
200
|
-
"
|
|
217
|
+
"anthropic/claude-sonnet-5.5",
|
|
218
|
+
"anthropic/claude-opus-5.5",
|
|
219
|
+
"openai/gpt-6-astra",
|
|
220
|
+
"google/gemini-3.8-flash",
|
|
221
|
+
"deepseek/deepseek-v4-pro",
|
|
201
222
|
],
|
|
202
223
|
},
|
|
203
224
|
{
|
|
@@ -205,37 +226,48 @@ export const PROVIDER_CATALOG: CatalogEntry[] = [
|
|
|
205
226
|
displayName: "Vercel AI Gateway",
|
|
206
227
|
credentials: apiKeyOnly("AI_GATEWAY_API_KEY"),
|
|
207
228
|
models: [
|
|
208
|
-
"anthropic/claude-sonnet-
|
|
209
|
-
"openai/gpt-
|
|
210
|
-
"google/gemini-
|
|
229
|
+
"anthropic/claude-sonnet-5.5",
|
|
230
|
+
"openai/gpt-6-sol",
|
|
231
|
+
"google/gemini-3.8-flash",
|
|
211
232
|
],
|
|
212
233
|
},
|
|
213
234
|
{
|
|
214
235
|
id: "zai",
|
|
215
236
|
displayName: "ZAI",
|
|
216
237
|
credentials: apiKeyOnly("ZAI_API_KEY"),
|
|
217
|
-
models: [
|
|
238
|
+
models: [
|
|
239
|
+
"glm-5.3",
|
|
240
|
+
"glm-5.3-flash",
|
|
241
|
+
"glm-5.2",
|
|
242
|
+
],
|
|
218
243
|
},
|
|
219
244
|
{
|
|
220
245
|
id: "fireworks",
|
|
221
246
|
displayName: "Fireworks",
|
|
222
247
|
credentials: apiKeyOnly("FIREWORKS_API_KEY"),
|
|
223
248
|
models: [
|
|
224
|
-
"accounts/fireworks/models/
|
|
225
|
-
"accounts/fireworks/models/
|
|
249
|
+
"accounts/fireworks/models/kimi-k3",
|
|
250
|
+
"accounts/fireworks/models/glm-5p3",
|
|
251
|
+
"accounts/fireworks/models/deepseek-v4p1-flash",
|
|
226
252
|
],
|
|
227
253
|
},
|
|
228
254
|
{
|
|
229
255
|
id: "kimi-coding",
|
|
230
256
|
displayName: "Kimi For Coding",
|
|
231
257
|
credentials: apiKeyOnly("KIMI_API_KEY"),
|
|
232
|
-
models: [
|
|
258
|
+
models: [
|
|
259
|
+
"kimi-for-coding",
|
|
260
|
+
"k3",
|
|
261
|
+
],
|
|
233
262
|
},
|
|
234
263
|
{
|
|
235
264
|
id: "minimax",
|
|
236
265
|
displayName: "MiniMax",
|
|
237
266
|
credentials: apiKeyOnly("MINIMAX_API_KEY"),
|
|
238
|
-
models: [
|
|
267
|
+
models: [
|
|
268
|
+
"MiniMax-M3",
|
|
269
|
+
"MiniMax-M2.7",
|
|
270
|
+
],
|
|
239
271
|
},
|
|
240
272
|
{
|
|
241
273
|
id: "minimax-cn",
|
package/src/core/models-store.ts
CHANGED
|
@@ -12,10 +12,9 @@
|
|
|
12
12
|
* }
|
|
13
13
|
* }
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* writes are
|
|
17
|
-
*
|
|
18
|
-
* the secrets store's restart-required model.
|
|
15
|
+
* File-based, plaintext. Read on every `load()` so PUT
|
|
16
|
+
* writes are visible to subsequent agent starts. The settings API requests
|
|
17
|
+
* a soft reload for affected running agents to refresh their environment.
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
20
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
package/src/core/oauth-logins.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Reuses pi-coding-agent's `ModelRuntime` — the exact machinery behind
|
|
6
6
|
* pi's `/login` command. Tokens persist to pi's own
|
|
7
|
-
* `<agentDir>/auth.json` (
|
|
7
|
+
* `<agentDir>/auth.json` (this harness's `.secrets/pi/auth.json`, 0600,
|
|
8
8
|
* file-locked), NOT to the harness's models.json. Rationale:
|
|
9
9
|
* - refresh tokens rotate on every refresh; spawned pi children
|
|
10
10
|
* already refresh + persist under a file lock, so the harness must
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* login per provider — the callback server binds a fixed port.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
import { ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
33
33
|
import type { AuthPrompt } from "@earendil-works/pi-ai";
|
|
34
34
|
import type { Logger } from "./logger.js";
|
|
35
35
|
|
|
@@ -96,7 +96,7 @@ export class OAuthLoginManager {
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Shared pi model runtime, created on first use. Owns login/logout
|
|
99
|
-
* orchestration; credentials go to
|
|
99
|
+
* orchestration; credentials go to the harness's pi auth.json.
|
|
100
100
|
*/
|
|
101
101
|
private runtime(): Promise<ModelRuntime> {
|
|
102
102
|
this.runtimePromise ??= ModelRuntime.create();
|
|
@@ -172,6 +172,10 @@ export class OAuthLoginManager {
|
|
|
172
172
|
}
|
|
173
173
|
},
|
|
174
174
|
prompt: (prompt) => this.handlePrompt(entry, prompt, surfaced),
|
|
175
|
+
}, {
|
|
176
|
+
// Sign in with ChatGPT identifies the installation by a stable UUID.
|
|
177
|
+
// Use pi's own (global settings `deviceId`), as pi's `/login` does.
|
|
178
|
+
getDeviceId: () => SettingsManager.create(process.cwd()).getOrCreateDeviceId(),
|
|
175
179
|
})
|
|
176
180
|
.then(() => {
|
|
177
181
|
this.results.set(providerId, { state: "success" });
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delivery-receipts — a pi extension the runner loads into every spawned
|
|
3
|
+
* `pi --mode rpc` child from the package (see `runner.ts:spawnPi`), never
|
|
4
|
+
* from the agent folder, so an agent can't lose or edit it.
|
|
5
|
+
*
|
|
6
|
+
* Every input the runner sends carries `EventId: <row id>` in its
|
|
7
|
+
* `<harness-metadata>` block. For each user message pi saves, this reports
|
|
8
|
+
* `{ entryId, eventIds }`: the saved session entry and the queue rows it
|
|
9
|
+
* contains. That one receipt answers both "did the row reach pi?" and "which
|
|
10
|
+
* saved message is it?", so the two can't disagree.
|
|
11
|
+
*
|
|
12
|
+
* Transport: `pi.appendEntry(RECEIPT_TYPE, { entryId, eventIds })`. Pi saves
|
|
13
|
+
* it as a custom session entry (kept out of the model's context) and reports
|
|
14
|
+
* it on stdout as an `entry_appended` event, which `PiRpcClient` routes to the
|
|
15
|
+
* runner. Saved receipts also let recovery rebuild row-to-entry links from
|
|
16
|
+
* the session file.
|
|
17
|
+
*
|
|
18
|
+
* Pi emits `message_end` before it appends the message to the session, so the
|
|
19
|
+
* entry id doesn't exist yet there. Instead we re-scan the entries on the
|
|
20
|
+
* events that follow and report each entry once. Entries already in the file
|
|
21
|
+
* when pi starts are history from earlier batches (their receipts were sent
|
|
22
|
+
* then, or the session predates saved receipts), so they're never reported
|
|
23
|
+
* again — otherwise every spawn would append duplicate receipts.
|
|
24
|
+
*/
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
|
|
27
|
+
export const RECEIPT_TYPE = "cognisphere.delivery";
|
|
28
|
+
|
|
29
|
+
/** Row ids named by `EventId:` lines inside `<harness-metadata>` blocks. */
|
|
30
|
+
export function eventIdsIn(text: string): number[] {
|
|
31
|
+
const ids: number[] = [];
|
|
32
|
+
for (const block of text.matchAll(/<harness-metadata>([\s\S]*?)<\/harness-metadata>/g)) {
|
|
33
|
+
const m = /^EventId: (\d+)$/m.exec(block[1]!);
|
|
34
|
+
if (m) ids.push(Number(m[1]));
|
|
35
|
+
}
|
|
36
|
+
return ids;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function messageText(entry: SessionEntry): string | null {
|
|
40
|
+
if (entry.type !== "message" || entry.message.role !== "user") return null;
|
|
41
|
+
const { content } = entry.message;
|
|
42
|
+
if (typeof content === "string") return content;
|
|
43
|
+
return content.map((c) => (c.type === "text" ? c.text : "")).join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function deliveryReceipts(pi: ExtensionAPI): void {
|
|
47
|
+
const reported = new Set<string>();
|
|
48
|
+
|
|
49
|
+
pi.on("session_start", (_event, ctx) => {
|
|
50
|
+
for (const entry of ctx.sessionManager.getEntries()) reported.add(entry.id);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const sweep = (_event: unknown, ctx: ExtensionContext): void => {
|
|
54
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
55
|
+
if (reported.has(entry.id)) continue;
|
|
56
|
+
const text = messageText(entry);
|
|
57
|
+
if (text === null) continue;
|
|
58
|
+
reported.add(entry.id);
|
|
59
|
+
const eventIds = eventIdsIn(text);
|
|
60
|
+
if (eventIds.length === 0) continue;
|
|
61
|
+
pi.appendEntry(RECEIPT_TYPE, { entryId: entry.id, eventIds });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
pi.on("message_start", sweep);
|
|
66
|
+
pi.on("turn_start", sweep);
|
|
67
|
+
pi.on("turn_end", sweep);
|
|
68
|
+
pi.on("agent_end", sweep);
|
|
69
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mirror `.secrets/models.json` `modelOverrides` into pi's own models.json
|
|
3
|
-
* (`<getAgentDir()>/models.json
|
|
4
|
-
* `
|
|
3
|
+
* (`<getAgentDir()>/models.json`; the harness points `PI_CODING_AGENT_DIR`
|
|
4
|
+
* at its own `.secrets/pi/`, see `main.ts`). Pi children re-read that file on
|
|
5
5
|
* every spawn, so overrides configured in the harness Models settings take
|
|
6
6
|
* effect natively inside pi — model registry, ContextUsage reporting, and
|
|
7
7
|
* pi's own compaction thresholds all see the overridden contextWindow /
|
package/src/core/queue.ts
CHANGED
|
@@ -68,8 +68,8 @@ export interface EventRow {
|
|
|
68
68
|
/**
|
|
69
69
|
* Derive a re-dispatch {@link RetryMode} from a row's persisted state. No
|
|
70
70
|
* dedicated column: a row that was delivered to the model has its
|
|
71
|
-
* `pi_entry_id` written in real time (by the runner, from
|
|
72
|
-
*
|
|
71
|
+
* `pi_entry_id` written in real time (by the runner, from a delivery receipt),
|
|
72
|
+
* so its presence on a retried row means "already in pi's
|
|
73
73
|
* history → continue, don't resend".
|
|
74
74
|
*/
|
|
75
75
|
function deriveRetryMode(row: EventRow): RetryMode | undefined {
|
|
@@ -278,17 +278,13 @@ export class AgentDb {
|
|
|
278
278
|
|
|
279
279
|
/**
|
|
280
280
|
* Bind a row to its position in pi's session JSONL. Called by the runner in
|
|
281
|
-
* real time as the
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* a prior attempt; the entry id is written outright (the latest delivery
|
|
285
|
-
* wins).
|
|
281
|
+
* real time as delivery receipts name the row (so the link exists even for
|
|
282
|
+
* rows whose batch later fails, and without waiting for pi to exit).
|
|
283
|
+
* COALESCE on the session id preserves a value from a prior attempt.
|
|
286
284
|
*/
|
|
287
285
|
setRowEntryId(rowId: number, sessionId: string, entryId: string): void {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// being bound twice (e.g. a stale absolute-index report or a logic error)
|
|
291
|
-
// — once a row carries an entry id it stays put.
|
|
286
|
+
// Never rebind: receipts re-report history on every batch, so the first
|
|
287
|
+
// (earliest) saved copy of a row keeps the link.
|
|
292
288
|
this.db
|
|
293
289
|
.prepare(
|
|
294
290
|
`UPDATE events
|
|
@@ -300,20 +296,6 @@ export class AgentDb {
|
|
|
300
296
|
.run(sessionId, entryId, Date.now(), rowId);
|
|
301
297
|
}
|
|
302
298
|
|
|
303
|
-
/** All distinct `pi_entry_id`s already bound to rows on this thread. The
|
|
304
|
-
* runner pre-seeds its per-batch seen-set with these so the harness-bridge's
|
|
305
|
-
* re-report of historical entries (it sweeps the whole reused session JSONL)
|
|
306
|
-
* is ignored — only entryIds new to this thread bind to this batch's rows. */
|
|
307
|
-
entryIdsForThread(threadId: string): string[] {
|
|
308
|
-
const rows = this.db
|
|
309
|
-
.prepare<unknown[], { pi_entry_id: string }>(
|
|
310
|
-
`SELECT DISTINCT pi_entry_id FROM events
|
|
311
|
-
WHERE thread_id = ? AND pi_entry_id IS NOT NULL`,
|
|
312
|
-
)
|
|
313
|
-
.all(threadId);
|
|
314
|
-
return rows.map((r) => r.pi_entry_id);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
299
|
/**
|
|
318
300
|
* Mark every id done. `sessionId` (when non-null) is written to all rows.
|
|
319
301
|
* Per-row `pi_entry_id`s are no longer threaded through here — they are
|
package/src/core/rpc.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChildProcess } from "node:child_process";
|
|
2
2
|
import { StringDecoder } from "node:string_decoder";
|
|
3
3
|
import type { Logger } from "./logger.js";
|
|
4
|
+
import { RECEIPT_TYPE } from "./pi-delivery-receipts.js";
|
|
4
5
|
|
|
5
6
|
interface RpcResponse {
|
|
6
7
|
type: "response";
|
|
@@ -16,14 +17,14 @@ interface ExtensionUiRequest {
|
|
|
16
17
|
id: string;
|
|
17
18
|
method: string;
|
|
18
19
|
title?: string;
|
|
19
|
-
/** Present on fire-and-forget `setStatus` requests (the harness-bridge
|
|
20
|
-
* extension uses these to report session entry ids — see {@link onHarnessEntry}). */
|
|
21
|
-
statusKey?: string;
|
|
22
|
-
statusText?: string;
|
|
23
20
|
}
|
|
24
21
|
|
|
22
|
+
/** What pi did with a `prompt`/`steer` frame. `handled` = an extension
|
|
23
|
+
* `input` handler consumed it: no session entry, so no receipt will come. */
|
|
24
|
+
export type InputDisposition = "started" | "queued" | "handled";
|
|
25
|
+
|
|
25
26
|
/**
|
|
26
|
-
* Minimal shape of an entry in `agent_end.messages
|
|
27
|
+
* Minimal shape of an entry in `agent_end.messages`.
|
|
27
28
|
* The harness only inspects the final message's `role` and (for assistants)
|
|
28
29
|
* `stopReason` / `errorMessage` to decide turn completion; the full pi message
|
|
29
30
|
* type isn't needed here.
|
|
@@ -34,11 +35,12 @@ export interface PiMessage {
|
|
|
34
35
|
errorMessage?: string;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** Reported by the
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
/** Reported by the delivery-receipts extension (`pi-delivery-receipts.ts`)
|
|
39
|
+
* as a custom session entry: pi saved the queue rows `eventIds` as session
|
|
40
|
+
* entry `entryId`. */
|
|
41
|
+
export interface DeliveryReceipt {
|
|
41
42
|
entryId: string;
|
|
43
|
+
eventIds: number[];
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
type RpcMessage =
|
|
@@ -46,7 +48,8 @@ type RpcMessage =
|
|
|
46
48
|
| ExtensionUiRequest
|
|
47
49
|
| { type: "agent_start" }
|
|
48
50
|
| { type: "agent_end"; messages?: PiMessage[] }
|
|
49
|
-
| { type: "
|
|
51
|
+
| { type: "agent_settled" }
|
|
52
|
+
| { type: "entry_appended"; entry?: { type?: string; customType?: string; data?: unknown } }
|
|
50
53
|
| { type: string; [k: string]: unknown };
|
|
51
54
|
|
|
52
55
|
/**
|
|
@@ -70,8 +73,8 @@ export class PiRpcClient {
|
|
|
70
73
|
>();
|
|
71
74
|
private nextId = 1;
|
|
72
75
|
private agentEndHandler: ((messages: PiMessage[]) => void) | undefined;
|
|
73
|
-
private
|
|
74
|
-
private
|
|
76
|
+
private agentSettledHandler: (() => void) | undefined;
|
|
77
|
+
private deliveryReceiptHandler: ((receipt: DeliveryReceipt) => void) | undefined;
|
|
75
78
|
private exitPromise: Promise<{ code: number | null; signal: NodeJS.Signals | null }>;
|
|
76
79
|
|
|
77
80
|
constructor(child: ChildProcess, log: Logger) {
|
|
@@ -111,24 +114,24 @@ export class PiRpcClient {
|
|
|
111
114
|
});
|
|
112
115
|
}
|
|
113
116
|
|
|
114
|
-
/** Fires
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
+
/** Fires each time one low-level pi run ends. Pi may still retry, recover
|
|
118
|
+
* from overflow or run a late steer afterwards — wait for
|
|
119
|
+
* {@link onAgentSettled} — so the caller keeps the latest run's messages
|
|
120
|
+
* and judges completion from the final message's role + stopReason. */
|
|
117
121
|
onAgentEnd(h: (messages: PiMessage[]) => void): void {
|
|
118
122
|
this.agentEndHandler = h;
|
|
119
123
|
}
|
|
120
124
|
|
|
121
|
-
/** Fires
|
|
122
|
-
*
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.userMessageStartHandler = h;
|
|
125
|
+
/** Fires when pi is fully settled: no retry, compaction or queued
|
|
126
|
+
* continuation remains. */
|
|
127
|
+
onAgentSettled(h: () => void): void {
|
|
128
|
+
this.agentSettledHandler = h;
|
|
126
129
|
}
|
|
127
130
|
|
|
128
|
-
/** Fires when the
|
|
129
|
-
*
|
|
130
|
-
|
|
131
|
-
this.
|
|
131
|
+
/** Fires when the delivery-receipts extension reports that pi saved a user
|
|
132
|
+
* message containing these queue rows. */
|
|
133
|
+
onDeliveryReceipt(h: (receipt: DeliveryReceipt) => void): void {
|
|
134
|
+
this.deliveryReceiptHandler = h;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
waitExit() {
|
|
@@ -139,7 +142,7 @@ export class PiRpcClient {
|
|
|
139
142
|
return this.stderrBuf;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
|
-
async sendPrompt(text: string): Promise<
|
|
145
|
+
async sendPrompt(text: string): Promise<InputDisposition> {
|
|
143
146
|
const id = `p${this.nextId++}`;
|
|
144
147
|
// Pi acks a prompt frame immediately (the agent loop runs after), so a
|
|
145
148
|
// missing ack means the child is wedged. Without the timeout, the runner
|
|
@@ -151,11 +154,15 @@ export class PiRpcClient {
|
|
|
151
154
|
60_000,
|
|
152
155
|
);
|
|
153
156
|
if (!r.success) throw new Error(`pi rejected prompt: ${r.error ?? "unknown"}`);
|
|
157
|
+
return dispositionOf(r);
|
|
154
158
|
}
|
|
155
159
|
|
|
156
|
-
|
|
160
|
+
/** Rejects if the frame can't be written, pi rejects it, or no ack comes. */
|
|
161
|
+
async sendSteer(text: string): Promise<InputDisposition> {
|
|
157
162
|
const id = `s${this.nextId++}`;
|
|
158
|
-
this.
|
|
163
|
+
const r = await this.writeAndAwait(id, { id, type: "steer", message: text }, 60_000);
|
|
164
|
+
if (!r.success) throw new Error(`pi rejected steer: ${r.error ?? "unknown"}`);
|
|
165
|
+
return dispositionOf(r);
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
sendAbort(): void {
|
|
@@ -295,9 +302,20 @@ export class PiRpcClient {
|
|
|
295
302
|
this.agentEndHandler?.(messages);
|
|
296
303
|
return;
|
|
297
304
|
}
|
|
298
|
-
if (p.type === "
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
if (p.type === "agent_settled") {
|
|
306
|
+
this.agentSettledHandler?.();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (p.type === "entry_appended") {
|
|
310
|
+
const entry = (p as { entry?: { type?: string; customType?: string; data?: unknown } }).entry;
|
|
311
|
+
if (entry?.type === "custom" && entry.customType === RECEIPT_TYPE) {
|
|
312
|
+
const r = entry.data as Partial<DeliveryReceipt> | undefined;
|
|
313
|
+
if (typeof r?.entryId === "string" && Array.isArray(r.eventIds)) {
|
|
314
|
+
this.deliveryReceiptHandler?.({ entryId: r.entryId, eventIds: r.eventIds });
|
|
315
|
+
} else {
|
|
316
|
+
this.log.debug({ data: entry.data }, "bad delivery receipt payload");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
301
319
|
return;
|
|
302
320
|
}
|
|
303
321
|
if (p.type === "extension_ui_request") {
|
|
@@ -336,25 +354,6 @@ export class PiRpcClient {
|
|
|
336
354
|
}
|
|
337
355
|
|
|
338
356
|
private handleExtensionUi(req: ExtensionUiRequest): void {
|
|
339
|
-
// The harness-bridge extension reports session entry ids as a
|
|
340
|
-
// fire-and-forget `setStatus` keyed "cognisphere". Intercept those and
|
|
341
|
-
// route to the harness-entry handler instead of treating them as UI.
|
|
342
|
-
if (req.method === "setStatus" && req.statusKey === "cognisphere") {
|
|
343
|
-
if (req.statusText == null) return;
|
|
344
|
-
try {
|
|
345
|
-
const report = JSON.parse(req.statusText) as HarnessEntryReport;
|
|
346
|
-
if (
|
|
347
|
-
report.kind === "user_entry" &&
|
|
348
|
-
typeof report.index === "number" &&
|
|
349
|
-
typeof report.entryId === "string"
|
|
350
|
-
) {
|
|
351
|
-
this.harnessEntryHandler?.(report);
|
|
352
|
-
}
|
|
353
|
-
} catch {
|
|
354
|
-
this.log.debug({ statusText: req.statusText }, "bad cognisphere setStatus payload");
|
|
355
|
-
}
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
357
|
const dialog = new Set(["select", "confirm", "input", "editor"]);
|
|
359
358
|
if (dialog.has(req.method)) {
|
|
360
359
|
this.log.warn({ method: req.method }, "auto-cancel extension_ui_request");
|
|
@@ -372,3 +371,10 @@ export class PiRpcClient {
|
|
|
372
371
|
this.pending.clear();
|
|
373
372
|
}
|
|
374
373
|
}
|
|
374
|
+
|
|
375
|
+
/** Pi (0.99+) reports `data.disposition` on successful prompt/steer acks.
|
|
376
|
+
* Missing → treat as a normal accepted input. */
|
|
377
|
+
function dispositionOf(r: RpcResponse): InputDisposition {
|
|
378
|
+
const d = (r.data as { disposition?: unknown } | undefined)?.disposition;
|
|
379
|
+
return d === "handled" || d === "queued" ? d : "started";
|
|
380
|
+
}
|