@tt-a1i/openpi 0.1.1 → 0.3.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 (49) hide show
  1. package/README.md +65 -28
  2. package/SETUP.md +8 -6
  3. package/extensions/ask-user/handoff.ts +5 -1
  4. package/extensions/ask-user/index.ts +44 -0
  5. package/extensions/background-terminals/index.ts +118 -29
  6. package/extensions/background-terminals/src/domain.ts +5 -1
  7. package/extensions/background-terminals/src/manager.ts +2 -1
  8. package/extensions/background-terminals/src/prompt.ts +35 -0
  9. package/extensions/background-terminals/src/result-delivery.ts +76 -3
  10. package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
  11. package/extensions/capabilities/index.ts +198 -0
  12. package/extensions/context-pivot/index.ts +21 -0
  13. package/extensions/cron/index.ts +42 -15
  14. package/extensions/execution-convergence/active-evidence.ts +129 -0
  15. package/extensions/execution-convergence/index.ts +442 -0
  16. package/extensions/execution-convergence/workspace-provenance.ts +338 -0
  17. package/extensions/file-search/index.ts +8 -1
  18. package/extensions/file-search/src/binaries.ts +2 -1
  19. package/extensions/git-info/src/runtime.ts +1 -1
  20. package/extensions/goal/controller.ts +2 -1
  21. package/extensions/goal/index.ts +20 -1
  22. package/extensions/plan-mode/bash-policy.ts +219 -42
  23. package/extensions/plan-mode/index.ts +56 -19
  24. package/extensions/setup/index.ts +96 -10
  25. package/extensions/shared/child-session.ts +40 -4
  26. package/extensions/shared/editor-layers.ts +150 -0
  27. package/extensions/shared/setup-config.ts +26 -15
  28. package/extensions/shared/setup-episode-state.ts +7 -0
  29. package/extensions/shared/tool-surface.ts +435 -0
  30. package/extensions/subagents/index.ts +179 -96
  31. package/extensions/subagents/src/manager.ts +13 -11
  32. package/extensions/subagents/src/prompt.ts +7 -7
  33. package/extensions/subagents/src/ui/takeover.ts +231 -133
  34. package/extensions/subagents/src/ui/transcript.ts +252 -37
  35. package/extensions/subagents/src/ui/wait-result.ts +6 -19
  36. package/extensions/suggestions/index.ts +27 -18
  37. package/extensions/tasks/index.ts +63 -18
  38. package/extensions/ui-customization/footer.ts +65 -11
  39. package/extensions/workflows/graph-projection.ts +6 -4
  40. package/extensions/workflows/index.ts +44 -21
  41. package/extensions/workflows/invocation-ledger.ts +8 -2
  42. package/extensions/workflows/model.ts +5 -1
  43. package/extensions/workflows/prompt.ts +10 -40
  44. package/extensions/workflows/replay-safety.ts +9 -8
  45. package/package.json +10 -10
  46. package/skills/subagents/SKILL.md +7 -1
  47. package/skills/workflows/EXAMPLES.md +58 -0
  48. package/skills/workflows/REFERENCE.md +44 -0
  49. package/skills/workflows/SKILL.md +39 -0
@@ -1,11 +1,11 @@
1
1
  import * as path from "node:path";
2
2
  import {
3
+ type AgentSession,
3
4
  DefaultResourceLoader,
4
5
  getAgentDir,
5
6
  ProjectTrustStore,
6
- SettingsManager,
7
- type AgentSession,
8
7
  type SessionShutdownEvent,
8
+ SettingsManager,
9
9
  } from "@earendil-works/pi-coding-agent";
10
10
 
11
11
  export const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
