@tt-a1i/openpi 0.1.1 → 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/README.md +37 -22
- 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/index.ts +12 -0
- package/extensions/setup/index.ts +93 -7
- package/extensions/shared/child-session.ts +40 -4
- package/extensions/shared/setup-config.ts +22 -0
- package/extensions/shared/setup-episode-state.ts +7 -0
- package/extensions/shared/tool-surface.ts +435 -0
- package/extensions/subagents/index.ts +15 -0
- 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/graph-projection.ts +6 -4
- package/extensions/workflows/index.ts +16 -1
- 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 +6 -0
- 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
|
+
}
|
|
@@ -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;
|
|
@@ -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:
|
|
@@ -6,6 +6,10 @@ import type {
|
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { Key, Text } from "@earendil-works/pi-tui";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
+
import {
|
|
10
|
+
OPENPI_TOOL_SURFACE,
|
|
11
|
+
patchOwnedTools,
|
|
12
|
+
} from "../shared/tool-surface.ts";
|
|
9
13
|
import {
|
|
10
14
|
TASKS_ENTRY_TYPE,
|
|
11
15
|
TASKS_LIMITS,
|
|
@@ -97,6 +101,14 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
97
101
|
let taskWidgetExpanded = false;
|
|
98
102
|
let ui: ExtensionContext["ui"] | undefined;
|
|
99
103
|
let uiMode: ExtensionContext["mode"] | undefined;
|
|
104
|
+
const hideLifecycleTools = () =>
|
|
105
|
+
patchOwnedTools(pi, "tasks", {
|
|
106
|
+
disable: OPENPI_TOOL_SURFACE.tasks.deferred,
|
|
107
|
+
});
|
|
108
|
+
const showLifecycleTools = () =>
|
|
109
|
+
patchOwnedTools(pi, "tasks", {
|
|
110
|
+
enable: OPENPI_TOOL_SURFACE.tasks.deferred,
|
|
111
|
+
});
|
|
100
112
|
|
|
101
113
|
const snapshot = () => tasks.snapshot();
|
|
102
114
|
|
|
@@ -199,13 +211,15 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
199
211
|
items,
|
|
200
212
|
total: snapshot().items.length,
|
|
201
213
|
revision: snapshot().revision,
|
|
202
|
-
// From the live snapshot, not `items`: a tools_update carries only the one
|
|
203
|
-
// row it touched, and a header counted from that would claim the batch is
|
|
204
|
-
// a single task.
|
|
205
214
|
counts: taskCounts(snapshot().items),
|
|
206
215
|
...(batchClosed ? { batchClosed: true } : {}),
|
|
207
216
|
});
|
|
208
217
|
|
|
218
|
+
const mutationResultText = (summary: string) => {
|
|
219
|
+
const current = snapshot();
|
|
220
|
+
return `${summary}\nCurrent task snapshot (${current.items.length} ${current.items.length === 1 ? "item" : "items"}):\n${tasks.render()}`;
|
|
221
|
+
};
|
|
222
|
+
|
|
209
223
|
const registerTools = () => {
|
|
210
224
|
if (toolsRegistered || conflict) return;
|
|
211
225
|
toolsRegistered = true;
|
|
@@ -218,6 +232,7 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
218
232
|
"Add stable work-intent items to the current session tasks",
|
|
219
233
|
promptGuidelines: [
|
|
220
234
|
"Use tasks_add only for work spanning multiple agent runs or user turns, or when the user explicitly provides a task list; do not use it as a per-step scratchpad within one run.",
|
|
235
|
+
"Before starting each tracked item, call tasks_update to mark it in_progress; concurrent work may have multiple in_progress items.",
|
|
221
236
|
"Task tools record advisory intent only; Subagents and Workflows execute work, while files, git, tests, tool results, artifacts, and user confirmation remain truth.",
|
|
222
237
|
],
|
|
223
238
|
parameters: Type.Object({
|
|
@@ -239,14 +254,17 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
239
254
|
assertAvailable();
|
|
240
255
|
const mutation = applyTaskAdd(snapshot(), params.items);
|
|
241
256
|
persistThenCommit(mutation.snapshot);
|
|
257
|
+
showLifecycleTools();
|
|
242
258
|
return Promise.resolve({
|
|
243
259
|
content: [
|
|
244
260
|
{
|
|
245
261
|
type: "text" as const,
|
|
246
|
-
text:
|
|
262
|
+
text: mutationResultText(
|
|
263
|
+
`Added ${mutation.items.map((item) => `T${item.id}`).join(", ")}.`,
|
|
264
|
+
),
|
|
247
265
|
},
|
|
248
266
|
],
|
|
249
|
-
details: toolDetails("add",
|
|
267
|
+
details: toolDetails("add", snapshot().items),
|
|
250
268
|
});
|
|
251
269
|
},
|
|
252
270
|
renderCall(args, theme) {
|
|
@@ -273,7 +291,9 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
273
291
|
description: `${TOOL_PURPOSE} Patch one task item by numeric ID. blocked, done, and dropped status changes require a fresh note explaining the blocker, observable evidence, or drop reason.`,
|
|
274
292
|
promptSnippet: "Update one session task item by stable ID",
|
|
275
293
|
promptGuidelines: [
|
|
276
|
-
"
|
|
294
|
+
"Immediately after each tracked item reaches a real outcome, call tasks_update to set done, blocked, or dropped before moving to the next tracked item.",
|
|
295
|
+
"Before sending a final answer, reconcile every task touched in the current request; do not leave completed work pending or in_progress.",
|
|
296
|
+
"A commit, passing test, or authorization is task-scoped evidence only; it does not by itself prove a task is done or identify which task to update.",
|
|
277
297
|
"Before setting a task item to done, include a note citing an observable check, artifact, commit, tool result, or user confirmation; Tasks record this claim but do not verify it.",
|
|
278
298
|
],
|
|
279
299
|
parameters: Type.Object({
|
|
@@ -305,18 +325,21 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
305
325
|
);
|
|
306
326
|
const mutation = applyTaskUpdate(before, params);
|
|
307
327
|
const changed = persistThenCommit(mutation.snapshot);
|
|
328
|
+
if (changed && closesBatch) hideLifecycleTools();
|
|
308
329
|
return Promise.resolve({
|
|
309
330
|
content: [
|
|
310
331
|
{
|
|
311
332
|
type: "text" as const,
|
|
312
|
-
text:
|
|
313
|
-
|
|
314
|
-
?
|
|
315
|
-
|
|
316
|
-
|
|
333
|
+
text: mutationResultText(
|
|
334
|
+
changed
|
|
335
|
+
? closesBatch
|
|
336
|
+
? `${params.status === "dropped" ? "Dropped" : "Completed"} T${params.id}. Task batch closed; the next tasks_add starts again at T1.`
|
|
337
|
+
: `Updated T${params.id}.`
|
|
338
|
+
: `T${params.id} already has that state; no update recorded.`,
|
|
339
|
+
),
|
|
317
340
|
},
|
|
318
341
|
],
|
|
319
|
-
details: toolDetails("update",
|
|
342
|
+
details: toolDetails("update", snapshot().items, closesBatch),
|
|
320
343
|
});
|
|
321
344
|
},
|
|
322
345
|
renderCall(args, theme) {
|
|
@@ -448,12 +471,16 @@ export default function sessionTasks(pi: ExtensionAPI) {
|
|
|
448
471
|
taskWidgetVisible = true;
|
|
449
472
|
taskWidgetExpanded = false;
|
|
450
473
|
registerTools();
|
|
474
|
+
if (hasActionableTasks()) showLifecycleTools();
|
|
475
|
+
else hideLifecycleTools();
|
|
451
476
|
notifyProblem(ctx);
|
|
452
477
|
updateTaskWidget(ctx);
|
|
453
478
|
});
|
|
454
479
|
|
|
455
480
|
pi.on("session_tree", (_event, ctx) => {
|
|
456
481
|
restore(ctx);
|
|
482
|
+
if (hasActionableTasks()) showLifecycleTools();
|
|
483
|
+
else hideLifecycleTools();
|
|
457
484
|
taskWidgetExpanded = false;
|
|
458
485
|
coldRun = true;
|
|
459
486
|
activeRun = false;
|
|
@@ -72,7 +72,12 @@ const MONO_COLORS: readonly PowerlineColors[] = [
|
|
|
72
72
|
];
|
|
73
73
|
|
|
74
74
|
export type SegmentTone =
|
|
75
|
-
|
|
75
|
+
| "text"
|
|
76
|
+
| "muted"
|
|
77
|
+
| "dim"
|
|
78
|
+
| "warning"
|
|
79
|
+
| "error"
|
|
80
|
+
| "accent";
|
|
76
81
|
|
|
77
82
|
export interface FooterSegment {
|
|
78
83
|
readonly id: FooterItem;
|
|
@@ -140,10 +140,12 @@ function findCycles(
|
|
|
140
140
|
if (!indexes.has(node.callId)) visit(node.callId);
|
|
141
141
|
}
|
|
142
142
|
cycles.sort((left, right) => order.get(left[0]!)! - order.get(right[0]!)!);
|
|
143
|
-
return cycles.map(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
return cycles.map(
|
|
144
|
+
(callIds): WorkflowGraphDiagnostic => ({
|
|
145
|
+
code: "cycle",
|
|
146
|
+
callIds,
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
export function projectWorkflowGraph<Record extends WorkflowGraphRecord>(
|