pi-sessions 0.12.1 → 0.13.1
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 +6 -3
- package/extensions/pi-sessions.ts +20 -6
- package/extensions/session-ask/install.ts +24 -10
- package/extensions/session-auto-title/generate.ts +17 -14
- package/extensions/session-auto-title/install.ts +6 -29
- package/extensions/session-auto-title/retitle.ts +6 -19
- package/extensions/session-auto-title/wizard.ts +97 -31
- package/extensions/session-handoff/board-loading.ts +93 -0
- package/extensions/session-handoff/board.ts +239 -127
- package/extensions/session-handoff/install.ts +5 -5
- package/extensions/session-handoff/picker.ts +167 -19
- package/extensions/session-handoff/review.ts +56 -7
- package/extensions/session-handoff/ui.ts +32 -7
- package/extensions/session-hooks/install.ts +7 -20
- package/extensions/session-index/install.ts +8 -86
- package/extensions/session-index/loader.ts +80 -0
- package/extensions/session-index/panel.ts +103 -0
- package/extensions/session-messaging/broker/spawn.ts +35 -12
- package/extensions/session-messaging/pi/reachable-tool.ts +6 -1
- package/extensions/session-messaging/pi/tools.ts +79 -12
- package/extensions/session-search/extract.ts +120 -91
- package/extensions/session-search/hooks.ts +25 -203
- package/extensions/session-search/reindex.ts +13 -8
- package/extensions/shared/legend.ts +116 -0
- package/extensions/shared/session-index/scoring.ts +0 -1
- package/extensions/shared/session-index/search.ts +76 -49
- package/extensions/shared/settings.ts +22 -0
- package/extensions/subagents/context-limit.ts +58 -0
- package/extensions/subagents/install.ts +110 -30
- package/extensions/subagents/reconcile.ts +58 -19
- package/extensions/subagents/report.ts +0 -2
- package/extensions/subagents/settle.ts +31 -0
- package/extensions/subagents/should-message.ts +59 -0
- package/package.json +13 -13
- package/extensions/session-handoff/refs.ts +0 -145
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
type KeybindingsManager,
|
|
4
|
+
type Theme,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import {
|
|
7
|
+
type Component,
|
|
8
|
+
Container,
|
|
9
|
+
Loader,
|
|
10
|
+
Spacer,
|
|
11
|
+
type TUI,
|
|
12
|
+
type TuiMouseEvent,
|
|
13
|
+
type TuiMouseEventResult,
|
|
14
|
+
} from "@earendil-works/pi-tui";
|
|
15
|
+
import { type LegendHit, LegendPointer, layoutLegend, legendHitAt } from "../shared/legend.ts";
|
|
16
|
+
|
|
17
|
+
export class ReindexLoader implements Component {
|
|
18
|
+
private readonly body = new Container();
|
|
19
|
+
private readonly loader: Loader;
|
|
20
|
+
private cancelRow = 0;
|
|
21
|
+
private cancelHits: LegendHit[] = [];
|
|
22
|
+
private readonly pointer: LegendPointer;
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
tui: TUI,
|
|
26
|
+
private readonly theme: Theme,
|
|
27
|
+
private readonly keybindings: KeybindingsManager,
|
|
28
|
+
private readonly cancel: () => void,
|
|
29
|
+
) {
|
|
30
|
+
this.pointer = new LegendPointer(() => tui.requestRender());
|
|
31
|
+
this.loader = new Loader(
|
|
32
|
+
tui,
|
|
33
|
+
(text) => theme.fg("accent", text),
|
|
34
|
+
(text) => theme.fg("muted", text),
|
|
35
|
+
"Rebuilding session index...",
|
|
36
|
+
);
|
|
37
|
+
this.body.addChild(new DynamicBorder((text) => theme.fg("border", text)));
|
|
38
|
+
this.body.addChild(this.loader);
|
|
39
|
+
this.body.addChild(new Spacer(1));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
render(width: number): string[] {
|
|
43
|
+
this.cancelHits = [];
|
|
44
|
+
const lines = this.body.render(width);
|
|
45
|
+
const legend = layoutLegend(
|
|
46
|
+
this.theme,
|
|
47
|
+
[
|
|
48
|
+
{
|
|
49
|
+
key: this.keybindings.getKeys("tui.select.cancel").join("/"),
|
|
50
|
+
description: "cancel",
|
|
51
|
+
run: this.cancel,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
{ width: Math.max(0, width - 2), pressedId: this.pointer.pressedId },
|
|
55
|
+
);
|
|
56
|
+
this.cancelRow = lines.length;
|
|
57
|
+
this.cancelHits = legend.hits;
|
|
58
|
+
lines.push(` ${legend.text}`, "", this.theme.fg("border", "─".repeat(Math.max(0, width))));
|
|
59
|
+
return lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
63
|
+
return this.pointer.handleMouse(
|
|
64
|
+
event,
|
|
65
|
+
event.y === this.cancelRow ? legendHitAt(this.cancelHits, event.x - 1) : undefined,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
handleInput(data: string): void {
|
|
70
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) this.cancel();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
invalidate(): void {
|
|
74
|
+
this.body.invalidate();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
dispose(): void {
|
|
78
|
+
this.loader.stop();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type Focusable,
|
|
4
|
+
matchesKey,
|
|
5
|
+
type TuiMouseEvent,
|
|
6
|
+
type TuiMouseEventResult,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import { type LegendHit, LegendPointer, layoutLegend, legendHitAt } from "../shared/legend.ts";
|
|
11
|
+
import type { SessionIndexStatus } from "../shared/session-index/index.ts";
|
|
12
|
+
|
|
13
|
+
export type SessionIndexAction = "reindex" | undefined;
|
|
14
|
+
|
|
15
|
+
export class SessionIndexPanel implements Focusable {
|
|
16
|
+
focused = false;
|
|
17
|
+
private actionRows = new Map<number, LegendHit[]>();
|
|
18
|
+
private readonly pointer: LegendPointer;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private readonly theme: Theme,
|
|
22
|
+
private readonly status: SessionIndexStatus,
|
|
23
|
+
private readonly done: (result: SessionIndexAction) => void,
|
|
24
|
+
requestRender: () => void,
|
|
25
|
+
) {
|
|
26
|
+
this.pointer = new LegendPointer(requestRender);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
invalidate(): void {}
|
|
30
|
+
|
|
31
|
+
handleInput(data: string): void {
|
|
32
|
+
if (matchesKey(data, "escape") || matchesKey(data, "enter")) {
|
|
33
|
+
this.done(undefined);
|
|
34
|
+
} else if (data === "r" || data === "R" || matchesKey(data, "r")) {
|
|
35
|
+
this.done("reindex");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
40
|
+
const result = this.pointer.handleMouse(
|
|
41
|
+
event,
|
|
42
|
+
legendHitAt(this.actionRows.get(event.y) ?? [], event.x - 2),
|
|
43
|
+
);
|
|
44
|
+
return event.type === "press" && result?.handled ? { ...result, focus: true } : result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
render(width: number): string[] {
|
|
48
|
+
this.actionRows.clear();
|
|
49
|
+
const innerWidth = Math.max(0, Math.min(72, width) - 2);
|
|
50
|
+
const lines: string[] = [this.theme.fg("border", `╭${"─".repeat(innerWidth)}╮`)];
|
|
51
|
+
lines.push(
|
|
52
|
+
this.renderRow(innerWidth, ` ${this.theme.bold(this.theme.fg("accent", "Session Index"))}`),
|
|
53
|
+
);
|
|
54
|
+
lines.push(this.renderRow(innerWidth, ""));
|
|
55
|
+
lines.push(
|
|
56
|
+
this.renderRow(
|
|
57
|
+
innerWidth,
|
|
58
|
+
` Path: ${this.status.exists ? this.status.dbPath : "<no index found>"}`,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
lines.push(
|
|
62
|
+
this.renderRow(
|
|
63
|
+
innerWidth,
|
|
64
|
+
` Schema version: ${this.status.schemaVersion !== undefined ? String(this.status.schemaVersion) : "n/a"}`,
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
lines.push(
|
|
68
|
+
this.renderRow(
|
|
69
|
+
innerWidth,
|
|
70
|
+
` Session count: ${this.status.sessionCount !== undefined ? String(this.status.sessionCount) : "n/a"}`,
|
|
71
|
+
),
|
|
72
|
+
);
|
|
73
|
+
lines.push(
|
|
74
|
+
this.renderRow(innerWidth, ` Last full reindex: ${this.status.lastFullReindexAt ?? "n/a"}`),
|
|
75
|
+
);
|
|
76
|
+
lines.push(this.renderRow(innerWidth, ""));
|
|
77
|
+
this.renderAction(lines, innerWidth, "R", "rebuild from disk", () => this.done("reindex"));
|
|
78
|
+
this.renderAction(lines, innerWidth, "Enter / Esc", "close", () => this.done(undefined));
|
|
79
|
+
lines.push(this.theme.fg("border", `╰${"─".repeat(innerWidth)}╯`));
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private renderAction(
|
|
84
|
+
lines: string[],
|
|
85
|
+
innerWidth: number,
|
|
86
|
+
key: string,
|
|
87
|
+
description: string,
|
|
88
|
+
run: () => void,
|
|
89
|
+
): void {
|
|
90
|
+
const legend = layoutLegend(this.theme, [{ key, description, run }], {
|
|
91
|
+
width: Math.max(0, innerWidth - 1),
|
|
92
|
+
pressedId: this.pointer.pressedId,
|
|
93
|
+
});
|
|
94
|
+
this.actionRows.set(lines.length, legend.hits);
|
|
95
|
+
lines.push(this.renderRow(innerWidth, ` ${legend.text}`));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private renderRow(innerWidth: number, content: string): string {
|
|
99
|
+
const text = truncateToWidth(content, innerWidth, "…");
|
|
100
|
+
const pad = Math.max(0, innerWidth - visibleWidth(text));
|
|
101
|
+
return `${this.theme.fg("border", "│")}${text}${" ".repeat(pad)}${this.theme.fg("border", "│")}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
3
11
|
import net from "node:net";
|
|
4
12
|
import { dirname, join } from "node:path";
|
|
5
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -16,6 +24,7 @@ const brokerDir = getSessionMessagingDir();
|
|
|
16
24
|
const brokerSocketPath = getSessionMessagingSocketPath();
|
|
17
25
|
const brokerPidPath = join(brokerDir, "broker.pid");
|
|
18
26
|
const brokerSpawnLockPath = join(brokerDir, "broker.spawn.lock");
|
|
27
|
+
const brokerLogPath = join(brokerDir, "broker.log");
|
|
19
28
|
|
|
20
29
|
export async function spawnSessionMessagingBrokerIfNeeded(): Promise<void> {
|
|
21
30
|
mkdirSync(brokerDir, { recursive: true, mode: 0o700 });
|
|
@@ -43,14 +52,23 @@ export async function spawnSessionMessagingBrokerIfNeeded(): Promise<void> {
|
|
|
43
52
|
`Session messaging broker build not found: ${brokerPath}. Run npm install in the pi-sessions package directory.`,
|
|
44
53
|
);
|
|
45
54
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
55
|
+
const log = openSync(brokerLogPath, "w", 0o600);
|
|
56
|
+
try {
|
|
57
|
+
const child = spawn(process.execPath, [brokerPath], {
|
|
58
|
+
detached: true,
|
|
59
|
+
stdio: ["ignore", log, log],
|
|
60
|
+
windowsHide: process.platform === "win32",
|
|
61
|
+
env: brokerSpawnEnv(),
|
|
62
|
+
});
|
|
63
|
+
await new Promise<void>((resolve, reject) => {
|
|
64
|
+
child.once("spawn", resolve);
|
|
65
|
+
child.once("error", reject);
|
|
66
|
+
});
|
|
67
|
+
child.unref();
|
|
68
|
+
await waitForBroker(child);
|
|
69
|
+
} finally {
|
|
70
|
+
closeSync(log);
|
|
71
|
+
}
|
|
54
72
|
} finally {
|
|
55
73
|
releaseSpawnLock();
|
|
56
74
|
}
|
|
@@ -103,16 +121,21 @@ function canConnectToBroker(): Promise<boolean> {
|
|
|
103
121
|
});
|
|
104
122
|
}
|
|
105
123
|
|
|
106
|
-
async function waitForBroker(): Promise<void> {
|
|
124
|
+
async function waitForBroker(child?: ChildProcess): Promise<void> {
|
|
107
125
|
const start = Date.now();
|
|
108
126
|
while (Date.now() - start < BROKER_START_TIMEOUT_MS) {
|
|
109
127
|
if (await canConnectToBroker()) {
|
|
110
128
|
return;
|
|
111
129
|
}
|
|
130
|
+
if (child && (child.exitCode !== null || child.signalCode !== null)) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Session messaging broker exited (${child.signalCode ?? child.exitCode}). See ${brokerLogPath}.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
112
135
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
113
136
|
}
|
|
114
137
|
|
|
115
|
-
throw new Error(
|
|
138
|
+
throw new Error(`Session messaging broker failed to start. See ${brokerLogPath}.`);
|
|
116
139
|
}
|
|
117
140
|
|
|
118
141
|
function acquireSpawnLock(): boolean {
|
|
@@ -53,6 +53,8 @@ export interface SessionReachableDeps {
|
|
|
53
53
|
scope: Exclude<SessionReachableScope, "user">,
|
|
54
54
|
) => Promise<readonly ReachableSubagentEntry[]>)
|
|
55
55
|
| undefined;
|
|
56
|
+
/** Resolved per call; the parent is unreachable from a subagent, so it stays off the menu. */
|
|
57
|
+
getParentSessionId?: (() => string | undefined) | undefined;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
interface SessionReachableRendererState {
|
|
@@ -123,7 +125,10 @@ async function listReachableUserSessions(
|
|
|
123
125
|
throw new Error(`Session messaging is unavailable: ${formatError(error)}`);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
const
|
|
128
|
+
const parentSessionId = deps.getParentSessionId?.();
|
|
129
|
+
const targetIds = liveSessionIds.filter(
|
|
130
|
+
(sessionId) => sessionId !== currentSessionId && sessionId !== parentSessionId,
|
|
131
|
+
);
|
|
127
132
|
return withSessionIndex(deps.indexPath, { mode: "read", required: true }, ({ db }) => {
|
|
128
133
|
const sessions: ReachableUserSession[] = [];
|
|
129
134
|
for (const sessionId of targetIds) {
|
|
@@ -46,9 +46,62 @@ interface CancelSessionService {
|
|
|
46
46
|
cancelSession(sessionId: string): Promise<CancelSessionResult | ManagedCancelSessionResult>;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export type SessionSendMessageRole =
|
|
50
|
+
| { kind: "plain" }
|
|
51
|
+
| { kind: "wakeCapable" }
|
|
52
|
+
| { kind: "subagent" };
|
|
53
|
+
|
|
49
54
|
export interface SessionSendMessageToolOptions {
|
|
50
|
-
|
|
55
|
+
role: SessionSendMessageRole;
|
|
51
56
|
getCachedRelationTo(sessionId: string | undefined): string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Resolved on every call: subagent identity depends on the active branch, so a rewind can
|
|
59
|
+
* turn a plain session into a subagent without re-registering this tool.
|
|
60
|
+
*/
|
|
61
|
+
getParentSessionId?: (() => string | undefined) | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type SessionCancelRole = { kind: "plain" } | { kind: "subagent" };
|
|
65
|
+
|
|
66
|
+
export interface SessionCancelToolOptions {
|
|
67
|
+
role: SessionCancelRole;
|
|
68
|
+
/**
|
|
69
|
+
* Resolved on every call: subagent identity depends on the active branch, so a rewind can
|
|
70
|
+
* turn a plain session into a subagent without re-registering this tool.
|
|
71
|
+
*/
|
|
72
|
+
getParentSessionId?: (() => string | undefined) | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface SendMessageToolWording {
|
|
76
|
+
description: string;
|
|
77
|
+
promptSnippet: string;
|
|
78
|
+
promptGuidelines: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getSendMessageWording(role: SessionSendMessageRole): SendMessageToolWording {
|
|
82
|
+
switch (role.kind) {
|
|
83
|
+
case "plain":
|
|
84
|
+
return {
|
|
85
|
+
description: "Send a message to another live pi session.",
|
|
86
|
+
promptSnippet: "Send a message to another live pi session",
|
|
87
|
+
promptGuidelines: ["Use session_reachable to list live sessions and find the target."],
|
|
88
|
+
};
|
|
89
|
+
case "wakeCapable":
|
|
90
|
+
return {
|
|
91
|
+
description: "Send a message to another live pi session or a subagent.",
|
|
92
|
+
promptSnippet: "Send a message to another pi session or a subagent",
|
|
93
|
+
promptGuidelines: ["Use session_reachable to discover live sessions and owned subagents."],
|
|
94
|
+
};
|
|
95
|
+
case "subagent":
|
|
96
|
+
return {
|
|
97
|
+
description: "Send a message to another subagent.",
|
|
98
|
+
promptSnippet: "Send a message to another subagent",
|
|
99
|
+
promptGuidelines: [
|
|
100
|
+
"Use session_reachable to discover reachable subagents.",
|
|
101
|
+
"Report to your parent with submit_task_report; it cannot be reached with session_send_message.",
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
52
105
|
}
|
|
53
106
|
|
|
54
107
|
interface SendMessageRendererState {
|
|
@@ -59,15 +112,7 @@ export function createSessionSendMessageTool(
|
|
|
59
112
|
service: SendMessageService,
|
|
60
113
|
options: SessionSendMessageToolOptions,
|
|
61
114
|
): ToolDefinition {
|
|
62
|
-
const description = options.
|
|
63
|
-
? "Send a message to another live pi session or a subagent."
|
|
64
|
-
: "Send a message to another live pi session.";
|
|
65
|
-
const promptSnippet = options.wakeCapable
|
|
66
|
-
? "Send a message to another pi session or a subagent"
|
|
67
|
-
: "Send a message to another live pi session";
|
|
68
|
-
const promptGuidelines = options.wakeCapable
|
|
69
|
-
? ["Use session_reachable to discover live sessions and owned subagents."]
|
|
70
|
-
: ["Use session_reachable to list live sessions and find the target."];
|
|
115
|
+
const { description, promptSnippet, promptGuidelines } = getSendMessageWording(options.role);
|
|
71
116
|
|
|
72
117
|
return defineTool({
|
|
73
118
|
name: "session_send_message",
|
|
@@ -109,6 +154,9 @@ export function createSessionSendMessageTool(
|
|
|
109
154
|
},
|
|
110
155
|
async execute(toolCallId, params: SendMessageParams, _signal, _onUpdate, _ctx) {
|
|
111
156
|
const target = parseSessionTarget(params.session, "session_send_message");
|
|
157
|
+
if (target === options.getParentSessionId?.()) {
|
|
158
|
+
throw new Error("The parent session cannot be messaged. Use submit_task_report.");
|
|
159
|
+
}
|
|
112
160
|
|
|
113
161
|
const body = params.message.trim();
|
|
114
162
|
if (!body) {
|
|
@@ -147,13 +195,28 @@ export function createSessionSendMessageTool(
|
|
|
147
195
|
});
|
|
148
196
|
}
|
|
149
197
|
|
|
150
|
-
|
|
198
|
+
function getCancelPromptGuidelines(role: SessionCancelRole): string[] {
|
|
199
|
+
switch (role.kind) {
|
|
200
|
+
case "plain":
|
|
201
|
+
return ["Only cancel a user session when the user directs it."];
|
|
202
|
+
case "subagent":
|
|
203
|
+
return [
|
|
204
|
+
"Only cancel a user session when the user directs it.",
|
|
205
|
+
"Never cancel your parent session; it is waiting on your report.",
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function createSessionCancelTool(
|
|
211
|
+
service: CancelSessionService,
|
|
212
|
+
options: SessionCancelToolOptions,
|
|
213
|
+
): ToolDefinition {
|
|
151
214
|
return defineTool<typeof CANCEL_SESSION_PARAMS, CancelSessionToolDetails>({
|
|
152
215
|
name: "session_cancel",
|
|
153
216
|
label: "Cancel a running session",
|
|
154
217
|
description: "Cancel another running pi session",
|
|
155
218
|
promptSnippet: "Cancel another running pi session",
|
|
156
|
-
promptGuidelines:
|
|
219
|
+
promptGuidelines: getCancelPromptGuidelines(options.role),
|
|
157
220
|
parameters: CANCEL_SESSION_PARAMS,
|
|
158
221
|
renderResult(result, options, theme, context) {
|
|
159
222
|
const output = getFirstText(result);
|
|
@@ -175,6 +238,10 @@ export function createSessionCancelTool(service: CancelSessionService): ToolDefi
|
|
|
175
238
|
},
|
|
176
239
|
async execute(_toolCallId, params: CancelSessionParams) {
|
|
177
240
|
const target = parseSessionTarget(params.session, "session_cancel");
|
|
241
|
+
if (target === options.getParentSessionId?.()) {
|
|
242
|
+
throw new Error("The parent session cannot be cancelled. It is waiting on your report.");
|
|
243
|
+
}
|
|
244
|
+
|
|
178
245
|
const result = await service.cancelSession(target);
|
|
179
246
|
if (result.kind === "managed") {
|
|
180
247
|
const details: CancelSessionToolDetails = {
|