@@ -51,6 +51,8 @@ function isPiIntercomNpmResource(resource: {
51
51
  * drift test in child-session.test.ts).
52
52
  */
53
53
  export const CHILD_EXCLUDED_TOOL_NAMES = [
54
+ // capability discovery mutates the parent model-facing tool surface
55
+ "openpi_load_tools",
54
56
  // subagents — children cannot spawn/observe more agents
55
57
  "subagent_spawn",
56
58
  "subagent_wait",
@@ -185,7 +187,9 @@ export function resolveStandaloneChildProjectTrust(options: {
185
187
 
186
188
  interface ChildSessionStartup {
187
189
  bindExtensions(bindings: { mode: "print" }): Promise<void>;
188
- getActiveToolNames(): string[];
190
+ getActiveToolNames?(): string[];
191
+ getAllTools?(): { name: string }[];
192
+ setActiveToolsByName?(toolNames: string[]): void;
189
193
  }
190
194
 
191
195
  function boundedToolNames(names: readonly string[]) {
@@ -208,9 +212,41 @@ export async function bindChildSessionExtensions(
208
212
  ) {
209
213
  await session.bindExtensions({ mode: "print" });
210
214
  const requested = effectiveChildToolAllowlist(requestedTools);
215
+ let active: Set<string> | undefined;
216
+ if (
217
+ session.getActiveToolNames &&
218
+ session.getAllTools &&
219
+ session.setActiveToolsByName
220
+ ) {
221
+ const requestedSet = requested ? new Set(requested) : undefined;
222
+ const available = new Set(session.getAllTools().map(({ name }) => name));
223
+ const activeNames = session.getActiveToolNames();
224
+ active = new Set(activeNames);
225
+ for (const name of CHILD_SAFE_PACKAGE_TOOL_NAMES) {
226
+ if (
227
+ available.has(name) &&
228
+ !active.has(name) &&
229
+ (requestedSet === undefined || requestedSet.has(name))
230
+ ) {
231
+ activeNames.push(name);
232
+ active.add(name);
233
+ }
234
+ }
235
+ if (activeNames.length !== session.getActiveToolNames().length) {
236
+ session.setActiveToolsByName(activeNames);
237
+ }
238
+ }
211
239
  if (!requested) return;
212
240
 
213
- const active = new Set(session.getActiveToolNames());
241
+ if (!active && session.getActiveToolNames) {
242
+ active = new Set(session.getActiveToolNames());
243
+ }
244
+ if (!active) {
245
+ throw new Error(
246
+ "Child tool preflight failed: the bound child session does not expose active-tool introspection.",
247
+ );
248
+ }
249
+
214
250
  const missing = [...new Set(requested)].filter((name) => !active.has(name));
215
251
  if (missing.length === 0) return;
216
252
 
@@ -0,0 +1,150 @@
1
+ import {
2
+ CustomEditor,
3
+ type ExtensionAPI,
4
+ type ExtensionContext,
5
+ type KeybindingsManager,
6
+ } from "@earendil-works/pi-coding-agent";
7
+ import type { EditorComponent, EditorTheme, TUI } from "@earendil-works/pi-tui";
8
+
9
+ const CLAIM_CHANNEL = "openpi:editor-layers:claim";
10
+ const REGISTER_CHANNEL = "openpi:editor-layers:register";
11
+ const REMOVE_CHANNEL = "openpi:editor-layers:remove";
12
+
13
+ type EditorFactory = NonNullable<
14
+ ReturnType<ExtensionContext["ui"]["getEditorComponent"]>
15
+ >;
16
+
17
+ export interface EditorLayer {
18
+ readonly id: string;
19
+ readonly order: number;
20
+ readonly wrap: (
21
+ base: EditorComponent,
22
+ tui: TUI,
23
+ theme: EditorTheme,
24
+ keybindings: KeybindingsManager,
25
+ ) => EditorComponent;
26
+ }
27
+
28
+ interface EditorLayerRegistration {
29
+ readonly ctx: ExtensionContext;
30
+ readonly layer: EditorLayer;
31
+ }
32
+
33
+ function isRecord(value: unknown): value is Record<string, unknown> {
34
+ return typeof value === "object" && value !== null;
35
+ }
36
+
37
+ function readRegistration(value: unknown) {
38
+ if (!isRecord(value) || !isRecord(value.layer)) return undefined;
39
+ const { ctx, layer } = value;
40
+ if (
41
+ !isRecord(ctx) ||
42
+ typeof layer.id !== "string" ||
43
+ typeof layer.order !== "number" ||
44
+ typeof layer.wrap !== "function"
45
+ ) {
46
+ return undefined;
47
+ }
48
+ return {
49
+ ctx: ctx as unknown as ExtensionContext,
50
+ layer: layer as unknown as EditorLayer,
51
+ } satisfies EditorLayerRegistration;
52
+ }
53
+
54
+ function readLayerId(value: unknown) {
55
+ if (!isRecord(value) || typeof value.id !== "string") return undefined;
56
+ return value.id;
57
+ }
58
+
59
+ function composeEditorFactory(
60
+ previous: EditorFactory | undefined,
61
+ layers: readonly EditorLayer[],
62
+ ) {
63
+ return ((tui, theme, keybindings) => {
64
+ let editor =
65
+ previous?.(tui, theme, keybindings) ??
66
+ new CustomEditor(tui, theme, keybindings);
67
+ for (const layer of layers) {
68
+ editor = layer.wrap(editor, tui, theme, keybindings);
69
+ }
70
+ return editor;
71
+ }) satisfies EditorFactory;
72
+ }
73
+
74
+ /**
75
+ * Each extension is evaluated in its own jiti module graph, so ordinary module
76
+ * singletons are not shared. The first OpenPI editor contributor claims the
77
+ * runtime EventBus and coordinates the rest through that host-owned boundary.
78
+ */
79
+ function ensureCoordinator(pi: ExtensionAPI) {
80
+ const claim = { claimed: false };
81
+ pi.events.emit(CLAIM_CHANNEL, claim);
82
+ if (claim.claimed) return;
83
+
84
+ let ctx: ExtensionContext | undefined;
85
+ let installTimer: ReturnType<typeof setTimeout> | undefined;
86
+ const layers = new Map<string, EditorLayer>();
87
+
88
+ const cancelInstall = () => {
89
+ if (installTimer) clearTimeout(installTimer);
90
+ installTimer = undefined;
91
+ };
92
+
93
+ const install = () => {
94
+ installTimer = undefined;
95
+ const current = ctx;
96
+ if (!current || current.mode !== "tui" || layers.size === 0) return;
97
+ const ordered = [...layers.values()].sort(
98
+ (left, right) =>
99
+ left.order - right.order || left.id.localeCompare(right.id),
100
+ );
101
+ current.ui.setEditorComponent(
102
+ composeEditorFactory(current.ui.getEditorComponent(), ordered),
103
+ );
104
+ };
105
+
106
+ const scheduleInstall = () => {
107
+ if (installTimer) return;
108
+ installTimer = setTimeout(install, 0);
109
+ };
110
+
111
+ pi.events.on(CLAIM_CHANNEL, (value) => {
112
+ if (isRecord(value) && value.claimed === false) value.claimed = true;
113
+ });
114
+ pi.events.on(REGISTER_CHANNEL, (value) => {
115
+ const registration = readRegistration(value);
116
+ if (!registration || registration.ctx.mode !== "tui") return;
117
+ if (ctx !== registration.ctx) {
118
+ cancelInstall();
119
+ layers.clear();
120
+ ctx = registration.ctx;
121
+ }
122
+ layers.set(registration.layer.id, registration.layer);
123
+ scheduleInstall();
124
+ });
125
+ pi.events.on(REMOVE_CHANNEL, (value) => {
126
+ const id = readLayerId(value);
127
+ if (!id) return;
128
+ layers.delete(id);
129
+ if (layers.size > 0) return;
130
+ cancelInstall();
131
+ ctx = undefined;
132
+ });
133
+ }
134
+
135
+ export function registerEditorLayer(
136
+ pi: ExtensionAPI,
137
+ ctx: ExtensionContext,
138
+ layer: EditorLayer,
139
+ ) {
140
+ if (ctx.mode !== "tui") return;
141
+ ensureCoordinator(pi);
142
+ pi.events.emit(REGISTER_CHANNEL, {
143
+ ctx,
144
+ layer,
145
+ } satisfies EditorLayerRegistration);
146
+ }
147
+
148
+ export function removeEditorLayer(pi: ExtensionAPI, id: string) {
149
+ pi.events.emit(REMOVE_CHANNEL, { id });
150
+ }
@@ -63,23 +63,16 @@ export type FooterLines = readonly (readonly FooterLayoutItem[])[];
63
63
  export const DETAIL_DISPLAYS = ["full", "compact"] as const;
64
64
  export type DetailDisplay = (typeof DETAIL_DISPLAYS)[number];
65
65
 
66
- /** Canonical default layout: one-line Powerline dashboard with flex alignment. */
66
+ export const CAPABILITY_DISCOVERY_MODES = ["explicit", "adaptive"] as const;
67
+ export type CapabilityDiscoveryMode =
68
+ (typeof CAPABILITY_DISCOVERY_MODES)[number];
69
+
70
+ /** Canonical default layout: one-line plain footer with flex alignment. */
67
71
  export const DEFAULT_FOOTER_LINES: FooterLines = [
68
- [
69
- "cwd",
70
- "model",
71
- "thinking",
72
- "context",
73
- "cache",
74
- "cost",
75
- "throughput",
76
- "flex",
77
- "git",
78
- "pr",
79
- ],
72
+ ["cwd", "git", "pr", "flex", "model", "context", "cost"],
80
73
  ];
81
74
 
82
- export const DEFAULT_FOOTER_STYLE: FooterStyle = "powerline";
75
+ export const DEFAULT_FOOTER_STYLE: FooterStyle = "plain";
83
76
 
84
77
  export const DEFAULT_FOOTER_ITEMS: readonly FooterItem[] =
85
78
  flattenFooterItems(DEFAULT_FOOTER_LINES);
@@ -123,6 +116,9 @@ export const POST_EDIT_COMMAND_MAX_CHARS = 500;
123
116
  export const SETUP_CONFIG_CHANGED_CHANNEL = "my-pi-setup:config-changed";
124
117
 
125
118
  export interface MyPiSetupConfig {
119
+ readonly capabilities: {
120
+ readonly discovery: CapabilityDiscoveryMode;
121
+ };
126
122
  readonly suggestions: {
127
123
  readonly enabled: boolean;
128
124
  readonly model?: SuggestionModelConfig;
@@ -157,6 +153,7 @@ export interface MyPiSetupConfig {
157
153
  }
158
154
 
159
155
  export const DEFAULT_SETUP_CONFIG: MyPiSetupConfig = {
156
+ capabilities: { discovery: "explicit" },
160
157
  suggestions: { enabled: false },
161
158
  workflows: {
162
159
  concurrency: DEFAULT_WORKFLOW_CONCURRENCY,
@@ -205,6 +202,12 @@ const isFooterStyle = (value: unknown): value is FooterStyle =>
205
202
  const isFooterPreset = (value: unknown): value is FooterPreset =>
206
203
  typeof value === "string" && FOOTER_PRESETS.includes(value as FooterPreset);
207
204
 
205
+ const isCapabilityDiscoveryMode = (
206
+ value: unknown,
207
+ ): value is CapabilityDiscoveryMode =>
208
+ typeof value === "string" &&
209
+ CAPABILITY_DISCOVERY_MODES.includes(value as CapabilityDiscoveryMode);
210
+
208
211
  export function flattenFooterItems(lines: FooterLines): readonly FooterItem[] {
209
212
  const items: FooterItem[] = [];
210
213
  const seen = new Set<FooterItem>();
@@ -401,6 +404,8 @@ function boundedInteger(value: unknown, fallback: number, maximum: number) {
401
404
  export function parseSetupConfig(value: unknown): MyPiSetupConfig {
402
405
  if (!isRecord(value)) return DEFAULT_SETUP_CONFIG;
403
406
 
407
+ const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
408
+
404
409
  // `summaries` is the pre-suggestion config key. Read it once as a migration
405
410
  // source; every subsequent save writes only the canonical `suggestions` key.
406
411
  const suggestions = isRecord(value.suggestions)
@@ -430,6 +435,11 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
430
435
  const subagents = isRecord(value.subagents) ? value.subagents : {};
431
436
  const footer = parseUiFooter(ui);
432
437
  return {
438
+ capabilities: {
439
+ discovery: isCapabilityDiscoveryMode(capabilities.discovery)
440
+ ? capabilities.discovery
441
+ : "explicit",
442
+ },
433
443
  suggestions: {
434
444
  enabled: requestedEnabled && Boolean(model),
435
445
  ...(model ? { model } : {}),
@@ -942,10 +952,11 @@ export function formatSetupConfig(
942
952
  ? `on · ${config.ui.footerStyle} · ${formatFooterLines(config.ui.footerLines)}`
943
953
  : "off";
944
954
  return [
955
+ `Capability discovery: ${config.capabilities.discovery}`,
945
956
  suggestions,
946
957
  `Workflows: ${config.workflows.concurrency} concurrent agents · ${config.workflows.maxAgentCalls} total calls`,
947
958
  `UI: large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
948
- `Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact preview (expand for full output)"}`,
959
+ `Subagent results: ${config.ui.subagentResultDisplay === "full" ? "full by default" : "compact status summary (Ctrl+O expands full output)"}`,
949
960
  `Bash operations: ${config.ui.bashToolDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
950
961
  `Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
951
962
  `Post-edit command: ${config.postEdit.command ? config.postEdit.command : "off"}`,
@@ -0,0 +1,7 @@
1
+ /** Broadcast whenever the package-owned setup episode becomes usable or ends. */
2
+ export const OPENPI_SETUP_EPISODE_CHANNEL = "openpi:setup-episode";
3
+
4
+ export interface OpenPiSetupEpisodeState {
5
+ /** True for both armed and actively running setup episodes. */
6
+ readonly active: boolean;
7
+ }