pi-sessions 0.18.0 → 0.19.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 +44 -40
- 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/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 +47 -33
- 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 +4 -9
- package/extensions/session-handoff/receipt.ts +1 -1
- package/extensions/session-handoff/tool-presenter.ts +1 -1
- package/extensions/session-handoff/tool-schema.ts +1 -9
- package/extensions/session-handoff/tool-view-model.ts +6 -2
- package/extensions/session-handoff/tool.ts +2 -2
- 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 +1 -1
- package/extensions/subagents/install.ts +0 -27
- package/extensions/subagents/launch-target.ts +0 -1
- package/extensions/subagents/reconcile.ts +10 -2
- package/package.json +5 -5
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SessionDirectory } from "../directory.ts";
|
|
3
|
+
import { createDirectoryTools } from "./directory-tools.ts";
|
|
4
|
+
import { SESSION_REACHABLE_PARAMS, SESSION_REACHABLE_USER_PARAMS } from "./reachable-contract.ts";
|
|
5
|
+
|
|
6
|
+
export function installDirectoryTools(
|
|
7
|
+
pi: ExtensionAPI,
|
|
8
|
+
deps: {
|
|
9
|
+
peers: SessionDirectory;
|
|
10
|
+
subagents?: SessionDirectory;
|
|
11
|
+
claimsPeers(): boolean;
|
|
12
|
+
},
|
|
13
|
+
): { refresh(): void } {
|
|
14
|
+
const ownedPeerNames = new Set<string>();
|
|
15
|
+
const refresh = (): void => {
|
|
16
|
+
const parentSessionId = deps.peers.getParentSessionId();
|
|
17
|
+
if (deps.claimsPeers()) {
|
|
18
|
+
// Pi has no unregisterTool. Remove only definitions we installed from the model's
|
|
19
|
+
// loadout, not a host's load-time tools. Retained definitions are execution-guarded.
|
|
20
|
+
pi.setActiveTools(pi.getActiveTools().filter((name) => !ownedPeerNames.has(name)));
|
|
21
|
+
} else {
|
|
22
|
+
const tools = createDirectoryTools(deps.peers, {
|
|
23
|
+
list: {
|
|
24
|
+
name: "session_reachable",
|
|
25
|
+
description: "List live user-facing Pi sessions.",
|
|
26
|
+
promptSnippet: "Find live peer sessions",
|
|
27
|
+
promptGuidelines: [],
|
|
28
|
+
parameters: SESSION_REACHABLE_USER_PARAMS,
|
|
29
|
+
defaultScope: "user",
|
|
30
|
+
},
|
|
31
|
+
send: {
|
|
32
|
+
name: "session_send_message",
|
|
33
|
+
description: "Send a message to another live Pi session.",
|
|
34
|
+
promptSnippet: "Message a live peer",
|
|
35
|
+
promptGuidelines: [
|
|
36
|
+
"Use session_reachable to find the target.",
|
|
37
|
+
...(parentSessionId
|
|
38
|
+
? ["Report to your parent with submit_task_report, never peer messaging."]
|
|
39
|
+
: []),
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
cancel: {
|
|
43
|
+
name: "session_cancel",
|
|
44
|
+
description: "Cancel another live Pi session's turn.",
|
|
45
|
+
promptSnippet: "Cancel a peer turn",
|
|
46
|
+
promptGuidelines: [
|
|
47
|
+
"Only cancel a user session when the user directs it.",
|
|
48
|
+
...(parentSessionId ? ["Never cancel your parent session."] : []),
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
for (const tool of tools) {
|
|
53
|
+
ownedPeerNames.add(tool.name);
|
|
54
|
+
pi.registerTool({
|
|
55
|
+
...tool,
|
|
56
|
+
execute: async (...args) => {
|
|
57
|
+
if (deps.claimsPeers()) throw new Error("Peer tools belong to the current host.");
|
|
58
|
+
return tool.execute(...args);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...ownedPeerNames])]);
|
|
63
|
+
}
|
|
64
|
+
if (deps.subagents) {
|
|
65
|
+
const tools = createDirectoryTools(deps.subagents, {
|
|
66
|
+
list: {
|
|
67
|
+
name: "subagent_list",
|
|
68
|
+
description: "List owned subagents in the branch or tree.",
|
|
69
|
+
promptSnippet: "Find delegated subagents",
|
|
70
|
+
promptGuidelines: [],
|
|
71
|
+
parameters: SESSION_REACHABLE_PARAMS,
|
|
72
|
+
defaultScope: "branch",
|
|
73
|
+
},
|
|
74
|
+
send: {
|
|
75
|
+
name: "subagent_send",
|
|
76
|
+
description: parentSessionId
|
|
77
|
+
? "Send a message to another subagent; dormant owned workers wake automatically."
|
|
78
|
+
: "Send a message to a subagent; dormant owned workers wake automatically.",
|
|
79
|
+
promptSnippet: "Steer or follow up with a subagent",
|
|
80
|
+
promptGuidelines: [
|
|
81
|
+
"Use subagent_list to find the target.",
|
|
82
|
+
...(parentSessionId
|
|
83
|
+
? ["Report to your parent with submit_task_report, not subagent_send."]
|
|
84
|
+
: []),
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
cancel: {
|
|
88
|
+
name: "subagent_cancel",
|
|
89
|
+
description: "Stop a subagent.",
|
|
90
|
+
promptSnippet: "Cancel delegated work",
|
|
91
|
+
promptGuidelines: parentSessionId
|
|
92
|
+
? ["Never cancel your parent session; it is waiting on your report."]
|
|
93
|
+
: [],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
for (const tool of tools) pi.registerTool(tool);
|
|
97
|
+
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...tools.map((tool) => tool.name)])]);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
pi.on("session_tree", refresh);
|
|
101
|
+
return { refresh };
|
|
102
|
+
}
|
|
@@ -11,12 +11,10 @@ export const SEND_MESSAGE_PARAMS = Type.Object({
|
|
|
11
11
|
message: Type.String({
|
|
12
12
|
description: "Message to send to the target session.",
|
|
13
13
|
}),
|
|
14
|
-
requestResponse: Type.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}),
|
|
19
|
-
),
|
|
14
|
+
requestResponse: Type.Boolean({
|
|
15
|
+
description:
|
|
16
|
+
"Whether the target session should respond with completion/results back to this session.",
|
|
17
|
+
}),
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
export const SEND_MESSAGE_TOOL_DETAILS_SCHEMA = Type.Object({
|
|
@@ -9,9 +9,9 @@ const SCOPE_SCHEMA = Type.Union([
|
|
|
9
9
|
/** Registered when subagents are active; the roster is what makes branch/tree answerable. */
|
|
10
10
|
export const SESSION_REACHABLE_PARAMS = Type.Object({
|
|
11
11
|
scope: Type.Optional(
|
|
12
|
-
Type.Union([Type.Literal("
|
|
12
|
+
Type.Union([Type.Literal("branch"), Type.Literal("tree")], {
|
|
13
13
|
description:
|
|
14
|
-
'
|
|
14
|
+
'"branch" lists subagents from the active conversation branch; "tree" lists subagents anywhere in this session\'s conversation tree. Defaults to "branch".',
|
|
15
15
|
}),
|
|
16
16
|
),
|
|
17
17
|
});
|
|
@@ -26,8 +26,7 @@ const REACHABLE_USER_SESSION_SCHEMA = Type.Object({
|
|
|
26
26
|
cwd: Type.Optional(Type.String()),
|
|
27
27
|
modifiedAt: Type.Optional(Type.String()),
|
|
28
28
|
relation: Type.Optional(Type.String()),
|
|
29
|
-
|
|
30
|
-
state: Type.Union([Type.Literal("live"), Type.Literal("starting"), Type.Literal("dormant")]),
|
|
29
|
+
state: Type.Union([Type.Literal("live"), Type.Literal("starting")]),
|
|
31
30
|
});
|
|
32
31
|
|
|
33
32
|
const REACHABLE_SUBAGENT_SCHEMA = Type.Object({
|
|
@@ -1,26 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
defineTool,
|
|
3
|
-
type ExtensionContext,
|
|
4
|
-
SessionManager,
|
|
5
|
-
type ToolDefinition,
|
|
6
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
7
2
|
import { Text } from "@earendil-works/pi-tui";
|
|
8
|
-
import type { HostedSession } from "../../hosts/contract.ts";
|
|
9
|
-
import { isSessionStarting } from "../../session-handoff/metadata.ts";
|
|
10
|
-
import { formatError } from "../../shared/errors.ts";
|
|
11
3
|
import { ExpandableContentLayout } from "../../shared/rendering/expandable-content-layout.ts";
|
|
12
|
-
import {
|
|
13
|
-
getSessionById,
|
|
14
|
-
type SessionLineageRow,
|
|
15
|
-
withSessionIndex,
|
|
16
|
-
} from "../../shared/session-index/index.ts";
|
|
17
4
|
import { safeParseTypeBoxValue } from "../../shared/typebox.ts";
|
|
5
|
+
import type { SessionDirectory } from "../directory.ts";
|
|
18
6
|
import {
|
|
19
|
-
type
|
|
20
|
-
type ReachableUserSession,
|
|
21
|
-
SESSION_REACHABLE_PARAMS,
|
|
7
|
+
type SESSION_REACHABLE_PARAMS,
|
|
22
8
|
SESSION_REACHABLE_TOOL_DETAILS_SCHEMA,
|
|
23
|
-
SESSION_REACHABLE_USER_PARAMS,
|
|
9
|
+
type SESSION_REACHABLE_USER_PARAMS,
|
|
24
10
|
type SessionReachableParams,
|
|
25
11
|
type SessionReachableScope,
|
|
26
12
|
type SessionReachableToolDetails,
|
|
@@ -28,76 +14,37 @@ import {
|
|
|
28
14
|
import { buildSessionReachablePresentation } from "./reachable-presenter.ts";
|
|
29
15
|
import { buildSessionReachableView } from "./reachable-view-model.ts";
|
|
30
16
|
|
|
31
|
-
/** The subagent facts this tool needs; the roster satisfies it structurally. */
|
|
32
|
-
export interface ReachableSubagentEntry {
|
|
33
|
-
sessionId: string;
|
|
34
|
-
title: string;
|
|
35
|
-
goal: string;
|
|
36
|
-
cwd: string;
|
|
37
|
-
state: string;
|
|
38
|
-
depth: number;
|
|
39
|
-
onActiveBranch: boolean;
|
|
40
|
-
launchedAt: string;
|
|
41
|
-
model?: string | undefined;
|
|
42
|
-
ownerSessionId: string;
|
|
43
|
-
ownerTitle: string;
|
|
44
|
-
ownerIsCurrentSession: boolean;
|
|
45
|
-
resumeCommand: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface SessionReachableDeps {
|
|
49
|
-
indexPath: string;
|
|
50
|
-
hostedSessions?: boolean;
|
|
51
|
-
listSessions(): Promise<string[]>;
|
|
52
|
-
listHostedSessions?: () => Promise<HostedSession[]>;
|
|
53
|
-
getRelationTo(sessionId: string): string | undefined;
|
|
54
|
-
listSubagents?:
|
|
55
|
-
| ((
|
|
56
|
-
scope: Exclude<SessionReachableScope, "user">,
|
|
57
|
-
) => Promise<readonly ReachableSubagentEntry[]>)
|
|
58
|
-
| undefined;
|
|
59
|
-
/** Resolved per call; the parent is unreachable from a subagent, so it stays off the menu. */
|
|
60
|
-
getParentSessionId?: (() => string | undefined) | undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
17
|
interface SessionReachableRendererState {
|
|
64
18
|
layout?: ExpandableContentLayout | undefined;
|
|
65
19
|
}
|
|
66
20
|
|
|
67
|
-
export
|
|
68
|
-
|
|
21
|
+
export interface ReachableToolOptions {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
promptSnippet: string;
|
|
25
|
+
promptGuidelines: string[];
|
|
26
|
+
parameters: typeof SESSION_REACHABLE_PARAMS | typeof SESSION_REACHABLE_USER_PARAMS;
|
|
27
|
+
defaultScope: SessionReachableScope;
|
|
28
|
+
}
|
|
69
29
|
|
|
30
|
+
export function createSessionReachableTool(
|
|
31
|
+
directory: SessionDirectory,
|
|
32
|
+
options: ReachableToolOptions,
|
|
33
|
+
): ToolDefinition {
|
|
70
34
|
return defineTool({
|
|
71
|
-
name:
|
|
35
|
+
name: options.name,
|
|
72
36
|
label: "Reachable Sessions",
|
|
73
|
-
description:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
promptSnippet: subagentsAvailable
|
|
77
|
-
? "List addressable live sessions and owned subagents"
|
|
78
|
-
: "List addressable live sessions",
|
|
79
|
-
promptGuidelines: [
|
|
80
|
-
"Before session_send_message, use session_reachable to find the target session id.",
|
|
81
|
-
...(deps.hostedSessions
|
|
82
|
-
? [
|
|
83
|
-
"User sessions include dormant hosted sessions. session_send_message wakes them automatically; the host field identifies their host.",
|
|
84
|
-
]
|
|
85
|
-
: []),
|
|
86
|
-
],
|
|
37
|
+
description: options.description,
|
|
38
|
+
promptSnippet: options.promptSnippet,
|
|
39
|
+
promptGuidelines: options.promptGuidelines,
|
|
87
40
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
88
|
-
parameters:
|
|
41
|
+
parameters: options.parameters,
|
|
89
42
|
outputSchema: SESSION_REACHABLE_TOOL_DETAILS_SCHEMA,
|
|
90
43
|
renderResult(result, options, theme, context) {
|
|
91
44
|
const output = result.content.find((item) => item.type === "text")?.text ?? "";
|
|
92
|
-
if (context.isError)
|
|
93
|
-
return new Text(output ? theme.fg("error", output) : "", 0, 0);
|
|
94
|
-
}
|
|
95
|
-
|
|
45
|
+
if (context.isError) return new Text(output ? theme.fg("error", output) : "", 0, 0);
|
|
96
46
|
const details = safeParseTypeBoxValue(SESSION_REACHABLE_TOOL_DETAILS_SCHEMA, result.details);
|
|
97
|
-
if (!details)
|
|
98
|
-
return new Text(output, 0, 0);
|
|
99
|
-
}
|
|
100
|
-
|
|
47
|
+
if (!details) return new Text(output, 0, 0);
|
|
101
48
|
const state = context.state as SessionReachableRendererState;
|
|
102
49
|
const layout = state.layout ?? new ExpandableContentLayout(theme);
|
|
103
50
|
state.layout = layout;
|
|
@@ -108,12 +55,8 @@ export function createSessionReachableTool(deps: SessionReachableDeps): ToolDefi
|
|
|
108
55
|
return layout;
|
|
109
56
|
},
|
|
110
57
|
async execute(_toolCallId, params: SessionReachableParams, _signal, _onUpdate, ctx) {
|
|
111
|
-
const scope = params.scope ??
|
|
112
|
-
const sessions =
|
|
113
|
-
scope === "user"
|
|
114
|
-
? await listReachableUserSessions(deps, ctx)
|
|
115
|
-
: await listReachableSubagents(deps, scope);
|
|
116
|
-
|
|
58
|
+
const scope = params.scope ?? options.defaultScope;
|
|
59
|
+
const sessions = await directory.list(scope, ctx);
|
|
117
60
|
const details: SessionReachableToolDetails = { scope, sessions };
|
|
118
61
|
return {
|
|
119
62
|
content: [{ type: "text" as const, text: JSON.stringify(details, null, 2) }],
|
|
@@ -123,96 +66,3 @@ export function createSessionReachableTool(deps: SessionReachableDeps): ToolDefi
|
|
|
123
66
|
},
|
|
124
67
|
});
|
|
125
68
|
}
|
|
126
|
-
|
|
127
|
-
async function listReachableUserSessions(
|
|
128
|
-
deps: SessionReachableDeps,
|
|
129
|
-
ctx: ExtensionContext,
|
|
130
|
-
): Promise<ReachableUserSession[]> {
|
|
131
|
-
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
132
|
-
let liveSessionIds: string[];
|
|
133
|
-
try {
|
|
134
|
-
liveSessionIds = await deps.listSessions();
|
|
135
|
-
} catch (error) {
|
|
136
|
-
throw new Error(`Session messaging is unavailable: ${formatError(error)}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const parentSessionId = deps.getParentSessionId?.();
|
|
140
|
-
const hostedSessions = new Map(
|
|
141
|
-
((await deps.listHostedSessions?.()) ?? []).map((session) => [session.sessionId, session]),
|
|
142
|
-
);
|
|
143
|
-
const liveIds = new Set(liveSessionIds);
|
|
144
|
-
const targetIds = [...new Set([...liveSessionIds, ...hostedSessions.keys()])].filter(
|
|
145
|
-
(sessionId) => sessionId !== currentSessionId && sessionId !== parentSessionId,
|
|
146
|
-
);
|
|
147
|
-
return withSessionIndex(deps.indexPath, { mode: "read", required: true }, ({ db }) => {
|
|
148
|
-
const sessions: ReachableUserSession[] = [];
|
|
149
|
-
for (const sessionId of targetIds) {
|
|
150
|
-
const row = getSessionById(db, sessionId);
|
|
151
|
-
if (row?.sessionOrigin === "subagent") {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
const relation = deps.getRelationTo(sessionId);
|
|
155
|
-
const hosted = hostedSessions.get(sessionId);
|
|
156
|
-
const title = hosted?.title ?? row?.sessionName;
|
|
157
|
-
const cwd = hosted?.cwd ?? row?.cwd;
|
|
158
|
-
sessions.push({
|
|
159
|
-
kind: "user",
|
|
160
|
-
sessionId,
|
|
161
|
-
state: !liveIds.has(sessionId)
|
|
162
|
-
? "dormant"
|
|
163
|
-
: row && isStartingSession(row)
|
|
164
|
-
? "starting"
|
|
165
|
-
: "live",
|
|
166
|
-
...(hosted ? { host: hosted.host } : {}),
|
|
167
|
-
...(title ? { title } : {}),
|
|
168
|
-
...(cwd ? { cwd } : {}),
|
|
169
|
-
...(row?.modifiedAt ? { modifiedAt: row.modifiedAt } : {}),
|
|
170
|
-
...(relation === undefined ? {} : { relation }),
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
return sessions.sort(compareByModifiedDesc);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
async function listReachableSubagents(
|
|
178
|
-
deps: SessionReachableDeps,
|
|
179
|
-
scope: Exclude<SessionReachableScope, "user">,
|
|
180
|
-
): Promise<ReachableSubagent[]> {
|
|
181
|
-
if (!deps.listSubagents) {
|
|
182
|
-
throw new Error(`session_reachable scope "${scope}" requires subagents to be active.`);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const entries = await deps.listSubagents(scope);
|
|
187
|
-
return entries.map((entry) => ({
|
|
188
|
-
kind: "subagent",
|
|
189
|
-
sessionId: entry.sessionId,
|
|
190
|
-
title: entry.title,
|
|
191
|
-
goal: entry.goal,
|
|
192
|
-
cwd: entry.cwd,
|
|
193
|
-
state: entry.state,
|
|
194
|
-
depth: entry.depth,
|
|
195
|
-
onActiveBranch: entry.onActiveBranch,
|
|
196
|
-
launchedAt: entry.launchedAt,
|
|
197
|
-
...(entry.model === undefined ? {} : { model: entry.model }),
|
|
198
|
-
ownerSessionId: entry.ownerSessionId,
|
|
199
|
-
ownerTitle: entry.ownerTitle,
|
|
200
|
-
ownerIsCurrentSession: entry.ownerIsCurrentSession,
|
|
201
|
-
resumeCommand: entry.resumeCommand,
|
|
202
|
-
}));
|
|
203
|
-
} catch (error) {
|
|
204
|
-
throw new Error(`Subagent roster is unavailable: ${formatError(error)}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function isStartingSession(row: SessionLineageRow): boolean {
|
|
209
|
-
try {
|
|
210
|
-
return isSessionStarting(SessionManager.open(row.sessionPath).getBranch());
|
|
211
|
-
} catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function compareByModifiedDesc(left: ReachableUserSession, right: ReachableUserSession): number {
|
|
217
|
-
return (right.modifiedAt ?? "").localeCompare(left.modifiedAt ?? "");
|
|
218
|
-
}
|
|
@@ -5,6 +5,7 @@ import type { SendMessageViewModel } from "./send-message-view-model.ts";
|
|
|
5
5
|
export function buildSendMessagePresentation(
|
|
6
6
|
model: SendMessageViewModel,
|
|
7
7
|
theme: RenderTheme,
|
|
8
|
+
toolName = "session_send_message",
|
|
8
9
|
): ExpandableContentPresentation {
|
|
9
10
|
const target =
|
|
10
11
|
model.status === "delivered"
|
|
@@ -18,7 +19,7 @@ export function buildSendMessagePresentation(
|
|
|
18
19
|
const action = model.status === "delivered" ? "delivered to" : "to";
|
|
19
20
|
|
|
20
21
|
return {
|
|
21
|
-
header: `${theme.fg("toolTitle", theme.bold(
|
|
22
|
+
header: `${theme.fg("toolTitle", theme.bold(toolName))} ${theme.fg(
|
|
22
23
|
"muted",
|
|
23
24
|
`${action} ${target}${metadataHint}`,
|
|
24
25
|
)}`,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
defineTool,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
6
|
import { Text } from "@earendil-works/pi-tui";
|
|
3
7
|
import { formatError } from "../../shared/errors.ts";
|
|
4
8
|
import { ExpandableContentLayout } from "../../shared/rendering/expandable-content-layout.ts";
|
|
@@ -26,10 +30,11 @@ import { buildDeliveredMessageView, buildSendingMessageView } from "./send-messa
|
|
|
26
30
|
import type { CancelSessionResult, SendMessageRequest, SendMessageResult } from "./service.ts";
|
|
27
31
|
|
|
28
32
|
interface SendMessageService {
|
|
29
|
-
sendMessage(request: SendMessageRequest): Promise<SendMessageResult>;
|
|
33
|
+
sendMessage(request: SendMessageRequest, ctx: ExtensionContext): Promise<SendMessageResult>;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
/** A subagent's cancel goes through its parent's ledger, which reports the state it reconciled to. */
|
|
37
|
+
export interface ManagedCancelSessionResult {
|
|
33
38
|
kind: "managed";
|
|
34
39
|
accepted: true;
|
|
35
40
|
target: {
|
|
@@ -43,17 +48,15 @@ interface ManagedCancelSessionResult {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
interface CancelSessionService {
|
|
46
|
-
cancelSession(
|
|
51
|
+
cancelSession(
|
|
52
|
+
sessionId: string,
|
|
53
|
+
ctx: ExtensionContext,
|
|
54
|
+
): Promise<CancelSessionResult | ManagedCancelSessionResult>;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
export type SessionSendMessageRole =
|
|
50
|
-
| { kind: "plain" }
|
|
51
|
-
| { kind: "wakeCapable" }
|
|
52
|
-
| { kind: "subagent" };
|
|
53
|
-
|
|
54
57
|
export interface SessionSendMessageToolOptions {
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
name: string;
|
|
59
|
+
wording: SendMessageToolWording;
|
|
57
60
|
getCachedRelationTo(sessionId: string | undefined): string | undefined;
|
|
58
61
|
/**
|
|
59
62
|
* Resolved on every call: subagent identity depends on the active branch, so a rewind can
|
|
@@ -62,10 +65,9 @@ export interface SessionSendMessageToolOptions {
|
|
|
62
65
|
getParentSessionId?: (() => string | undefined) | undefined;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
export type SessionCancelRole = { kind: "plain" } | { kind: "subagent" };
|
|
66
|
-
|
|
67
68
|
export interface SessionCancelToolOptions {
|
|
68
|
-
|
|
69
|
+
name: string;
|
|
70
|
+
wording: SendMessageToolWording;
|
|
69
71
|
/**
|
|
70
72
|
* Resolved on every call: subagent identity depends on the active branch, so a rewind can
|
|
71
73
|
* turn a plain session into a subagent without re-registering this tool.
|
|
@@ -73,38 +75,12 @@ export interface SessionCancelToolOptions {
|
|
|
73
75
|
getParentSessionId?: (() => string | undefined) | undefined;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
interface SendMessageToolWording {
|
|
78
|
+
export interface SendMessageToolWording {
|
|
77
79
|
description: string;
|
|
78
80
|
promptSnippet: string;
|
|
79
81
|
promptGuidelines: string[];
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
function getSendMessageWording(role: SessionSendMessageRole): SendMessageToolWording {
|
|
83
|
-
switch (role.kind) {
|
|
84
|
-
case "plain":
|
|
85
|
-
return {
|
|
86
|
-
description: "Send a message to another live pi session.",
|
|
87
|
-
promptSnippet: "Send a message to another live pi session",
|
|
88
|
-
promptGuidelines: ["Use session_reachable to list live sessions and find the target."],
|
|
89
|
-
};
|
|
90
|
-
case "wakeCapable":
|
|
91
|
-
return {
|
|
92
|
-
description: "Send a message to another live pi session or a subagent.",
|
|
93
|
-
promptSnippet: "Send a message to another pi session or a subagent",
|
|
94
|
-
promptGuidelines: ["Use session_reachable to discover live sessions and owned subagents."],
|
|
95
|
-
};
|
|
96
|
-
case "subagent":
|
|
97
|
-
return {
|
|
98
|
-
description: "Send a message to another subagent.",
|
|
99
|
-
promptSnippet: "Send a message to another subagent",
|
|
100
|
-
promptGuidelines: [
|
|
101
|
-
"Use session_reachable to discover reachable subagents.",
|
|
102
|
-
"Report to your parent with submit_task_report; it cannot be reached with session_send_message.",
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
84
|
interface SendMessageRendererState {
|
|
109
85
|
callComponent?: ExpandableContentLayout | undefined;
|
|
110
86
|
}
|
|
@@ -113,21 +89,14 @@ export function createSessionSendMessageTool(
|
|
|
113
89
|
service: SendMessageService,
|
|
114
90
|
options: SessionSendMessageToolOptions,
|
|
115
91
|
): ToolDefinition {
|
|
116
|
-
const { description, promptSnippet, promptGuidelines } =
|
|
92
|
+
const { description, promptSnippet, promptGuidelines } = options.wording;
|
|
117
93
|
|
|
118
94
|
return defineTool({
|
|
119
|
-
name:
|
|
95
|
+
name: options.name,
|
|
120
96
|
label: "Send Message to Session",
|
|
121
97
|
description,
|
|
122
98
|
promptSnippet,
|
|
123
|
-
promptGuidelines
|
|
124
|
-
...promptGuidelines,
|
|
125
|
-
...(options.hostedSessions
|
|
126
|
-
? [
|
|
127
|
-
"session_reachable also lists dormant hosted sessions; sending a message wakes them automatically.",
|
|
128
|
-
]
|
|
129
|
-
: []),
|
|
130
|
-
],
|
|
99
|
+
promptGuidelines,
|
|
131
100
|
parameters: SEND_MESSAGE_PARAMS,
|
|
132
101
|
renderCall(args, theme, context) {
|
|
133
102
|
const state = context.state as SendMessageRendererState;
|
|
@@ -137,12 +106,13 @@ export function createSessionSendMessageTool(
|
|
|
137
106
|
buildSendMessagePresentation(
|
|
138
107
|
buildSendingMessageView(args, options.getCachedRelationTo(args?.session)),
|
|
139
108
|
theme,
|
|
109
|
+
options.name,
|
|
140
110
|
),
|
|
141
111
|
context.expanded,
|
|
142
112
|
);
|
|
143
113
|
return component;
|
|
144
114
|
},
|
|
145
|
-
renderResult(result,
|
|
115
|
+
renderResult(result, renderOptions, theme, context) {
|
|
146
116
|
if (context.isError) {
|
|
147
117
|
const output = getFirstText(result);
|
|
148
118
|
return new Text(output ? `\n${theme.fg("error", output)}` : "", 0, 0);
|
|
@@ -155,29 +125,36 @@ export function createSessionSendMessageTool(
|
|
|
155
125
|
}
|
|
156
126
|
|
|
157
127
|
state.callComponent.update(
|
|
158
|
-
buildSendMessagePresentation(
|
|
159
|
-
|
|
128
|
+
buildSendMessagePresentation(
|
|
129
|
+
buildDeliveredMessageView(context.args, details),
|
|
130
|
+
theme,
|
|
131
|
+
options.name,
|
|
132
|
+
),
|
|
133
|
+
renderOptions.expanded,
|
|
160
134
|
);
|
|
161
135
|
return new Text("", 0, 0);
|
|
162
136
|
},
|
|
163
|
-
async execute(toolCallId, params: SendMessageParams, _signal, _onUpdate,
|
|
164
|
-
const target = parseSessionTarget(params.session,
|
|
137
|
+
async execute(toolCallId, params: SendMessageParams, _signal, _onUpdate, ctx) {
|
|
138
|
+
const target = parseSessionTarget(params.session, options.name);
|
|
165
139
|
if (target === options.getParentSessionId?.()) {
|
|
166
140
|
throw new Error("The parent session cannot be messaged. Use submit_task_report.");
|
|
167
141
|
}
|
|
168
142
|
|
|
169
143
|
const body = params.message.trim();
|
|
170
144
|
if (!body) {
|
|
171
|
-
throw new Error(
|
|
145
|
+
throw new Error(`${options.name} requires a non-empty message.`);
|
|
172
146
|
}
|
|
173
147
|
|
|
174
148
|
try {
|
|
175
|
-
const result = await service.sendMessage(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
149
|
+
const result = await service.sendMessage(
|
|
150
|
+
{
|
|
151
|
+
target,
|
|
152
|
+
body,
|
|
153
|
+
requestResponse: params.requestResponse,
|
|
154
|
+
sourceToolCallId: toolCallId,
|
|
155
|
+
},
|
|
156
|
+
ctx,
|
|
157
|
+
);
|
|
181
158
|
if (!result.delivered) {
|
|
182
159
|
throw new Error(result.error ?? "Message was not delivered.");
|
|
183
160
|
}
|
|
@@ -203,28 +180,14 @@ export function createSessionSendMessageTool(
|
|
|
203
180
|
});
|
|
204
181
|
}
|
|
205
182
|
|
|
206
|
-
function getCancelPromptGuidelines(role: SessionCancelRole): string[] {
|
|
207
|
-
switch (role.kind) {
|
|
208
|
-
case "plain":
|
|
209
|
-
return ["Only cancel a user session when the user directs it."];
|
|
210
|
-
case "subagent":
|
|
211
|
-
return [
|
|
212
|
-
"Only cancel a user session when the user directs it.",
|
|
213
|
-
"Never cancel your parent session; it is waiting on your report.",
|
|
214
|
-
];
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
183
|
export function createSessionCancelTool(
|
|
219
184
|
service: CancelSessionService,
|
|
220
185
|
options: SessionCancelToolOptions,
|
|
221
186
|
): ToolDefinition {
|
|
222
187
|
return defineTool<typeof CANCEL_SESSION_PARAMS, CancelSessionToolDetails>({
|
|
223
|
-
name:
|
|
188
|
+
name: options.name,
|
|
224
189
|
label: "Cancel a running session",
|
|
225
|
-
|
|
226
|
-
promptSnippet: "Cancel another running pi session",
|
|
227
|
-
promptGuidelines: getCancelPromptGuidelines(options.role),
|
|
190
|
+
...options.wording,
|
|
228
191
|
parameters: CANCEL_SESSION_PARAMS,
|
|
229
192
|
renderResult(result, options, theme, context) {
|
|
230
193
|
const output = getFirstText(result);
|
|
@@ -244,14 +207,17 @@ export function createSessionCancelTool(
|
|
|
244
207
|
}
|
|
245
208
|
return new Text(text, 0, 0);
|
|
246
209
|
},
|
|
247
|
-
async execute(_toolCallId, params: CancelSessionParams) {
|
|
248
|
-
const target = parseSessionTarget(params.session,
|
|
210
|
+
async execute(_toolCallId, params: CancelSessionParams, _signal, _onUpdate, ctx) {
|
|
211
|
+
const target = parseSessionTarget(params.session, options.name);
|
|
249
212
|
if (target === options.getParentSessionId?.()) {
|
|
250
213
|
throw new Error("The parent session cannot be cancelled. It is waiting on your report.");
|
|
251
214
|
}
|
|
252
215
|
|
|
253
|
-
const result = await service.cancelSession(target);
|
|
216
|
+
const result = await service.cancelSession(target, ctx);
|
|
254
217
|
if (result.kind === "managed") {
|
|
218
|
+
if (!isConfirmedManagedCancellation(result.state)) {
|
|
219
|
+
throw new Error(buildUnknownCancellationError(result.target));
|
|
220
|
+
}
|
|
255
221
|
const details: CancelSessionToolDetails = {
|
|
256
222
|
kind: "managed",
|
|
257
223
|
accepted: true,
|
|
@@ -260,16 +226,8 @@ export function createSessionCancelTool(
|
|
|
260
226
|
...(result.cancelId === undefined ? {} : { cancelId: result.cancelId }),
|
|
261
227
|
...(result.relation === undefined ? {} : { relation: result.relation }),
|
|
262
228
|
};
|
|
263
|
-
if (!isConfirmedManagedCancellation(result.state)) {
|
|
264
|
-
throw new Error(buildUnknownCancellationError(result.target));
|
|
265
|
-
}
|
|
266
229
|
return {
|
|
267
|
-
content: [
|
|
268
|
-
{
|
|
269
|
-
type: "text" as const,
|
|
270
|
-
text: buildCancelSessionModelText(details),
|
|
271
|
-
},
|
|
272
|
-
],
|
|
230
|
+
content: [{ type: "text" as const, text: buildCancelSessionModelText(details) }],
|
|
273
231
|
details,
|
|
274
232
|
};
|
|
275
233
|
}
|