@mono-agent/agent-runtime 0.20.11 → 0.20.14
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/MIGRATION.md +264 -25
- package/README.md +139 -448
- package/package.json +7 -43
- package/src/agent/tools/agent-tool.js +5 -5
- package/src/agent/tools/bash.js +1 -24
- package/src/agent/tools/codex-subscription-search.js +1 -1
- package/src/agent/tools/index.js +7 -0
- package/src/agent/tools/monitor.js +140 -0
- package/src/agent/tools/pi-bridge.js +92 -7
- package/src/agent/tools/shared/bash-environment.js +31 -0
- package/src/agent/tools/shared/monitors.js +274 -0
- package/src/agent/tools/shared/process-runner.js +23 -5
- package/src/ai/cost.js +13 -68
- package/src/ai/index.js +4 -17
- package/src/ai/pi-interop.js +65 -1
- package/src/ai/pi-oauth-compat.js +1 -1
- package/src/ai/providers/codex/app-server-client.js +592 -0
- package/src/ai/providers/pi-models.js +18 -10
- package/src/ai/providers/pi-native/compaction-driver.js +49 -21
- package/src/ai/providers/pi-native/harness-adapter.js +338 -0
- package/src/ai/providers/pi-native/result-builder.js +10 -10
- package/src/ai/providers/pi-native/session-lifecycle.js +95 -40
- package/src/ai/providers/pi-native/stream-subscriber.js +23 -5
- package/src/ai/providers/pi-native/turn-runner.js +34 -15
- package/src/ai/providers/pi-native.js +48 -22
- package/src/ai/runtime/capabilities.js +11 -56
- package/src/ai/runtime/model-refs.js +118 -153
- package/src/ai/runtime/registry.js +22 -56
- package/src/ai/runtime/router.js +48 -408
- package/src/ai/runtime/session-liveness.js +3 -4
- package/src/ai/runtime/sessions.js +4 -5
- package/src/ai/runtime/tool-policy.js +0 -2
- package/src/ai/types.js +12 -108
- package/src/index.js +0 -6
- package/src/runtime.js +8 -14
- package/types/agent/tools/agent-tool.d.ts +1 -2
- package/types/agent/tools/codex-subscription-search.d.ts +1 -1
- package/types/agent/tools/index.d.ts +1 -0
- package/types/agent/tools/monitor.d.ts +44 -0
- package/types/agent/tools/pi-bridge.d.ts +3 -2
- package/types/agent/tools/shared/bash-environment.d.ts +4 -0
- package/types/agent/tools/shared/monitors.d.ts +83 -0
- package/types/agent/tools/shared/process-runner.d.ts +11 -2
- package/types/ai/cost.d.ts +1 -2
- package/types/ai/index.d.ts +1 -4
- package/types/ai/pi-interop.d.ts +20 -0
- package/types/ai/providers/codex/app-server-client.d.ts +37 -0
- package/types/ai/providers/pi-native/harness-adapter.d.ts +56 -0
- package/types/ai/providers/pi-native/result-builder.d.ts +3 -3
- package/types/ai/providers/pi-native/session-lifecycle.d.ts +2 -1
- package/types/ai/providers/pi-native/stream-subscriber.d.ts +2 -2
- package/types/ai/providers/pi-native/turn-runner.d.ts +33 -6
- package/types/ai/providers/pi-native.d.ts +21 -4
- package/types/ai/runtime/capabilities.d.ts +21 -70
- package/types/ai/runtime/model-refs.d.ts +0 -24
- package/types/ai/runtime/router.d.ts +3 -10
- package/types/ai/runtime/tool-policy.d.ts +0 -2
- package/types/ai/types.d.ts +28 -250
- package/types/index.d.ts +0 -1
- package/src/ai/providers/acp-client.js +0 -1149
- package/src/ai/providers/acp-privacy.js +0 -124
- package/src/ai/providers/acp-public.js +0 -21
- package/src/ai/providers/acp-session-tokens.js +0 -282
- package/src/ai/providers/acp-transport.js +0 -356
- package/src/ai/providers/acp.js +0 -543
- package/src/ai/providers/claude-cli.js +0 -883
- package/src/ai/providers/claude-sandbox.js +0 -71
- package/src/ai/providers/claude-sdk-discovery-worker.js +0 -53
- package/src/ai/providers/claude-sdk-discovery.js +0 -352
- package/src/ai/providers/claude-sdk.js +0 -1127
- package/src/ai/providers/claude-subagent-activity.js +0 -719
- package/src/ai/providers/claude-subagents.js +0 -88
- package/src/ai/providers/codex-app.js +0 -2946
- package/src/ai/providers/opencode-app.js +0 -1109
- package/src/ai/providers/opencode-discovery.js +0 -39
- package/src/ai/providers/opencode-server.js +0 -508
- package/src/ai/runtime/context-windows.js +0 -46
- package/src/ai/runtime/fast-mode.js +0 -8
- package/src/ai/streaming/codex-events.js +0 -146
- package/src/ai/streaming/opencode-events.js +0 -59
- package/types/ai/providers/acp-client.d.ts +0 -227
- package/types/ai/providers/acp-privacy.d.ts +0 -25
- package/types/ai/providers/acp-public.d.ts +0 -7
- package/types/ai/providers/acp-session-tokens.d.ts +0 -41
- package/types/ai/providers/acp-transport.d.ts +0 -45
- package/types/ai/providers/acp.d.ts +0 -93
- package/types/ai/providers/claude-cli.d.ts +0 -305
- package/types/ai/providers/claude-sandbox.d.ts +0 -79
- package/types/ai/providers/claude-sdk-discovery-worker.d.ts +0 -1
- package/types/ai/providers/claude-sdk-discovery.d.ts +0 -97
- package/types/ai/providers/claude-sdk.d.ts +0 -138
- package/types/ai/providers/claude-subagent-activity.d.ts +0 -53
- package/types/ai/providers/claude-subagents.d.ts +0 -18
- package/types/ai/providers/codex-app.d.ts +0 -151
- package/types/ai/providers/opencode-app.d.ts +0 -96
- package/types/ai/providers/opencode-discovery.d.ts +0 -4
- package/types/ai/providers/opencode-server.d.ts +0 -20
- package/types/ai/runtime/context-windows.d.ts +0 -9
- package/types/ai/runtime/fast-mode.d.ts +0 -2
- package/types/ai/streaming/codex-events.d.ts +0 -40
- package/types/ai/streaming/opencode-events.d.ts +0 -42
|
@@ -10,12 +10,13 @@
|
|
|
10
10
|
// createSessionLiveness primitives so the await-free spans are enforced by
|
|
11
11
|
// construction rather than by inline sequencing.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { JsonlSessionRepo, MemorySessionRepo } from "@earendil-works/pi-agent-core";
|
|
14
14
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
15
|
-
import { open } from "node:fs/promises";
|
|
15
|
+
import { access, open } from "node:fs/promises";
|
|
16
16
|
import { dirname, resolve } from "node:path";
|
|
17
17
|
import { createSessionRegistry } from "../../runtime/sessions.js";
|
|
18
18
|
import { createSessionLiveness } from "../../runtime/session-liveness.js";
|
|
19
|
+
import { createPiSessionAdapter, PI_CONTEXT } from "./harness-adapter.js";
|
|
19
20
|
|
|
20
21
|
async function syncPath(path) {
|
|
21
22
|
const handle = await open(path, "r");
|
|
@@ -40,31 +41,61 @@ async function syncDurableTranscript(entry) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async function invalidateNativeSession(entry) {
|
|
43
|
-
await entry.repo.delete(entry.metadata);
|
|
44
44
|
if (entry.durable) {
|
|
45
45
|
const path = entry.metadata?.path;
|
|
46
46
|
if (typeof path !== "string" || !path) {
|
|
47
47
|
throw new Error("Durable Pi session metadata is missing its JSONL path");
|
|
48
48
|
}
|
|
49
|
+
// Explicit invalidation is idempotent. A preceding failed sync may have
|
|
50
|
+
// observed that the transcript was already removed outside this process.
|
|
51
|
+
try {
|
|
52
|
+
await access(path);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error?.code === "ENOENT") return;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await entry.repo.delete(entry.metadata, PI_CONTEXT);
|
|
59
|
+
if (entry.durable) {
|
|
60
|
+
const path = entry.metadata.path;
|
|
49
61
|
// Make the unlink durable before the registry forgets the busy marker.
|
|
50
62
|
await syncPath(dirname(path));
|
|
51
63
|
}
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Release a Pi session handle and remove its repository record as independent
|
|
68
|
+
* best-effort operations. A close failure must not prevent deletion of a fresh
|
|
69
|
+
* or otherwise poisoned transcript.
|
|
70
|
+
* @param {any} session
|
|
71
|
+
* @param {any} repo
|
|
72
|
+
* @param {any} [knownMetadata]
|
|
73
|
+
*/
|
|
74
|
+
async function closeAndDeleteSession(session, repo, knownMetadata) {
|
|
75
|
+
let metadata = knownMetadata;
|
|
76
|
+
if (!metadata) {
|
|
77
|
+
try { metadata = await session.getMetadata(); } catch { /* best-effort */ }
|
|
78
|
+
}
|
|
79
|
+
try { await session.close(); } catch { /* best-effort */ }
|
|
80
|
+
if (metadata) {
|
|
81
|
+
try { await repo.delete(metadata, PI_CONTEXT); } catch { /* best-effort */ }
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
// Live pi-native sessions, keyed by provider session id. Entries are
|
|
55
|
-
// {
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
86
|
+
// { metadata, repo, durable, busy }. Pi 0.85 sessions are closed after every
|
|
87
|
+
// turn so their repository record can be reopened safely; the registry owns
|
|
88
|
+
// only liveness and metadata. In-memory transcripts are freed when the registry
|
|
89
|
+
// evicts them; durable (jsonl) transcripts survive eviction. Registering here gives
|
|
59
90
|
// runtime.disposeSession / disposeProviderSession + idle-TTL eviction the same
|
|
60
91
|
// reach over native pi sessions that the legacy bridge had.
|
|
61
|
-
const nativeSessionRepo = new
|
|
92
|
+
const nativeSessionRepo = new MemorySessionRepo();
|
|
62
93
|
const nativeSessions = createSessionRegistry({
|
|
63
94
|
isBusy: (entry) => entry.busy === true,
|
|
64
95
|
onSync: syncDurableTranscript,
|
|
65
96
|
onEvict: async (entry, reason) => {
|
|
66
|
-
// Ordinary disposal/TTL only drops
|
|
67
|
-
// reopen
|
|
97
|
+
// Ordinary disposal/TTL only drops registry metadata so durable sessions
|
|
98
|
+
// can reopen later. Explicit invalidation means the host rejected the
|
|
68
99
|
// turn before canonical history commit; that poisoned transcript must be
|
|
69
100
|
// deleted too or it could silently reappear on the next stable-id resume.
|
|
70
101
|
if (reason === "invalidated") {
|
|
@@ -75,7 +106,7 @@ const nativeSessions = createSessionRegistry({
|
|
|
75
106
|
return;
|
|
76
107
|
}
|
|
77
108
|
if (entry.durable) return;
|
|
78
|
-
await entry.repo.delete(entry.metadata);
|
|
109
|
+
await entry.repo.delete(entry.metadata, PI_CONTEXT);
|
|
79
110
|
},
|
|
80
111
|
});
|
|
81
112
|
const liveness = createSessionLiveness(nativeSessions);
|
|
@@ -88,7 +119,7 @@ export function resolveDurableNativeSessionRepo(piSessionsRoot) {
|
|
|
88
119
|
let repo = durableNativeSessionRepos.get(root);
|
|
89
120
|
if (!repo) {
|
|
90
121
|
repo = new JsonlSessionRepo({
|
|
91
|
-
|
|
122
|
+
fileSystem: new NodeExecutionEnv({ cwd: process.cwd() }),
|
|
92
123
|
sessionsRoot: root,
|
|
93
124
|
});
|
|
94
125
|
durableNativeSessionRepos.set(root, repo);
|
|
@@ -118,19 +149,19 @@ export async function retireDurableNativeSession(providerSessionId, piSessionsRo
|
|
|
118
149
|
|
|
119
150
|
const repo = resolveDurableNativeSessionRepo(piSessionsRoot);
|
|
120
151
|
if (!repo) throw new Error("Durable Pi session repository is unavailable");
|
|
121
|
-
const matches = (await repo.list()).filter((entry) => entry?.id === providerSessionId);
|
|
152
|
+
const matches = (await repo.list(undefined, PI_CONTEXT)).filter((entry) => entry?.id === providerSessionId);
|
|
122
153
|
const changedDirectories = new Set();
|
|
123
154
|
for (const metadata of matches) {
|
|
124
155
|
if (typeof metadata?.path !== "string" || !metadata.path) {
|
|
125
156
|
throw new Error(`Durable Pi session ${providerSessionId} has invalid metadata`);
|
|
126
157
|
}
|
|
127
|
-
await repo.delete(metadata);
|
|
158
|
+
await repo.delete(metadata, PI_CONTEXT);
|
|
128
159
|
changedDirectories.add(dirname(metadata.path));
|
|
129
160
|
}
|
|
130
161
|
for (const directory of changedDirectories) await syncPath(directory);
|
|
131
162
|
if (changedDirectories.size > 0) await syncPath(resolve(piSessionsRoot));
|
|
132
163
|
|
|
133
|
-
const remaining = (await repo.list()).filter((entry) => entry?.id === providerSessionId);
|
|
164
|
+
const remaining = (await repo.list(undefined, PI_CONTEXT)).filter((entry) => entry?.id === providerSessionId);
|
|
134
165
|
if (remaining.length > 0) {
|
|
135
166
|
throw new Error(`Durable Pi session ${providerSessionId} could not be retired completely`);
|
|
136
167
|
}
|
|
@@ -158,10 +189,9 @@ function isSafeSessionId(id) {
|
|
|
158
189
|
|
|
159
190
|
async function reopenDurableNativeSession(repo, sessionId) {
|
|
160
191
|
try {
|
|
161
|
-
const metadata = (await repo.list()).find((entry) => entry?.id === sessionId);
|
|
192
|
+
const metadata = (await repo.list(undefined, PI_CONTEXT)).find((entry) => entry?.id === sessionId);
|
|
162
193
|
if (!metadata) return null;
|
|
163
|
-
|
|
164
|
-
return { session, metadata, repo, durable: true, busy: false };
|
|
194
|
+
return { metadata, repo, durable: true, busy: false };
|
|
165
195
|
} catch {
|
|
166
196
|
return null;
|
|
167
197
|
}
|
|
@@ -185,9 +215,9 @@ function sessionUnavailableResult({
|
|
|
185
215
|
usage: {},
|
|
186
216
|
durationMs: Date.now() - start,
|
|
187
217
|
numTurns: 0,
|
|
188
|
-
model: resolved?.reference || resolved?.model
|
|
218
|
+
model: resolved?.reference || (resolved?.provider && resolved?.model ? `${resolved.provider}:${resolved.model}` : null),
|
|
189
219
|
effort: options.effort || null,
|
|
190
|
-
sdk:
|
|
220
|
+
sdk: "pi",
|
|
191
221
|
cancelled: false,
|
|
192
222
|
error: errorMessage,
|
|
193
223
|
failureKind,
|
|
@@ -290,11 +320,14 @@ export async function resolveSession(runState, {
|
|
|
290
320
|
} else {
|
|
291
321
|
// Reserved the id with a busy placeholder BEFORE the create await so a
|
|
292
322
|
// second concurrent first turn observes busy and returns session_busy.
|
|
293
|
-
// The keep-alive success path
|
|
294
|
-
//
|
|
295
|
-
// release it. Keyed by requestedSessionId === providerSessionId.
|
|
323
|
+
// The keep-alive success path overwrites this placeholder with an
|
|
324
|
+
// entry that remains busy until its harness closes; drop/abort/catch
|
|
325
|
+
// paths release it. Keyed by requestedSessionId === providerSessionId.
|
|
296
326
|
runState.reservation = reservation;
|
|
297
|
-
runState.session = await durableRepo.create(
|
|
327
|
+
runState.session = createPiSessionAdapter(await durableRepo.create(
|
|
328
|
+
{ id: providerSessionId, cwd: cwd || process.cwd() },
|
|
329
|
+
PI_CONTEXT,
|
|
330
|
+
));
|
|
298
331
|
runState.createdOnMiss = true;
|
|
299
332
|
}
|
|
300
333
|
} else {
|
|
@@ -353,14 +386,28 @@ export async function resolveSession(runState, {
|
|
|
353
386
|
};
|
|
354
387
|
}
|
|
355
388
|
runState.sessionEntry = claimed.entry;
|
|
356
|
-
|
|
389
|
+
try {
|
|
390
|
+
runState.session = createPiSessionAdapter(await claimed.entry.repo.open(
|
|
391
|
+
claimed.entry.metadata,
|
|
392
|
+
PI_CONTEXT,
|
|
393
|
+
));
|
|
394
|
+
} catch (error) {
|
|
395
|
+
// The claim made this registry entry busy. An open failure means the
|
|
396
|
+
// entry cannot be driven, so remove its liveness record before
|
|
397
|
+
// propagating; otherwise every later resume retries the same broken
|
|
398
|
+
// entry forever. Preserve the durable transcript for a later cold
|
|
399
|
+
// reopen/recovery attempt.
|
|
400
|
+
liveness.release(requestedSessionId);
|
|
401
|
+
runState.sessionEntry = null;
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
357
404
|
}
|
|
358
405
|
} else {
|
|
359
406
|
// Fresh runs persist into the durable jsonl repo when piSessionsRoot is
|
|
360
407
|
// set, so a kept-alive session can be reopened from disk after the live
|
|
361
408
|
// entry is evicted; otherwise the in-memory repo is used.
|
|
362
|
-
runState.session = await (durableRepo || nativeSessionRepo)
|
|
363
|
-
.create({ id: providerSessionId, cwd: cwd || process.cwd() });
|
|
409
|
+
runState.session = createPiSessionAdapter(await (durableRepo || nativeSessionRepo)
|
|
410
|
+
.create({ id: providerSessionId, cwd: cwd || process.cwd() }, PI_CONTEXT));
|
|
364
411
|
}
|
|
365
412
|
return { done: false };
|
|
366
413
|
}
|
|
@@ -379,7 +426,10 @@ export async function discardUncommittedSession(runState, { durableRepo }) {
|
|
|
379
426
|
// transcript was appended yet (prompt never ran), so the live session is
|
|
380
427
|
// already at its pre-turn leaf and needs no rollback.
|
|
381
428
|
if (runState.session && !runState.sessionEntry) {
|
|
382
|
-
|
|
429
|
+
await closeAndDeleteSession(
|
|
430
|
+
runState.session,
|
|
431
|
+
durableRepo || nativeSessionRepo,
|
|
432
|
+
);
|
|
383
433
|
}
|
|
384
434
|
// Drop the create-on-miss BUSY reservation too, else the busy placeholder
|
|
385
435
|
// leaks and every future resume of this conversation's stable id returns
|
|
@@ -419,16 +469,16 @@ export async function commitSession(runState, {
|
|
|
419
469
|
} else {
|
|
420
470
|
const metadata = await session.getMetadata();
|
|
421
471
|
const entry = {
|
|
422
|
-
session,
|
|
423
472
|
metadata,
|
|
424
473
|
repo: durableRepo || nativeSessionRepo,
|
|
425
474
|
durable: !!durableRepo,
|
|
426
|
-
busy:
|
|
475
|
+
busy: true,
|
|
427
476
|
};
|
|
428
477
|
// A create-on-miss reservation is overwritten by its commit (same id);
|
|
429
478
|
// a plain fresh keep-alive run registers directly.
|
|
430
479
|
if (reservation) reservation.commit(entry);
|
|
431
480
|
else nativeSessions.set(providerSessionId, entry, { idleTimeoutMs: sessionTtlMs });
|
|
481
|
+
runState.registeredSessionEntry = entry;
|
|
432
482
|
}
|
|
433
483
|
} catch (err) {
|
|
434
484
|
// Session persistence must never fail the run; drop the (now
|
|
@@ -442,7 +492,7 @@ export async function commitSession(runState, {
|
|
|
442
492
|
if (requestedSessionId) nativeSessions.delete(requestedSessionId);
|
|
443
493
|
const broken = sessionEntry;
|
|
444
494
|
if (broken) {
|
|
445
|
-
|
|
495
|
+
await closeAndDeleteSession(session, broken.repo, broken.metadata);
|
|
446
496
|
}
|
|
447
497
|
}
|
|
448
498
|
} else if (sessionEntry) {
|
|
@@ -463,9 +513,7 @@ export async function commitSession(runState, {
|
|
|
463
513
|
// (the success keep-alive path overwrites it with the finalized entry, so
|
|
464
514
|
// it is only this drop branch that must clean it up).
|
|
465
515
|
if (reservation) reservation.release();
|
|
466
|
-
|
|
467
|
-
await (durableRepo || nativeSessionRepo).delete(await session.getMetadata());
|
|
468
|
-
} catch { /* best-effort */ }
|
|
516
|
+
await closeAndDeleteSession(session, durableRepo || nativeSessionRepo);
|
|
469
517
|
}
|
|
470
518
|
}
|
|
471
519
|
|
|
@@ -487,7 +535,7 @@ export async function rollbackAbortedTurn(runState, { requestedSessionId, provid
|
|
|
487
535
|
nativeSessions.delete(requestedSessionId);
|
|
488
536
|
} else {
|
|
489
537
|
nativeSessions.delete(providerSessionId);
|
|
490
|
-
|
|
538
|
+
await closeAndDeleteSession(session, durableRepo || nativeSessionRepo);
|
|
491
539
|
}
|
|
492
540
|
}
|
|
493
541
|
|
|
@@ -495,7 +543,8 @@ export async function rollbackAbortedTurn(runState, { requestedSessionId, provid
|
|
|
495
543
|
* Outer-catch session cleanup: drop a just-created fresh durable session, drop a
|
|
496
544
|
* create-on-miss reservation placeholder, and roll a resumed session back to its
|
|
497
545
|
* pre-turn leaf for host/runtime-side throws that landed after the harness
|
|
498
|
-
* already mutated the live session.
|
|
546
|
+
* already mutated the live session. Resumed handles are always closed here so
|
|
547
|
+
* setup failures before a harness is returned cannot leave the repo wedged.
|
|
499
548
|
* @param {any} runState
|
|
500
549
|
* @param {{durableRepo: any}} params
|
|
501
550
|
*/
|
|
@@ -509,7 +558,7 @@ export async function cleanupSessionOnThrow(runState, { durableRepo }) {
|
|
|
509
558
|
// resumed user session here would be data loss) and never when the throw
|
|
510
559
|
// preceded session create.
|
|
511
560
|
if (session && !sessionEntry) {
|
|
512
|
-
|
|
561
|
+
await closeAndDeleteSession(session, durableRepo || nativeSessionRepo);
|
|
513
562
|
}
|
|
514
563
|
// Drop a create-on-miss BUSY placeholder (R8) left in the registry by a throw
|
|
515
564
|
// during/after the reservation — including a throw inside the create await
|
|
@@ -522,8 +571,14 @@ export async function cleanupSessionOnThrow(runState, { durableRepo }) {
|
|
|
522
571
|
// harness already mutated the live session. Mirrors the success-path
|
|
523
572
|
// rollback: move the live session back to the pre-turn leaf so the failed
|
|
524
573
|
// turn never leaks into a later resume. Gated on `sessionEntry &&
|
|
525
|
-
// baselineLeafId` so
|
|
526
|
-
|
|
527
|
-
|
|
574
|
+
// baselineLeafId` so rollback only fires for resumes that captured a baseline.
|
|
575
|
+
// Closing is independently gated on the resumed session existing: a failure
|
|
576
|
+
// may land before the baseline was readable, but that handle must still be
|
|
577
|
+
// released without deleting the user-owned transcript.
|
|
578
|
+
if (sessionEntry && session) {
|
|
579
|
+
if (baselineLeafId) {
|
|
580
|
+
try { await session.moveTo(baselineLeafId); } catch { /* best-effort */ }
|
|
581
|
+
}
|
|
582
|
+
try { await session.close(); } catch { /* best-effort */ }
|
|
528
583
|
}
|
|
529
584
|
}
|
|
@@ -76,8 +76,8 @@ function toolResultOutcome(result) {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Build the harness subscribe handler. `harness` is passed for the maxTurns
|
|
79
|
-
* abort; it is already constructed when this is wired (subscribe follows
|
|
80
|
-
* AgentHarness
|
|
79
|
+
* abort; it is already constructed when this is wired (subscribe follows
|
|
80
|
+
* AgentHarness.create()).
|
|
81
81
|
* @param {StreamSubscriberState} runState
|
|
82
82
|
* @param {{onEvent: (event: any) => void, options: any, toolLimits: any, harness: any, sdk: string, model: string}} deps
|
|
83
83
|
* @returns {(event: any) => void}
|
|
@@ -107,7 +107,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
107
107
|
onEvent({ type: "assistant", message: { content: [{ type: "thinking", text: streamEvent.content }] } });
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
} else if (event.type === "message_end") {
|
|
110
|
+
} else if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
111
111
|
const contextUsage = contextUsageFromAssistantMessage(event.message);
|
|
112
112
|
if (contextUsage) {
|
|
113
113
|
const contextWindow = Number(harness?.getModel?.()?.contextWindow) || 0;
|
|
@@ -124,6 +124,24 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
124
124
|
tokens: contextUsage,
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
|
+
const hasVisibleContent = Array.isArray(event.message?.content)
|
|
128
|
+
&& event.message.content.some((block) => block
|
|
129
|
+
&& typeof block === "object"
|
|
130
|
+
&& ((block.type === "text"
|
|
131
|
+
&& typeof block.text === "string"
|
|
132
|
+
&& block.text.length > 0)
|
|
133
|
+
|| (block.type === "thinking"
|
|
134
|
+
&& ((typeof block.thinking === "string" && block.thinking.length > 0)
|
|
135
|
+
|| (typeof block.text === "string" && block.text.length > 0)))));
|
|
136
|
+
if (hasVisibleContent) {
|
|
137
|
+
const messageId = typeof event.message?.id === "string" && event.message.id.trim().length > 0
|
|
138
|
+
? event.message.id
|
|
139
|
+
: undefined;
|
|
140
|
+
onEvent({
|
|
141
|
+
type: "assistant_message_boundary",
|
|
142
|
+
...(messageId === undefined ? {} : { messageId }),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
127
145
|
} else if (event.type === "tool_execution_start") {
|
|
128
146
|
if (event.toolName) runState.lastToolName = event.toolName;
|
|
129
147
|
if (event.toolCallId) runState.toolStartTimes.set(event.toolCallId, Date.now());
|
|
@@ -198,7 +216,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
198
216
|
});
|
|
199
217
|
} else if (event.type === "turn_end") {
|
|
200
218
|
runState.turnCount += 1;
|
|
201
|
-
// NON-DELEGABLE (verified against @earendil-works/pi-agent-core 0.
|
|
219
|
+
// NON-DELEGABLE (verified against @earendil-works/pi-agent-core 0.85.0).
|
|
202
220
|
// pi's only after-turn stop hook is `shouldStopAfterTurn` on the LOW-LEVEL
|
|
203
221
|
// `AgentLoopConfig` (dist/types.d.ts) — the config passed to the raw
|
|
204
222
|
// `agentLoop`. It is NOT surfaced on `AgentHarnessOptions`
|
|
@@ -217,7 +235,7 @@ export function createStreamSubscriber(runState, { onEvent, options, toolLimits,
|
|
|
217
235
|
&& runState.turnCount >= Number(options.maxTurns)
|
|
218
236
|
&& event.message?.stopReason === "toolUse") {
|
|
219
237
|
runState.maxTurnsHit = true;
|
|
220
|
-
harness.abort();
|
|
238
|
+
void Promise.resolve(harness.abort()).catch(() => {});
|
|
221
239
|
}
|
|
222
240
|
}
|
|
223
241
|
};
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
// state; run state (harness, removeAbortHandler, externalAbort) lives on the
|
|
8
8
|
// caller-owned runState.
|
|
9
9
|
|
|
10
|
-
import { AgentHarness } from "@earendil-works/pi-agent-core";
|
|
11
10
|
import {
|
|
12
11
|
createStructuredOutputTool,
|
|
13
12
|
getPiBuiltinTools,
|
|
@@ -17,6 +16,7 @@ import { createNodeReplController } from "../../../agent/tools/node-repl.js";
|
|
|
17
16
|
import { createWebToolController } from "../../../agent/tools/web-controller.js";
|
|
18
17
|
import { readToolRuntime } from "../../../agent/tools/shared/runtime-context.js";
|
|
19
18
|
import { formatLiveInputGuidance } from "../../live-input-prompt.js";
|
|
19
|
+
import { createPiHarnessAdapter } from "./harness-adapter.js";
|
|
20
20
|
import { appendStructuredOutputInstruction } from "./structured-output.js";
|
|
21
21
|
import { createStreamSubscriber } from "./stream-subscriber.js";
|
|
22
22
|
|
|
@@ -121,13 +121,13 @@ export async function buildTurnTools(runState, {
|
|
|
121
121
|
nodeReplController,
|
|
122
122
|
webController,
|
|
123
123
|
processJobsController: options.processJobs,
|
|
124
|
+
monitorsController: options.monitors,
|
|
124
125
|
toolExecutionMode,
|
|
125
126
|
subagents: options.subagents,
|
|
126
127
|
// The child inherits the parent's route and workspace unless its profile
|
|
127
128
|
// pins a model; the tool closure reads these to build each child request.
|
|
128
129
|
subagentContext: {
|
|
129
130
|
model: options.model,
|
|
130
|
-
executionMode: options.executionMode,
|
|
131
131
|
cwd: options.cwd,
|
|
132
132
|
parentRunId: runCtx?.runId,
|
|
133
133
|
// Same policy + engine this turn's own tools are confined by, so a
|
|
@@ -251,7 +251,7 @@ export function toolResultErrorOverride(details) {
|
|
|
251
251
|
return failed ? { isError: true } : undefined;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
export function buildTurnHarness(runState, {
|
|
254
|
+
export async function buildTurnHarness(runState, {
|
|
255
255
|
session,
|
|
256
256
|
piModels,
|
|
257
257
|
model,
|
|
@@ -263,18 +263,9 @@ export function buildTurnHarness(runState, {
|
|
|
263
263
|
maxRetries,
|
|
264
264
|
maxRetryDelayMs,
|
|
265
265
|
steeringMode,
|
|
266
|
-
onEvent,
|
|
267
266
|
options,
|
|
268
|
-
toolLimits,
|
|
269
|
-
sdk,
|
|
270
|
-
reference,
|
|
271
267
|
}) {
|
|
272
|
-
|
|
273
|
-
// ExecutionEnv now reaches tools through the generic per-turn `toolContext`
|
|
274
|
-
// instead. mono-agent needs neither — it uses none of pi's built-in
|
|
275
|
-
// file/shell tools, and its own tools close over what they need — so the
|
|
276
|
-
// option is dropped rather than migrated.
|
|
277
|
-
const harness = new AgentHarness({
|
|
268
|
+
const harness = await createPiHarnessAdapter(session, {
|
|
278
269
|
session,
|
|
279
270
|
models: piModels,
|
|
280
271
|
model,
|
|
@@ -297,8 +288,37 @@ export function buildTurnHarness(runState, {
|
|
|
297
288
|
// every resolved execute(), so without this the model would be told a failed,
|
|
298
289
|
// timed-out, or empty delegation succeeded.
|
|
299
290
|
harness.on("tool_result", (event) => toolResultErrorOverride(event?.details));
|
|
291
|
+
// Pi 0.85 records in-flight work durably. mono-agent tools do not yet use the
|
|
292
|
+
// new replay-memo contract, so abort an interrupted operation instead of
|
|
293
|
+
// risking a duplicate external effect. Do this before subscribing so stale
|
|
294
|
+
// recovery events cannot contaminate the new run's transcript counters.
|
|
295
|
+
try {
|
|
296
|
+
await harness.abortOpenOperations();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
try { await harness.close(); } catch { /* best-effort */ }
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
|
|
300
302
|
runState.harness = harness;
|
|
301
303
|
|
|
304
|
+
return harness;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Subscribe only after prior history has been seeded. Pi emits message
|
|
309
|
+
* lifecycle events for manual appendMessage() calls; subscribing earlier makes
|
|
310
|
+
* restored assistant messages look like fresh output boundaries.
|
|
311
|
+
* @param {any} runState
|
|
312
|
+
* @param {{harness: any, onEvent: (event: any) => void, options: any, toolLimits: any, sdk: string, reference: string}} deps
|
|
313
|
+
*/
|
|
314
|
+
export function activateTurnHarness(runState, {
|
|
315
|
+
harness,
|
|
316
|
+
onEvent,
|
|
317
|
+
options,
|
|
318
|
+
toolLimits,
|
|
319
|
+
sdk,
|
|
320
|
+
reference,
|
|
321
|
+
}) {
|
|
302
322
|
harness.subscribe(createStreamSubscriber(runState, {
|
|
303
323
|
onEvent,
|
|
304
324
|
options,
|
|
@@ -310,13 +330,12 @@ export function buildTurnHarness(runState, {
|
|
|
310
330
|
|
|
311
331
|
const abortHandler = () => {
|
|
312
332
|
runState.externalAbort = true;
|
|
313
|
-
harness.abort();
|
|
333
|
+
void harness.abort().catch(() => {});
|
|
314
334
|
};
|
|
315
335
|
if (options.abortSignal) {
|
|
316
336
|
options.abortSignal.addEventListener("abort", abortHandler, { once: true });
|
|
317
337
|
runState.removeAbortHandler = () => options.abortSignal.removeEventListener?.("abort", abortHandler);
|
|
318
338
|
}
|
|
319
|
-
return harness;
|
|
320
339
|
}
|
|
321
340
|
|
|
322
341
|
/**
|
|
@@ -74,6 +74,7 @@ import {
|
|
|
74
74
|
runReactiveCompaction,
|
|
75
75
|
} from "./pi-native/compaction-driver.js";
|
|
76
76
|
import {
|
|
77
|
+
activateTurnHarness,
|
|
77
78
|
buildTurnHarness,
|
|
78
79
|
buildTurnTools,
|
|
79
80
|
runHarnessPrompt,
|
|
@@ -83,9 +84,10 @@ import {
|
|
|
83
84
|
import { resolvePiTransport } from "./pi-native/transport.js";
|
|
84
85
|
|
|
85
86
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
87
|
+
* Resolve mono-agent's programmatic mode once per run. Tool builders mark
|
|
88
|
+
* stateful/mutating tools sequential; the Pi 0.85 harness adapter promotes any
|
|
89
|
+
* such marker to its global toolExecution setting because mixed scheduling is
|
|
90
|
+
* no longer available upstream.
|
|
89
91
|
*/
|
|
90
92
|
export function resolvePiToolExecutionMode(options = {}) {
|
|
91
93
|
const warnings = [];
|
|
@@ -258,6 +260,14 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
258
260
|
// an intermediate layer flattens the Error to its message.
|
|
259
261
|
installTransportErrorProbe();
|
|
260
262
|
const resolved = options.model;
|
|
263
|
+
if (
|
|
264
|
+
typeof resolved?.provider !== "string"
|
|
265
|
+
|| resolved.provider.length === 0
|
|
266
|
+
|| typeof resolved.model !== "string"
|
|
267
|
+
|| resolved.model.length === 0
|
|
268
|
+
) {
|
|
269
|
+
throw new Error("invalid pi model reference: provider and model are required");
|
|
270
|
+
}
|
|
261
271
|
const start = Date.now();
|
|
262
272
|
const events = [];
|
|
263
273
|
const runtimeWarnings = [];
|
|
@@ -304,6 +314,10 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
304
314
|
},
|
|
305
315
|
session: null,
|
|
306
316
|
sessionEntry: null,
|
|
317
|
+
// Fresh keep-alive sessions are registered busy until the harness closes;
|
|
318
|
+
// this prevents another turn from reopening the same repo record while its
|
|
319
|
+
// first handle is still live.
|
|
320
|
+
registeredSessionEntry: null,
|
|
307
321
|
// True when a requestedSessionId had no live entry AND no durable transcript,
|
|
308
322
|
// so a fresh durable session was created under that id (cross-restart resume,
|
|
309
323
|
// first turn for the conversation). Distinct from a true resume (sessionEntry
|
|
@@ -352,13 +366,17 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
352
366
|
}
|
|
353
367
|
emitCaptured(events, options.onEvent, event);
|
|
354
368
|
};
|
|
369
|
+
// Exec and Monitor both run a command a host configured Bash's risk tier for.
|
|
370
|
+
// Leaving either unmapped would let the same shell command take a lower
|
|
371
|
+
// approval tier simply by being started through a different tool.
|
|
372
|
+
const bashRiskTier = options.toolRiskTiers?.Bash;
|
|
355
373
|
const approvalRiskTiers = {
|
|
356
374
|
...(options.toolRiskTiers || {}),
|
|
357
|
-
...(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
375
|
+
...(bashRiskTier !== undefined
|
|
376
|
+
? Object.fromEntries(["Exec", "Monitor"]
|
|
377
|
+
.filter((name) => options.toolRiskTiers?.[name] === undefined)
|
|
378
|
+
.map((name) => [name, bashRiskTier]))
|
|
379
|
+
: {}),
|
|
362
380
|
};
|
|
363
381
|
const approvalManager = options.onToolApprovalRequest
|
|
364
382
|
? createApprovalManager({
|
|
@@ -422,8 +440,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
422
440
|
: resolvePiRuntimeModel(resolved, options);
|
|
423
441
|
const capabilities = runtime.capabilities || {};
|
|
424
442
|
const effectiveThinkingLevel = thinkingLevelForEffort(options.effort || "medium", capabilities);
|
|
425
|
-
const reference = resolved.reference
|
|
426
|
-
|| (resolved.sdk === "pi" ? `pi:${resolved.provider}:${resolved.model}` : `${resolved.sdk}:${resolved.model}`);
|
|
443
|
+
const reference = resolved.reference || `${resolved.provider}:${resolved.model}`;
|
|
427
444
|
|
|
428
445
|
// Resolve the typed `toolLimits` / `compaction` policy objects against the
|
|
429
446
|
// deprecated `settings` fallback (per-group precedence: a present typed
|
|
@@ -486,15 +503,17 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
486
503
|
? Number(options.maxRetryDelayMs)
|
|
487
504
|
: 60_000;
|
|
488
505
|
// Steering controls user follow-up delivery, not tool scheduling. Keep it
|
|
489
|
-
// independent from piToolExecutionMode;
|
|
506
|
+
// independent from piToolExecutionMode; the adapter derives Pi 0.85's
|
|
507
|
+
// global scheduling mode from the tools' executionMode markers.
|
|
490
508
|
const toolSteeringMode = "one-at-a-time";
|
|
491
509
|
|
|
492
510
|
const piModels = buildRunModels(runtime, options, runtimeWarnings);
|
|
493
511
|
|
|
494
|
-
// Construct the harness
|
|
495
|
-
// external abort handler
|
|
496
|
-
//
|
|
497
|
-
|
|
512
|
+
// Construct the harness and recover any interrupted durable operation.
|
|
513
|
+
// Stream subscription and the external abort handler are activated only
|
|
514
|
+
// after prior transcript seeding, so manual append lifecycle events cannot
|
|
515
|
+
// surface as output from this run.
|
|
516
|
+
harness = await buildTurnHarness(runState, {
|
|
498
517
|
session: runState.session,
|
|
499
518
|
piModels,
|
|
500
519
|
model: runtime.model,
|
|
@@ -506,11 +525,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
506
525
|
maxRetries,
|
|
507
526
|
maxRetryDelayMs,
|
|
508
527
|
steeringMode: toolSteeringMode,
|
|
509
|
-
onEvent,
|
|
510
528
|
options,
|
|
511
|
-
toolLimits,
|
|
512
|
-
sdk: resolved.sdk,
|
|
513
|
-
reference,
|
|
514
529
|
});
|
|
515
530
|
|
|
516
531
|
// Seed prior transcript (everything before the trailing user turn) into the
|
|
@@ -536,6 +551,15 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
536
551
|
try { runState.baselineLeafId = await runState.session.getLeafId(); } catch { /* best-effort */ }
|
|
537
552
|
}
|
|
538
553
|
|
|
554
|
+
activateTurnHarness(runState, {
|
|
555
|
+
harness,
|
|
556
|
+
onEvent,
|
|
557
|
+
options,
|
|
558
|
+
toolLimits,
|
|
559
|
+
sdk: "pi",
|
|
560
|
+
reference,
|
|
561
|
+
});
|
|
562
|
+
|
|
539
563
|
// Live steering: consume follow-up messages and steer the harness mid-run.
|
|
540
564
|
// The consumer is tied to run completion so it stops steering once the run
|
|
541
565
|
// finishes and does not swallow messages meant for a later turn.
|
|
@@ -584,7 +608,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
584
608
|
|
|
585
609
|
onEvent({
|
|
586
610
|
type: "provider_request_started",
|
|
587
|
-
sdk:
|
|
611
|
+
sdk: "pi",
|
|
588
612
|
model: reference,
|
|
589
613
|
runtime: "pi",
|
|
590
614
|
timestamp: Date.now(),
|
|
@@ -765,7 +789,7 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
765
789
|
// runtime.compaction.enabled. See docs/reference/feature-registry.md runtime.context-compaction.
|
|
766
790
|
contextCompactionApplied: runState.compaction.policy?.enabled ? runState.compaction.applied : null,
|
|
767
791
|
});
|
|
768
|
-
emitCapabilitiesResolved(onEvent, { sdk:
|
|
792
|
+
emitCapabilitiesResolved(onEvent, { sdk: "pi", model: reference, capabilitiesUsed });
|
|
769
793
|
|
|
770
794
|
// Re-check the abort signal: a cancel can land during the post-run work above
|
|
771
795
|
// (live-input teardown, structured-output finalization retry) after the line
|
|
@@ -861,7 +885,9 @@ export async function generatePiNativeResponse(systemPrompt, options = {}) {
|
|
|
861
885
|
piTransport,
|
|
862
886
|
});
|
|
863
887
|
} finally {
|
|
888
|
+
try { await harness?.close?.(); } catch { /* best-effort */ }
|
|
864
889
|
if (runState.sessionEntry) runState.sessionEntry.busy = false;
|
|
890
|
+
if (runState.registeredSessionEntry) runState.registeredSessionEntry.busy = false;
|
|
865
891
|
runState.removeAbortHandler?.();
|
|
866
892
|
try {
|
|
867
893
|
await closeRunTools();
|
|
@@ -875,6 +901,6 @@ export const piNativeRuntimeBridge = {
|
|
|
875
901
|
id: "pi",
|
|
876
902
|
kind: "pi",
|
|
877
903
|
capabilities: runtimeCapabilities("pi"),
|
|
878
|
-
supports: (ref) => ref?.
|
|
904
|
+
supports: (ref) => Boolean(ref?.provider && ref?.model),
|
|
879
905
|
execute: generatePiNativeResponse,
|
|
880
906
|
};
|