@junghanacs/entwurf 0.31.0 → 0.32.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 +3 -2
- package/CHANGELOG.md +82 -0
- package/README.md +46 -36
- package/VERIFY.md +35 -16
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +112 -95
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +1 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +8 -10
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +153 -205
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +6 -4
- package/mcp/entwurf-bridge/src/index.ts +1 -1
- package/package.json +14 -17
- package/pi/pi-durable/bootstrap.mjs +142 -19
- package/pi/pi-durable/carrier/LICENSE +21 -0
- package/pi/pi-durable/carrier/harness-setup.js +123 -0
- package/pi/pi-durable/carrier/prompt.js +74 -0
- package/pi/pi-durable/carrier/runtime.js +402 -0
- package/pi/pi-durable/carrier/sessions.js +56 -0
- package/pi/pi-durable/carrier/subagent.js +55 -0
- package/pi/pi-durable/carrier/tui.js +655 -0
- package/pi/pi-durable/carrier-relocation.mjs +160 -0
- package/pi/pi-durable/carrier-resolver.mjs +38 -0
- package/pi/pi-durable/overlay/upstream-pin.json +103 -11
- package/pi-extensions/lib/fresh-call-composition.ts +1 -1
- package/pi-extensions/lib/mux-fresh-call.ts +8 -10
- package/pi-extensions/lib/pi-durable-fresh-preflight.ts +3 -3
- package/pi-extensions/lib/pi-durable-runtime.ts +197 -263
- package/pi-extensions/meta-bridge-pi-durable.ts +11 -7
- package/run.sh +508 -235
- package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
- package/scripts/check-fresh-call-dispatch.ts +1 -1
- package/scripts/check-gate-qualification.ts +86 -34
- package/scripts/check-herdr-activation.ts +5 -4
- package/scripts/check-pi-durable-contact.ts +26 -93
- package/scripts/check-pi-durable-receive.ts +22 -59
- package/scripts/check-pi-durable-send.ts +235 -75
- package/scripts/emit-pi-durable-carrier.ts +355 -0
- package/scripts/lib/mutation-qualify.ts +21 -45
- package/scripts/lib/pi-durable-operator-guard.ts +89 -0
- package/scripts/lib/qualification-receipt.ts +7 -11
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/gate-qualification.json +13 -0
- package/scripts/mutants/mux-fresh-call.json +13 -13
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +442 -109
- package/scripts/mutants/pi-floor.json +24 -2
- package/scripts/pi-durable-contact-host.mjs +3 -3
- package/scripts/pi-durable-receive-host.mjs +3 -3
- package/scripts/pi-durable-send-host.mjs +76 -5
- package/scripts/smoke-herdr-raw-install-live.sh +1 -1
- package/scripts/smoke-pi-durable-fresh-live.ts +3 -3
- package/scripts/smoke-setup-verdict.sh +15 -15
- package/scripts/smoke-user-scope-citizen.sh +1 -1
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
// (`withGardenIdentity`).
|
|
17
17
|
// Nothing of the native registry, controller or TUI is rebuilt here.
|
|
18
18
|
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
19
|
+
// The durable app itself is this package's CARRIER (./carrier/, the pinned upstream files emitted by
|
|
20
|
+
// the maintainer — upstream publishes no experimental app), and it runs under the carrier's resolver,
|
|
21
|
+
// which maps the app's pi-coding-agent internals to the published package the carrier resolves.
|
|
22
|
+
// `entwurf pi-durable` (run.sh) verifies the carrier and its SDK set (pi-durable-runtime.ts) and
|
|
23
|
+
// execs:
|
|
22
24
|
//
|
|
23
|
-
// node --import <
|
|
25
|
+
// node --import <entwurf>/pi/pi-durable/carrier-resolver.mjs \
|
|
24
26
|
// <entwurf>/pi/pi-durable/bootstrap.mjs \
|
|
25
27
|
// --provider <provider> --model <model id> --width task-wide [--entwurf-bootstrap <json>]
|
|
26
28
|
// ... bootstrap.mjs --continue
|
|
@@ -43,21 +45,36 @@
|
|
|
43
45
|
// `--continue` opens the newest session for this directory as upstream does (not a chosen
|
|
44
46
|
// record), so every flag that only a new session can honour is refused beside it by name.
|
|
45
47
|
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// `--native-module <absolute file>` (#130 P2) names ONE operator module, for a new or a continued
|
|
49
|
+
// session. It is an ordinary ES module: its own top-level evaluation (top-level await included) is
|
|
50
|
+
// its initialization, and its default export is a native durable Extension, handed to `openDurable`
|
|
51
|
+
// by reference after the contact. Initialization runs before the TUI or the runtime is imported —
|
|
52
|
+
// the runtime creates the provider runtime before it installs any extension (carrier/runtime.js
|
|
53
|
+
// 140-146), so an extension object cannot carry an environment phase. Entwurf checks only what the
|
|
54
|
+
// ingress needs: a default export object with a name, no extension name the app installs itself (a
|
|
55
|
+
// same-name install REPLACES), and no tool name of the contact. Every other field is the native
|
|
56
|
+
// registry's to validate. The module is trusted operator code: the identity carriers and the
|
|
57
|
+
// process directory are compared across its initialization and a change is refused by name — a
|
|
58
|
+
// mistake guard, not a sandbox, and nothing is rolled back. Nothing is discovered: no directory,
|
|
59
|
+
// setting, environment variable or profile names a module.
|
|
60
|
+
//
|
|
61
|
+
// Without that resolver the carrier's first pi-coding-agent internal import fails by name (an
|
|
62
|
+
// unsupported entwurf-pi-dist: URL scheme), and that failure is the honest one.
|
|
63
|
+
import { realpathSync, statSync } from "node:fs";
|
|
64
|
+
import { isAbsolute } from "node:path";
|
|
65
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
51
66
|
import { decodeOmpBootstrapPayload } from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-omp.js";
|
|
52
67
|
import {
|
|
53
68
|
closeAll,
|
|
54
69
|
createPiDurableContact,
|
|
55
70
|
failAfterCleanup,
|
|
71
|
+
PI_DURABLE_EXTENSION_NAME,
|
|
72
|
+
PI_DURABLE_TOOL_NAMES,
|
|
56
73
|
withGardenIdentity,
|
|
57
74
|
} from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js";
|
|
58
75
|
|
|
59
|
-
const RUNTIME = "
|
|
60
|
-
const TUI = "
|
|
76
|
+
const RUNTIME = "./carrier/runtime.js";
|
|
77
|
+
const TUI = "./carrier/tui.js";
|
|
61
78
|
|
|
62
79
|
/** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
|
|
63
80
|
export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
|
|
@@ -66,10 +83,19 @@ export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
|
|
|
66
83
|
|
|
67
84
|
/** Flags that take a value. Anything else but `--continue`/`-c` is an unknown argument — a bridge
|
|
68
85
|
* entry included: the launch argv cannot point the contact at another bridge. */
|
|
69
|
-
const VALUE_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
|
|
86
|
+
const VALUE_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap", "--native-module"];
|
|
70
87
|
/** Flags only a NEW session can honour. */
|
|
71
88
|
const FRESH_ONLY_FLAGS = ["--provider", "--model", "--width", "--entwurf-bootstrap"];
|
|
72
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Extension names the durable app installs itself — the contact, then the carrier's own
|
|
92
|
+
* (`coding-tools` pi-durable/tools, `pi-prompt` carrier/prompt.js:70, `subagent`
|
|
93
|
+
* carrier/subagent.js:25). The native registry REPLACES an installed extension of the same name.
|
|
94
|
+
*/
|
|
95
|
+
const RESERVED_EXTENSION_NAMES = [PI_DURABLE_EXTENSION_NAME, "coding-tools", "pi-prompt", "subagent"];
|
|
96
|
+
/** The identity carriers a module's initialization must leave as it found them, every ENTWURF_* included. */
|
|
97
|
+
const IDENTITY_KEYS = ["PI_SESSION_ID", "PI_CODING_AGENT_DIR", "HOME"];
|
|
98
|
+
|
|
73
99
|
/** A refusal with a stable name first, so a launcher and an operator can tell the repairs apart. */
|
|
74
100
|
function refuse(reason, detail) {
|
|
75
101
|
return Object.assign(new Error(`${reason}: ${detail}`), { reason });
|
|
@@ -77,6 +103,90 @@ function refuse(reason, detail) {
|
|
|
77
103
|
|
|
78
104
|
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
|
|
79
105
|
|
|
106
|
+
/** The identity carriers present now: names and values, compared in memory and never printed. */
|
|
107
|
+
function identitySnapshot() {
|
|
108
|
+
return Object.fromEntries(
|
|
109
|
+
Object.entries(process.env).filter(([key]) => IDENTITY_KEYS.includes(key) || key.startsWith("ENTWURF_")),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Keys changed, added or removed between two snapshots, sorted. */
|
|
114
|
+
function identityChanges(before, after) {
|
|
115
|
+
return [...new Set([...Object.keys(before), ...Object.keys(after)])]
|
|
116
|
+
.filter((key) => before[key] !== after[key])
|
|
117
|
+
.sort();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The operator's module path: absolute, resolving to a regular file. Decided before anything loads. */
|
|
121
|
+
function nativeModuleFile(value) {
|
|
122
|
+
if (!isAbsolute(value))
|
|
123
|
+
throw refuse("native-module-path-not-absolute", `--native-module takes an absolute path, got ${value}`);
|
|
124
|
+
let file;
|
|
125
|
+
try {
|
|
126
|
+
file = realpathSync(value);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw Object.assign(refuse("native-module-path-invalid", `${value} does not resolve`), { cause: error });
|
|
129
|
+
}
|
|
130
|
+
if (!statSync(file).isFile()) throw refuse("native-module-path-invalid", `${value} is not a regular file`);
|
|
131
|
+
return file;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The module's default export, checked only for what the ingress needs and returned BY REFERENCE. */
|
|
135
|
+
function nativeExtensionOf(extension, file) {
|
|
136
|
+
if (typeof extension === "function") {
|
|
137
|
+
throw refuse(
|
|
138
|
+
"native-module-export-invalid",
|
|
139
|
+
`${file} exports a function by default — a classic ExtensionAPI factory is not a native durable extension; export the Extension object ({ name, tools?, … })`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (typeof extension !== "object" || extension === null || Array.isArray(extension)) {
|
|
143
|
+
throw refuse("native-module-export-invalid", `${file} has no default export object`);
|
|
144
|
+
}
|
|
145
|
+
if (!nonEmpty(extension.name)) throw refuse("native-module-export-invalid", `${file}'s default export has no name`);
|
|
146
|
+
if (RESERVED_EXTENSION_NAMES.includes(extension.name)) {
|
|
147
|
+
throw refuse(
|
|
148
|
+
"native-module-name-reserved",
|
|
149
|
+
`${file} names its extension ${extension.name}, which the durable app installs itself; the same name would replace it`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const taken = (Array.isArray(extension.tools) ? extension.tools : [])
|
|
153
|
+
.map((tool) => tool?.name)
|
|
154
|
+
.filter((name) => PI_DURABLE_TOOL_NAMES.includes(name));
|
|
155
|
+
if (taken.length > 0) {
|
|
156
|
+
throw refuse("native-module-name-reserved", `${file} registers ${taken.join(", ")}, the contact's own tool name`);
|
|
157
|
+
}
|
|
158
|
+
return extension;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Initialize the operator's module and return its native Extension. The identity carriers and the
|
|
163
|
+
* process directory are read BEFORE its first line runs and compared once its evaluation completes.
|
|
164
|
+
*/
|
|
165
|
+
async function loadNativeModule(file) {
|
|
166
|
+
const before = identitySnapshot();
|
|
167
|
+
const cwd = process.cwd();
|
|
168
|
+
let namespace;
|
|
169
|
+
try {
|
|
170
|
+
namespace = await import(pathToFileURL(file).href);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw Object.assign(
|
|
173
|
+
refuse("native-module-import-failed", `${file}: ${error instanceof Error ? error.message : String(error)}`),
|
|
174
|
+
{ cause: error },
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
const changed = identityChanges(before, identitySnapshot());
|
|
178
|
+
if (changed.length > 0) {
|
|
179
|
+
throw refuse(
|
|
180
|
+
"native-module-env-identity-changed",
|
|
181
|
+
`initializing ${file} changed ${changed.join(", ")}; a module sets its own environment, never an identity carrier`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (process.cwd() !== cwd) {
|
|
185
|
+
throw refuse("native-module-cwd-changed", `initializing ${file} moved the process out of ${cwd}`);
|
|
186
|
+
}
|
|
187
|
+
return nativeExtensionOf(namespace.default, file);
|
|
188
|
+
}
|
|
189
|
+
|
|
80
190
|
/**
|
|
81
191
|
* Open the durable app with the contact installed, then birth the citizen and arm its doorbell on
|
|
82
192
|
* the root conversation, then admit the first input (when given) to the root once. On any failure
|
|
@@ -88,6 +198,8 @@ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0
|
|
|
88
198
|
* roots; tests also pass a connector). `open` replaces the runtime's `openDurable` for tests.
|
|
89
199
|
* `bridgeEntry` defaults to this package's own compiled bridge; only a programmatic caller (the
|
|
90
200
|
* checkout-only gates, which drive a private bundle) passes another one — never the launch argv.
|
|
201
|
+
* `extensions` are the operator's native extensions (`--native-module`), installed after the
|
|
202
|
+
* contact, by reference.
|
|
91
203
|
*
|
|
92
204
|
* @param {{
|
|
93
205
|
* bridgeEntry?: string,
|
|
@@ -95,6 +207,7 @@ const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0
|
|
|
95
207
|
* cwd?: string,
|
|
96
208
|
* model?: { provider: string, model: string },
|
|
97
209
|
* firstInput?: string,
|
|
210
|
+
* extensions?: readonly object[],
|
|
98
211
|
* contact?: Record<string, unknown>,
|
|
99
212
|
* open?: (options: { cwd: string, continueSession: boolean, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
|
|
100
213
|
* }} options
|
|
@@ -108,6 +221,7 @@ export async function openDurableCitizen({
|
|
|
108
221
|
cwd = process.cwd(),
|
|
109
222
|
model,
|
|
110
223
|
firstInput,
|
|
224
|
+
extensions = [],
|
|
111
225
|
contact: contactOptions = {},
|
|
112
226
|
open,
|
|
113
227
|
}) {
|
|
@@ -130,7 +244,7 @@ export async function openDurableCitizen({
|
|
|
130
244
|
durable = await openDurable({
|
|
131
245
|
cwd,
|
|
132
246
|
continueSession,
|
|
133
|
-
extensions: [contact.extension],
|
|
247
|
+
extensions: [contact.extension, ...extensions],
|
|
134
248
|
...(model === undefined ? {} : { model }),
|
|
135
249
|
});
|
|
136
250
|
} catch (error) {
|
|
@@ -205,7 +319,9 @@ function parseArgs(argv) {
|
|
|
205
319
|
`${fresh.join(", ")} apply only to a new session; --continue reopens this directory's newest session as it was saved`,
|
|
206
320
|
);
|
|
207
321
|
}
|
|
208
|
-
|
|
322
|
+
const moduleArg = values.get("--native-module");
|
|
323
|
+
const nativeModule = moduleArg === undefined ? {} : { nativeModule: nativeModuleFile(moduleArg) };
|
|
324
|
+
if (continueSession) return { continueSession, ...nativeModule };
|
|
209
325
|
const provider = values.get("--provider");
|
|
210
326
|
if (provider === undefined) {
|
|
211
327
|
throw refuse(
|
|
@@ -233,26 +349,33 @@ function parseArgs(argv) {
|
|
|
233
349
|
continueSession,
|
|
234
350
|
model: { provider, model },
|
|
235
351
|
...(firstInput === undefined ? {} : { firstInput }),
|
|
352
|
+
...nativeModule,
|
|
236
353
|
};
|
|
237
354
|
}
|
|
238
355
|
|
|
239
356
|
/**
|
|
240
|
-
* Parse, load the TUI, open, run, close. `loadTui`, `open` and
|
|
357
|
+
* Parse, initialize the operator's module, load the TUI, open, run, close. `loadTui`, `open` and
|
|
358
|
+
* `contact` are test seams; `bridgeEntry` is the checkout-only gates' private bundle, as in
|
|
359
|
+
* `openDurableCitizen` — never the launch argv.
|
|
241
360
|
*
|
|
242
361
|
* @param {readonly string[]} argv
|
|
243
362
|
* @param {{
|
|
244
363
|
* loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void> }>,
|
|
245
364
|
* open?: (options: any) => Promise<any>,
|
|
246
365
|
* contact?: Record<string, unknown>,
|
|
366
|
+
* bridgeEntry?: string,
|
|
247
367
|
* }} [seams]
|
|
248
368
|
*/
|
|
249
|
-
export async function main(argv, { loadTui = () => import(TUI), open, contact } = {}) {
|
|
250
|
-
// Every refusal is decided here, before the TUI
|
|
251
|
-
const args = parseArgs(argv);
|
|
369
|
+
export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry } = {}) {
|
|
370
|
+
// Every argument refusal is decided here, before any module, the TUI or the app loads.
|
|
371
|
+
const { nativeModule, ...args } = parseArgs(argv);
|
|
372
|
+
// The caller's directory, fixed before any operator code runs and handed to the app explicitly.
|
|
373
|
+
const cwd = process.cwd();
|
|
374
|
+
const extensions = nativeModule === undefined ? [] : [await loadNativeModule(nativeModule)];
|
|
252
375
|
// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot
|
|
253
376
|
// load opens no session, mints no citizen and leaves nothing to clean up.
|
|
254
377
|
const { runDurableTui } = await loadTui();
|
|
255
|
-
const citizen = await openDurableCitizen({ ...args, open, contact });
|
|
378
|
+
const citizen = await openDurableCitizen({ ...args, cwd, extensions, bridgeEntry, open, contact });
|
|
256
379
|
try {
|
|
257
380
|
await runDurableTui(
|
|
258
381
|
withGardenIdentity(citizen.durable.view, citizen.attachment.gardenId),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mario Zechner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createRegistry,
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
} from "@earendil-works/pi-durable";
|
|
10
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
11
|
+
import { CodingTools } from "@earendil-works/pi-durable/tools";
|
|
12
|
+
import { applyHttpProxySettings, configureHttpDispatcher } from "entwurf-pi-dist:core/http-dispatcher.js";
|
|
13
|
+
import { findInitialModel, resolveCliModel } from "entwurf-pi-dist:core/model-resolver.js";
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
import { createPiPrompt } from "./prompt.js";
|
|
17
|
+
|
|
18
|
+
/** pi's HTTP setup: proxy, idle timeouts, and one undici for fetch. Without it, some provider streams break off. */
|
|
19
|
+
export function configureHarnessHttp(settingsManager ) {
|
|
20
|
+
applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);
|
|
21
|
+
configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Harness settings read at every use from pi's settings as loaded at startup. */
|
|
25
|
+
export function createHarnessSettings(settingsManager ) {
|
|
26
|
+
return {
|
|
27
|
+
get stream() {
|
|
28
|
+
const provider = settingsManager.getProviderRetrySettings();
|
|
29
|
+
const idle = settingsManager.getHttpIdleTimeoutMs();
|
|
30
|
+
return {
|
|
31
|
+
timeoutMs: provider.timeoutMs ?? (idle === 0 ? 2147483647 : idle),
|
|
32
|
+
maxRetryDelayMs: provider.maxRetryDelayMs,
|
|
33
|
+
...(provider.maxRetries === undefined ? {} : { maxRetries: provider.maxRetries }),
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
get compaction() {
|
|
37
|
+
return settingsManager.getCompactionSettings();
|
|
38
|
+
},
|
|
39
|
+
get retry() {
|
|
40
|
+
return settingsManager.getRetrySettings();
|
|
41
|
+
},
|
|
42
|
+
get steeringMode() {
|
|
43
|
+
return settingsManager.getSteeringMode();
|
|
44
|
+
},
|
|
45
|
+
get followUpMode() {
|
|
46
|
+
return settingsManager.getFollowUpMode();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A registry with pi's coding tools and system prompt. */
|
|
52
|
+
export function createCodingRegistry(settingsManager , cwd ) {
|
|
53
|
+
const registry = createRegistry();
|
|
54
|
+
registry.install(CodingTools);
|
|
55
|
+
registry.install(createPiPrompt(settingsManager, cwd));
|
|
56
|
+
return registry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One execution environment per directory, shared by every conversation in it. */
|
|
60
|
+
export class ExecutionEnvs {
|
|
61
|
+
#defaultCwd ;
|
|
62
|
+
#envs = new Map ();
|
|
63
|
+
|
|
64
|
+
constructor(defaultCwd ) {
|
|
65
|
+
this.#defaultCwd = defaultCwd;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
env = ({ cwd = this.#defaultCwd } ) => {
|
|
69
|
+
let env = this.#envs.get(cwd);
|
|
70
|
+
if (env === undefined) {
|
|
71
|
+
env = new NodeExecutionEnv({ cwd });
|
|
72
|
+
this.#envs.set(cwd, env);
|
|
73
|
+
}
|
|
74
|
+
return env;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
async cleanup(context ) {
|
|
78
|
+
const envs = [...this.#envs.values()];
|
|
79
|
+
this.#envs.clear();
|
|
80
|
+
for (const env of envs) await env.cleanup(context);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
/** The model a new root conversation starts with: an explicit `--provider`/`--model`, or pi's default resolution. */
|
|
91
|
+
export async function findInitialAgentModel(
|
|
92
|
+
settingsManager ,
|
|
93
|
+
modelRuntime ,
|
|
94
|
+
cli ,
|
|
95
|
+
) {
|
|
96
|
+
if (cli !== undefined) {
|
|
97
|
+
const resolved = resolveCliModel({ cliProvider: cli.provider, cliModel: cli.model, modelRuntime });
|
|
98
|
+
if (resolved.error !== undefined || resolved.model === undefined) {
|
|
99
|
+
throw new Error(`Could not resolve model: ${resolved.error ?? cli.model}`);
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
model: { provider: resolved.model.provider, modelId: resolved.model.id },
|
|
103
|
+
...(resolved.thinkingLevel === undefined ? {} : { thinkingLevel: resolved.thinkingLevel }),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const initial = await findInitialModel({
|
|
107
|
+
scopedModels: [],
|
|
108
|
+
isContinuing: false,
|
|
109
|
+
defaultProvider: settingsManager.getDefaultProvider(),
|
|
110
|
+
defaultModelId: settingsManager.getDefaultModel(),
|
|
111
|
+
defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),
|
|
112
|
+
modelRuntime,
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
...(initial.model === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: {
|
|
118
|
+
model: { provider: initial.model.provider, modelId: initial.model.id },
|
|
119
|
+
thinkingLevel: initial.thinkingLevel,
|
|
120
|
+
}),
|
|
121
|
+
...(initial.fallbackMessage === undefined ? {} : { fallbackMessage: initial.fallbackMessage }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { defineExtension, section } from "@earendil-works/pi-durable";
|
|
2
|
+
import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
3
|
+
import { loadProjectContextFiles } from "entwurf-pi-dist:core/resource-loader.js";
|
|
4
|
+
|
|
5
|
+
import { loadSkills, } from "entwurf-pi-dist:core/skills.js";
|
|
6
|
+
import { buildSystemPromptSections } from "entwurf-pi-dist:core/system-prompt.js";
|
|
7
|
+
import { bashToolSystemPromptContribution } from "entwurf-pi-dist:core/tools/bash.js";
|
|
8
|
+
import { editToolSystemPromptContribution } from "entwurf-pi-dist:core/tools/edit.js";
|
|
9
|
+
import { readToolSystemPromptContribution } from "entwurf-pi-dist:core/tools/read.js";
|
|
10
|
+
import { writeToolSystemPromptContribution } from "entwurf-pi-dist:core/tools/write.js";
|
|
11
|
+
|
|
12
|
+
const CONTRIBUTIONS = {
|
|
13
|
+
read: readToolSystemPromptContribution,
|
|
14
|
+
bash: bashToolSystemPromptContribution,
|
|
15
|
+
edit: editToolSystemPromptContribution,
|
|
16
|
+
write: writeToolSystemPromptContribution,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** pi's section order; `buildSystemPromptSections()` omits the ones without content. */
|
|
20
|
+
const KEYS = ["preamble", "tools", "rules", "docs", "project_context", "skills", "cwd"] ;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* pi's system prompt as one extension: the sections of `buildSystemPromptSections()` for the request's tools and the
|
|
24
|
+
* conversation's directory. Context files and skills load once per directory, like pi at startup.
|
|
25
|
+
*/
|
|
26
|
+
export function createPiPrompt(settings , fallbackCwd ) {
|
|
27
|
+
const resources = new Map ();
|
|
28
|
+
const load = (cwd ) => {
|
|
29
|
+
let found = resources.get(cwd);
|
|
30
|
+
if (found === undefined) {
|
|
31
|
+
const agentDir = getAgentDir();
|
|
32
|
+
found = {
|
|
33
|
+
contextFiles: loadProjectContextFiles({ cwd, agentDir }),
|
|
34
|
+
skills: loadSkills({ cwd, agentDir, skillPaths: settings.getSkillPaths(), includeDefaults: true }).skills,
|
|
35
|
+
};
|
|
36
|
+
resources.set(cwd, found);
|
|
37
|
+
}
|
|
38
|
+
return found;
|
|
39
|
+
};
|
|
40
|
+
// The sections of one request render from one build.
|
|
41
|
+
const built = new WeakMap ();
|
|
42
|
+
const build = (input ) => {
|
|
43
|
+
let sections = built.get(input);
|
|
44
|
+
if (sections === undefined) {
|
|
45
|
+
sections = buildSections(input);
|
|
46
|
+
built.set(input, sections);
|
|
47
|
+
}
|
|
48
|
+
return sections;
|
|
49
|
+
};
|
|
50
|
+
const buildSections = (input ) => {
|
|
51
|
+
const cwd = input.env?.cwd ?? input.agent.cwd ?? fallbackCwd;
|
|
52
|
+
const selectedTools = input.agent.tools.map((tool) => tool.name);
|
|
53
|
+
const snippets = {};
|
|
54
|
+
const guidelines = {};
|
|
55
|
+
for (const name of selectedTools) {
|
|
56
|
+
const contribution = CONTRIBUTIONS[name ];
|
|
57
|
+
if (contribution === undefined) continue;
|
|
58
|
+
snippets[name] = contribution.snippet;
|
|
59
|
+
guidelines[name] = [...contribution.guidelines];
|
|
60
|
+
}
|
|
61
|
+
return buildSystemPromptSections({
|
|
62
|
+
cwd,
|
|
63
|
+
selectedTools,
|
|
64
|
+
toolSnippets: snippets,
|
|
65
|
+
toolGuidelines: guidelines,
|
|
66
|
+
...load(cwd),
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
return defineExtension({
|
|
70
|
+
name: "pi-prompt",
|
|
71
|
+
// The built sections carry their own tags.
|
|
72
|
+
sections: KEYS.map((key) => section(key, (input) => build(input)[key], { tag: false })),
|
|
73
|
+
});
|
|
74
|
+
}
|