@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.
- package/README.md +65 -28
- package/SETUP.md +8 -6
- 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/bash-policy.ts +219 -42
- package/extensions/plan-mode/index.ts +56 -19
- package/extensions/setup/index.ts +96 -10
- package/extensions/shared/child-session.ts +40 -4
- package/extensions/shared/editor-layers.ts +150 -0
- package/extensions/shared/setup-config.ts +26 -15
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +179 -96
- package/extensions/subagents/src/manager.ts +13 -11
- package/extensions/subagents/src/prompt.ts +7 -7
- package/extensions/subagents/src/ui/takeover.ts +231 -133
- package/extensions/subagents/src/ui/transcript.ts +252 -37
- package/extensions/subagents/src/ui/wait-result.ts +6 -19
- package/extensions/suggestions/index.ts +27 -18
- package/extensions/tasks/index.ts +63 -18
- package/extensions/ui-customization/footer.ts +65 -11
- package/extensions/workflows/graph-projection.ts +6 -4
- package/extensions/workflows/index.ts +44 -21
- package/extensions/workflows/invocation-ledger.ts +8 -2
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/prompt.ts +10 -40
- package/extensions/workflows/replay-safety.ts +9 -8
- package/package.json +10 -10
- package/skills/subagents/SKILL.md +7 -1
- package/skills/workflows/EXAMPLES.md +58 -0
- package/skills/workflows/REFERENCE.md +44 -0
- package/skills/workflows/SKILL.md +39 -0
|
@@ -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
|
+
}
|