pi-sessions 0.18.1 → 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.
Files changed (33) hide show
  1. package/README.md +43 -39
  2. package/extensions/hosts/contract.ts +10 -46
  3. package/extensions/hosts/registry.ts +4 -78
  4. package/extensions/pi-sessions.ts +27 -53
  5. package/extensions/session-handoff/board-loading.ts +4 -1
  6. package/extensions/session-handoff/board-view-model.ts +1 -0
  7. package/extensions/session-handoff/board.ts +4 -1
  8. package/extensions/session-handoff/install.ts +47 -33
  9. package/extensions/session-handoff/launch/backend.ts +16 -4
  10. package/extensions/session-handoff/launch-options.ts +2 -17
  11. package/extensions/session-handoff/launch-target.ts +4 -9
  12. package/extensions/session-handoff/receipt.ts +1 -1
  13. package/extensions/session-handoff/tool-presenter.ts +1 -1
  14. package/extensions/session-handoff/tool-schema.ts +1 -9
  15. package/extensions/session-handoff/tool-view-model.ts +6 -2
  16. package/extensions/session-handoff/tool.ts +2 -2
  17. package/extensions/session-messaging/directory-list.ts +115 -0
  18. package/extensions/session-messaging/directory.ts +114 -0
  19. package/extensions/session-messaging/message-router.ts +3 -8
  20. package/extensions/session-messaging/pi/directory-tools.ts +41 -0
  21. package/extensions/session-messaging/pi/install.ts +102 -0
  22. package/extensions/session-messaging/pi/message-contracts.ts +4 -6
  23. package/extensions/session-messaging/pi/reachable-contract.ts +3 -4
  24. package/extensions/session-messaging/pi/reachable-tool.ts +25 -175
  25. package/extensions/session-messaging/pi/send-message-presenter.ts +2 -1
  26. package/extensions/session-messaging/pi/tools.ts +49 -91
  27. package/extensions/session-search/extract.ts +12 -2
  28. package/extensions/shared/lineage.ts +8 -0
  29. package/extensions/shared/session-index/sync.ts +3 -0
  30. package/extensions/subagents/install.ts +0 -27
  31. package/extensions/subagents/launch-target.ts +0 -1
  32. package/extensions/subagents/reconcile.ts +10 -2
  33. 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.Optional(
15
- Type.Boolean({
16
- description:
17
- "Whether the target session should respond with completion/results back to this session.",
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("user"), Type.Literal("branch"), Type.Literal("tree")], {
12
+ Type.Union([Type.Literal("branch"), Type.Literal("tree")], {
13
13
  description:
14
- 'Which sessions to list: "user" for live user-facing sessions, "branch" for subagents launched from the active conversation branch, "tree" for subagents launched anywhere in this session\'s conversation tree. Defaults to "user".',
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
- host: Type.Optional(Type.String()),
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 ReachableSubagent,
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 function createSessionReachableTool(deps: SessionReachableDeps): ToolDefinition {
68
- const subagentsAvailable = Boolean(deps.listSubagents);
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: "session_reachable",
35
+ name: options.name,
72
36
  label: "Reachable Sessions",
73
- description: subagentsAvailable
74
- ? "List the sessions this session can address: live user sessions, or its own subagents."
75
- : "List the live user sessions this session can address.",
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: subagentsAvailable ? SESSION_REACHABLE_PARAMS : SESSION_REACHABLE_USER_PARAMS,
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 ?? "user";
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("session_send_message"))} ${theme.fg(
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 { defineTool, type ToolDefinition } from "@earendil-works/pi-coding-agent";
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
- interface ManagedCancelSessionResult {
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(sessionId: string): Promise<CancelSessionResult | ManagedCancelSessionResult>;
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
- role: SessionSendMessageRole;
56
- hostedSessions?: boolean;
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
- role: SessionCancelRole;
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 } = getSendMessageWording(options.role);
92
+ const { description, promptSnippet, promptGuidelines } = options.wording;
117
93
 
118
94
  return defineTool({
119
- name: "session_send_message",
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, options, theme, context) {
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(buildDeliveredMessageView(context.args, details), theme),
159
- options.expanded,
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, _ctx) {
164
- const target = parseSessionTarget(params.session, "session_send_message");
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("session_send_message requires a non-empty message.");
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
- target,
177
- body,
178
- requestResponse: params.requestResponse,
179
- sourceToolCallId: toolCallId,
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: "session_cancel",
188
+ name: options.name,
224
189
  label: "Cancel a running session",
225
- description: "Cancel another running pi session",
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, "session_cancel");
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
  }