@tt-a1i/openpi 0.3.1 → 0.5.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 (129) hide show
  1. package/README.md +184 -59
  2. package/SETUP.md +23 -7
  3. package/assets/openpi-launch-card-v1.webp +0 -0
  4. package/bin/openpi.js +145 -0
  5. package/extensions/ask-user/index.ts +30 -14
  6. package/extensions/background-terminals/index.ts +30 -2
  7. package/extensions/background-terminals/src/domain.ts +2 -0
  8. package/extensions/background-terminals/src/manager.ts +486 -106
  9. package/extensions/background-terminals/src/output.ts +33 -0
  10. package/extensions/background-terminals/src/prompt.ts +14 -6
  11. package/extensions/background-terminals/src/result-delivery.ts +4 -1
  12. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  13. package/extensions/capabilities/index.ts +30 -42
  14. package/extensions/capabilities/src/ui.ts +93 -0
  15. package/extensions/clear-context/index.ts +83 -0
  16. package/extensions/context-pivot/index.ts +16 -6
  17. package/extensions/cron/schedule.ts +7 -1
  18. package/extensions/file-mutation-display/index.ts +34 -76
  19. package/extensions/file-mutation-display/render.ts +146 -87
  20. package/extensions/file-search/index.ts +8 -7
  21. package/extensions/file-search/src/binaries.ts +75 -59
  22. package/extensions/git-info/src/changed-files-view.ts +47 -14
  23. package/extensions/git-read/index.ts +328 -0
  24. package/extensions/git-read/src/args.ts +171 -0
  25. package/extensions/git-read/src/process.ts +81 -0
  26. package/extensions/git-read/src/prompt.ts +56 -0
  27. package/extensions/model-info/index.ts +21 -33
  28. package/extensions/model-info/session-metrics.ts +96 -0
  29. package/extensions/plan-mode/bash-policy.ts +54 -9
  30. package/extensions/plan-mode/index.ts +7 -2
  31. package/extensions/post-edit/index.ts +16 -6
  32. package/extensions/sessions/git-stats.ts +258 -72
  33. package/extensions/sessions/index.ts +222 -140
  34. package/extensions/sessions/preview-cache.ts +104 -0
  35. package/extensions/sessions/preview-loader.ts +856 -0
  36. package/extensions/sessions/sessions.ts +43 -4
  37. package/extensions/setup/index.ts +127 -131
  38. package/extensions/shared/activity-status.ts +36 -5
  39. package/extensions/shared/agent-session-page.ts +319 -0
  40. package/extensions/shared/agent-tool-renderer.ts +218 -0
  41. package/extensions/shared/agent-transcript.ts +524 -0
  42. package/extensions/shared/below-editor-navigation.ts +26 -0
  43. package/extensions/shared/capability-intent.ts +53 -0
  44. package/extensions/shared/child-session.ts +444 -22
  45. package/extensions/shared/result-budget.ts +134 -0
  46. package/extensions/shared/result-delivery.ts +34 -0
  47. package/extensions/shared/screen-chrome.ts +133 -0
  48. package/extensions/shared/setup-config.ts +97 -38
  49. package/extensions/shared/setup-episode-state.ts +1 -1
  50. package/extensions/shared/spinner.ts +28 -0
  51. package/extensions/shared/terminal-text.ts +110 -23
  52. package/extensions/shared/text-projection.ts +113 -0
  53. package/extensions/shared/tool-activity.ts +382 -0
  54. package/extensions/shared/tool-surface.ts +42 -8
  55. package/extensions/shared/transcript-viewport.ts +46 -0
  56. package/extensions/shared/web-observer-registry.ts +390 -0
  57. package/extensions/shared/worktree.ts +11 -0
  58. package/extensions/subagents/index.ts +461 -186
  59. package/extensions/subagents/navigation.ts +86 -28
  60. package/extensions/subagents/src/agent-types.ts +37 -15
  61. package/extensions/subagents/src/backend.ts +12 -1
  62. package/extensions/subagents/src/backends/pi.ts +375 -66
  63. package/extensions/subagents/src/domain.ts +5 -0
  64. package/extensions/subagents/src/id-sequence.ts +84 -0
  65. package/extensions/subagents/src/manager.ts +651 -536
  66. package/extensions/subagents/src/prompt.ts +185 -42
  67. package/extensions/subagents/src/result-artifact.ts +146 -0
  68. package/extensions/subagents/src/result-delivery.ts +7 -1
  69. package/extensions/subagents/src/runtime.ts +23 -6
  70. package/extensions/subagents/src/ui/takeover.ts +128 -337
  71. package/extensions/subagents/src/ui/transcript.ts +38 -501
  72. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  73. package/extensions/suggestions/src/ui.ts +10 -4
  74. package/extensions/tasks/index.ts +0 -3
  75. package/extensions/tasks/ui.ts +79 -62
  76. package/extensions/ui-customization/footer.ts +7 -44
  77. package/extensions/ui-customization/index.ts +0 -4
  78. package/extensions/user-input-fold/index.ts +185 -0
  79. package/extensions/web/index.ts +234 -0
  80. package/extensions/workflows/artifacts.ts +147 -22
  81. package/extensions/workflows/completion-projection.ts +457 -0
  82. package/extensions/workflows/controller.ts +14 -2
  83. package/extensions/workflows/coordinator.ts +62 -0
  84. package/extensions/workflows/dashboard.ts +458 -339
  85. package/extensions/workflows/handoff.ts +121 -25
  86. package/extensions/workflows/index.ts +1042 -492
  87. package/extensions/workflows/journal.ts +148 -13
  88. package/extensions/workflows/model.ts +131 -19
  89. package/extensions/workflows/navigation.ts +61 -18
  90. package/extensions/workflows/progress-projection.ts +306 -0
  91. package/extensions/workflows/prompt.ts +166 -10
  92. package/extensions/workflows/replay-safety.ts +58 -27
  93. package/extensions/workflows/result-delivery.ts +253 -0
  94. package/extensions/workflows/retention.ts +593 -0
  95. package/extensions/workflows/runner.ts +388 -279
  96. package/extensions/workflows/sandbox-child.cjs +36 -3
  97. package/extensions/workflows/sandbox.ts +62 -8
  98. package/extensions/workflows/serialization.ts +325 -17
  99. package/extensions/workflows/tool-renderer.ts +22 -0
  100. package/extensions/workflows/transcript.ts +149 -0
  101. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  102. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  103. package/package.json +28 -8
  104. package/skills/subagents/REFERENCE.md +189 -0
  105. package/skills/subagents/SKILL.md +2 -2
  106. package/skills/workflows/REFERENCE.md +10 -5
  107. package/skills/workflows/SKILL.md +53 -10
  108. package/web/adapter/pi-adapter.ts +661 -0
  109. package/web/host/browser-launcher.ts +20 -0
  110. package/web/host/static-assets.ts +4 -0
  111. package/web/host/terminal-status.ts +38 -0
  112. package/web/host/web-host.ts +789 -0
  113. package/web/http-dispatcher.ts +125 -0
  114. package/web/protocol/types.ts +462 -0
  115. package/web/runtime/pi-runtime.ts +991 -0
  116. package/web/runtime/types.ts +71 -0
  117. package/web/runtime/web-host-lease.ts +497 -0
  118. package/web/trace.ts +18 -0
  119. package/web/ui/app.js +1398 -0
  120. package/web/ui/index.html +139 -0
  121. package/web/ui/styles.css +598 -0
  122. package/web/vite.config.mjs +34 -0
  123. package/extensions/execution-convergence/active-evidence.ts +0 -129
  124. package/extensions/execution-convergence/index.ts +0 -442
  125. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  126. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  127. package/extensions/setup/intercom.ts +0 -603
  128. package/extensions/subagents/src/backends/stub.ts +0 -296
  129. package/extensions/subagents/src/format.ts +0 -48
