@junghanacs/entwurf 0.45.0 → 0.51.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/AGENTS.md +5 -3
- package/BASELINE.md +110 -1
- package/CHANGELOG.md +249 -0
- package/CONTRIBUTING.md +1 -1
- package/ENTWURF-CLI.json +1229 -0
- package/ENTWURF.json +63 -15
- package/FAQ.md +4 -4
- package/README.md +132 -188
- package/VERIFY.md +18 -11
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/external-mcp-host.md +4 -4
- package/docs/setup-clean-host.md +94 -22
- package/mcp/entwurf-bridge/dist/ENTWURF.json +63 -15
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +19 -8
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-visible-resume.js +483 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-dispatch.js +47 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-fresh-call.js +613 -33
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/herdr-resume-call.js +497 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-resume-call.js +50 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-location.js +347 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-owner-fence.js +1132 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/resume-launch-identity.js +18 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +30 -15
- package/mcp/entwurf-bridge/dist/scripts/herdr-rails.mjs +40 -7
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +1 -27
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-prune.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/meta-facts.js +1 -1
- package/mcp/entwurf-bridge/dist/scripts/peer-facts.js +1 -1
- package/mcp/entwurf-bridge/src/index.ts +28 -8
- package/mcp/entwurf-bridge/tsconfig.build.json +7 -1
- package/native/win32-x64/manifest.json +1 -1
- package/package.json +16 -20
- package/pi/pi-durable/bootstrap.mjs +179 -6
- package/pi/pi-durable/carrier/runtime.js +327 -57
- package/pi/pi-durable/carrier/session-name.js +11 -0
- package/pi/pi-durable/carrier/sessions.js +96 -20
- package/pi/pi-durable/carrier/subagent.js +5 -2
- package/pi/pi-durable/carrier/tui.js +60 -22
- package/pi/pi-durable/overlay/upstream-pin.json +26 -19
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +73 -69
- package/pi-extensions/lib/entwurf-v2-contract.ts +4 -4
- package/pi-extensions/lib/entwurf-v2-visible-resume.ts +710 -38
- package/pi-extensions/lib/fresh-call-dispatch.ts +64 -12
- package/pi-extensions/lib/herdr-fresh-call.ts +919 -40
- package/pi-extensions/lib/herdr-resume-call.ts +810 -0
- package/pi-extensions/lib/meta-session.ts +13 -8
- package/pi-extensions/lib/mux-resume-call.ts +77 -4
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +9 -2
- package/pi-extensions/lib/pi-durable-location.ts +446 -0
- package/pi-extensions/lib/pi-durable-owner-fence.ts +1461 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -0
- package/pi-extensions/lib/resume-launch-identity.ts +31 -12
- package/pi-extensions/meta-bridge-pi-durable.ts +30 -16
- package/run.sh +552 -699
- package/scripts/check-claude-launch.ts +341 -0
- package/scripts/check-entwurf-v2-contract.ts +4 -4
- package/scripts/check-entwurf-v2-surface.ts +2 -2
- package/scripts/check-fresh-call-dispatch.ts +70 -9
- package/scripts/check-fresh-cut-gate.sh +23 -9
- package/scripts/check-gate-qualification.ts +19 -14
- package/scripts/check-herdr-activation.ts +301 -23
- package/scripts/check-herdr-fresh-call.ts +161 -25
- package/scripts/check-herdr-plugin-build.ts +173 -1
- package/scripts/check-herdr-plugin.ts +28 -2
- package/scripts/check-herdr-runtime-bootstrap.ts +136 -0
- package/scripts/check-install-surface.ts +1236 -6
- package/scripts/check-meta-doctor-oracle.sh +1 -1
- package/scripts/check-meta-facts.ts +20 -1
- package/scripts/check-mux-resume-call.ts +28 -0
- package/scripts/check-peer-facts.ts +39 -2
- package/scripts/cli-help.mjs +222 -0
- package/scripts/dev-bin.sh +39 -2
- package/scripts/herdr-activation.mjs +28 -6
- package/scripts/herdr-operator-link.mjs +389 -0
- package/scripts/herdr-plugin-activate.mjs +5 -0
- package/scripts/herdr-plugin-deactivate.mjs +18 -5
- package/scripts/herdr-rails.mjs +43 -7
- package/scripts/herdr-runtime.mjs +27 -0
- package/scripts/meta-bridge-fresh-cut.ts +1 -27
- package/scripts/meta-bridge-prune.ts +1 -1
- package/scripts/meta-facts.ts +1 -1
- package/scripts/mutants/agent-guidance.json +4 -4
- package/scripts/mutants/claude-launch.json +142 -0
- package/scripts/mutants/cli-help-source.json +420 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/dev-gate-dispatch.json +109 -0
- package/scripts/mutants/fresh-call-dispatch.json +38 -2
- package/scripts/mutants/fresh-cut.json +15 -0
- package/scripts/mutants/herdr-activation.json +128 -0
- package/scripts/mutants/herdr-fresh-call.json +278 -5
- package/scripts/mutants/herdr-operator-link.json +318 -0
- package/scripts/mutants/herdr-plugin-build.json +55 -0
- package/scripts/mutants/herdr-plugin.json +17 -0
- package/scripts/mutants/herdr-resume-call.json +288 -0
- package/scripts/mutants/herdr-runtime-bootstrap.json +48 -0
- package/scripts/mutants/meta-facts.json +15 -0
- package/scripts/mutants/mux-resume-call.json +63 -2
- package/scripts/mutants/pack-install.json +4 -4
- package/scripts/mutants/peer-facts.json +15 -0
- package/scripts/mutants/pi-durable.json +1167 -3
- package/scripts/mutants/pi-floor.json +22 -5
- package/scripts/mutants/source-install.json +35 -0
- package/scripts/mutants/v2-surface.json +2 -2
- package/scripts/mutants/v2-visible-resume.json +255 -6
- package/scripts/peer-facts.ts +1 -1
- package/scripts/smoke-agy-install-state.sh +66 -0
- package/scripts/smoke-meta-prune.sh +23 -0
- package/scripts/smoke-omp-fresh-live.ts +12 -3
- package/scripts/smoke-omp-receive-live.ts +7 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +12 -2
- package/team.json.example +39 -0
|
@@ -4,6 +4,55 @@ import { mkdir, readdir, realpath, stat } from "node:fs/promises";
|
|
|
4
4
|
import { basename, join, resolve } from "node:path";
|
|
5
5
|
import lockfile from "proper-lockfile";
|
|
6
6
|
import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
7
|
+
import { isSessionName } from "./session-name.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* An owner's end as the opener observed it (#157 B3): the storage outcome of the first actual close, and the
|
|
11
|
+
* lease outcome of its release.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
/** One admitted owner of a session directory, from before its native lease through its end. */
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/** What an owned open hands the fence once its directory is chosen, before its native lease. */
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The owner fence every owned open takes before its native lease (#157 B3). Supplied by the host, never imported
|
|
51
|
+
* here; it admits this opener or refuses, and a refusal opens nothing.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
7
56
|
|
|
8
57
|
/** One session directory holding `session.sqlite`, locked by this process. */
|
|
9
58
|
|
|
@@ -13,6 +62,8 @@ import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
|
13
62
|
|
|
14
63
|
|
|
15
64
|
|
|
65
|
+
|
|
66
|
+
|
|
16
67
|
|
|
17
68
|
|
|
18
69
|
/** One session of a directory as its files show it (#143): nothing in it is opened. */
|
|
@@ -27,8 +78,6 @@ import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
|
27
78
|
|
|
28
79
|
|
|
29
80
|
|
|
30
|
-
const SESSION_NAME = /^\d{13}-[0-9a-f-]{36}$/u;
|
|
31
|
-
|
|
32
81
|
/** A path's stat, or undefined when nothing is there; any other failure is thrown, never read as absence. */
|
|
33
82
|
async function statIfPresent(path ) {
|
|
34
83
|
try {
|
|
@@ -39,15 +88,17 @@ async function statIfPresent(path ) {
|
|
|
39
88
|
}
|
|
40
89
|
}
|
|
41
90
|
|
|
42
|
-
async function sessionRoot(cwdInput )
|
|
91
|
+
async function sessionRoot(cwdInput ) {
|
|
43
92
|
const cwd = await realpath(resolve(cwdInput));
|
|
93
|
+
// Read once: the root is joined from this value, and the fence is handed this same value as it came (#157 B3).
|
|
94
|
+
const agentDir = getAgentDir();
|
|
44
95
|
const root = join(
|
|
45
|
-
|
|
96
|
+
agentDir,
|
|
46
97
|
"experimental",
|
|
47
98
|
"durable-sessions",
|
|
48
99
|
createHash("sha256").update(cwd).digest("hex").slice(0, 24),
|
|
49
100
|
);
|
|
50
|
-
return { cwd, root };
|
|
101
|
+
return { cwd, root, profile: agentDir };
|
|
51
102
|
}
|
|
52
103
|
|
|
53
104
|
/** The sessions of `cwd`, most recently written first. Reads names, file times and locks; creates nothing. */
|
|
@@ -62,7 +113,7 @@ export async function listSessions(cwdInput ) {
|
|
|
62
113
|
}
|
|
63
114
|
const sessions = [];
|
|
64
115
|
for (const entry of entries) {
|
|
65
|
-
if (!entry.isDirectory() || !
|
|
116
|
+
if (!entry.isDirectory() || !isSessionName(entry.name)) continue;
|
|
66
117
|
const directory = join(root, entry.name);
|
|
67
118
|
let lastWriteAt ;
|
|
68
119
|
for (const file of ["session.sqlite", "session.sqlite-wal"]) {
|
|
@@ -80,19 +131,23 @@ export async function listSessions(cwdInput ) {
|
|
|
80
131
|
return sessions.sort((a, b) => (b.lastWriteAt ?? b.createdAt) - (a.lastWriteAt ?? a.createdAt));
|
|
81
132
|
}
|
|
82
133
|
|
|
83
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* A new session for `cwd`, its newest one with `continueSession`, or the existing one named by `sessionId`, admitted
|
|
136
|
+
* by `fence` before its native lease is taken.
|
|
137
|
+
*/
|
|
84
138
|
export async function selectSession(
|
|
139
|
+
fence ,
|
|
85
140
|
cwdInput ,
|
|
86
141
|
continueSession ,
|
|
87
142
|
sessionId ,
|
|
88
143
|
) {
|
|
89
|
-
const { cwd, root } = await sessionRoot(cwdInput);
|
|
144
|
+
const { cwd, root, profile } = await sessionRoot(cwdInput);
|
|
90
145
|
await mkdir(root, { recursive: true });
|
|
91
146
|
|
|
92
147
|
let directory ;
|
|
93
148
|
let created = false;
|
|
94
149
|
if (sessionId !== undefined) {
|
|
95
|
-
const chosen =
|
|
150
|
+
const chosen = isSessionName(sessionId) ? join(root, sessionId) : undefined;
|
|
96
151
|
if (chosen === undefined || !(await statIfPresent(chosen))?.isDirectory()) {
|
|
97
152
|
throw new Error(`No durable session ${sessionId} exists for ${cwd}`);
|
|
98
153
|
}
|
|
@@ -100,7 +155,7 @@ export async function selectSession(
|
|
|
100
155
|
} else if (continueSession) {
|
|
101
156
|
const entries = await readdir(root, { withFileTypes: true });
|
|
102
157
|
const newest = entries
|
|
103
|
-
.filter((entry) => entry.isDirectory() &&
|
|
158
|
+
.filter((entry) => entry.isDirectory() && isSessionName(entry.name))
|
|
104
159
|
.map((entry) => entry.name)
|
|
105
160
|
.sort()
|
|
106
161
|
.at(-1);
|
|
@@ -112,15 +167,36 @@ export async function selectSession(
|
|
|
112
167
|
created = true;
|
|
113
168
|
}
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
170
|
+
// The owner fence admits this opener BEFORE the native lease, or refuses with nothing taken (#157 B3).
|
|
171
|
+
const ticket = await fence.admit({ directory, selectedN: basename(directory), created, profile });
|
|
172
|
+
// A lock left by a crashed process goes stale after 10 s; wait that long before giving up.
|
|
173
|
+
const lease = lockfile.lock(directory, {
|
|
174
|
+
realpath: false,
|
|
175
|
+
retries: { retries: 12, minTimeout: 1000, maxTimeout: 1000 },
|
|
176
|
+
});
|
|
177
|
+
const release = await lease.catch(async (error) => {
|
|
178
|
+
const refused = new Error(`Session is already open in another process: ${directory}`, { cause: error });
|
|
179
|
+
// No acquisition was observed: this owner ends with nothing opened and no lease held.
|
|
180
|
+
await ticket.endUnleased().catch((fenceError) => {
|
|
181
|
+
throw new AggregateError([refused, fenceError], `${refused.message}; its owner end was not recorded`);
|
|
121
182
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
183
|
+
throw refused;
|
|
184
|
+
});
|
|
185
|
+
// Apart from the lease's own failure: the lease IS held here, so a failed checkpoint is never recorded as an
|
|
186
|
+
// acquire failure. The open fails and releases the lease; the claim stays fenced under this live owner.
|
|
187
|
+
await ticket.leased().catch(async (error) => {
|
|
188
|
+
await release().catch((releaseError) => {
|
|
189
|
+
throw new AggregateError([error, releaseError], `lease checkpoint and release both failed: ${directory}`);
|
|
190
|
+
});
|
|
191
|
+
throw error;
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
id: basename(directory),
|
|
195
|
+
directory,
|
|
196
|
+
database: join(directory, "session.sqlite"),
|
|
197
|
+
cwd,
|
|
198
|
+
created,
|
|
199
|
+
release,
|
|
200
|
+
ticket,
|
|
201
|
+
};
|
|
126
202
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import {
|
|
4
4
|
AssistantEntry,
|
|
5
5
|
configure,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
} from "@earendil-works/pi-durable";
|
|
13
|
+
import { obj, str } from "@earendil-works/pi-schema";
|
|
13
14
|
|
|
14
15
|
async function answerText(api , answer , context ) {
|
|
15
16
|
const entry = await api.commit((tx) => tx.entry(AssistantEntry, answer), context);
|
|
@@ -29,7 +30,9 @@ export const Subagent = defineExtension({
|
|
|
29
30
|
name: "subagent",
|
|
30
31
|
description:
|
|
31
32
|
"Delegate a self-contained task to a subagent with the same tools and get its answer back. Give it everything it needs to know; it does not see this conversation.",
|
|
32
|
-
|
|
33
|
+
// pi-ai 1.5 no longer exports TypeBox's `Type`; pi-schema builds the same JSON Schema. `open` keeps the input
|
|
34
|
+
// as open as the TypeBox object was, since `obj()` alone closes it (SDK 1.5's own tools do the same, #155).
|
|
35
|
+
parameters: obj({ task: str({ description: "What the subagent should do" }) }, { open: true }),
|
|
33
36
|
// A rerun after a crash finds the child it created and the submission it made.
|
|
34
37
|
replay: "safe",
|
|
35
38
|
execute: async (args, api, context) => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
import {
|
|
@@ -176,8 +177,9 @@ class CompactionComponent extends Box {
|
|
|
176
177
|
|
|
177
178
|
|
|
178
179
|
/**
|
|
179
|
-
* The lines above the editor: first what stays true for as long as it is true, the session's failure
|
|
180
|
-
* compaction failure a conversation keeps until a compaction produces a summary,
|
|
180
|
+
* The lines above the editor: first what stays true for as long as it is true, the session's failure, the
|
|
181
|
+
* compaction failure a conversation keeps until a compaction produces a summary, the run that ended without an
|
|
182
|
+
* answer until another run starts, and a view that stopped updating; then the latest notices.
|
|
181
183
|
*/
|
|
182
184
|
export function durableNotices(view ) {
|
|
183
185
|
const lines = [];
|
|
@@ -188,9 +190,22 @@ export function durableNotices(view )
|
|
|
188
190
|
message: `Session failed${cause}. It takes no more input, and a tool still running may take a while to stop. Close this session, then reopen it with --continue.`,
|
|
189
191
|
});
|
|
190
192
|
}
|
|
191
|
-
const
|
|
193
|
+
const live = (view.conversation.docs["pi.live"] ?? {}) ;
|
|
194
|
+
const compaction = live.compactionFailure;
|
|
192
195
|
if (compaction !== undefined) {
|
|
193
|
-
lines.push({ level: "error", message: `Compaction failed (${compaction.
|
|
196
|
+
lines.push({ level: "error", message: `Compaction failed (${compaction.trigger}): ${compaction.error}` });
|
|
197
|
+
}
|
|
198
|
+
// SDK 1.5 keeps why the newest run ended without an answer. An aborted run is the user's own stop, as before:
|
|
199
|
+
// the answer it cut off already says so.
|
|
200
|
+
const run = live.runFailure;
|
|
201
|
+
if (run !== undefined && run.reason !== "aborted") {
|
|
202
|
+
const detail = run.detail === undefined ? "" : ` (${run.detail})`;
|
|
203
|
+
lines.push({ level: "error", message: `No answer: ${run.reason}${detail}` });
|
|
204
|
+
}
|
|
205
|
+
for (const [part, error] of Object.entries(view.stalled ?? {})) {
|
|
206
|
+
if (error === undefined) continue;
|
|
207
|
+
const what = part === "tasks" ? "The task panel" : "The conversation";
|
|
208
|
+
lines.push({ level: "error", message: `${what} stopped updating: ${error}` });
|
|
194
209
|
}
|
|
195
210
|
return [...lines, ...view.notices.slice(-4)];
|
|
196
211
|
}
|
|
@@ -289,7 +304,8 @@ class DurableTui {
|
|
|
289
304
|
#summaries = [];
|
|
290
305
|
/** Tool output and summaries shown in full; toggled like pi. */
|
|
291
306
|
#expanded = false;
|
|
292
|
-
|
|
307
|
+
/** One key per transcript item on screen, in order (`keyOf`). */
|
|
308
|
+
#renderedKeys = [];
|
|
293
309
|
#streaming ;
|
|
294
310
|
#indicator ;
|
|
295
311
|
#statusText = "";
|
|
@@ -391,10 +407,11 @@ class DurableTui {
|
|
|
391
407
|
|
|
392
408
|
apply(view ) {
|
|
393
409
|
const live = (view.conversation.docs["pi.live"] ?? {}) ;
|
|
394
|
-
|
|
410
|
+
// The active transcript, not the window: every entry of the active range, head marker first (#155 D1).
|
|
411
|
+
this.#syncTranscript(view.transcript.items);
|
|
395
412
|
const message = live.generation?.message ;
|
|
396
413
|
// A partial without its entry was dropped, for example by a retry: render the transcript again.
|
|
397
|
-
if (message === undefined && this.#streaming !== undefined) this.#rebuild(view.
|
|
414
|
+
if (message === undefined && this.#streaming !== undefined) this.#rebuild(view.transcript.items);
|
|
398
415
|
if (message !== undefined) this.#syncStreaming(message);
|
|
399
416
|
for (const slot of live.tools ?? []) {
|
|
400
417
|
if (slot.status === "pending") continue;
|
|
@@ -476,8 +493,8 @@ class DurableTui {
|
|
|
476
493
|
} else if (generation?.deferred !== undefined) text = "Waiting for deferred response...";
|
|
477
494
|
else if (compaction !== undefined) {
|
|
478
495
|
text = compaction.retry
|
|
479
|
-
? `Retrying ${compaction.
|
|
480
|
-
: `Compacting (${compaction.
|
|
496
|
+
? `Retrying ${compaction.trigger} compaction (attempt ${compaction.attempt + 1})...`
|
|
497
|
+
: `Compacting (${compaction.trigger})...`;
|
|
481
498
|
} else if (runningTool !== undefined) text = `Running ${runningTool.name}... (esc to abort)`;
|
|
482
499
|
else if (live.run !== undefined) text = "Working... (esc to abort)";
|
|
483
500
|
if (text === this.#statusText) return;
|
|
@@ -502,7 +519,7 @@ class DurableTui {
|
|
|
502
519
|
view.models.find((model) => model.provider === agent.model?.provider && model.modelId === agent.model.modelId)
|
|
503
520
|
?.contextWindow ?? 0;
|
|
504
521
|
if (contextWindow > 0) {
|
|
505
|
-
const tokens = contextTokens(view.
|
|
522
|
+
const tokens = contextTokens(entriesOf(view.transcript.items));
|
|
506
523
|
const percent = tokens === undefined ? undefined : (tokens / contextWindow) * 100;
|
|
507
524
|
const text = `${percent === undefined ? "?" : percent.toFixed(1)}%/${formatTokens(contextWindow)}`;
|
|
508
525
|
stats.push(percent !== undefined && percent > 90 ? theme.fg("error", text) : text);
|
|
@@ -519,28 +536,38 @@ class DurableTui {
|
|
|
519
536
|
);
|
|
520
537
|
}
|
|
521
538
|
|
|
522
|
-
#syncTranscript(
|
|
523
|
-
//
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
539
|
+
#syncTranscript(items ) {
|
|
540
|
+
// The transcript grows at its end. A compaction or reset starts a new active range, its head marker first,
|
|
541
|
+
// which replaces what came before (#155 D1).
|
|
542
|
+
const keys = items.map(keyOf);
|
|
543
|
+
if (this.#renderedKeys.some((key, index) => keys[index] !== key)) this.#rebuild(items);
|
|
544
|
+
for (const item of items.slice(this.#renderedKeys.length)) {
|
|
545
|
+
this.#addItem(item);
|
|
546
|
+
this.#renderedKeys.push(keyOf(item));
|
|
528
547
|
}
|
|
529
548
|
}
|
|
530
549
|
|
|
531
|
-
#rebuild(
|
|
550
|
+
#rebuild(items ) {
|
|
532
551
|
this.#chat.clear();
|
|
533
552
|
this.#discardTools();
|
|
534
553
|
this.#summaries.length = 0;
|
|
535
554
|
this.#rebuilt = true;
|
|
536
|
-
this.#
|
|
555
|
+
this.#renderedKeys = [];
|
|
537
556
|
this.#streaming = undefined;
|
|
538
|
-
for (const
|
|
539
|
-
this.#
|
|
540
|
-
this.#
|
|
557
|
+
for (const item of items) {
|
|
558
|
+
this.#addItem(item);
|
|
559
|
+
this.#renderedKeys.push(keyOf(item));
|
|
541
560
|
}
|
|
542
561
|
}
|
|
543
562
|
|
|
563
|
+
#addItem(item ) {
|
|
564
|
+
if (item.kind === "entry") this.#addEntry(item.entry);
|
|
565
|
+
// The runtime's active transcript holds entries only; these two placeholders are the SDK item type's other kinds
|
|
566
|
+
// (#156 owns any load-more input).
|
|
567
|
+
else if (item.kind === "earlier") this.#addText("[earlier entries not shown]");
|
|
568
|
+
else this.#addText("[missing entries, loading...]");
|
|
569
|
+
}
|
|
570
|
+
|
|
544
571
|
#addEntry(entry ) {
|
|
545
572
|
const message = entry.model?.[0];
|
|
546
573
|
if (entry.kind === "pi.user" && message?.role === "user") {
|
|
@@ -676,9 +703,20 @@ function totalUsage(state ) {
|
|
|
676
703
|
return total;
|
|
677
704
|
}
|
|
678
705
|
|
|
706
|
+
/** The entries of transcript items, in ID order. */
|
|
707
|
+
function entriesOf(items ) {
|
|
708
|
+
return items.flatMap((item) => (item.kind === "entry" ? [item.entry] : []));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** What was rendered for an item stays valid while the same key stays in the same place. */
|
|
712
|
+
function keyOf(item ) {
|
|
713
|
+
if (item.kind === "entry") return `entry:${item.entry.id}`;
|
|
714
|
+
return item.kind === "gap" ? `gap:${item.after}:${item.upTo}` : "earlier";
|
|
715
|
+
}
|
|
716
|
+
|
|
679
717
|
/** Context size from the newest successful answer after the newest compaction; unknown before one. */
|
|
680
718
|
function contextTokens(entries ) {
|
|
681
|
-
//
|
|
719
|
+
// Entries a summary keeps and those it replaced are older than it; only later answers measure the new context.
|
|
682
720
|
const compacted = Math.max(0, ...entries.filter((entry) => entry.kind === "pi.compaction").map((entry) => entry.id));
|
|
683
721
|
for (const entry of [...entries].reverse()) {
|
|
684
722
|
const message = entry.model?.[0];
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"carrier": {
|
|
58
|
-
"comment": "files: the six app files bootstrap.mjs needs (main.ts is replaced by the bootstrap), each emitted from its owned `source`. distTargets: every pi-coding-agent internal the stripped files import, as paths under the published dist/; the emitter refuses a set that differs and the resolver answers no other.",
|
|
58
|
+
"comment": "files: the six app files bootstrap.mjs needs (main.ts is replaced by the bootstrap) and the import-free session-name grammar Entwurf owns beside them (#157 G3 B-L1, no upstream origin), each emitted from its owned `source`. distTargets: every pi-coding-agent internal the stripped files import, as paths under the published dist/; the emitter refuses a set that differs and the resolver answers no other.",
|
|
59
59
|
"emitter": {
|
|
60
60
|
"tool": "node:module.stripTypeScriptTypes",
|
|
61
61
|
"mode": "strip",
|
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
{
|
|
71
71
|
"name": "runtime.js",
|
|
72
72
|
"source": "pi/pi-durable/src/experimental/durable/runtime.ts",
|
|
73
|
-
"blob": "
|
|
74
|
-
"sha256": "
|
|
73
|
+
"blob": "00646b1b1fbd7e935fb3e66b7578a2befc01f3c8",
|
|
74
|
+
"sha256": "b47113364b7525abd3bf9286ecb0069f78ac96688b8a271bdbca3a54b9d864c5"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"name": "tui.js",
|
|
78
78
|
"source": "pi/pi-durable/src/experimental/durable/tui.ts",
|
|
79
|
-
"blob": "
|
|
80
|
-
"sha256": "
|
|
79
|
+
"blob": "4b5dd1876ceac8f51910bb11ed5f0024003f4f34",
|
|
80
|
+
"sha256": "7fefe490d9d462e6a91621901b82b767546926f4a9f4e88d585fe74b727a02c1"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "harness-setup.js",
|
|
@@ -94,14 +94,20 @@
|
|
|
94
94
|
{
|
|
95
95
|
"name": "sessions.js",
|
|
96
96
|
"source": "pi/pi-durable/src/experimental/durable/sessions.ts",
|
|
97
|
-
"blob": "
|
|
98
|
-
"sha256": "
|
|
97
|
+
"blob": "f107238fefb63c95b29add5aa76fcbd9153c94db",
|
|
98
|
+
"sha256": "0ac7c919f2bf8a3b0e5dbd307045108a803b161f87298cd9fe440f239784c7ac"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"name": "subagent.js",
|
|
102
102
|
"source": "pi/pi-durable/src/experimental/durable/subagent.ts",
|
|
103
|
-
"blob": "
|
|
104
|
-
"sha256": "
|
|
103
|
+
"blob": "e842d8a6b8dc55825d0381d803b6d6496b205c19",
|
|
104
|
+
"sha256": "c9931fabd60f5fbf75220e896a57e5c8b131b4936887bf6f9660d50d01709f3a"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "session-name.js",
|
|
108
|
+
"source": "pi/pi-durable/src/experimental/durable/session-name.ts",
|
|
109
|
+
"blob": "c57c760ca3c14363f195ac9d9c2add984e4118d5",
|
|
110
|
+
"sha256": "9eadecfe9a48bba0d221370f4a236286e0d6f10d663b15ff4cb5975adee67018"
|
|
105
111
|
}
|
|
106
112
|
],
|
|
107
113
|
"distTargets": [
|
|
@@ -132,17 +138,18 @@
|
|
|
132
138
|
]
|
|
133
139
|
},
|
|
134
140
|
"sdk": {
|
|
135
|
-
"comment": "The published library set the carrier runs against, derived from the 1.
|
|
141
|
+
"comment": "The published library set the carrier runs against, derived from the 1.5.0 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare (1.5.0 adds pi-schema, declared by chord, pi-agent-core, pi-ai and pi-durable). direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.5.0 pins). Each is an exact production dependency in package.json. Since 1.3.0 the members' own edges are exact too (upstream #10865), and the installed physical edges are still what must equal these versions.",
|
|
136
142
|
"members": {
|
|
137
|
-
"@earendil-works/chord": "1.
|
|
138
|
-
"@earendil-works/pi-agent-core": "1.
|
|
139
|
-
"@earendil-works/pi-ai": "1.
|
|
140
|
-
"@earendil-works/pi-codemode": "1.
|
|
141
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
142
|
-
"@earendil-works/pi-durable": "1.
|
|
143
|
-
"@earendil-works/pi-mcp": "1.
|
|
144
|
-
"@earendil-works/pi-
|
|
145
|
-
"@earendil-works/pi-
|
|
143
|
+
"@earendil-works/chord": "1.5.0",
|
|
144
|
+
"@earendil-works/pi-agent-core": "1.5.0",
|
|
145
|
+
"@earendil-works/pi-ai": "1.5.0",
|
|
146
|
+
"@earendil-works/pi-codemode": "1.5.0",
|
|
147
|
+
"@earendil-works/pi-coding-agent": "1.5.0",
|
|
148
|
+
"@earendil-works/pi-durable": "1.5.0",
|
|
149
|
+
"@earendil-works/pi-mcp": "1.5.0",
|
|
150
|
+
"@earendil-works/pi-schema": "1.5.0",
|
|
151
|
+
"@earendil-works/pi-telemetry": "1.5.0",
|
|
152
|
+
"@earendil-works/pi-tui": "1.5.0"
|
|
146
153
|
},
|
|
147
154
|
"direct": {
|
|
148
155
|
"proper-lockfile": "4.1.2"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* entwurf-v2-contract-schema — the pi-side
|
|
3
|
-
* `entwurf_v2` contract. This module exists ONLY to carry the pi
|
|
4
|
-
*
|
|
2
|
+
* entwurf-v2-contract-schema — the pi-side JSON Schema REPRESENTATION of the frozen
|
|
3
|
+
* `entwurf_v2` contract. This module exists ONLY to carry the pi schema builders
|
|
4
|
+
* (pi-ai's `StringEnum`, pi-schema's `obj`/`str`/…) out of the pi-free core
|
|
5
5
|
* (`entwurf-v2-contract.ts`); the constants/types/decision logic live there.
|
|
6
6
|
*
|
|
7
7
|
* ⚠️ pi LANE ONLY — the MCP bridge (`mcp/entwurf-bridge/src/index.ts`) MUST NOT
|
|
@@ -11,18 +11,21 @@
|
|
|
11
11
|
* the contract CONSTANTS + `resolveDispatch` (pi-free core), never these schemas.
|
|
12
12
|
* Consumer: `check-entwurf-v2-contract` ONLY. This is a gate-side REPRESENTATION of the
|
|
13
13
|
* frozen contract, not a shipped tool schema — measured 2026-07-27, nothing else in the
|
|
14
|
-
* import graph reaches it. The
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* import graph reaches it. The surface a model actually reads builds its own:
|
|
15
|
+
* `mcp/entwurf-bridge/src/index.ts` (Zod, descriptions from ENTWURF.json), which a pi citizen
|
|
16
|
+
* reaches through Pi's built-in MCP since the native pi tools were retired (2f7c883, Pi 0.99).
|
|
17
|
+
* Change a model-facing description THERE; changing it here reaches no model.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
19
|
+
* Pi SDK 1.5 (#155) removed pi-ai's TypeBox re-export (`Type`); its builders are now
|
|
20
|
+
* `@earendil-works/pi-schema`, which emits plain JSON Schema (obj() closed by default,
|
|
21
|
+
* opt() leaves a property out of `required`, lit() is `{const}`), and pi-ai's
|
|
22
|
+
* `StringEnum` still emits `{type:"string", enum}`. The logic types in the core are
|
|
23
|
+
* hand-written unions, NOT `Static<>` inferences; `check-entwurf-v2-contract` keeps
|
|
24
|
+
* these schemas and the core types in lockstep.
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
|
-
import { StringEnum
|
|
27
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
28
|
+
import { bool, lit, nul, obj, opt, type Properties, type Schema, str, union } from "@earendil-works/pi-schema";
|
|
26
29
|
import {
|
|
27
30
|
ENTWURF_INTENTS,
|
|
28
31
|
ENTWURF_V2_ACTIONS,
|
|
@@ -34,36 +37,43 @@ import {
|
|
|
34
37
|
} from "./entwurf-v2-contract.ts";
|
|
35
38
|
import { SESSION_ID_RE } from "./session-id.js";
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The face the drift guard reads. pi-schema types a schema as `{[keyword]: unknown}`; obj() always emits
|
|
42
|
+
* `properties`, so this names that one keyword without asserting anything else about the shape.
|
|
43
|
+
*/
|
|
44
|
+
type ObjectSchema = Schema & { readonly properties: Readonly<Record<string, Schema>> };
|
|
45
|
+
|
|
46
|
+
/** obj() — closed, so it emits `additionalProperties: false` — under the face above. */
|
|
47
|
+
const closedObject = (properties: Properties): ObjectSchema => obj(properties) as ObjectSchema;
|
|
48
|
+
|
|
49
|
+
export const EntwurfV2InputSchema = closedObject({
|
|
50
|
+
// R2/F6 executable: the garden-id shape is enforced by pattern, not prose —
|
|
51
|
+
// a malformed/typo gid fails the schema (→ bad-target) and can never reach a
|
|
52
|
+
// spawn. SSOT regex = SESSION_ID_RE (pi-extensions/lib/session-id.js).
|
|
53
|
+
target: str({
|
|
54
|
+
pattern: SESSION_ID_RE.source,
|
|
55
|
+
description:
|
|
56
|
+
"garden-id of an EXISTING citizen (pattern-enforced). Fresh sibling creation is out of scope on every current surface — there is no fallback verb that still spawns. A malformed/typo gid is bad-target, never a new spawn.",
|
|
57
|
+
}),
|
|
58
|
+
intent: StringEnum(ENTWURF_INTENTS, {
|
|
59
|
+
description:
|
|
60
|
+
"caller's declared outcome contract (F1). ONE member since the visible-first cut: fire-and-forget = ack only. The second, owned-outcome (caller owns completion), resumed a dormant citizen by launching a hidden background child and was withdrawn — a dormant citizen is currently unreachable by this verb.",
|
|
61
|
+
}),
|
|
62
|
+
mode: opt(
|
|
63
|
+
StringEnum(ENTWURF_V2_MODES, {
|
|
44
64
|
description:
|
|
45
|
-
"
|
|
65
|
+
"delivery mode — NOT the ownership axis (F1) nor liveness routing. steer (ask the receiver's steering queue) or follow_up (ask its follow-up queue). NEITHER is an interrupt: a busy receiver drains steering after each turn and follow-ups only when its inner loop ends, so a later steer overtakes every earlier follow_up and there is no order between the two. Both queues are volatile process memory — an abort drops what is in them. The receipt names which queue accepted the message (`queued-steer` / `queued-follow-up`); an idle receiver answers `sent`, meaning the turn was TRIGGERED, not that the model has seen the text. MEANINGLESS on the meta-mailbox transport (F-mailbox): a mailbox ack is enqueue+doorbell, not a turn injection, so steer/follow_up does not apply when the verdict transport is meta-mailbox.",
|
|
46
66
|
}),
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
),
|
|
68
|
+
wantsReply: opt(
|
|
69
|
+
bool({
|
|
70
|
+
description: "conversation etiquette only — NOT ownership; never triggers an auto-send (Q2).",
|
|
50
71
|
}),
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
),
|
|
57
|
-
wantsReply: Type.Optional(
|
|
58
|
-
Type.Boolean({
|
|
59
|
-
description: "conversation etiquette only — NOT ownership; never triggers an auto-send (Q2).",
|
|
60
|
-
}),
|
|
61
|
-
),
|
|
62
|
-
// `additionalProperties: false` — a frozen contract input is exact; an unknown
|
|
63
|
-
// key is a caller error, not silently ignored.
|
|
64
|
-
},
|
|
65
|
-
{ additionalProperties: false },
|
|
66
|
-
);
|
|
72
|
+
),
|
|
73
|
+
// `additionalProperties: false` — a frozen contract input is exact; an unknown
|
|
74
|
+
// key is a caller error, not silently ignored. obj() is closed by default (it
|
|
75
|
+
// emits `additionalProperties: false` unless `open: true`).
|
|
76
|
+
});
|
|
67
77
|
|
|
68
78
|
// Receipt = a DISCRIMINATED union on `ok` (R3/F6) — NOT one flat object with
|
|
69
79
|
// optionals. Each branch is EXACT (`additionalProperties: false`): without it,
|
|
@@ -72,35 +82,29 @@ export const EntwurfV2InputSchema = Type.Object(
|
|
|
72
82
|
// With it, success carries action/transport/ownership and rejects a stray reason;
|
|
73
83
|
// reject carries reason and rejects any allow facet — the branches are mutually
|
|
74
84
|
// exclusive at the schema level, not merely by declared-property convention.
|
|
75
|
-
export const EntwurfV2ReceiptSuccessSchema =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
{ additionalProperties: false },
|
|
86
|
-
);
|
|
85
|
+
export const EntwurfV2ReceiptSuccessSchema = closedObject({
|
|
86
|
+
ok: lit(true),
|
|
87
|
+
action: StringEnum(ENTWURF_V2_ACTIONS),
|
|
88
|
+
transport: StringEnum(ENTWURF_V2_TRANSPORTS),
|
|
89
|
+
ownership: StringEnum(ENTWURF_V2_OWNERSHIPS),
|
|
90
|
+
observedLiveness: StringEnum(FACT_LIVENESSES, {
|
|
91
|
+
description: "the 4-value fact liveness the verdict was computed from (R1/R3).",
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
87
94
|
|
|
88
|
-
export const EntwurfV2ReceiptRejectSchema =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
},
|
|
103
|
-
{ additionalProperties: false },
|
|
104
|
-
);
|
|
95
|
+
export const EntwurfV2ReceiptRejectSchema = closedObject({
|
|
96
|
+
ok: lit(false),
|
|
97
|
+
reason: StringEnum(ENTWURF_V2_REJECT_REASONS),
|
|
98
|
+
// ?6: required-nullable, NOT optional — a reject branch ALWAYS carries the
|
|
99
|
+
// key, and it is `null` for the pre-probe rejects (PRE_PROBE_REJECT_REASONS)
|
|
100
|
+
// and a real FactLiveness otherwise. Optional would lose the "key always
|
|
101
|
+
// present, value may be null" shape and weaken the discriminated union; the
|
|
102
|
+
// reason-dependent null/non-null rule is enforced semantically (the gate's
|
|
103
|
+
// rejectObservedLivenessWellFormed fixture), not by this blanket union.
|
|
104
|
+
observedLiveness: union([StringEnum(FACT_LIVENESSES), nul()], {
|
|
105
|
+
description:
|
|
106
|
+
"the 4-value fact liveness the reject was computed from (R1/R3); null for the pre-probe rejects (bad-target / target-locked / target-address-conflict) where no probe ran.",
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
105
109
|
|
|
106
|
-
export const EntwurfV2ReceiptSchema =
|
|
110
|
+
export const EntwurfV2ReceiptSchema = union([EntwurfV2ReceiptSuccessSchema, EntwurfV2ReceiptRejectSchema]);
|