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
|
@@ -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
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type HandoffSessionMetadata,
|
|
24
24
|
parseHandoffSessionMetadata,
|
|
25
25
|
} from "../session-handoff/metadata.ts";
|
|
26
|
+
import { LINEAGE_CUSTOM_TYPE, LINEAGE_SCHEMA, type SessionLineage } from "../shared/lineage.ts";
|
|
26
27
|
import type { SessionOrigin } from "../shared/session-index/index.ts";
|
|
27
28
|
import { contentToText, isRecord } from "../shared/text.ts";
|
|
28
29
|
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
@@ -97,6 +98,7 @@ export interface SessionEntryScan {
|
|
|
97
98
|
sessionNameEntryId?: string | undefined;
|
|
98
99
|
sessionNameTs?: string | undefined;
|
|
99
100
|
handoffMetadata?: DurableHandoffMetadataRecord | undefined;
|
|
101
|
+
lineage?: SessionLineage | undefined;
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
export interface SessionTailBaseline {
|
|
@@ -194,7 +196,10 @@ export function extractSessionRecord(sessionPath: string): ExtractedSessionRecor
|
|
|
194
196
|
chunks.push(...scan.chunks);
|
|
195
197
|
appendDurableHandoffMetadataChunks(chunks, scan.handoffMetadata);
|
|
196
198
|
|
|
197
|
-
const
|
|
199
|
+
const lineage = header.parentSession ? undefined : scan.lineage;
|
|
200
|
+
const parentSessionPath = normalizeParentSessionPath(
|
|
201
|
+
header.parentSession ?? lineage?.parentSessionFile,
|
|
202
|
+
);
|
|
198
203
|
const parentSession = parentSessionPath
|
|
199
204
|
? inspectParentSession(parentSessionPath, header.id, sessionPath)
|
|
200
205
|
: undefined;
|
|
@@ -211,7 +216,7 @@ export function extractSessionRecord(sessionPath: string): ExtractedSessionRecor
|
|
|
211
216
|
messageCount: scan.messageCount,
|
|
212
217
|
entryCount: scan.entryCount,
|
|
213
218
|
parentSessionPath,
|
|
214
|
-
parentSessionId: parentSession?.sessionId,
|
|
219
|
+
parentSessionId: lineage?.parentSessionId ?? parentSession?.sessionId,
|
|
215
220
|
sessionOrigin: deriveSessionOrigin(
|
|
216
221
|
parentSessionPath,
|
|
217
222
|
parentSession?.launchedSubagent ?? false,
|
|
@@ -306,6 +311,7 @@ export function scanSessionEntries(
|
|
|
306
311
|
let sessionNameEntryId: string | undefined;
|
|
307
312
|
let sessionNameTs: string | undefined;
|
|
308
313
|
let handoffMetadata: DurableHandoffMetadataRecord | undefined;
|
|
314
|
+
let lineage: SessionLineage | undefined;
|
|
309
315
|
|
|
310
316
|
for (const entry of entries) {
|
|
311
317
|
if (!isSessionEntry(entry)) continue;
|
|
@@ -336,6 +342,9 @@ export function scanSessionEntries(
|
|
|
336
342
|
continue;
|
|
337
343
|
}
|
|
338
344
|
case "custom": {
|
|
345
|
+
if (entry.customType === LINEAGE_CUSTOM_TYPE && !lineage) {
|
|
346
|
+
lineage = safeParseTypeBoxValue(LINEAGE_SCHEMA, entry.data);
|
|
347
|
+
}
|
|
339
348
|
const nextHandoffMetadata = extractHandoffMetadata(entry, entryTs);
|
|
340
349
|
if (nextHandoffMetadata && !handoffMetadata) {
|
|
341
350
|
handoffMetadata = nextHandoffMetadata;
|
|
@@ -393,6 +402,7 @@ export function scanSessionEntries(
|
|
|
393
402
|
sessionNameEntryId,
|
|
394
403
|
sessionNameTs,
|
|
395
404
|
handoffMetadata,
|
|
405
|
+
lineage,
|
|
396
406
|
};
|
|
397
407
|
}
|
|
398
408
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
export const LINEAGE_CUSTOM_TYPE = "pi-sessions.lineage";
|
|
4
|
+
export const LINEAGE_SCHEMA = Type.Object({
|
|
5
|
+
parentSessionId: Type.String({ minLength: 1 }),
|
|
6
|
+
parentSessionFile: Type.Optional(Type.String({ minLength: 1 })),
|
|
7
|
+
});
|
|
8
|
+
export type SessionLineage = Static<typeof LINEAGE_SCHEMA>;
|
|
@@ -231,6 +231,9 @@ function applyTailSync(
|
|
|
231
231
|
sessionOrigin?: SessionOrigin,
|
|
232
232
|
): boolean {
|
|
233
233
|
return db.transaction((): boolean => {
|
|
234
|
+
// A lineage entry can arrive after the initial header sync. Re-read the header to
|
|
235
|
+
// preserve its precedence and rebuild lineage through the normal full-sync path.
|
|
236
|
+
if (tail.scan.lineage) return false;
|
|
234
237
|
// Another process may have advanced the index between our baseline read
|
|
235
238
|
// and this transaction; the tail deltas would then double-count.
|
|
236
239
|
const current = getSessionRowByPath(db, baseline.sessionPath);
|
|
@@ -23,6 +23,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
23
23
|
enable: Type.Optional(Type.Boolean()),
|
|
24
24
|
maxDepth: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
25
25
|
contextLimit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
26
|
+
agentsDir: Type.Optional(Type.String()),
|
|
26
27
|
}),
|
|
27
28
|
),
|
|
28
29
|
handoff: Type.Optional(
|
|
@@ -125,6 +126,8 @@ export interface SessionSettings {
|
|
|
125
126
|
maxDepth: number;
|
|
126
127
|
/** Effective context window for a subagent, when smaller than its model's. */
|
|
127
128
|
contextLimit?: number;
|
|
129
|
+
/** Where agent profiles for subagent launches live. */
|
|
130
|
+
agentsDir: string;
|
|
128
131
|
};
|
|
129
132
|
handoff: HandoffSettings;
|
|
130
133
|
index: {
|
|
@@ -140,6 +143,10 @@ export function getDefaultIndexDir(): string {
|
|
|
140
143
|
return path.join(getAgentDir(), "pi-sessions");
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
export function getDefaultAgentsDir(): string {
|
|
147
|
+
return path.join(getAgentDir(), "agents");
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
export function getDefaultIndexPath(): string {
|
|
144
151
|
return path.join(getDefaultIndexDir(), "index.sqlite");
|
|
145
152
|
}
|
|
@@ -168,19 +175,19 @@ function expandHome(rawPath: string): string {
|
|
|
168
175
|
return rawPath;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
function
|
|
178
|
+
function normalizeDir(value: string | undefined, defaultDir: string, key: string): string {
|
|
172
179
|
if (value === undefined) {
|
|
173
|
-
return
|
|
180
|
+
return defaultDir;
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
const trimmed = value.trim();
|
|
177
184
|
if (trimmed.length === 0) {
|
|
178
|
-
return
|
|
185
|
+
return defaultDir;
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
const expanded = expandHome(trimmed);
|
|
182
189
|
if (!path.isAbsolute(expanded)) {
|
|
183
|
-
throw new Error(
|
|
190
|
+
throw new Error(`${key} must be an absolute path or start with "~/".`);
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
return path.normalize(expanded);
|
|
@@ -254,7 +261,11 @@ function resolveFeatureToggles(value: SessionFileSettings): FeatureToggles {
|
|
|
254
261
|
}
|
|
255
262
|
|
|
256
263
|
function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSettings {
|
|
257
|
-
const indexDir =
|
|
264
|
+
const indexDir = normalizeDir(
|
|
265
|
+
fileSettings.index?.dir,
|
|
266
|
+
getDefaultIndexDir(),
|
|
267
|
+
"sessions.index.dir",
|
|
268
|
+
);
|
|
258
269
|
|
|
259
270
|
return {
|
|
260
271
|
features: resolveFeatureToggles(fileSettings),
|
|
@@ -263,6 +274,11 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
|
|
|
263
274
|
...(fileSettings.subagents?.contextLimit === undefined
|
|
264
275
|
? {}
|
|
265
276
|
: { contextLimit: fileSettings.subagents.contextLimit }),
|
|
277
|
+
agentsDir: normalizeDir(
|
|
278
|
+
fileSettings.subagents?.agentsDir,
|
|
279
|
+
getDefaultAgentsDir(),
|
|
280
|
+
"sessions.subagents.agentsDir",
|
|
281
|
+
),
|
|
266
282
|
},
|
|
267
283
|
handoff: {
|
|
268
284
|
...resolveAgentModelSettings(fileSettings.handoff),
|
|
@@ -10,10 +10,6 @@ import type {
|
|
|
10
10
|
SendMessageResult,
|
|
11
11
|
} from "../session-messaging/install.ts";
|
|
12
12
|
import type { SessionWaker } from "../session-messaging/message-router.ts";
|
|
13
|
-
import {
|
|
14
|
-
createSessionCancelTool,
|
|
15
|
-
createSessionSendMessageTool,
|
|
16
|
-
} from "../session-messaging/pi/tools.ts";
|
|
17
13
|
import type { SessionLifecycle } from "../shared/composition.ts";
|
|
18
14
|
import type { CompactionThresholdSettings, SessionSettings } from "../shared/settings.ts";
|
|
19
15
|
import { isTmuxInstalled } from "../shared/tmux.ts";
|
|
@@ -82,7 +78,6 @@ export function installSubagents(
|
|
|
82
78
|
messaging: MessagingHandle;
|
|
83
79
|
readCompactionSettings: (cwd: string) => CompactionThresholdSettings;
|
|
84
80
|
sendMessage(request: SendMessageRequest): Promise<SendMessageResult>;
|
|
85
|
-
hasHostedSessions?: () => boolean;
|
|
86
81
|
},
|
|
87
82
|
): SubagentsHandle {
|
|
88
83
|
pi.registerMessageRenderer(SUBAGENT_REPORT_MESSAGE_CUSTOM_TYPE, renderSubagentReportMessage);
|
|
@@ -154,7 +149,10 @@ export function installSubagents(
|
|
|
154
149
|
|
|
155
150
|
pi.on("before_agent_start", (event) => {
|
|
156
151
|
if (current?.child) {
|
|
157
|
-
|
|
152
|
+
const agent = current.child.identity.agent;
|
|
153
|
+
event.systemPromptOptions.sections[SUBAGENT_PROMPT_SECTION] = agent
|
|
154
|
+
? `${SUBAGENT_PROMPT}\n\n${agent.instructions}`
|
|
155
|
+
: SUBAGENT_PROMPT;
|
|
158
156
|
}
|
|
159
157
|
});
|
|
160
158
|
|
|
@@ -171,12 +169,12 @@ export function installSubagents(
|
|
|
171
169
|
|
|
172
170
|
pi.on("agent_start", (_event, ctx) => {
|
|
173
171
|
settledChildLifecycle.cancel();
|
|
174
|
-
if (current?.child) {
|
|
172
|
+
if (current?.child && !current.child.paused) {
|
|
175
173
|
current.child.reportsAtTurnStart = countSubagentReports(ctx.sessionManager.getBranch());
|
|
176
174
|
}
|
|
177
175
|
});
|
|
178
176
|
|
|
179
|
-
pi.on("agent_settled", async (
|
|
177
|
+
pi.on("agent_settled", async (event, ctx) => {
|
|
180
178
|
const session = current;
|
|
181
179
|
if (!session || session.parent.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
182
180
|
return;
|
|
@@ -192,7 +190,11 @@ export function installSubagents(
|
|
|
192
190
|
}
|
|
193
191
|
if (session.child) {
|
|
194
192
|
await contextLimit.compactIfOverLimit(ctx);
|
|
195
|
-
|
|
193
|
+
// Escape in the child's pane pauses the delegated turn; it still owes its parent a report.
|
|
194
|
+
session.child.paused = event.aborted;
|
|
195
|
+
if (!event.aborted) {
|
|
196
|
+
await settledChildLifecycle.settle(session.parent, session.child, reconciliation);
|
|
197
|
+
}
|
|
196
198
|
}
|
|
197
199
|
});
|
|
198
200
|
|
|
@@ -225,28 +227,9 @@ export function installSubagents(
|
|
|
225
227
|
}
|
|
226
228
|
};
|
|
227
229
|
|
|
228
|
-
const registerMessagingTools = (): void => {
|
|
229
|
-
const isSubagent = handle.getParentSessionId() !== undefined;
|
|
230
|
-
pi.registerTool(
|
|
231
|
-
createSessionSendMessageTool(handle, {
|
|
232
|
-
role: isSubagent ? { kind: "subagent" } : { kind: "wakeCapable" },
|
|
233
|
-
hostedSessions: deps.hasHostedSessions?.() ?? false,
|
|
234
|
-
getCachedRelationTo: deps.messaging.getCachedRelationTo,
|
|
235
|
-
getParentSessionId: () => handle.getParentSessionId(),
|
|
236
|
-
}),
|
|
237
|
-
);
|
|
238
|
-
pi.registerTool(
|
|
239
|
-
createSessionCancelTool(handle, {
|
|
240
|
-
role: isSubagent ? { kind: "subagent" } : { kind: "plain" },
|
|
241
|
-
getParentSessionId: () => handle.getParentSessionId(),
|
|
242
|
-
}),
|
|
243
|
-
);
|
|
244
|
-
};
|
|
245
|
-
|
|
246
230
|
pi.on("session_tree", async (_event, ctx) => {
|
|
247
231
|
settledChildLifecycle.cancel();
|
|
248
232
|
refreshChildSession();
|
|
249
|
-
registerMessagingTools();
|
|
250
233
|
try {
|
|
251
234
|
await reconciler.reconcileAndRestoreSuspended();
|
|
252
235
|
} catch (error) {
|
|
@@ -303,9 +286,6 @@ export function installSubagents(
|
|
|
303
286
|
reconciler.beginSession();
|
|
304
287
|
current = { parent };
|
|
305
288
|
refreshChildSession();
|
|
306
|
-
if (identity) {
|
|
307
|
-
registerMessagingTools();
|
|
308
|
-
}
|
|
309
289
|
try {
|
|
310
290
|
if (_event.reason === "reload") {
|
|
311
291
|
await reconciler.reconcile();
|
|
@@ -19,15 +19,16 @@ export function createSubagentLaunchTarget(
|
|
|
19
19
|
value: SUBAGENT_LAUNCH,
|
|
20
20
|
description:
|
|
21
21
|
"'subagent' delegates one task to a detached tmux worker and requests a report by default.",
|
|
22
|
-
requestResponseDefault: true,
|
|
23
22
|
bootstrapMode: "automatic",
|
|
24
23
|
approveProjectTrust: true,
|
|
24
|
+
requiredTools: ["submit_task_report"],
|
|
25
25
|
describeSubagentChild(input) {
|
|
26
26
|
return {
|
|
27
27
|
childSessionId: input.childSessionId,
|
|
28
28
|
ownerSessionId: input.ownerSessionId,
|
|
29
29
|
depth: state.depth + 1,
|
|
30
30
|
requestResponse: input.requestResponse,
|
|
31
|
+
...(input.agent ? { agent: input.agent } : {}),
|
|
31
32
|
};
|
|
32
33
|
},
|
|
33
34
|
prepareChild(input) {
|
|
@@ -8,7 +8,12 @@ import {
|
|
|
8
8
|
type TmuxExecutor,
|
|
9
9
|
tmuxSessionName,
|
|
10
10
|
} from "../shared/tmux.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
classifySubagent,
|
|
13
|
+
isRunningSubagentState,
|
|
14
|
+
type SubagentEvidence,
|
|
15
|
+
type SubagentState,
|
|
16
|
+
} from "./classify.ts";
|
|
12
17
|
import {
|
|
13
18
|
type ChildSubagentLifecycle,
|
|
14
19
|
collectParentLedger,
|
|
@@ -220,8 +225,11 @@ export class SubagentReconciler {
|
|
|
220
225
|
continue;
|
|
221
226
|
}
|
|
222
227
|
|
|
228
|
+
// A running child writes its report to its own transcript before sending it live; recovering it in
|
|
229
|
+
// between would deliver it as recovered. Once the child stops, recovery is the report's only path.
|
|
230
|
+
const reports = isRunningSubagentState(child.state) ? [] : child.reports;
|
|
223
231
|
let reportLedger: ParentSubagentLedger | undefined;
|
|
224
|
-
for (const report of
|
|
232
|
+
for (const report of reports) {
|
|
225
233
|
if (this.hasSentReport(report.reportId)) {
|
|
226
234
|
continue;
|
|
227
235
|
}
|
|
@@ -17,6 +17,7 @@ export interface SubagentChildSessionState {
|
|
|
17
17
|
identity: HandoffSubagent;
|
|
18
18
|
requestResponse: boolean;
|
|
19
19
|
reportsAtTurnStart: number;
|
|
20
|
+
paused: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface SettledChildParentSession {
|
|
@@ -159,6 +160,7 @@ export function refreshSubagentChildState(
|
|
|
159
160
|
identity,
|
|
160
161
|
requestResponse: identity.requestResponse,
|
|
161
162
|
reportsAtTurnStart: reports,
|
|
163
|
+
paused: false,
|
|
162
164
|
};
|
|
163
165
|
}
|
|
164
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi session search, ask, handoff, messaging, auto-titling, and indexing tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@biomejs/biome": "^2.5.10",
|
|
41
|
-
"@earendil-works/pi-agent-core": "^1.
|
|
42
|
-
"@earendil-works/pi-ai": "^1.
|
|
43
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
44
|
-
"@earendil-works/pi-tui": "^1.
|
|
41
|
+
"@earendil-works/pi-agent-core": "^1.3.0",
|
|
42
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
44
|
+
"@earendil-works/pi-tui": "^1.3.0",
|
|
45
45
|
"@types/node": "^24.13.3",
|
|
46
46
|
"strip-ansi": "^7.2.0",
|
|
47
47
|
"typebox": "1.3.27",
|