@@ -1,12 +1,22 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
1
2
  import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import {
3
- type AgentSession,
5
+ DefaultPackageManager,
4
6
  DefaultResourceLoader,
5
7
  getAgentDir,
8
+ type LoadExtensionsResult,
9
+ type PackageSource,
6
10
  ProjectTrustStore,
11
+ type ResolvedPaths,
7
12
  type SessionShutdownEvent,
8
13
  SettingsManager,
9
14
  } from "@earendil-works/pi-coding-agent";
15
+ import {
16
+ OPENPI_OWNER_SOURCE_PATHS,
17
+ OPENPI_TOOL_SURFACE,
18
+ type OpenPiToolOwner,
19
+ } from "./tool-surface.ts";
10
20
 
11
21
  export const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
12
22
 
@@ -22,7 +32,13 @@ export const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
22
32
  * `registerTool(createEditToolDefinition(...))`); an unrecognized factory
23
33
  * registration fails the guard rather than escaping it.
24
34
  */
25
- export const CHILD_SAFE_PACKAGE_TOOL_NAMES = ["fd", "rg"] as const;
35
+ export const CHILD_SAFE_PACKAGE_TOOL_NAMES = [
36
+ "fd",
37
+ "rg",
38
+ "git_show",
39
+ "git_diff",
40
+ "git_log",
41
+ ] as const;
26
42
 
