pi-sessions 0.18.1 → 0.20.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 +64 -39
- package/extensions/hosts/contract.ts +10 -46
- package/extensions/hosts/registry.ts +4 -78
- package/extensions/pi-sessions.ts +27 -53
- package/extensions/session-handoff/agent-profiles.ts +110 -0
- package/extensions/session-handoff/board-loading.ts +4 -1
- package/extensions/session-handoff/board-view-model.ts +1 -0
- package/extensions/session-handoff/board.ts +4 -1
- package/extensions/session-handoff/install.ts +78 -34
- package/extensions/session-handoff/launch/backend.ts +16 -4
- package/extensions/session-handoff/launch-options.ts +2 -17
- package/extensions/session-handoff/launch-target.ts +8 -10
- package/extensions/session-handoff/metadata.ts +10 -0
- package/extensions/session-handoff/model.ts +22 -0
- package/extensions/session-handoff/receipt.ts +1 -1
- package/extensions/session-handoff/spawn.ts +6 -0
- package/extensions/session-handoff/tool-presenter.ts +1 -1
- package/extensions/session-handoff/tool-schema.ts +14 -9
- package/extensions/session-handoff/tool-view-model.ts +6 -2
- package/extensions/session-handoff/tool.ts +20 -6
- package/extensions/session-messaging/directory-list.ts +115 -0
- package/extensions/session-messaging/directory.ts +114 -0
- package/extensions/session-messaging/message-router.ts +3 -8
- package/extensions/session-messaging/pi/directory-tools.ts +41 -0
- package/extensions/session-messaging/pi/install.ts +102 -0
- package/extensions/session-messaging/pi/message-contracts.ts +4 -6
- package/extensions/session-messaging/pi/reachable-contract.ts +3 -4
- package/extensions/session-messaging/pi/reachable-tool.ts +25 -175
- package/extensions/session-messaging/pi/send-message-presenter.ts +2 -1
- package/extensions/session-messaging/pi/tools.ts +49 -91
- package/extensions/session-search/extract.ts +12 -2
- package/extensions/shared/lineage.ts +8 -0
- package/extensions/shared/session-index/sync.ts +3 -0
- package/extensions/shared/settings.ts +21 -5
- package/extensions/subagents/install.ts +11 -31
- package/extensions/subagents/launch-target.ts +2 -1
- package/extensions/subagents/reconcile.ts +10 -2
- package/extensions/subagents/settle.ts +2 -0
- package/package.json +5 -5
|
@@ -2,13 +2,17 @@ import type { Api, Model } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "typebox";
|
|
5
|
-
import type { Host } from "../hosts/contract.ts";
|
|
6
5
|
import type { IndexHandle, SessionLifecycle } from "../shared/composition.ts";
|
|
7
6
|
import type { ModelRuntimeProvider } from "../shared/model-runtime.ts";
|
|
8
7
|
import { isTuiMode } from "../shared/pi-mode.ts";
|
|
9
8
|
import type { SessionSettings } from "../shared/settings.ts";
|
|
10
9
|
import { THINKING_LEVELS } from "../shared/thinking-levels.ts";
|
|
11
10
|
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
11
|
+
import {
|
|
12
|
+
type AgentProfileCatalog,
|
|
13
|
+
EMPTY_AGENT_PROFILE_CATALOG,
|
|
14
|
+
loadAgentProfiles,
|
|
15
|
+
} from "./agent-profiles.ts";
|
|
12
16
|
import { type HandoffBoardServices, openHandoffBoard } from "./board.ts";
|
|
13
17
|
import { consumePendingHandoffBootstrap } from "./bootstrap.ts";
|
|
14
18
|
import { renderHandoffBootstrapFailedEntry } from "./bootstrap-failed-renderer.ts";
|
|
@@ -21,11 +25,7 @@ import { HANDOFF_BOOTSTRAP_FAILED_CUSTOM_TYPE } from "./metadata.ts";
|
|
|
21
25
|
import { openSessionReferencePicker } from "./picker.ts";
|
|
22
26
|
import { SESSION_TOKEN_PREFIX } from "./query.ts";
|
|
23
27
|
import { type HandoffRoster, resolveHandoffRoster } from "./roster.ts";
|
|
24
|
-
import {
|
|
25
|
-
executeSessionHandoffTool,
|
|
26
|
-
type HandoffToolParams,
|
|
27
|
-
MAX_HANDOFF_TITLE_LENGTH,
|
|
28
|
-
} from "./tool.ts";
|
|
28
|
+
import { executeSessionHandoffTool, MAX_HANDOFF_TITLE_LENGTH } from "./tool.ts";
|
|
29
29
|
import { HANDOFF_TOOL_DETAILS_SCHEMA } from "./tool-contract.ts";
|
|
30
30
|
import { HandoffToolComponent } from "./tool-renderer.ts";
|
|
31
31
|
import {
|
|
@@ -51,7 +51,7 @@ export function installHandoff(
|
|
|
51
51
|
index: IndexHandle;
|
|
52
52
|
getModelRuntime: ModelRuntimeProvider;
|
|
53
53
|
getLaunchTargets?: (() => readonly HandoffLaunchTarget[]) | undefined;
|
|
54
|
-
|
|
54
|
+
claimsPeers: () => boolean;
|
|
55
55
|
board: HandoffBoardServices;
|
|
56
56
|
},
|
|
57
57
|
): SessionLifecycle {
|
|
@@ -69,18 +69,26 @@ export function installHandoff(
|
|
|
69
69
|
models: readonly Model<Api>[],
|
|
70
70
|
launchTargets: readonly HandoffLaunchTarget[],
|
|
71
71
|
roster: HandoffRoster | undefined,
|
|
72
|
+
agentProfiles: AgentProfileCatalog,
|
|
73
|
+
name: "session_handoff" | "subagent_launch",
|
|
72
74
|
): void {
|
|
73
75
|
const offersSubagent = offersSubagentLaunch(launchTargets);
|
|
76
|
+
const offersAgents = name === "subagent_launch" && agentProfiles.profiles.length > 0;
|
|
74
77
|
pi.registerTool({
|
|
75
|
-
name
|
|
78
|
+
name,
|
|
76
79
|
label: "Session Handoff",
|
|
77
80
|
description: "Start a new Pi session with a self-contained task.",
|
|
78
81
|
// Nested calls write no toolResult entry, so the board would never see the launch receipt.
|
|
79
82
|
exposure: "model-only",
|
|
80
83
|
promptSnippet: offersSubagent
|
|
81
|
-
? "Delegate bounded work to a background subagent
|
|
84
|
+
? "Delegate bounded work to a background subagent"
|
|
82
85
|
: "Hand off context to another Pi session",
|
|
83
|
-
promptGuidelines: buildHandoffPromptGuidelines(
|
|
86
|
+
promptGuidelines: buildHandoffPromptGuidelines(
|
|
87
|
+
launchTargets,
|
|
88
|
+
models,
|
|
89
|
+
roster,
|
|
90
|
+
offersAgents ? agentProfiles.profiles : [],
|
|
91
|
+
),
|
|
84
92
|
parameters: Type.Object({
|
|
85
93
|
goal: Type.String({
|
|
86
94
|
description:
|
|
@@ -91,17 +99,16 @@ export function installHandoff(
|
|
|
91
99
|
description:
|
|
92
100
|
"Short display title for the child session, 64 characters or less. Summarize the mission; do not repeat the full goal.",
|
|
93
101
|
}),
|
|
94
|
-
launch: buildHandoffLaunchSchema(launchTargets),
|
|
102
|
+
...(name === "session_handoff" ? { launch: buildHandoffLaunchSchema(launchTargets) } : {}),
|
|
95
103
|
cwd: Type.Optional(
|
|
96
104
|
Type.String({
|
|
97
105
|
description: "Optional target working directory (defaults to current).",
|
|
98
106
|
}),
|
|
99
107
|
),
|
|
100
|
-
requestResponse: Type.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
),
|
|
108
|
+
requestResponse: Type.Boolean({
|
|
109
|
+
description:
|
|
110
|
+
"Whether the child session should report completion/results of its task back to this session.",
|
|
111
|
+
}),
|
|
105
112
|
provider: Type.Optional(
|
|
106
113
|
Type.String({
|
|
107
114
|
description: "Model provider for the child session.",
|
|
@@ -122,12 +129,22 @@ export function installHandoff(
|
|
|
122
129
|
},
|
|
123
130
|
),
|
|
124
131
|
),
|
|
132
|
+
...(offersAgents
|
|
133
|
+
? {
|
|
134
|
+
agent: Type.Optional(
|
|
135
|
+
Type.String({
|
|
136
|
+
description:
|
|
137
|
+
"Agent profile to run the subagent as. The profile adds instructions to the subagent's system prompt and may set its model and thinking level; explicit provider/model and thinkingLevel take precedence.",
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
125
142
|
}),
|
|
126
143
|
renderCall(args, theme, context) {
|
|
127
144
|
const state = context.state as HandoffToolRendererState;
|
|
128
145
|
const component = state.callComponent ?? new HandoffToolComponent(theme);
|
|
129
146
|
state.callComponent = component;
|
|
130
|
-
component.update(buildHandoffToolView(args), context.expanded);
|
|
147
|
+
component.update(buildHandoffToolView(args, undefined, name), context.expanded);
|
|
131
148
|
return component;
|
|
132
149
|
},
|
|
133
150
|
renderResult(result, options, theme, context) {
|
|
@@ -143,21 +160,31 @@ export function installHandoff(
|
|
|
143
160
|
}
|
|
144
161
|
|
|
145
162
|
state.callComponent.update(
|
|
146
|
-
buildHandoffToolView(context.args, details),
|
|
163
|
+
buildHandoffToolView(context.args, details, name),
|
|
147
164
|
options.expanded,
|
|
148
165
|
clipboardStatusBySessionId.get(details.sessionId),
|
|
149
166
|
);
|
|
150
167
|
return new Text("", 0, 0);
|
|
151
168
|
},
|
|
152
|
-
async execute(_toolCallId, params
|
|
169
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
170
|
+
if (name === "session_handoff" && deps.claimsPeers())
|
|
171
|
+
throw new Error("Peer tools belong to the current host.");
|
|
172
|
+
const launch = name === "subagent_launch" ? "subagent" : params.launch;
|
|
173
|
+
if (typeof launch !== "string") throw new Error("session_handoff requires launch.");
|
|
174
|
+
if (name === "subagent_launch" && !deps.getLaunchTargets?.().length)
|
|
175
|
+
throw new Error("Subagent launch is unavailable.");
|
|
176
|
+
const { agent, ...rest } = params;
|
|
177
|
+
if (agent !== undefined && typeof agent !== "string")
|
|
178
|
+
throw new Error("agent must name an agent profile.");
|
|
153
179
|
const modelRuntime = await deps.getModelRuntime(ctx.modelRegistry);
|
|
154
180
|
return executeSessionHandoffTool(
|
|
155
181
|
pi,
|
|
156
|
-
|
|
182
|
+
{ ...rest, launch, ...(agent === undefined ? {} : { agent }) },
|
|
157
183
|
ctx,
|
|
158
184
|
modelRuntime,
|
|
159
185
|
launchTargets,
|
|
160
186
|
roster,
|
|
187
|
+
agentProfiles,
|
|
161
188
|
(sessionId, status) => clipboardStatusBySessionId.set(sessionId, status),
|
|
162
189
|
);
|
|
163
190
|
},
|
|
@@ -205,7 +232,7 @@ export function installHandoff(
|
|
|
205
232
|
});
|
|
206
233
|
|
|
207
234
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
208
|
-
if (!deps.
|
|
235
|
+
if (!deps.claimsPeers() && splitBackend?.identifyTerminalId && ctx) {
|
|
209
236
|
identifiedGhosttyTerminalId =
|
|
210
237
|
(await splitBackend.identifyTerminalId(ctx.cwd)) ?? identifiedGhosttyTerminalId;
|
|
211
238
|
}
|
|
@@ -216,21 +243,38 @@ export function installHandoff(
|
|
|
216
243
|
return {
|
|
217
244
|
async onSessionStart(_event, ctx) {
|
|
218
245
|
const models = ctx.modelRegistry.getAvailable();
|
|
219
|
-
|
|
246
|
+
const roster = resolveHandoffRoster({
|
|
220
247
|
models,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
248
|
+
patterns: settings.handoff.roster,
|
|
249
|
+
scopedModels: ctx.scopedModels,
|
|
250
|
+
});
|
|
251
|
+
const subagentTargets = deps.getLaunchTargets?.() ?? [];
|
|
252
|
+
const agentProfiles = subagentTargets.length
|
|
253
|
+
? loadAgentProfiles(settings.subagents.agentsDir)
|
|
254
|
+
: EMPTY_AGENT_PROFILE_CATALOG;
|
|
255
|
+
if (!deps.claimsPeers?.()) {
|
|
256
|
+
registerHandoffTool(
|
|
229
257
|
models,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
258
|
+
createHandoffLaunchTargets({
|
|
259
|
+
pi,
|
|
260
|
+
splitBackend,
|
|
261
|
+
deferred: settings.handoff.deferred,
|
|
262
|
+
additionalTargets: [],
|
|
263
|
+
}),
|
|
264
|
+
roster,
|
|
265
|
+
EMPTY_AGENT_PROFILE_CATALOG,
|
|
266
|
+
"session_handoff",
|
|
267
|
+
);
|
|
268
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), "session_handoff"])]);
|
|
269
|
+
} else {
|
|
270
|
+
pi.setActiveTools(pi.getActiveTools().filter((name) => name !== "session_handoff"));
|
|
271
|
+
}
|
|
272
|
+
if (subagentTargets.length) {
|
|
273
|
+
registerHandoffTool(models, subagentTargets, roster, agentProfiles, "subagent_launch");
|
|
274
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), "subagent_launch"])]);
|
|
275
|
+
} else {
|
|
276
|
+
pi.setActiveTools(pi.getActiveTools().filter((name) => name !== "subagent_launch"));
|
|
277
|
+
}
|
|
234
278
|
|
|
235
279
|
await consumePendingHandoffBootstrap(
|
|
236
280
|
pi,
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import type { Host, HostLaunchInput } from "../../hosts/contract.ts";
|
|
2
|
-
|
|
3
1
|
export type HandoffSplitDirection = "left" | "right" | "up" | "down";
|
|
4
2
|
|
|
5
|
-
export
|
|
3
|
+
export interface LaunchInput {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
sessionFile: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
title: string;
|
|
8
|
+
model: string;
|
|
9
|
+
resumeCommand: string;
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
export type ClipboardStatus = "copied" | "failed";
|
|
8
13
|
|
|
9
|
-
export
|
|
14
|
+
export interface LaunchBackend {
|
|
15
|
+
name: string;
|
|
16
|
+
launch(
|
|
17
|
+
input: LaunchInput,
|
|
18
|
+
): Promise<
|
|
19
|
+
{ success: true; clipboardStatus?: ClipboardStatus } | { success: false; error: string }
|
|
20
|
+
>;
|
|
21
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Host } from "../hosts/contract.ts";
|
|
3
2
|
import type { HandoffSettings } from "../shared/settings.ts";
|
|
4
3
|
import { createDeferredLaunchBackend } from "./launch/deferred.ts";
|
|
5
4
|
import type { SplitLaunchBackend } from "./launch/resolution.ts";
|
|
@@ -21,10 +20,8 @@ export function createHandoffLaunchTargets(options: {
|
|
|
21
20
|
splitBackend: SplitLaunchBackend | undefined;
|
|
22
21
|
deferred: HandoffSettings["deferred"];
|
|
23
22
|
additionalTargets: readonly HandoffLaunchTarget[];
|
|
24
|
-
hosts?: readonly Host[];
|
|
25
23
|
}): HandoffLaunchTarget[] {
|
|
26
|
-
const
|
|
27
|
-
const splitBackend = hosts.length ? undefined : options.splitBackend;
|
|
24
|
+
const splitBackend = options.splitBackend;
|
|
28
25
|
const splitTargets = splitBackend
|
|
29
26
|
? LAUNCH_DIRECTIONS.map((direction, index) =>
|
|
30
27
|
createBackendLaunchTarget(
|
|
@@ -45,19 +42,7 @@ export function createHandoffLaunchTargets(options: {
|
|
|
45
42
|
),
|
|
46
43
|
]
|
|
47
44
|
: [];
|
|
48
|
-
return [
|
|
49
|
-
...hosts.map((host) =>
|
|
50
|
-
createBackendLaunchTarget(
|
|
51
|
-
host.name,
|
|
52
|
-
host,
|
|
53
|
-
`'${host.name}' opens a session in ${host.name}.`,
|
|
54
|
-
true,
|
|
55
|
-
),
|
|
56
|
-
),
|
|
57
|
-
...splitTargets,
|
|
58
|
-
...deferredTargets,
|
|
59
|
-
...options.additionalTargets,
|
|
60
|
-
];
|
|
45
|
+
return [...splitTargets, ...deferredTargets, ...options.additionalTargets];
|
|
61
46
|
}
|
|
62
47
|
|
|
63
48
|
export function resolveHandoffLaunchTarget(
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
|
-
import type { Host } from "../hosts/contract.ts";
|
|
4
3
|
import { formatError } from "../shared/errors.ts";
|
|
5
|
-
import type { ClipboardStatus } from "./launch/backend.ts";
|
|
6
|
-
import type { HandoffSubagent } from "./metadata.ts";
|
|
4
|
+
import type { ClipboardStatus, LaunchBackend } from "./launch/backend.ts";
|
|
5
|
+
import type { HandoffSubagent, HandoffSubagentAgent } from "./metadata.ts";
|
|
7
6
|
import type { PreparedHandoff } from "./spawn.ts";
|
|
8
7
|
|
|
9
8
|
export const DEFERRED_LAUNCH = "deferred" as const;
|
|
@@ -58,16 +57,18 @@ export type HandoffLaunchTargetOutcome =
|
|
|
58
57
|
export interface HandoffLaunchTarget {
|
|
59
58
|
value: HandoffLaunchValue;
|
|
60
59
|
description?: string | undefined;
|
|
61
|
-
requestResponseDefault: boolean;
|
|
62
60
|
bootstrapMode: "review" | "automatic";
|
|
63
61
|
/** Unattended children skip the startup trust prompt; watched ones let the user answer it. */
|
|
64
62
|
approveProjectTrust: boolean;
|
|
65
63
|
useDefaultSessionDir?: boolean;
|
|
64
|
+
/** Tools the child keeps when an agent profile narrows its tool allowlist. */
|
|
65
|
+
requiredTools?: readonly string[];
|
|
66
66
|
/** Subagent launches stamp their child's identity into the child bootstrap. */
|
|
67
67
|
describeSubagentChild?(input: {
|
|
68
68
|
childSessionId: string;
|
|
69
69
|
ownerSessionId: string;
|
|
70
70
|
requestResponse: boolean;
|
|
71
|
+
agent: HandoffSubagentAgent | undefined;
|
|
71
72
|
}): HandoffSubagent;
|
|
72
73
|
prepareChild(input: HandoffLaunchTargetPreparation): void;
|
|
73
74
|
launch(input: HandoffLaunchTargetInput): Promise<HandoffLaunchTargetOutcome>;
|
|
@@ -75,17 +76,14 @@ export interface HandoffLaunchTarget {
|
|
|
75
76
|
|
|
76
77
|
export function createBackendLaunchTarget(
|
|
77
78
|
value: string,
|
|
78
|
-
host:
|
|
79
|
+
host: LaunchBackend,
|
|
79
80
|
description?: string | undefined,
|
|
80
|
-
external = false,
|
|
81
81
|
): HandoffLaunchTarget {
|
|
82
82
|
return {
|
|
83
83
|
value,
|
|
84
84
|
...(description ? { description } : {}),
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
approveProjectTrust: external,
|
|
88
|
-
useDefaultSessionDir: external,
|
|
85
|
+
bootstrapMode: "review",
|
|
86
|
+
approveProjectTrust: false,
|
|
89
87
|
prepareChild() {},
|
|
90
88
|
async launch(input) {
|
|
91
89
|
try {
|
|
@@ -20,6 +20,14 @@ export const HANDOFF_STALE_SESSION_MESSAGE =
|
|
|
20
20
|
export const SESSION_STARTING_MESSAGE =
|
|
21
21
|
"Target session is still starting. wait for session_reachable to report it as live, then resend.";
|
|
22
22
|
|
|
23
|
+
// An agent profile's instructions travel with the child, which adds them to
|
|
24
|
+
// its subagent prompt section, so they survive wakes and restarts without
|
|
25
|
+
// depending on the profile file.
|
|
26
|
+
export const HANDOFF_SUBAGENT_AGENT_SCHEMA = Type.Object({
|
|
27
|
+
name: Type.String(),
|
|
28
|
+
instructions: Type.String(),
|
|
29
|
+
});
|
|
30
|
+
|
|
23
31
|
// A subagent's self-knowledge, stamped into its child-local bootstrap so the
|
|
24
32
|
// child never opens the parent transcript to learn who it is.
|
|
25
33
|
export const HANDOFF_SUBAGENT_SCHEMA = Type.Object({
|
|
@@ -27,6 +35,7 @@ export const HANDOFF_SUBAGENT_SCHEMA = Type.Object({
|
|
|
27
35
|
ownerSessionId: Type.String(),
|
|
28
36
|
depth: Type.Integer({ minimum: 1 }),
|
|
29
37
|
requestResponse: Type.Boolean(),
|
|
38
|
+
agent: Type.Optional(HANDOFF_SUBAGENT_AGENT_SCHEMA),
|
|
30
39
|
});
|
|
31
40
|
|
|
32
41
|
const HANDOFF_METADATA_BASE = {
|
|
@@ -84,6 +93,7 @@ export const HANDOFF_BOOTSTRAP_CONSUMED_SCHEMA = Type.Object({
|
|
|
84
93
|
});
|
|
85
94
|
|
|
86
95
|
export type HandoffSubagent = Static<typeof HANDOFF_SUBAGENT_SCHEMA>;
|
|
96
|
+
export type HandoffSubagentAgent = Static<typeof HANDOFF_SUBAGENT_AGENT_SCHEMA>;
|
|
87
97
|
export type HandoffSessionMetadata = Static<typeof HANDOFF_SESSION_METADATA_SCHEMA>;
|
|
88
98
|
export type HandoffBootstrap = Static<typeof HANDOFF_BOOTSTRAP_SCHEMA>;
|
|
89
99
|
export type ChildGeneratedHandoffBootstrap = HandoffBootstrap;
|
|
@@ -52,6 +52,28 @@ export function resolveChildModel(
|
|
|
52
52
|
return { model, thinkingLevel: level };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Resolves an agent profile's model. The pattern is user-written, so it gets Pi's
|
|
57
|
+
* CLI grammar, but the model it lands on must still clear the roster.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveProfileModel(
|
|
60
|
+
modelRuntime: ModelRuntime,
|
|
61
|
+
roster: HandoffRoster | undefined,
|
|
62
|
+
modelPattern: string,
|
|
63
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
64
|
+
): HandoffModelOverride {
|
|
65
|
+
const resolved = resolveModelPattern(modelRuntime, modelPattern, thinkingLevel);
|
|
66
|
+
if (!roster) {
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
return resolveChildModel(
|
|
70
|
+
modelRuntime,
|
|
71
|
+
roster,
|
|
72
|
+
{ provider: resolved.model.provider, modelId: resolved.model.id },
|
|
73
|
+
resolved.thinkingLevel,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
/**
|
|
56
78
|
* Resolves a user-configured model pattern with Pi's CLI grammar, including fuzzy
|
|
57
79
|
* aliases. Only for settings the user writes by hand; never for a model's choice.
|
|
@@ -35,7 +35,7 @@ export function parseHandoffLaunchReceiptEntry(
|
|
|
35
35
|
if (
|
|
36
36
|
entry.type === "message" &&
|
|
37
37
|
entry.message.role === "toolResult" &&
|
|
38
|
-
entry.message.toolName === "session_handoff"
|
|
38
|
+
(entry.message.toolName === "session_handoff" || entry.message.toolName === "subagent_launch")
|
|
39
39
|
) {
|
|
40
40
|
return safeParseTypeBoxValue(HANDOFF_LAUNCH_RECEIPT_SCHEMA, entry.message.details);
|
|
41
41
|
}
|
|
@@ -21,6 +21,7 @@ export function prepareHandoffLaunch(options: {
|
|
|
21
21
|
parentSessionFile: string;
|
|
22
22
|
title: string;
|
|
23
23
|
model: string | undefined;
|
|
24
|
+
tools: readonly string[] | undefined;
|
|
24
25
|
approveProjectTrust: boolean;
|
|
25
26
|
useDefaultSessionDir?: boolean;
|
|
26
27
|
buildBootstrap: (sessionId: string) => HandoffBootstrap;
|
|
@@ -54,6 +55,7 @@ export function prepareHandoffLaunch(options: {
|
|
|
54
55
|
sessionId: manager.getSessionId(),
|
|
55
56
|
sessionDir: manager.usesDefaultSessionDir() ? undefined : manager.getSessionDir(),
|
|
56
57
|
model: options.model,
|
|
58
|
+
tools: options.tools,
|
|
57
59
|
approveProjectTrust: options.approveProjectTrust,
|
|
58
60
|
});
|
|
59
61
|
|
|
@@ -94,6 +96,7 @@ export function buildPiResumeCommand(options: {
|
|
|
94
96
|
sessionId: string;
|
|
95
97
|
sessionDir?: string | undefined;
|
|
96
98
|
model?: string | undefined;
|
|
99
|
+
tools?: readonly string[] | undefined;
|
|
97
100
|
approveProjectTrust: boolean;
|
|
98
101
|
}): string {
|
|
99
102
|
const args = ["pi"];
|
|
@@ -107,6 +110,9 @@ export function buildPiResumeCommand(options: {
|
|
|
107
110
|
if (options.model) {
|
|
108
111
|
args.push("--model", shellQuote(options.model));
|
|
109
112
|
}
|
|
113
|
+
if (options.tools) {
|
|
114
|
+
args.push("--tools", shellQuote(options.tools.join(",")));
|
|
115
|
+
}
|
|
110
116
|
|
|
111
117
|
const command = args.join(" ");
|
|
112
118
|
return options.targetCwd === options.parentCwd
|
|
@@ -18,7 +18,7 @@ export function buildHandoffToolPresentation(
|
|
|
18
18
|
|
|
19
19
|
return {
|
|
20
20
|
header: [
|
|
21
|
-
theme.fg("accent", "session_handoff"),
|
|
21
|
+
theme.fg("accent", model.toolName ?? "session_handoff"),
|
|
22
22
|
theme.fg("dim", `[${model.launch ?? PENDING}]`),
|
|
23
23
|
theme.bold(model.title ?? PENDING),
|
|
24
24
|
].join(" "),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
+
import type { AgentProfile } from "./agent-profiles.ts";
|
|
3
4
|
import {
|
|
4
5
|
DEFERRED_LAUNCH,
|
|
5
6
|
type HandoffLaunchTarget,
|
|
@@ -21,7 +22,7 @@ export function buildHandoffLaunchSchema(targets: readonly HandoffLaunchTarget[]
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const SUBAGENT_GUIDELINES = [
|
|
24
|
-
|
|
25
|
+
"Use subagent_launch for a concrete, bounded task that can proceed independently while useful work continues in the current session.",
|
|
25
26
|
"Do not launch a subagent for work that is trivial, duplicates work already underway, requires frequent coordination, or is tightly coupled to the current session's immediate next step.",
|
|
26
27
|
"Tell every subagent explicitly whether it may modify files or should only investigate and report.",
|
|
27
28
|
"Give concurrent subagents disjoint responsibilities and non-overlapping write scopes.",
|
|
@@ -42,10 +43,14 @@ const SUBAGENT_MODEL_OVERRIDE_GUIDELINE =
|
|
|
42
43
|
const DEFAULT_MODEL_OVERRIDE_GUIDELINE =
|
|
43
44
|
"Only override the model when the task clearly warrants it.";
|
|
44
45
|
|
|
46
|
+
const AGENT_PROFILE_GUIDELINE =
|
|
47
|
+
"Set agent to run a subagent as one of the agent profiles below when its description fits the task; the profile supplies the model, so leave provider, model, and thinkingLevel unset unless the user asks otherwise.";
|
|
48
|
+
|
|
45
49
|
export function buildHandoffPromptGuidelines(
|
|
46
50
|
targets: readonly HandoffLaunchTarget[],
|
|
47
51
|
models: readonly Model<Api>[],
|
|
48
52
|
roster: HandoffRoster | undefined,
|
|
53
|
+
agentProfiles: readonly AgentProfile[],
|
|
49
54
|
): string[] {
|
|
50
55
|
const hasSubagent = offersSubagentLaunch(targets);
|
|
51
56
|
const userFacingKinds = describeUserFacingLaunchKinds(targets);
|
|
@@ -62,6 +67,14 @@ export function buildHandoffPromptGuidelines(
|
|
|
62
67
|
? [`Available models, given as provider/model-id: ${selectable.join(", ")}.`]
|
|
63
68
|
: []),
|
|
64
69
|
...(roster?.some((entry) => entry.thinkingLevels) ? [RESTRICTED_THINKING_GUIDELINE] : []),
|
|
70
|
+
...(agentProfiles.length > 0
|
|
71
|
+
? [
|
|
72
|
+
AGENT_PROFILE_GUIDELINE,
|
|
73
|
+
...agentProfiles.map(
|
|
74
|
+
(profile) => `Agent profile "${profile.name}": ${profile.description}`,
|
|
75
|
+
),
|
|
76
|
+
]
|
|
77
|
+
: []),
|
|
65
78
|
];
|
|
66
79
|
}
|
|
67
80
|
|
|
@@ -73,14 +86,6 @@ function describeUserFacingLaunchKinds(targets: readonly HandoffLaunchTarget[]):
|
|
|
73
86
|
const isDirection = (value: string) => LAUNCH_DIRECTIONS.some((direction) => direction === value);
|
|
74
87
|
const kinds = [
|
|
75
88
|
targets.some((target) => isDirection(target.value)) ? "directional" : undefined,
|
|
76
|
-
targets.some(
|
|
77
|
-
(target) =>
|
|
78
|
-
target.value !== SUBAGENT_LAUNCH &&
|
|
79
|
-
target.value !== DEFERRED_LAUNCH &&
|
|
80
|
-
!isDirection(target.value),
|
|
81
|
-
)
|
|
82
|
-
? "host"
|
|
83
|
-
: undefined,
|
|
84
89
|
targets.some((target) => target.value === DEFERRED_LAUNCH) ? "deferred" : undefined,
|
|
85
90
|
];
|
|
86
91
|
return kinds.filter((kind) => kind !== undefined).join(" or ");
|
|
@@ -4,6 +4,7 @@ import { isThinkingLevel } from "../shared/thinking-levels.ts";
|
|
|
4
4
|
import type { HandoffToolDetails } from "./tool-contract.ts";
|
|
5
5
|
|
|
6
6
|
export interface HandoffToolViewModel {
|
|
7
|
+
toolName: string;
|
|
7
8
|
launch?: string | undefined;
|
|
8
9
|
title?: string | undefined;
|
|
9
10
|
goal?: string | undefined;
|
|
@@ -15,14 +16,17 @@ export interface HandoffToolViewModel {
|
|
|
15
16
|
|
|
16
17
|
export function buildHandoffToolView(
|
|
17
18
|
args: unknown,
|
|
18
|
-
result
|
|
19
|
+
result: HandoffToolDetails | undefined,
|
|
20
|
+
toolName: string,
|
|
19
21
|
): HandoffToolViewModel {
|
|
20
22
|
const record = isRecord(args) ? args : {};
|
|
21
23
|
const provider = normalizeOptionalText(result?.provider) ?? readString(record.provider);
|
|
22
24
|
const model = normalizeOptionalText(result?.modelName) ?? readString(record.model);
|
|
23
25
|
const thinkingLevel = result?.thinkingLevel ?? readThinkingLevel(record.thinkingLevel);
|
|
24
26
|
return {
|
|
25
|
-
|
|
27
|
+
toolName,
|
|
28
|
+
launch:
|
|
29
|
+
toolName === "subagent_launch" ? "subagent" : (readString(record.launch) ?? result?.launch),
|
|
26
30
|
title: readString(record.title) ?? result?.title,
|
|
27
31
|
goal: readString(record.goal),
|
|
28
32
|
...(provider ? { provider } : {}),
|
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute, resolve } from "node:path";
|
|
4
4
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { type AgentProfileCatalog, requireAgentProfile } from "./agent-profiles.ts";
|
|
6
7
|
import { resolveHandoffSource } from "./extract.ts";
|
|
7
8
|
import type { ClipboardStatus } from "./launch/backend.ts";
|
|
8
9
|
import { resolveHandoffLaunchTarget } from "./launch-options.ts";
|
|
@@ -10,9 +11,10 @@ import {
|
|
|
10
11
|
DEFERRED_LAUNCH,
|
|
11
12
|
type HandoffLaunchTarget,
|
|
12
13
|
type HandoffLaunchValue,
|
|
14
|
+
SUBAGENT_LAUNCH,
|
|
13
15
|
} from "./launch-target.ts";
|
|
14
16
|
import { createChildGeneratedHandoffBootstrap } from "./metadata.ts";
|
|
15
|
-
import { formatModelArgument, resolveChildModel } from "./model.ts";
|
|
17
|
+
import { formatModelArgument, resolveChildModel, resolveProfileModel } from "./model.ts";
|
|
16
18
|
import { buildLaunchReceipt } from "./receipt.ts";
|
|
17
19
|
import type { HandoffRoster } from "./roster.ts";
|
|
18
20
|
import { formatHandoffLaunchFailure, prepareHandoffLaunch } from "./spawn.ts";
|
|
@@ -28,10 +30,11 @@ export interface HandoffToolParams {
|
|
|
28
30
|
title: string;
|
|
29
31
|
launch: HandoffLaunchValue;
|
|
30
32
|
cwd?: string | undefined;
|
|
31
|
-
requestResponse
|
|
33
|
+
requestResponse: boolean;
|
|
32
34
|
provider?: string | undefined;
|
|
33
35
|
model?: string | undefined;
|
|
34
36
|
thinkingLevel?: ThinkingLevel | undefined;
|
|
37
|
+
agent?: string | undefined;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export async function executeSessionHandoffTool(
|
|
@@ -41,6 +44,7 @@ export async function executeSessionHandoffTool(
|
|
|
41
44
|
modelRuntime: ModelRuntime,
|
|
42
45
|
launchTargets: readonly HandoffLaunchTarget[],
|
|
43
46
|
roster: HandoffRoster | undefined,
|
|
47
|
+
agentProfiles: AgentProfileCatalog,
|
|
44
48
|
recordClipboardStatus: (sessionId: string, status: ClipboardStatus) => void,
|
|
45
49
|
) {
|
|
46
50
|
const goal = params.goal.trim();
|
|
@@ -61,6 +65,11 @@ export async function executeSessionHandoffTool(
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
const { target, degradedFrom } = resolveHandoffLaunchTarget(params.launch, launchTargets);
|
|
68
|
+
if (params.agent !== undefined && target.value !== SUBAGENT_LAUNCH) {
|
|
69
|
+
throw new Error("Agent profiles apply only to subagent launches.");
|
|
70
|
+
}
|
|
71
|
+
const profile =
|
|
72
|
+
params.agent === undefined ? undefined : requireAgentProfile(agentProfiles, params.agent);
|
|
64
73
|
|
|
65
74
|
const targetCwd = resolveHandoffCwd(ctx.cwd, params.cwd);
|
|
66
75
|
if (targetCwd.error) {
|
|
@@ -84,18 +93,21 @@ export async function executeSessionHandoffTool(
|
|
|
84
93
|
|
|
85
94
|
resolveHandoffSource(ctx.sessionManager, sourceLeafId);
|
|
86
95
|
|
|
87
|
-
const requestResponse = params.requestResponse
|
|
96
|
+
const requestResponse = params.requestResponse;
|
|
88
97
|
const provider = params.provider?.trim();
|
|
89
98
|
const modelId = params.model?.trim();
|
|
90
99
|
if (Boolean(provider) !== Boolean(modelId)) {
|
|
91
100
|
throw new Error("session_handoff requires provider and model together, or neither.");
|
|
92
101
|
}
|
|
102
|
+
const requestedThinkingLevel = params.thinkingLevel ?? profile?.thinkingLevel;
|
|
93
103
|
const override =
|
|
94
104
|
provider && modelId
|
|
95
|
-
? resolveChildModel(modelRuntime, roster, { provider, modelId },
|
|
96
|
-
:
|
|
105
|
+
? resolveChildModel(modelRuntime, roster, { provider, modelId }, requestedThinkingLevel)
|
|
106
|
+
: profile?.model
|
|
107
|
+
? resolveProfileModel(modelRuntime, roster, profile.model, requestedThinkingLevel)
|
|
108
|
+
: undefined;
|
|
97
109
|
const childModel = override?.model ?? ctx.model;
|
|
98
|
-
const thinkingLevel = override?.thinkingLevel ??
|
|
110
|
+
const thinkingLevel = override?.thinkingLevel ?? requestedThinkingLevel ?? pi.getThinkingLevel();
|
|
99
111
|
const model = formatModelArgument(childModel, thinkingLevel);
|
|
100
112
|
if (!model) {
|
|
101
113
|
throw new Error("No active model is available for the handoff.");
|
|
@@ -107,6 +119,7 @@ export async function executeSessionHandoffTool(
|
|
|
107
119
|
parentSessionFile,
|
|
108
120
|
title,
|
|
109
121
|
model,
|
|
122
|
+
tools: profile?.tools && [...new Set([...profile.tools, ...(target.requiredTools ?? [])])],
|
|
110
123
|
approveProjectTrust: target.approveProjectTrust,
|
|
111
124
|
useDefaultSessionDir: target.useDefaultSessionDir ?? false,
|
|
112
125
|
buildBootstrap: (sessionId) =>
|
|
@@ -123,6 +136,7 @@ export async function executeSessionHandoffTool(
|
|
|
123
136
|
childSessionId: sessionId,
|
|
124
137
|
ownerSessionId: ctx.sessionManager.getSessionId(),
|
|
125
138
|
requestResponse,
|
|
139
|
+
agent: profile && { name: profile.name, instructions: profile.instructions },
|
|
126
140
|
}),
|
|
127
141
|
}),
|
|
128
142
|
prepareChild: (manager, childSessionId) =>
|