@tt-a1i/openpi 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +295 -389
- package/SETUP.md +24 -22
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/assets/readme-hero-mobile.svg +2 -2
- package/assets/readme-hero.svg +10 -10
- package/extensions/ask-user/handoff.ts +5 -1
- package/extensions/ask-user/index.ts +44 -0
- package/extensions/background-terminals/index.ts +118 -29
- package/extensions/background-terminals/src/domain.ts +5 -1
- package/extensions/background-terminals/src/manager.ts +2 -1
- package/extensions/background-terminals/src/prompt.ts +35 -0
- package/extensions/background-terminals/src/result-delivery.ts +76 -3
- package/extensions/background-terminals/src/ui/tool-result.ts +52 -1
- package/extensions/capabilities/index.ts +198 -0
- package/extensions/context-pivot/index.ts +21 -0
- package/extensions/cron/index.ts +42 -15
- package/extensions/execution-convergence/active-evidence.ts +129 -0
- package/extensions/execution-convergence/index.ts +442 -0
- package/extensions/execution-convergence/workspace-provenance.ts +338 -0
- package/extensions/file-search/index.ts +8 -1
- package/extensions/file-search/src/binaries.ts +2 -1
- package/extensions/git-info/src/runtime.ts +1 -1
- package/extensions/goal/controller.ts +2 -1
- package/extensions/goal/index.ts +20 -1
- package/extensions/plan-mode/index.ts +12 -0
- package/extensions/setup/index.ts +241 -45
- package/extensions/setup/intercom-fs-helper.cjs +130 -0
- package/extensions/setup/intercom.ts +603 -0
- package/extensions/shared/child-session.ts +42 -5
- package/extensions/shared/setup-config.ts +27 -1
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +16 -1
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +1 -1
- package/extensions/tasks/index.ts +39 -12
- package/extensions/ui-customization/footer.ts +6 -1
- package/extensions/workflows/artifacts.ts +6 -1
- package/extensions/workflows/dashboard.ts +138 -27
- package/extensions/workflows/graph-projection.ts +240 -0
- package/extensions/workflows/handoff.ts +194 -0
- package/extensions/workflows/index.ts +258 -56
- package/extensions/workflows/invocation-ledger.ts +368 -0
- package/extensions/workflows/model.ts +57 -1
- package/extensions/workflows/operator.ts +131 -0
- package/extensions/workflows/prompt.ts +10 -38
- package/extensions/workflows/replay-safety.ts +9 -8
- package/extensions/workflows/runner.ts +10 -2
- package/extensions/workflows/sandbox.ts +5 -0
- package/package.json +15 -15
- package/skills/subagents/SKILL.md +6 -0
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
|
@@ -63,6 +63,10 @@ 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
|
+
export const CAPABILITY_DISCOVERY_MODES = ["explicit", "adaptive"] as const;
|
|
67
|
+
export type CapabilityDiscoveryMode =
|
|
68
|
+
(typeof CAPABILITY_DISCOVERY_MODES)[number];
|
|
69
|
+
|
|
66
70
|
/** Canonical default layout: one-line Powerline dashboard with flex alignment. */
|
|
67
71
|
export const DEFAULT_FOOTER_LINES: FooterLines = [
|
|
68
72
|
[
|
|
@@ -123,6 +127,9 @@ export const POST_EDIT_COMMAND_MAX_CHARS = 500;
|
|
|
123
127
|
export const SETUP_CONFIG_CHANGED_CHANNEL = "my-pi-setup:config-changed";
|
|
124
128
|
|
|
125
129
|
export interface MyPiSetupConfig {
|
|
130
|
+
readonly capabilities: {
|
|
131
|
+
readonly discovery: CapabilityDiscoveryMode;
|
|
132
|
+
};
|
|
126
133
|
readonly suggestions: {
|
|
127
134
|
readonly enabled: boolean;
|
|
128
135
|
readonly model?: SuggestionModelConfig;
|
|
@@ -157,6 +164,7 @@ export interface MyPiSetupConfig {
|
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
export const DEFAULT_SETUP_CONFIG: MyPiSetupConfig = {
|
|
167
|
+
capabilities: { discovery: "explicit" },
|
|
160
168
|
suggestions: { enabled: false },
|
|
161
169
|
workflows: {
|
|
162
170
|
concurrency: DEFAULT_WORKFLOW_CONCURRENCY,
|
|
@@ -205,6 +213,12 @@ const isFooterStyle = (value: unknown): value is FooterStyle =>
|
|
|
205
213
|
const isFooterPreset = (value: unknown): value is FooterPreset =>
|
|
206
214
|
typeof value === "string" && FOOTER_PRESETS.includes(value as FooterPreset);
|
|
207
215
|
|
|
216
|
+
const isCapabilityDiscoveryMode = (
|
|
217
|
+
value: unknown,
|
|
218
|
+
): value is CapabilityDiscoveryMode =>
|
|
219
|
+
typeof value === "string" &&
|
|
220
|
+
CAPABILITY_DISCOVERY_MODES.includes(value as CapabilityDiscoveryMode);
|
|
221
|
+
|
|
208
222
|
export function flattenFooterItems(lines: FooterLines): readonly FooterItem[] {
|
|
209
223
|
const items: FooterItem[] = [];
|
|
210
224
|
const seen = new Set<FooterItem>();
|
|
@@ -401,6 +415,8 @@ function boundedInteger(value: unknown, fallback: number, maximum: number) {
|
|
|
401
415
|
export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
402
416
|
if (!isRecord(value)) return DEFAULT_SETUP_CONFIG;
|
|
403
417
|
|
|
418
|
+
const capabilities = isRecord(value.capabilities) ? value.capabilities : {};
|
|
419
|
+
|
|
404
420
|
// `summaries` is the pre-suggestion config key. Read it once as a migration
|
|
405
421
|
// source; every subsequent save writes only the canonical `suggestions` key.
|
|
406
422
|
const suggestions = isRecord(value.suggestions)
|
|
@@ -430,6 +446,11 @@ export function parseSetupConfig(value: unknown): MyPiSetupConfig {
|
|
|
430
446
|
const subagents = isRecord(value.subagents) ? value.subagents : {};
|
|
431
447
|
const footer = parseUiFooter(ui);
|
|
432
448
|
return {
|
|
449
|
+
capabilities: {
|
|
450
|
+
discovery: isCapabilityDiscoveryMode(capabilities.discovery)
|
|
451
|
+
? capabilities.discovery
|
|
452
|
+
: "explicit",
|
|
453
|
+
},
|
|
433
454
|
suggestions: {
|
|
434
455
|
enabled: requestedEnabled && Boolean(model),
|
|
435
456
|
...(model ? { model } : {}),
|
|
@@ -929,7 +950,10 @@ export async function saveSetupConfig(config: MyPiSetupConfig) {
|
|
|
929
950
|
});
|
|
930
951
|
}
|
|
931
952
|
|
|
932
|
-
export function formatSetupConfig(
|
|
953
|
+
export function formatSetupConfig(
|
|
954
|
+
config = loadSetupConfig(),
|
|
955
|
+
integrationLines: readonly string[] = [],
|
|
956
|
+
) {
|
|
933
957
|
const suggestionModel = config.suggestions.model;
|
|
934
958
|
const suggestions =
|
|
935
959
|
!config.suggestions.enabled || !suggestionModel
|
|
@@ -939,6 +963,7 @@ export function formatSetupConfig(config = loadSetupConfig()) {
|
|
|
939
963
|
? `on · ${config.ui.footerStyle} · ${formatFooterLines(config.ui.footerLines)}`
|
|
940
964
|
: "off";
|
|
941
965
|
return [
|
|
966
|
+
`Capability discovery: ${config.capabilities.discovery}`,
|
|
942
967
|
suggestions,
|
|
943
968
|
`Workflows: ${config.workflows.concurrency} concurrent agents · ${config.workflows.maxAgentCalls} total calls`,
|
|
944
969
|
`UI: large header ${config.ui.showHeader ? "on" : "off"} · custom footer ${footer}`,
|
|
@@ -947,6 +972,7 @@ export function formatSetupConfig(config = loadSetupConfig()) {
|
|
|
947
972
|
`Write/Edit operations: ${config.ui.fileMutationDisplay === "full" ? "expanded by default" : "folded preview (Ctrl+O expands all)"}`,
|
|
948
973
|
`Post-edit command: ${config.postEdit.command ? config.postEdit.command : "off"}`,
|
|
949
974
|
`Agent role models (Subagents + Workflows): ${SUBAGENT_ROLE_NAMES.map((role) => `${role} ${config.subagents.roleModels[role] ? `${config.subagents.roleModels[role].provider}/${config.subagents.roleModels[role].model}` : "inherit"}`).join(" · ")}`,
|
|
975
|
+
...integrationLines,
|
|
950
976
|
].join("\n");
|
|
951
977
|
}
|
|
952
978
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OpenPI-owned model tools, grouped by the extension that owns their runtime
|
|
5
|
+
* state. Ordinary parent sessions add no resident OpenPI tool. Explicit user
|
|
6
|
+
* intent can reveal the capability gateway or load one capability group;
|
|
7
|
+
* capability-owned tools remain registered but are projected only after their
|
|
8
|
+
* group is loaded. Lifecycle tools add a second resource/mode-state gate inside
|
|
9
|
+
* that loaded group.
|
|
10
|
+
*/
|
|
11
|
+
export const OPENPI_TOOL_SURFACE = {
|
|
12
|
+
capabilities: {
|
|
13
|
+
entry: ["openpi_load_tools"],
|
|
14
|
+
deferred: [],
|
|
15
|
+
},
|
|
16
|
+
fileSearch: {
|
|
17
|
+
entry: ["fd", "rg"],
|
|
18
|
+
deferred: [],
|
|
19
|
+
},
|
|
20
|
+
subagents: {
|
|
21
|
+
entry: ["subagent_spawn"],
|
|
22
|
+
deferred: [
|
|
23
|
+
"subagent_wait",
|
|
24
|
+
"subagent_cancel",
|
|
25
|
+
"subagent_send",
|
|
26
|
+
"subagent_check",
|
|
27
|
+
"subagent_list",
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
workflows: {
|
|
31
|
+
entry: ["workflow"],
|
|
32
|
+
deferred: ["workflow_stop", "workflow_status"],
|
|
33
|
+
},
|
|
34
|
+
background: {
|
|
35
|
+
entry: ["bg_start"],
|
|
36
|
+
deferred: ["bg_status", "bg_list", "bg_kill", "bg_watch"],
|
|
37
|
+
},
|
|
38
|
+
tasks: {
|
|
39
|
+
entry: ["tasks_add"],
|
|
40
|
+
deferred: ["tasks_update", "tasks_list"],
|
|
41
|
+
},
|
|
42
|
+
goal: {
|
|
43
|
+
entry: ["create_goal"],
|
|
44
|
+
deferred: ["get_goal", "update_goal"],
|
|
45
|
+
},
|
|
46
|
+
interaction: {
|
|
47
|
+
entry: [],
|
|
48
|
+
deferred: ["ask_user", "human_handoff"],
|
|
49
|
+
},
|
|
50
|
+
plan: {
|
|
51
|
+
entry: [],
|
|
52
|
+
deferred: ["plan_ready"],
|
|
53
|
+
},
|
|
54
|
+
setup: {
|
|
55
|
+
entry: [],
|
|
56
|
+
deferred: ["configure_my_pi_setup"],
|
|
57
|
+
},
|
|
58
|
+
context: {
|
|
59
|
+
entry: [],
|
|
60
|
+
deferred: ["context_pivot"],
|
|
61
|
+
},
|
|
62
|
+
} as const;
|
|
63
|
+
|
|
64
|
+
export type OpenPiToolOwner = keyof typeof OPENPI_TOOL_SURFACE;
|
|
65
|
+
|
|
66
|
+
export const OPENPI_CAPABILITY_GROUPS = {
|
|
67
|
+
search: {
|
|
68
|
+
owners: ["fileSearch"],
|
|
69
|
+
summary: "Fast structured file and content search with fd and rg.",
|
|
70
|
+
},
|
|
71
|
+
delegate: {
|
|
72
|
+
owners: ["subagents"],
|
|
73
|
+
summary: "Spawn and manage isolated in-process Pi subagents.",
|
|
74
|
+
},
|
|
75
|
+
workflow: {
|
|
76
|
+
owners: ["workflows"],
|
|
77
|
+
summary: "Run replay-safe multi-stage workflows.",
|
|
78
|
+
},
|
|
79
|
+
background: {
|
|
80
|
+
owners: ["background"],
|
|
81
|
+
summary: "Start and manage long-running background terminals.",
|
|
82
|
+
},
|
|
83
|
+
session: {
|
|
84
|
+
owners: ["tasks", "goal"],
|
|
85
|
+
summary:
|
|
86
|
+
"Track explicit session tasks and persistent user-requested goals.",
|
|
87
|
+
},
|
|
88
|
+
} as const satisfies Record<
|
|
89
|
+
string,
|
|
90
|
+
{ owners: readonly OpenPiToolOwner[]; summary: string }
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
export type OpenPiCapability = keyof typeof OPENPI_CAPABILITY_GROUPS;
|
|
94
|
+
|
|
95
|
+
export const OPENPI_CAPABILITY_NAMES = Object.keys(
|
|
96
|
+
OPENPI_CAPABILITY_GROUPS,
|
|
97
|
+
) as OpenPiCapability[];
|
|
98
|
+
|
|
99
|
+
export const DEFAULT_OPENPI_ACTIVE_TOOL_NAMES: readonly string[] = [];
|
|
100
|
+
|
|
101
|
+
export const OPENPI_TOOL_SURFACE_NAMES = Object.values(
|
|
102
|
+
OPENPI_TOOL_SURFACE,
|
|
103
|
+
).flatMap(({ entry, deferred }) => [...entry, ...deferred]);
|
|
104
|
+
|
|
105
|
+
interface ActiveToolSurface {
|
|
106
|
+
events?: {
|
|
107
|
+
emit(channel: string, data: unknown): void;
|
|
108
|
+
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
109
|
+
};
|
|
110
|
+
getActiveTools(): string[];
|
|
111
|
+
getAllTools?(): {
|
|
112
|
+
name: string;
|
|
113
|
+
sourceInfo?: { path: string; source?: string };
|
|
114
|
+
}[];
|
|
115
|
+
setActiveTools(names: string[]): void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface OwnedToolPatch {
|
|
119
|
+
enable?: readonly string[];
|
|
120
|
+
disable?: readonly string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface ToolSurfaceState {
|
|
124
|
+
loaded: Set<OpenPiCapability>;
|
|
125
|
+
desiredByOwner: Map<OpenPiToolOwner, Set<string>>;
|
|
126
|
+
sourceByOwner: Map<OpenPiToolOwner, string>;
|
|
127
|
+
managedOwners: Set<OpenPiToolOwner>;
|
|
128
|
+
knownAvailable: Set<string>;
|
|
129
|
+
subscribed: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const OWNER_SOURCE_PATHS = {
|
|
133
|
+
capabilities: fileURLToPath(
|
|
134
|
+
new URL("../capabilities/index.ts", import.meta.url),
|
|
135
|
+
),
|
|
136
|
+
fileSearch: fileURLToPath(
|
|
137
|
+
new URL("../file-search/index.ts", import.meta.url),
|
|
138
|
+
),
|
|
139
|
+
subagents: fileURLToPath(new URL("../subagents/index.ts", import.meta.url)),
|
|
140
|
+
workflows: fileURLToPath(new URL("../workflows/index.ts", import.meta.url)),
|
|
141
|
+
background: fileURLToPath(
|
|
142
|
+
new URL("../background-terminals/index.ts", import.meta.url),
|
|
143
|
+
),
|
|
144
|
+
tasks: fileURLToPath(new URL("../tasks/index.ts", import.meta.url)),
|
|
145
|
+
goal: fileURLToPath(new URL("../goal/index.ts", import.meta.url)),
|
|
146
|
+
interaction: fileURLToPath(new URL("../ask-user/index.ts", import.meta.url)),
|
|
147
|
+
plan: fileURLToPath(new URL("../plan-mode/index.ts", import.meta.url)),
|
|
148
|
+
setup: fileURLToPath(new URL("../setup/index.ts", import.meta.url)),
|
|
149
|
+
context: fileURLToPath(new URL("../context-pivot/index.ts", import.meta.url)),
|
|
150
|
+
} as const satisfies Record<OpenPiToolOwner, string>;
|
|
151
|
+
|
|
152
|
+
const states = new WeakMap<object, ToolSurfaceState>();
|
|
153
|
+
export const OPENPI_CAPABILITY_STATE_CHANNEL = "openpi:capability-state";
|
|
154
|
+
|
|
155
|
+
function initialDesiredByOwner() {
|
|
156
|
+
return new Map<OpenPiToolOwner, Set<string>>(
|
|
157
|
+
(Object.keys(OPENPI_TOOL_SURFACE) as OpenPiToolOwner[]).map((owner) => [
|
|
158
|
+
owner,
|
|
159
|
+
new Set<string>(
|
|
160
|
+
owner === "capabilities" ? [] : OPENPI_TOOL_SURFACE[owner].entry,
|
|
161
|
+
),
|
|
162
|
+
]),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function newState(): ToolSurfaceState {
|
|
167
|
+
return {
|
|
168
|
+
loaded: new Set<OpenPiCapability>(),
|
|
169
|
+
desiredByOwner: initialDesiredByOwner(),
|
|
170
|
+
sourceByOwner: new Map<OpenPiToolOwner, string>(),
|
|
171
|
+
managedOwners: new Set<OpenPiToolOwner>(),
|
|
172
|
+
knownAvailable: new Set<string>(),
|
|
173
|
+
subscribed: false,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function capabilityStateFromEvent(data: unknown) {
|
|
178
|
+
if (typeof data !== "object" || data === null) return undefined;
|
|
179
|
+
const loaded = (data as { loaded?: unknown }).loaded;
|
|
180
|
+
if (!Array.isArray(loaded)) return undefined;
|
|
181
|
+
if (
|
|
182
|
+
loaded.some(
|
|
183
|
+
(name) =>
|
|
184
|
+
typeof name !== "string" ||
|
|
185
|
+
!OPENPI_CAPABILITY_NAMES.includes(name as OpenPiCapability),
|
|
186
|
+
)
|
|
187
|
+
) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
return loaded as OpenPiCapability[];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function subscribeToCapabilityState(
|
|
194
|
+
pi: ActiveToolSurface,
|
|
195
|
+
state: ToolSurfaceState,
|
|
196
|
+
) {
|
|
197
|
+
if (
|
|
198
|
+
state.subscribed ||
|
|
199
|
+
typeof pi.events?.on !== "function" ||
|
|
200
|
+
typeof pi.events?.emit !== "function"
|
|
201
|
+
) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
state.subscribed = true;
|
|
205
|
+
pi.events.on(OPENPI_CAPABILITY_STATE_CHANNEL, (data) => {
|
|
206
|
+
const loaded = capabilityStateFromEvent(data);
|
|
207
|
+
if (!loaded) return;
|
|
208
|
+
state.loaded = new Set(loaded);
|
|
209
|
+
reconcileManagedOwners(pi, state);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function stateFor(pi: ActiveToolSurface) {
|
|
214
|
+
let state = states.get(pi);
|
|
215
|
+
if (!state) {
|
|
216
|
+
state = newState();
|
|
217
|
+
if (
|
|
218
|
+
typeof pi.events?.on !== "function" ||
|
|
219
|
+
typeof pi.events?.emit !== "function"
|
|
220
|
+
) {
|
|
221
|
+
state.loaded = new Set(OPENPI_CAPABILITY_NAMES);
|
|
222
|
+
}
|
|
223
|
+
states.set(pi, state);
|
|
224
|
+
}
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function capabilityForOwner(owner: OpenPiToolOwner) {
|
|
229
|
+
return OPENPI_CAPABILITY_NAMES.find((capability) =>
|
|
230
|
+
OPENPI_CAPABILITY_GROUPS[capability].owners.includes(owner as never),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function ownerIsVisible(state: ToolSurfaceState, owner: OpenPiToolOwner) {
|
|
235
|
+
const capability = capabilityForOwner(owner);
|
|
236
|
+
return capability === undefined || state.loaded.has(capability);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function ownedToolNames(owner: OpenPiToolOwner) {
|
|
240
|
+
return [
|
|
241
|
+
...OPENPI_TOOL_SURFACE[owner].entry,
|
|
242
|
+
...OPENPI_TOOL_SURFACE[owner].deferred,
|
|
243
|
+
] as readonly string[];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function availableOwnedToolNames(
|
|
247
|
+
pi: ActiveToolSurface,
|
|
248
|
+
state: ToolSurfaceState,
|
|
249
|
+
owner: OpenPiToolOwner,
|
|
250
|
+
) {
|
|
251
|
+
for (const name of pi.getActiveTools()) state.knownAvailable.add(name);
|
|
252
|
+
const reportedTools = pi.getAllTools?.();
|
|
253
|
+
const owned = new Set<string>(ownedToolNames(owner));
|
|
254
|
+
if (!reportedTools || reportedTools.length === 0) {
|
|
255
|
+
return new Set([...state.knownAvailable].filter((name) => owned.has(name)));
|
|
256
|
+
}
|
|
257
|
+
if (reportedTools.every((tool) => tool.sourceInfo === undefined)) {
|
|
258
|
+
return new Set(
|
|
259
|
+
reportedTools.map(({ name }) => name).filter((name) => owned.has(name)),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const expectedSource =
|
|
264
|
+
state.sourceByOwner.get(owner) ?? OWNER_SOURCE_PATHS[owner];
|
|
265
|
+
return new Set(
|
|
266
|
+
reportedTools
|
|
267
|
+
.filter(
|
|
268
|
+
(tool) =>
|
|
269
|
+
owned.has(tool.name) && tool.sourceInfo?.path === expectedSource,
|
|
270
|
+
)
|
|
271
|
+
.map(({ name }) => name),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function projectedOwnerTools(
|
|
276
|
+
pi: ActiveToolSurface,
|
|
277
|
+
state: ToolSurfaceState,
|
|
278
|
+
owner: OpenPiToolOwner,
|
|
279
|
+
) {
|
|
280
|
+
const available = availableOwnedToolNames(pi, state, owner);
|
|
281
|
+
const owned = ownedToolNames(owner);
|
|
282
|
+
const ownedAvailable = new Set(owned.filter((name) => available.has(name)));
|
|
283
|
+
const desired = state.desiredByOwner.get(owner)!;
|
|
284
|
+
const visible = ownerIsVisible(state, owner);
|
|
285
|
+
const next = pi
|
|
286
|
+
.getActiveTools()
|
|
287
|
+
.filter(
|
|
288
|
+
(name) => !ownedAvailable.has(name) || (visible && desired.has(name)),
|
|
289
|
+
);
|
|
290
|
+
const nextSet = new Set(next);
|
|
291
|
+
|
|
292
|
+
if (visible) {
|
|
293
|
+
for (const name of owned) {
|
|
294
|
+
if (desired.has(name) && !nextSet.has(name) && available.has(name)) {
|
|
295
|
+
next.push(name);
|
|
296
|
+
nextSet.add(name);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return next;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function applyActiveTools(pi: ActiveToolSurface, next: string[]) {
|
|
305
|
+
const active = pi.getActiveTools();
|
|
306
|
+
if (
|
|
307
|
+
active.length === next.length &&
|
|
308
|
+
active.every((name, index) => name === next[index])
|
|
309
|
+
) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
pi.setActiveTools(next);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function reconcileOwner(
|
|
317
|
+
pi: ActiveToolSurface,
|
|
318
|
+
state: ToolSurfaceState,
|
|
319
|
+
owner: OpenPiToolOwner,
|
|
320
|
+
) {
|
|
321
|
+
return applyActiveTools(pi, projectedOwnerTools(pi, state, owner));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function reconcileManagedOwners(
|
|
325
|
+
pi: ActiveToolSurface,
|
|
326
|
+
state: ToolSurfaceState,
|
|
327
|
+
) {
|
|
328
|
+
let changed = false;
|
|
329
|
+
for (const owner of state.managedOwners) {
|
|
330
|
+
changed = reconcileOwner(pi, state, owner) || changed;
|
|
331
|
+
}
|
|
332
|
+
return changed;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Reset one bound Pi Session to the minimal parent surface. */
|
|
336
|
+
export function resetOpenPiToolSurface(
|
|
337
|
+
pi: ActiveToolSurface,
|
|
338
|
+
sourceByOwner: Readonly<Partial<Record<OpenPiToolOwner, string>>> = {},
|
|
339
|
+
) {
|
|
340
|
+
const state = newState();
|
|
341
|
+
for (const [owner, source] of Object.entries(sourceByOwner)) {
|
|
342
|
+
if (source) state.sourceByOwner.set(owner as OpenPiToolOwner, source);
|
|
343
|
+
}
|
|
344
|
+
states.set(pi, state);
|
|
345
|
+
state.managedOwners.add("capabilities");
|
|
346
|
+
const changed = reconcileOwner(pi, state, "capabilities");
|
|
347
|
+
pi.events?.emit(OPENPI_CAPABILITY_STATE_CHANNEL, { loaded: [] });
|
|
348
|
+
return changed;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function getLoadedOpenPiCapabilities(pi: ActiveToolSurface) {
|
|
352
|
+
return OPENPI_CAPABILITY_NAMES.filter((capability) =>
|
|
353
|
+
stateFor(pi).loaded.has(capability),
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Load capability groups monotonically for this Session. There is deliberately
|
|
359
|
+
* no unload operation: a stable surface is more cache-friendly and easier for
|
|
360
|
+
* the model to reason about than tools that repeatedly disappear and return.
|
|
361
|
+
*/
|
|
362
|
+
export function loadOpenPiCapabilities(
|
|
363
|
+
pi: ActiveToolSurface,
|
|
364
|
+
capabilities: readonly OpenPiCapability[],
|
|
365
|
+
) {
|
|
366
|
+
const invalid = capabilities.filter(
|
|
367
|
+
(capability) => !OPENPI_CAPABILITY_NAMES.includes(capability),
|
|
368
|
+
);
|
|
369
|
+
if (invalid.length > 0) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`Unknown OpenPI ${invalid.length === 1 ? "capability" : "capabilities"}: ${invalid.map((name) => JSON.stringify(name)).join(", ")}.`,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const state = stateFor(pi);
|
|
376
|
+
const before = pi.getActiveTools();
|
|
377
|
+
const newlyLoaded: OpenPiCapability[] = [];
|
|
378
|
+
for (const capability of capabilities) {
|
|
379
|
+
if (!state.loaded.has(capability)) {
|
|
380
|
+
state.loaded.add(capability);
|
|
381
|
+
newlyLoaded.push(capability);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
reconcileManagedOwners(pi, state);
|
|
385
|
+
pi.events?.emit(OPENPI_CAPABILITY_STATE_CHANNEL, {
|
|
386
|
+
loaded: getLoadedOpenPiCapabilities(pi),
|
|
387
|
+
});
|
|
388
|
+
const beforeSet = new Set(before);
|
|
389
|
+
return {
|
|
390
|
+
newlyLoaded,
|
|
391
|
+
loaded: getLoadedOpenPiCapabilities(pi),
|
|
392
|
+
activatedTools: pi.getActiveTools().filter((name) => !beforeSet.has(name)),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Record one owner's desired tools and reconcile them through both gates:
|
|
398
|
+
* capability loaded first, then the owner's authoritative resource/mode state.
|
|
399
|
+
* Foreign and Pi-native tools are always preserved from the latest active list.
|
|
400
|
+
*/
|
|
401
|
+
export function patchOwnedTools(
|
|
402
|
+
pi: ActiveToolSurface,
|
|
403
|
+
owner: OpenPiToolOwner,
|
|
404
|
+
patch: OwnedToolPatch,
|
|
405
|
+
) {
|
|
406
|
+
const owned = [
|
|
407
|
+
...OPENPI_TOOL_SURFACE[owner].entry,
|
|
408
|
+
...OPENPI_TOOL_SURFACE[owner].deferred,
|
|
409
|
+
] as readonly string[];
|
|
410
|
+
const ownedSet = new Set(owned);
|
|
411
|
+
const enable = new Set(patch.enable ?? []);
|
|
412
|
+
const disable = new Set(patch.disable ?? []);
|
|
413
|
+
|
|
414
|
+
for (const name of [...enable, ...disable]) {
|
|
415
|
+
if (!ownedSet.has(name)) {
|
|
416
|
+
throw new Error(`${owner} does not own tool ${JSON.stringify(name)}.`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const name of enable) {
|
|
420
|
+
if (disable.has(name)) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`${owner} cannot enable and disable tool ${JSON.stringify(name)} in one patch.`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const state = stateFor(pi);
|
|
428
|
+
state.managedOwners.add(owner);
|
|
429
|
+
if (capabilityForOwner(owner)) subscribeToCapabilityState(pi, state);
|
|
430
|
+
const desired = state.desiredByOwner.get(owner)!;
|
|
431
|
+
for (const name of enable) state.knownAvailable.add(name);
|
|
432
|
+
for (const name of disable) desired.delete(name);
|
|
433
|
+
for (const name of enable) desired.add(name);
|
|
434
|
+
return reconcileOwner(pi, state, owner);
|
|
435
|
+
}
|
|
@@ -65,6 +65,10 @@ import {
|
|
|
65
65
|
hasActivity,
|
|
66
66
|
unreadActivityCounts,
|
|
67
67
|
} from "../shared/activity-status.ts";
|
|
68
|
+
import {
|
|
69
|
+
OPENPI_TOOL_SURFACE,
|
|
70
|
+
patchOwnedTools,
|
|
71
|
+
} from "../shared/tool-surface.ts";
|
|
68
72
|
import { formatContextUtilization } from "./src/format.ts";
|
|
69
73
|
import { SubagentManager, type SubagentManagerShape } from "./src/manager.ts";
|
|
70
74
|
import {
|
|
@@ -198,6 +202,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
198
202
|
let requestWidgetRender: (() => void) | undefined;
|
|
199
203
|
let dashboardOpen = false;
|
|
200
204
|
const resultDelivery = createDeferredResultDelivery<SubagentSnapshot>();
|
|
205
|
+
const hideLifecycleTools = () =>
|
|
206
|
+
patchOwnedTools(pi, "subagents", {
|
|
207
|
+
disable: OPENPI_TOOL_SURFACE.subagents.deferred,
|
|
208
|
+
});
|
|
209
|
+
const showLifecycleTools = () =>
|
|
210
|
+
patchOwnedTools(pi, "subagents", {
|
|
211
|
+
enable: OPENPI_TOOL_SURFACE.subagents.deferred,
|
|
212
|
+
});
|
|
201
213
|
|
|
202
214
|
const getRuntime = () => (runtime ??= createSubagentRuntime());
|
|
203
215
|
|
|
@@ -408,6 +420,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
408
420
|
|
|
409
421
|
pi.on("session_start", (_event, ctx) => {
|
|
410
422
|
refreshAgentTypes(ctx.cwd, ctx.isProjectTrusted());
|
|
423
|
+
hideLifecycleTools();
|
|
411
424
|
sessionContext = ctx;
|
|
412
425
|
settledAcknowledgedAt = 0;
|
|
413
426
|
if (ctx.hasUI) ui = ctx.ui;
|
|
@@ -633,7 +646,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
633
646
|
? planModeChildTools(declaredChildTools)
|
|
634
647
|
: declaredChildTools;
|
|
635
648
|
const childTools = effectiveChildToolAllowlist(requestedChildTools);
|
|
636
|
-
// Read at spawn time so `/
|
|
649
|
+
// Read at spawn time so `/openpi-setup` changes affect the next child
|
|
637
650
|
// without reloading this extension. Undefined preserves parent-model
|
|
638
651
|
// inheritance in the backend.
|
|
639
652
|
const model = selectSubagentModel(
|
|
@@ -681,6 +694,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
681
694
|
throw error;
|
|
682
695
|
}
|
|
683
696
|
|
|
697
|
+
showLifecycleTools();
|
|
698
|
+
|
|
684
699
|
return {
|
|
685
700
|
content: [
|
|
686
701
|
{
|
|
@@ -205,7 +205,8 @@ const makeManager = Effect.gen(function* () {
|
|
|
205
205
|
let reservedBtw = 0;
|
|
206
206
|
let disposed = false;
|
|
207
207
|
let onSettled:
|
|
208
|
-
((snap: SubagentSnapshot, consumed: boolean) => void)
|
|
208
|
+
| ((snap: SubagentSnapshot, consumed: boolean) => void)
|
|
209
|
+
| undefined;
|
|
209
210
|
|
|
210
211
|
const notify = (id?: string) => {
|
|
211
212
|
const waiters = changeWaiters;
|
|
@@ -636,16 +637,17 @@ const makeManager = Effect.gen(function* () {
|
|
|
636
637
|
pruneSettled();
|
|
637
638
|
}),
|
|
638
639
|
),
|
|
639
|
-
Effect.map(
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
640
|
+
Effect.map(
|
|
641
|
+
(): ReadonlyArray<CancelResult> =>
|
|
642
|
+
unique.map((id) => {
|
|
643
|
+
const snapshot = entries.get(id)?.snapshot;
|
|
644
|
+
return {
|
|
645
|
+
id,
|
|
646
|
+
title: snapshot?.title ?? "?",
|
|
647
|
+
status: snapshot?.status ?? "error",
|
|
648
|
+
cancelled: runningIds.includes(id),
|
|
649
|
+
};
|
|
650
|
+
}),
|
|
649
651
|
),
|
|
650
652
|
);
|
|
651
653
|
});
|
|
@@ -75,7 +75,7 @@ export const SUBAGENT_SPAWN_PARAMETER_DESCRIPTIONS = {
|
|
|
75
75
|
workingDir:
|
|
76
76
|
"Trusted working directory for the autonomous child (default: current working directory)",
|
|
77
77
|
isolation:
|
|
78
|
-
'Set to "worktree"
|
|
78
|
+
'Set to "worktree" for concurrent writers and tell the child to commit. Requires Git and a clean checkout. Read the subagents Skill for lifecycle, merge location, and costs.',
|
|
79
79
|
model:
|
|
80
80
|
'Optional model override, as "provider/model-id" or a bare id resolved against the current provider. Precedence: explicit spawn model > selected type file model > configured built-in role model > parent model. Never guess a model name.',
|
|
81
81
|
reasoningEffort:
|