27
43
  /**
28
44
  * pi-intercom resources are unsafe inside concurrent in-process child sessions.
@@ -31,16 +47,385 @@ export const CHILD_SAFE_PACKAGE_TOOL_NAMES = ["fd", "rg"] as const;
31
47
  * Loading it in children can therefore cross-wire identities; the parent
32
48
  * extension remains loaded and fully usable.
33
49
  */
34
- function isPiIntercomNpmResource(resource: {
35
- resolvedPath?: string;
36
- filePath?: string;
50
+ function manifestNamesPiIntercom(manifestPath: string) {
51
+ try {
52
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as unknown;
53
+ return (
54
+ typeof manifest === "object" &&
55
+ manifest !== null &&
56
+ !Array.isArray(manifest) &&
57
+ (manifest as Record<string, unknown>).name === "pi-intercom"
58
+ );
59
+ } catch (error) {
60
+ throw new Error(
61
+ `Cannot verify child package identity from ${manifestPath}`,
62
+ { cause: error },
63
+ );
64
+ }
65
+ }
66
+
67
+ function installedPathNamesPiIntercom(installedPath: string) {
68
+ try {
69
+ const stats = statSync(installedPath);
70
+ if (stats.isDirectory()) {
71
+ const manifestPath = path.join(installedPath, "package.json");
72
+ if (existsSync(manifestPath)) {
73
+ return manifestNamesPiIntercom(manifestPath);
74
+ }
75
+ if (path.basename(installedPath) === "pi-intercom") {
76
+ throw new Error(
77
+ `Cannot verify child package identity from ${installedPath}`,
78
+ );
79
+ }
80
+ return false;
81
+ }
82
+ if (!stats.isFile()) {
83
+ throw new Error(
84
+ `Cannot verify child package identity from ${installedPath}`,
85
+ );
86
+ }
87
+
88
+ let current = path.dirname(installedPath);
89
+ let hasIntercomDirectoryName = false;
90
+ while (true) {
91
+ hasIntercomDirectoryName ||= path.basename(current) === "pi-intercom";
92
+ const manifestPath = path.join(current, "package.json");
93
+ if (existsSync(manifestPath)) {
94
+ return manifestNamesPiIntercom(manifestPath);
95
+ }
96
+ const parent = path.dirname(current);
97
+ if (parent === current) {
98
+ if (hasIntercomDirectoryName) {
99
+ throw new Error(
100
+ `Cannot verify child package identity from ${installedPath}`,
101
+ );
102
+ }
103
+ return false;
104
+ }
105
+ current = parent;
106
+ }
107
+ } catch (error) {
108
+ if (
109
+ error instanceof Error &&
110
+ error.message.startsWith("Cannot verify child package identity")
111
+ ) {
112
+ throw error;
113
+ }
114
+ throw new Error(
115
+ `Cannot verify child package identity from ${installedPath}`,
116
+ { cause: error },
117
+ );
118
+ }
119
+ }
120
+
121
+ function piMatchesPublishedIntercomSource(options: {
122
+ source: string;
123
+ cwd: string;
124
+ agentDir: string;
125
+ }) {
126
+ const settingsManager = SettingsManager.inMemory({
127
+ packages: [options.source],
128
+ });
129
+ const packageManager = new DefaultPackageManager({
130
+ cwd: options.cwd,
131
+ agentDir: options.agentDir,
132
+ settingsManager,
133
+ });
134
+ return (
135
+ packageManager.removeSourceFromSettings("npm:pi-intercom") ||
136
+ packageManager.removeSourceFromSettings(
137
+ "git:https://github.com/nicobailon/pi-intercom",
138
+ )
139
+ );
140
+ }
141
+
142
+ function createPiIntercomPackageMatcher(options: {
143
+ cwd: string;
144
+ agentDir: string;
145
+ }) {
146
+ const sourceMatches = new Map<string, boolean>();
147
+ const installedPathMatches = new Map<string, boolean>();
148
+ return (source: string, installedPath?: string) => {
149
+ let sourceMatch = sourceMatches.get(source);
150
+ if (sourceMatch === undefined) {
151
+ sourceMatch = piMatchesPublishedIntercomSource({
152
+ source,
153
+ cwd: options.cwd,
154
+ agentDir: options.agentDir,
155
+ });
156
+ sourceMatches.set(source, sourceMatch);
157
+ }
158
+ if (sourceMatch || installedPath === undefined) return sourceMatch;
159
+
160
+ const resolvedPath = path.resolve(installedPath);
161
+ let installedPathMatch = installedPathMatches.get(resolvedPath);
162
+ if (installedPathMatch === undefined) {
163
+ installedPathMatch = installedPathNamesPiIntercom(resolvedPath);
164
+ installedPathMatches.set(resolvedPath, installedPathMatch);
165
+ }
166
+ return installedPathMatch;
167
+ };
168
+ }
169
+
170
+ function packageSourceValue(source: PackageSource) {
171
+ return typeof source === "string" ? source : source.source;
172
+ }
173
+
174
+ const CHILD_DISABLED_OPENPI_EXTENSION =
175
+ "-extensions/git-info/index.ts" as const;
176
+ const OPENPI_GIT_INFO_EXTENSION_PATH = realpathSync.native(
177
+ fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
178
+ );
179
+
180
+ function canonicalExistingPath(value: string) {
181
+ try {
182
+ return realpathSync.native(value);
183
+ } catch {
184
+ return undefined;
185
+ }
186
+ }
187
+
188
+ function excludeOpenPiGitInfoExtension(
189
+ resources: LoadExtensionsResult,
190
+ ): LoadExtensionsResult {
191
+ return {
192
+ ...resources,
193
+ extensions: resources.extensions.filter(
194
+ (extension) =>
195
+ canonicalExistingPath(extension.resolvedPath) !==
196
+ OPENPI_GIT_INFO_EXTENSION_PATH,
197
+ ),
198
+ };
199
+ }
200
+
201
+ function installedPathNamesOpenPi(installedPath: string) {
202
+ try {
203
+ const stats = statSync(installedPath);
204
+ if (stats.isDirectory()) {
205
+ const manifestPath = path.join(installedPath, "package.json");
206
+ if (!existsSync(manifestPath)) return false;
207
+ const manifest = JSON.parse(
208
+ readFileSync(manifestPath, "utf8"),
209
+ ) as unknown;
210
+ return (
211
+ typeof manifest === "object" &&
212
+ manifest !== null &&
213
+ !Array.isArray(manifest) &&
214
+ (manifest as Record<string, unknown>).name === "@tt-a1i/openpi"
215
+ );
216
+ }
217
+ let current = stats.isFile() ? path.dirname(installedPath) : undefined;
218
+ while (current) {
219
+ const manifestPath = path.join(current, "package.json");
220
+ if (existsSync(manifestPath)) {
221
+ const manifest = JSON.parse(
222
+ readFileSync(manifestPath, "utf8"),
223
+ ) as unknown;
224
+ return (
225
+ typeof manifest === "object" &&
226
+ manifest !== null &&
227
+ !Array.isArray(manifest) &&
228
+ (manifest as Record<string, unknown>).name === "@tt-a1i/openpi"
229
+ );
230
+ }
231
+ const parent = path.dirname(current);
232
+ if (parent === current) return false;
233
+ current = parent;
234
+ }
235
+ return false;
236
+ } catch {
237
+ return false;
238
+ }
239
+ }
240
+
241
+ function piMatchesPublishedOpenPiSource(options: {
242
+ source: string;
243
+ cwd: string;
244
+ agentDir: string;
245
+ }) {
246
+ const settingsManager = SettingsManager.inMemory({
247
+ packages: [options.source],
248
+ });
249
+ const packageManager = new DefaultPackageManager({
250
+ cwd: options.cwd,
251
+ agentDir: options.agentDir,
252
+ settingsManager,
253
+ });
254
+ return (
255
+ packageManager.removeSourceFromSettings("npm:@tt-a1i/openpi") ||
256
+ packageManager.removeSourceFromSettings(
257
+ "git:https://github.com/openpi-dev/openpi",
258
+ ) ||
259
+ packageManager.removeSourceFromSettings(
260
+ "git:https://github.com/tt-a1i/openpi",
261
+ )
262
+ );
263
+ }
264
+
265
+ function createOpenPiPackageMatcher(options: {
266
+ cwd: string;
267
+ agentDir: string;
37
268
  }) {
38
- const resourcePath = resource.resolvedPath ?? resource.filePath;
39
- if (!resourcePath) return false;
40
- const segments = path.resolve(resourcePath).split(path.sep);
41
- return segments.some(
42
- (segment, index) =>
43
- segment === "node_modules" && segments[index + 1] === "pi-intercom",
269
+ const sourceMatches = new Map<string, boolean>();
270
+ const installedPathMatches = new Map<string, boolean>();
271
+ return (source: string, installedPath?: string) => {
272
+ let sourceMatch = sourceMatches.get(source);
273
+ if (sourceMatch === undefined) {
274
+ sourceMatch = piMatchesPublishedOpenPiSource({ source, ...options });
275
+ sourceMatches.set(source, sourceMatch);
276
+ }
277
+ if (sourceMatch || installedPath === undefined) return sourceMatch;
278
+
279
+ const resolvedPath = path.resolve(installedPath);
280
+ let installedPathMatch = installedPathMatches.get(resolvedPath);
281
+ if (installedPathMatch === undefined) {
282
+ installedPathMatch = installedPathNamesOpenPi(resolvedPath);
283
+ installedPathMatches.set(resolvedPath, installedPathMatch);
284
+ }
285
+ return installedPathMatch;
286
+ };
287
+ }
288
+
289
+ function openPiPackageSources(
290
+ packageManager: DefaultPackageManager,
291
+ options: { cwd: string; agentDir: string },
292
+ ) {
293
+ const isOpenPiPackage = createOpenPiPackageMatcher(options);
294
+ const sources = {
295
+ user: new Set<string>(),
296
+ project: new Set<string>(),
297
+ };
298
+ for (const configured of packageManager.listConfiguredPackages()) {
299
+ if (isOpenPiPackage(configured.source, configured.installedPath)) {
300
+ sources[configured.scope].add(configured.source);
301
+ }
302
+ }
303
+ return sources;
304
+ }
305
+
306
+ function disablesOpenPiGitInfo(pattern: string) {
307
+ return (
308
+ pattern.startsWith("-") &&
309
+ pattern.slice(1).replace(/^\.\//, "") ===
310
+ CHILD_DISABLED_OPENPI_EXTENSION.slice(1)
311
+ );
312
+ }
313
+
314
+ function disableOpenPiGitInfo(source: PackageSource): PackageSource {
315
+ if (typeof source !== "string") {
316
+ if (source.extensions?.length === 0) return source;
317
+ if (source.autoload === false && source.extensions === undefined) {
318
+ return source;
319
+ }
320
+ }
321
+ const configured = typeof source === "string" ? { source } : source;
322
+ const extensions = [...(configured.extensions ?? [])];
323
+ if (!extensions.some(disablesOpenPiGitInfo)) {
324
+ extensions.push(CHILD_DISABLED_OPENPI_EXTENSION);
325
+ }
326
+ return { ...configured, extensions };
327
+ }
328
+
329
+ function blockedPackageSources(
330
+ packageManager: DefaultPackageManager,
331
+ resolvedPaths: ResolvedPaths,
332
+ options: { cwd: string; agentDir: string },
333
+ ) {
334
+ const isPiIntercomPackage = createPiIntercomPackageMatcher(options);
335
+ const blocked = {
336
+ user: new Set<string>(),
337
+ project: new Set<string>(),
338
+ };
339
+ for (const configured of packageManager.listConfiguredPackages()) {
340
+ if (isPiIntercomPackage(configured.source, configured.installedPath)) {
341
+ blocked[configured.scope].add(configured.source);
342
+ }
343
+ }
344
+
345
+ const resources = [
346
+ ...resolvedPaths.extensions,
347
+ ...resolvedPaths.skills,
348
+ ...resolvedPaths.prompts,
349
+ ...resolvedPaths.themes,
350
+ ];
351
+ for (const resource of resources) {
352
+ const { metadata } = resource;
353
+ if (
354
+ metadata.origin !== "package" ||
355
+ metadata.scope === "temporary" ||
356
+ !isPiIntercomPackage(metadata.source, metadata.baseDir)
357
+ ) {
358
+ continue;
359
+ }
360
+ blocked[metadata.scope].add(metadata.source);
361
+ }
362
+ return blocked;
363
+ }
364
+
365
+ function createEphemeralChildSettings(
366
+ sourceSettings: SettingsManager,
367
+ blockedSources: { user: Set<string>; project: Set<string> },
368
+ openPiSources: { user: Set<string>; project: Set<string> },
369
+ projectTrusted: boolean,
370
+ ) {
371
+ const globalSettings = sourceSettings.getGlobalSettings();
372
+ const projectSettings = sourceSettings.getProjectSettings();
373
+ const settingsForScope = (
374
+ settings: typeof globalSettings,
375
+ scope: "user" | "project",
376
+ ) => ({
377
+ ...settings,
378
+ packages: settings.packages
379
+ ?.filter(
380
+ (source) => !blockedSources[scope].has(packageSourceValue(source)),
381
+ )
382
+ .map((source) =>
383
+ openPiSources[scope].has(packageSourceValue(source))
384
+ ? disableOpenPiGitInfo(source)
385
+ : source,
386
+ ),
387
+ });
388
+ const contents = {
389
+ global: JSON.stringify(settingsForScope(globalSettings, "user")),
390
+ project: JSON.stringify(settingsForScope(projectSettings, "project")),
391
+ };
392
+ const storage: Parameters<typeof SettingsManager.fromStorage>[0] = {
393
+ withLock(scope, update) {
394
+ const next = update(contents[scope]);
395
+ if (next !== undefined) contents[scope] = next;
396
+ },
397
+ };
398
+ return SettingsManager.fromStorage(storage, { projectTrusted });
399
+ }
400
+
401
+ async function createChildSettingsManager(options: {
402
+ cwd: string;
403
+ agentDir: string;
404
+ projectTrusted: boolean;
405
+ }) {
406
+ const sourceSettings = SettingsManager.create(options.cwd, options.agentDir, {
407
+ projectTrusted: options.projectTrusted,
408
+ });
409
+ const packageManager = new DefaultPackageManager({
410
+ cwd: options.cwd,
411
+ agentDir: options.agentDir,
412
+ settingsManager: sourceSettings,
413
+ });
414
+ const resolvedPaths = await packageManager.resolve(async () => "skip");
415
+ const blockedSources = blockedPackageSources(packageManager, resolvedPaths, {
416
+ cwd: options.cwd,
417
+ agentDir: options.agentDir,
418
+ });
419
+ const openPiSources = openPiPackageSources(packageManager, {
420
+ cwd: options.cwd,
421
+ agentDir: options.agentDir,
422
+ });
423
+
424
+ return createEphemeralChildSettings(
425
+ sourceSettings,
426
+ blockedSources,
427
+ openPiSources,
428
+ options.projectTrusted,
44
429
  );
45
430
  }
46
431
 
@@ -90,6 +475,42 @@ export const CHILD_EXCLUDED_TOOL_NAMES = [
90
475
  "context_pivot",
91
476
  ] as const;
92
477
 
478
+ const PARENT_ONLY_OPENPI_EXTENSION_PATHS = new Set(
479
+ (Object.keys(OPENPI_TOOL_SURFACE) as OpenPiToolOwner[])
480
+ .filter((owner) => {
481
+ const { entry, deferred } = OPENPI_TOOL_SURFACE[owner];
482
+ const toolNames = [...entry, ...deferred];
483
+ return (
484
+ toolNames.length > 0 &&
485
+ toolNames.every((name) =>
486
+ CHILD_EXCLUDED_TOOL_NAMES.includes(name as never),
487
+ )
488
+ );
489
+ })
490
+ .map((owner) => canonicalExistingPath(OPENPI_OWNER_SOURCE_PATHS[owner]))
491
+ .filter(
492
+ (extensionPath): extensionPath is string => extensionPath !== undefined,
493
+ ),
494
+ );
495
+
496
+ function isVerifiedParentOnlyOpenPiExtension(extension: {
497
+ path: string;
498
+ resolvedPath: string;
499
+ sourceInfo: { path: string };
500
+ }) {
501
+ return [
502
+ extension.path,
503
+ extension.resolvedPath,
504
+ extension.sourceInfo.path,
505
+ ].some((candidate) => {
506
+ const canonicalPath = canonicalExistingPath(candidate);
507
+ return (
508
+ canonicalPath !== undefined &&
509
+ PARENT_ONLY_OPENPI_EXTENSION_PATHS.has(canonicalPath)
510
+ );
511
+ });
512
+ }
513
+
93
514
  /**
94
515
  * Fresh SDK options avoid turning the denylist into an accidental allowlist.
95
516
  *
@@ -127,26 +548,27 @@ export interface ChildResourceOptions {
127
548
  /** Load normal global/package resources and trust-gated project resources. */
128
549
  export async function createChildResources(options: ChildResourceOptions) {
129
550
  const agentDir = options.agentDir ?? getAgentDir();
130
- const settingsManager = SettingsManager.create(options.cwd, agentDir, {
551
+ const settingsManager = await createChildSettingsManager({
552
+ cwd: options.cwd,
553
+ agentDir,
131
554
  projectTrusted: options.projectTrusted,
132
555
  });
133
556
  const loader = new DefaultResourceLoader({
134
557
  cwd: options.cwd,
135
558
  agentDir,
136
559
  settingsManager,
560
+ extensionsOverride(base) {
561
+ const withoutGitInfo = excludeOpenPiGitInfoExtension(base);
562
+ return {
563
+ ...withoutGitInfo,
564
+ extensions: withoutGitInfo.extensions.filter(
565
+ (extension) => !isVerifiedParentOnlyOpenPiExtension(extension),
566
+ ),
567
+ };
568
+ },
137
569
  ...(options.appendSystemPrompt
138
570
  ? { appendSystemPrompt: options.appendSystemPrompt }
139
571
  : {}),
140
- extensionsOverride: (base) => ({
141
- ...base,
142
- extensions: base.extensions.filter(
143
- (extension) => !isPiIntercomNpmResource(extension),
144
- ),
145
- }),
146
- skillsOverride: (base) => ({
147
- ...base,
148
- skills: base.skills.filter((skill) => !isPiIntercomNpmResource(skill)),
149
- }),
150
572
  });
151
573
  await loader.reload();
152
574
  return { loader, settingsManager };
@@ -0,0 +1,134 @@
1
+ export interface ParentContextUsage {
2
+ readonly tokens: number | null;
3
+ readonly contextWindow: number;
4
+ }
5
+
6
+ export interface ResultBudgetPolicy {
7
+ readonly maxBatchBytes: number;
8
+ readonly maxResultBytes: number;
9
+ readonly minResultBytes: number;
10
+ readonly headroomShare: number;
11
+ readonly estimatedBytesPerToken: number;
12
+ /** Batch metadata that consumes the same parent-context headroom. */
13
+ readonly fixedBytes?: number;
14
+ }
15
+
16
+ export interface ResultBudgetAllocation {
17
+ readonly budgets: readonly number[];
18
+ readonly batchBytes: number;
19
+ readonly source: "static" | "dynamic";
20
+ }
21
+
22
+ function validUsage(
23
+ usage: ParentContextUsage | null | undefined,
24
+ ): usage is { readonly tokens: number; readonly contextWindow: number } {
25
+ return Boolean(
26
+ usage &&
27
+ typeof usage.tokens === "number" &&
28
+ Number.isFinite(usage.tokens) &&
29
+ usage.tokens >= 0 &&
30
+ Number.isFinite(usage.contextWindow) &&
31
+ usage.contextWindow > 0,
32
+ );
33
+ }
34
+
35
+ function distribute(
36
+ desired: readonly number[],
37
+ batchBytes: number,
38
+ minResultBytes: number,
39
+ ) {
40
+ const budgets = desired.map((bytes) => Math.min(bytes, minResultBytes));
41
+ let remaining = Math.max(
42
+ 0,
43
+ batchBytes - budgets.reduce((sum, bytes) => sum + bytes, 0),
44
+ );
45
+ let active = desired
46
+ .map((bytes, index) => ({ bytes, index }))
47
+ .filter(({ bytes, index }) => bytes > budgets[index]!);
48
+
49
+ while (remaining > 0 && active.length > 0) {
50
+ const share = Math.floor(remaining / active.length);
51
+ if (share === 0) {
52
+ for (const { bytes, index } of active) {
53
+ if (remaining === 0) break;
54
+ if (budgets[index]! >= bytes) continue;
55
+ budgets[index]! += 1;
56
+ remaining--;
57
+ }
58
+ break;
59
+ }
60
+
61
+ const satisfied = active.filter(
62
+ ({ bytes, index }) => bytes - budgets[index]! <= share,
63
+ );
64
+ if (satisfied.length > 0) {
65
+ for (const { bytes, index } of satisfied) {
66
+ const addition = bytes - budgets[index]!;
67
+ budgets[index]! = bytes;
68
+ remaining -= addition;
69
+ }
70
+ const settled = new Set(satisfied.map(({ index }) => index));
71
+ active = active.filter(({ index }) => !settled.has(index));
72
+ continue;
73
+ }
74
+
75
+ for (const { index } of active) budgets[index]! += share;
76
+ remaining -= share * active.length;
77
+ }
78
+
79
+ return budgets;
80
+ }
81
+
82
+ /**
83
+ * Allocate one bounded projection budget per result. Small results yield their
84
+ * unused share to larger siblings. Parent context usage is optional: Pi marks
85
+ * it unknown after compaction until a fresh response, in which case the
86
+ * deterministic static batch cap remains the source of truth.
87
+ */
88
+ export function allocateResultBudgets(
89
+ resultBytes: readonly number[],
90
+ usage: ParentContextUsage | null | undefined,
91
+ policy: ResultBudgetPolicy,
92
+ ): ResultBudgetAllocation {
93
+ if (resultBytes.length === 0) {
94
+ return { budgets: [], batchBytes: 0, source: "static" };
95
+ }
96
+
97
+ const desired = resultBytes.map((bytes) =>
98
+ Math.min(policy.maxResultBytes, Math.max(0, Math.floor(bytes))),
99
+ );
100
+ const perResultFloor = Math.min(
101
+ policy.minResultBytes,
102
+ Math.floor(policy.maxBatchBytes / resultBytes.length),
103
+ );
104
+ const desiredTotal = desired.reduce((sum, bytes) => sum + bytes, 0);
105
+ const staticBatchBytes = Math.min(policy.maxBatchBytes, desiredTotal);
106
+ const minimumBatchBytes = desired.reduce(
107
+ (sum, bytes) => sum + Math.min(bytes, perResultFloor),
108
+ 0,
109
+ );
110
+
111
+ let batchBytes = staticBatchBytes;
112
+ let source: ResultBudgetAllocation["source"] = "static";
113
+ if (validUsage(usage)) {
114
+ const headroomTokens = Math.max(0, usage.contextWindow - usage.tokens);
115
+ const dynamicBytes = Math.max(
116
+ 0,
117
+ Math.floor(
118
+ headroomTokens * policy.headroomShare * policy.estimatedBytesPerToken,
119
+ ) - Math.max(0, policy.fixedBytes ?? 0),
120
+ );
121
+ const narrowed = Math.max(
122
+ minimumBatchBytes,
123
+ Math.min(staticBatchBytes, dynamicBytes),
124
+ );
125
+ if (narrowed < staticBatchBytes) source = "dynamic";
126
+ batchBytes = narrowed;
127
+ }
128
+
129
+ return {
130
+ budgets: distribute(desired, batchBytes, perResultFloor),
131
+ batchBytes,
132
+ source,
133
+ };
134
+ }
@@ -0,0 +1,34 @@
1
+ /** Minimum lifecycle contract shared by deferred result queues. */
2
+ export interface ResultDeliveryQueue<T> {
3
+ defer(result: T): void;
4
+ size(): number;
5
+ clear(): void;
6
+ }
7
+
8
+ /** One-shot results are removed by either an explicit consumer or delivery. */
9
+ export interface ConsumableResultDeliveryQueue<T, TId = string>
10
+ extends ResultDeliveryQueue<T> {
11
+ consume(ids: Iterable<TId>): void;
12
+ }
13
+
14
+ /** Stable identity carried by a durable, at-least-once result. */
15
+ export interface DurableResultEnvelope {
16
+ deliveryId: string;
17
+ }
18
+
19
+ /** Per-result acknowledgement for a durable delivery attempt. */
20
+ export interface DurableResultDeliveryReceipt {
21
+ deliveryId: string;
22
+ delivered: boolean;
23
+ error?: string;
24
+ }
25
+
26
+ /**
27
+ * Durable queues can reconstruct pending ownership after restart and retry it
28
+ * at an explicit lifecycle boundary without changing the stable delivery id.
29
+ */
30
+ export interface DurableResultDeliveryQueue<T extends DurableResultEnvelope>
31
+ extends ResultDeliveryQueue<T> {
32
+ restore(result: T): boolean;
33
+ retryPending(): Promise<void>;
34
+ }