@junghanacs/entwurf 0.30.1 → 0.31.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 +1 -0
- package/CHANGELOG.md +112 -0
- package/DELIVERY.md +1 -0
- package/README.md +47 -7
- package/VERIFY.md +17 -1
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +104 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +7 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/entwurf-capabilities.json +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +34 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +28 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +30 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +92 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +288 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +744 -0
- package/mcp/entwurf-bridge/src/index.ts +7 -7
- package/mcp/entwurf-bridge/tsconfig.build.json +8 -1
- package/package.json +13 -10
- package/pi/entwurf-capabilities.json +1 -0
- package/pi/pi-durable/bootstrap.mjs +281 -0
- package/pi/pi-durable/overlay/runtime-contacts.patch +91 -0
- package/pi/pi-durable/overlay/upstream-pin.json +16 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +7 -1
- package/pi-extensions/lib/fresh-call-composition.ts +42 -7
- package/pi-extensions/lib/meta-sender-identity.ts +7 -1
- package/pi-extensions/lib/meta-session.ts +28 -2
- package/pi-extensions/lib/mux-fresh-call.ts +34 -2
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +113 -0
- package/pi-extensions/lib/pi-durable-runtime.ts +384 -0
- package/pi-extensions/meta-bridge-pi-durable.ts +1020 -0
- package/run.sh +200 -33
- package/scripts/check-fresh-call-dispatch.ts +18 -0
- package/scripts/check-gate-qualification.ts +2 -1
- package/scripts/check-pi-durable-contact.ts +834 -0
- package/scripts/check-pi-durable-receive.ts +1116 -0
- package/scripts/check-pi-durable-send.ts +1661 -0
- package/scripts/lib/pi-durable-fresh-live-receipts.ts +139 -0
- package/scripts/lib/pi-durable-r1.ts +100 -0
- package/scripts/lib/pi-durable-reap.ts +27 -0
- package/scripts/lib/pi-durable-receive.ts +137 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/copilot-birth.json +2 -2
- package/scripts/mutants/meta-hook-session-switch.json +4 -2
- package/scripts/mutants/mux-fresh-call.json +69 -3
- package/scripts/mutants/omp-birth.json +2 -2
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +390 -0
- package/scripts/mutants/pi-floor.json +2 -2
- package/scripts/pi-durable-contact-host.mjs +105 -0
- package/scripts/pi-durable-receive-host.mjs +127 -0
- package/scripts/pi-durable-send-host.mjs +213 -0
- package/scripts/smoke-pi-durable-fresh-live.ts +624 -0
- package/scripts/smoke-setup-verdict.sh +21 -2
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// G-send host half (#129 L-send first cell). Runs AS the durable host, under the pinned overlay
|
|
2
|
+
// checkout's source resolver, and is spawned only by scripts/check-pi-durable-send.ts:
|
|
3
|
+
//
|
|
4
|
+
// node --import <checkout>/packages/coding-agent/src/experimental/source-resolver.ts \
|
|
5
|
+
// scripts/pi-durable-send-host.mjs <bridge entry> <prompt>
|
|
6
|
+
//
|
|
7
|
+
// It opens the durable app through the production bootstrap composition, then gives the app ONE
|
|
8
|
+
// user input through its native controller — the call its TUI makes when the operator presses
|
|
9
|
+
// enter — and watches only the native view. Everything after that input is the app's own: the
|
|
10
|
+
// model request, the GenerationTask, the ToolTask, the adapter's bridge call. This driver never
|
|
11
|
+
// calls a tool or the bridge itself.
|
|
12
|
+
//
|
|
13
|
+
// `controller.submit` settling is NOT the turn completing: the controller queues the command and
|
|
14
|
+
// turns its failures into notices (runtime.ts:229-252). The driver waits on the view instead —
|
|
15
|
+
// the run document is present exactly while the conversation is busy — and reports what the view
|
|
16
|
+
// holds. It prints one JSON report line, holds the host alive until the gate writes `close`,
|
|
17
|
+
// closes, prints `{"closed":true}` and exits. With ENTWURF_PI_DURABLE_SEND_FAULT=malformed-report
|
|
18
|
+
// (the gate's control) it gives the app no input and sends a non-JSON report line instead.
|
|
19
|
+
import { createInterface } from "node:readline";
|
|
20
|
+
import { openDurableCitizen } from "../plugins/pi-durable/bootstrap.mjs";
|
|
21
|
+
|
|
22
|
+
const USAGE =
|
|
23
|
+
"usage: pi-durable-send-host.mjs <bridge entry> <prompt> [--await-go] [--report-intent <callId> | --recover <callId>]";
|
|
24
|
+
const [bridgeEntry, prompt, ...flags] = process.argv.slice(2);
|
|
25
|
+
if (bridgeEntry === undefined || prompt === undefined) throw new Error(USAGE);
|
|
26
|
+
// --await-go: report `born` once the citizen exists, and give the app its one input only after the
|
|
27
|
+
// gate writes `go` — test-driver sequencing so the gate can read the bridge's counters first. It is
|
|
28
|
+
// not a second input and not another controller.
|
|
29
|
+
// --report-intent <callId>: after the input, watch the native view and report ONCE the frame in
|
|
30
|
+
// which the task graph shows a `pi.tool` node running in phase `execute` and the live slot with the
|
|
31
|
+
// same taskId is that call, running. The phase is the task's committed checkpoint (task-graph.ts
|
|
32
|
+
// 210-221), written in the same commit as the slot (tool.ts 84-90). Then do nothing: the gate kills
|
|
33
|
+
// this process.
|
|
34
|
+
// --recover <callId>: continue the newest session, give the app NO input, and report once the view
|
|
35
|
+
// holds that call's settled result and the conversation is idle.
|
|
36
|
+
let awaitGo = false;
|
|
37
|
+
let reportIntent;
|
|
38
|
+
let recoverCall;
|
|
39
|
+
for (let i = 0; i < flags.length; i++) {
|
|
40
|
+
if (flags[i] === "--await-go") awaitGo = true;
|
|
41
|
+
else if (flags[i] === "--report-intent" && flags[i + 1] !== undefined) reportIntent = flags[++i];
|
|
42
|
+
else if (flags[i] === "--recover" && flags[i + 1] !== undefined) recoverCall = flags[++i];
|
|
43
|
+
else throw new Error(USAGE);
|
|
44
|
+
}
|
|
45
|
+
if (reportIntent !== undefined && recoverCall !== undefined) throw new Error(USAGE);
|
|
46
|
+
|
|
47
|
+
const stdinLines = createInterface({ input: process.stdin })[Symbol.asyncIterator]();
|
|
48
|
+
/** Wait for one exact line from the gate; the gate closing stdin first is a failure. */
|
|
49
|
+
async function waitFor(word) {
|
|
50
|
+
for (;;) {
|
|
51
|
+
const next = await stdinLines.next();
|
|
52
|
+
if (next.done) throw new Error(`stdin closed before "${word}"`);
|
|
53
|
+
if (next.value.trim() === word) return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One entry of the native view, reduced to what the gate compares. */
|
|
58
|
+
function summarize(entry) {
|
|
59
|
+
const message = entry.model?.[0];
|
|
60
|
+
const blocks = Array.isArray(message?.content) ? message.content : [];
|
|
61
|
+
const text = (typeof message?.content === "string" ? [message.content] : [])
|
|
62
|
+
.concat(blocks.flatMap((b) => (b.type === "text" ? [b.text] : [])))
|
|
63
|
+
.join("\n");
|
|
64
|
+
return {
|
|
65
|
+
kind: entry.kind,
|
|
66
|
+
role: message?.role,
|
|
67
|
+
toolCalls: blocks
|
|
68
|
+
.filter((b) => b.type === "toolCall")
|
|
69
|
+
.map((b) => ({ id: b.id, name: b.name, arguments: b.arguments })),
|
|
70
|
+
...(message?.role === "toolResult"
|
|
71
|
+
? {
|
|
72
|
+
toolCallId: message.toolCallId,
|
|
73
|
+
toolName: message.toolName,
|
|
74
|
+
isError: message.isError === true,
|
|
75
|
+
// The entry's own diagnostics (`data.diagnostics`), copied as they are.
|
|
76
|
+
diagnostics: Array.isArray(entry.data?.diagnostics) ? entry.data.diagnostics : [],
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
79
|
+
text,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const citizen = await openDurableCitizen({
|
|
84
|
+
bridgeEntry,
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
continueSession: recoverCall !== undefined,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** The frame the gate needs: graph node + same-task live slot, as the native view holds them. */
|
|
90
|
+
function intentFrame(view, callId) {
|
|
91
|
+
const nodes = Object.values(view.tasks?.tasks ?? {});
|
|
92
|
+
const slots = view.conversation.docs["pi.live"]?.tools ?? [];
|
|
93
|
+
for (const node of nodes) {
|
|
94
|
+
if (node.kind !== "pi.tool" || node.conversationId !== 1) continue;
|
|
95
|
+
if (node.state?.status !== "running" || node.state?.phase !== "execute") continue;
|
|
96
|
+
const slot = slots.find((s) => s.taskId === node.id);
|
|
97
|
+
if (slot && slot.name === "entwurf_v2" && slot.callId === callId && slot.status === "running") {
|
|
98
|
+
return { node, slot, nodes, slots };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function reportOf(view, before) {
|
|
105
|
+
return {
|
|
106
|
+
hostPid: process.pid,
|
|
107
|
+
session: view.session,
|
|
108
|
+
attachment: citizen.attachment,
|
|
109
|
+
conversationIdBefore: before.conversation.conversation.id,
|
|
110
|
+
conversationIdAfter: view.conversation.conversation.id,
|
|
111
|
+
entries: view.conversation.entries.filter((e) => e.kind.startsWith("pi.")).map(summarize),
|
|
112
|
+
busy: view.conversation.docs["pi.live"]?.run !== undefined,
|
|
113
|
+
notices: view.notices.map((n) => `${n.level}: ${n.message}`),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
let code = 0;
|
|
117
|
+
try {
|
|
118
|
+
if (process.env.ENTWURF_PI_DURABLE_SEND_FAULT === "malformed-report") {
|
|
119
|
+
// The gate's before-valid-report cleanup control: born, no input given to the app, a report
|
|
120
|
+
// line that is deliberately not JSON, then alive until told to close.
|
|
121
|
+
process.stdout.write(`{"malformed send report line\n`);
|
|
122
|
+
await waitFor("close");
|
|
123
|
+
throw new Error("malformed-report control closed");
|
|
124
|
+
}
|
|
125
|
+
const { durable } = citizen;
|
|
126
|
+
if (recoverCall !== undefined) {
|
|
127
|
+
// Recovery may already have run inside openDurable (resume precedes attach): check the current
|
|
128
|
+
// view as well as every later one, so a settled outcome is never missed.
|
|
129
|
+
const before = durable.view.current();
|
|
130
|
+
await new Promise((resolve) => {
|
|
131
|
+
let done = false;
|
|
132
|
+
const check = () => {
|
|
133
|
+
if (done) return;
|
|
134
|
+
const view = durable.view.current();
|
|
135
|
+
const entries = view.conversation.entries;
|
|
136
|
+
const resultAt = entries.findIndex(
|
|
137
|
+
(e) => e.kind === "pi.tool-result" && e.model?.[0]?.toolCallId === recoverCall,
|
|
138
|
+
);
|
|
139
|
+
const answeredAfter = resultAt >= 0 && entries.slice(resultAt + 1).some((e) => e.kind === "pi.assistant");
|
|
140
|
+
const busy = view.conversation.docs["pi.live"]?.run !== undefined;
|
|
141
|
+
const failed = view.notices.some((n) => n.level === "error");
|
|
142
|
+
if ((answeredAfter && !busy) || failed) {
|
|
143
|
+
done = true;
|
|
144
|
+
unsubscribe();
|
|
145
|
+
resolve();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const unsubscribe = durable.view.subscribe(check);
|
|
149
|
+
check();
|
|
150
|
+
});
|
|
151
|
+
process.stdout.write(`${JSON.stringify({ recovered: true, ...reportOf(durable.view.current(), before) })}\n`);
|
|
152
|
+
await waitFor("close");
|
|
153
|
+
throw new Error("__closed__");
|
|
154
|
+
}
|
|
155
|
+
if (awaitGo) {
|
|
156
|
+
process.stdout.write(
|
|
157
|
+
`${JSON.stringify({ born: true, hostPid: process.pid, attachment: citizen.attachment, session: durable.view.current().session })}\n`,
|
|
158
|
+
);
|
|
159
|
+
await waitFor("go");
|
|
160
|
+
}
|
|
161
|
+
if (reportIntent !== undefined) {
|
|
162
|
+
let reported = false;
|
|
163
|
+
const check = () => {
|
|
164
|
+
if (reported) return;
|
|
165
|
+
const view = durable.view.current();
|
|
166
|
+
const frame = intentFrame(view, reportIntent);
|
|
167
|
+
if (frame === undefined) return;
|
|
168
|
+
reported = true;
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`${JSON.stringify({ intent: true, hostPid: process.pid, session: view.session, frame, entries: view.conversation.entries.filter((e) => e.kind.startsWith("pi.")).map(summarize) })}\n`,
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
durable.view.subscribe(check);
|
|
174
|
+
durable.controller.submit(prompt, "followUp").then(check);
|
|
175
|
+
check();
|
|
176
|
+
// The gate kills this process after the frame; nothing else happens here.
|
|
177
|
+
await waitFor("close");
|
|
178
|
+
throw new Error("report-intent host was asked to close instead of being killed");
|
|
179
|
+
}
|
|
180
|
+
const before = durable.view.current();
|
|
181
|
+
const noticesBefore = before.notices.length;
|
|
182
|
+
let submitted = false;
|
|
183
|
+
const settled = new Promise((resolve) => {
|
|
184
|
+
const check = () => {
|
|
185
|
+
if (!submitted) return;
|
|
186
|
+
const view = durable.view.current();
|
|
187
|
+
const busy = view.conversation.docs["pi.live"]?.run !== undefined;
|
|
188
|
+
const answered = view.conversation.entries.some((e) => e.kind === "pi.assistant");
|
|
189
|
+
const failed = view.notices.slice(noticesBefore).some((n) => n.level === "error");
|
|
190
|
+
if ((!busy && answered) || failed) {
|
|
191
|
+
unsubscribe();
|
|
192
|
+
resolve();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
const unsubscribe = durable.view.subscribe(check);
|
|
196
|
+
durable.controller.submit(prompt, "followUp").then(() => {
|
|
197
|
+
submitted = true;
|
|
198
|
+
check();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
await settled;
|
|
202
|
+
process.stdout.write(`${JSON.stringify(reportOf(durable.view.current(), before))}\n`);
|
|
203
|
+
await waitFor("close");
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (!(error instanceof Error && error.message === "__closed__")) {
|
|
206
|
+
code = 1;
|
|
207
|
+
process.stdout.write(`${JSON.stringify({ error: error instanceof Error ? error.message : String(error) })}\n`);
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
await citizen.close();
|
|
211
|
+
}
|
|
212
|
+
process.stdout.write(`${JSON.stringify({ closed: true })}\n`);
|
|
213
|
+
process.exit(code);
|