@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.
Files changed (56) hide show
  1. package/AGENTS.md +3 -2
  2. package/CHANGELOG.md +82 -0
  3. package/README.md +46 -36
  4. package/VERIFY.md +35 -16
  5. package/demo/README.md +1 -1
  6. package/docs/acp-backend-rail.md +1 -1
  7. package/docs/setup-clean-host.md +112 -95
  8. package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +1 -1
  9. package/mcp/entwurf-bridge/dist/pi-extensions/lib/fresh-call-composition.js +1 -1
  10. package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +8 -10
  11. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-fresh-preflight.js +3 -3
  12. package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +153 -205
  13. package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +6 -4
  14. package/mcp/entwurf-bridge/src/index.ts +1 -1
  15. package/package.json +14 -17
  16. package/pi/pi-durable/bootstrap.mjs +142 -19
  17. package/pi/pi-durable/carrier/LICENSE +21 -0
  18. package/pi/pi-durable/carrier/harness-setup.js +123 -0
  19. package/pi/pi-durable/carrier/prompt.js +74 -0
  20. package/pi/pi-durable/carrier/runtime.js +402 -0
  21. package/pi/pi-durable/carrier/sessions.js +56 -0
  22. package/pi/pi-durable/carrier/subagent.js +55 -0
  23. package/pi/pi-durable/carrier/tui.js +655 -0
  24. package/pi/pi-durable/carrier-relocation.mjs +160 -0
  25. package/pi/pi-durable/carrier-resolver.mjs +38 -0
  26. package/pi/pi-durable/overlay/upstream-pin.json +103 -11
  27. package/pi-extensions/lib/fresh-call-composition.ts +1 -1
  28. package/pi-extensions/lib/mux-fresh-call.ts +8 -10
  29. package/pi-extensions/lib/pi-durable-fresh-preflight.ts +3 -3
  30. package/pi-extensions/lib/pi-durable-runtime.ts +197 -263
  31. package/pi-extensions/meta-bridge-pi-durable.ts +11 -7
  32. package/run.sh +508 -235
  33. package/scripts/check-entwurf-bridge-pi-free.ts +7 -5
  34. package/scripts/check-fresh-call-dispatch.ts +1 -1
  35. package/scripts/check-gate-qualification.ts +86 -34
  36. package/scripts/check-herdr-activation.ts +5 -4
  37. package/scripts/check-pi-durable-contact.ts +26 -93
  38. package/scripts/check-pi-durable-receive.ts +22 -59
  39. package/scripts/check-pi-durable-send.ts +235 -75
  40. package/scripts/emit-pi-durable-carrier.ts +355 -0
  41. package/scripts/lib/mutation-qualify.ts +21 -45
  42. package/scripts/lib/pi-durable-operator-guard.ts +89 -0
  43. package/scripts/lib/qualification-receipt.ts +7 -11
  44. package/scripts/mutants/control-send-receipt.json +1 -1
  45. package/scripts/mutants/gate-qualification.json +13 -0
  46. package/scripts/mutants/mux-fresh-call.json +13 -13
  47. package/scripts/mutants/pack-install.json +3 -3
  48. package/scripts/mutants/pi-durable.json +442 -109
  49. package/scripts/mutants/pi-floor.json +24 -2
  50. package/scripts/pi-durable-contact-host.mjs +3 -3
  51. package/scripts/pi-durable-receive-host.mjs +3 -3
  52. package/scripts/pi-durable-send-host.mjs +76 -5
  53. package/scripts/smoke-herdr-raw-install-live.sh +1 -1
  54. package/scripts/smoke-pi-durable-fresh-live.ts +3 -3
  55. package/scripts/smoke-setup-verdict.sh +15 -15
  56. 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
- // It runs under the operator-provided runtime's own source resolver, which is what selects the
20
- // runtime — this file carries no path into it. `entwurf pi-durable` (run.sh) locates that runtime
21
- // at its one fixed place, verifies it against the pin, and execs:
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 <runtime>/packages/coding-agent/src/experimental/source-resolver.ts \
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
- // Without that resolver the two pi-coding-agent experimental subpath imports below fail: the
47
- // published package ships no experimental source, and that failure is the honest one. They are the
48
- // one sanctioned pi private-subpath exception (check-pi-import-surface, #129).
49
- import { realpathSync } from "node:fs";
50
- import { fileURLToPath } from "node:url";
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 = "@earendil-works/pi-coding-agent/experimental/durable/runtime.ts";
60
- const TUI = "@earendil-works/pi-coding-agent/experimental/durable/tui.ts";
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
- if (continueSession) return { continueSession };
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 `contact` are test seams.
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 loads and before anything opens.
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
+ }