pi-sessions 0.8.0 → 0.10.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 +74 -37
- package/dist/session-messaging/broker/process.js +234 -0
- package/dist/shared/session-broker/framing.js +58 -0
- package/dist/shared/session-broker/protocol.js +163 -0
- package/dist/shared/session-broker/socket-path.js +19 -0
- package/dist/shared/typebox.js +36 -0
- package/extensions/pi-sessions.ts +125 -0
- package/extensions/session-ask/agent.ts +34 -14
- package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
- package/extensions/session-ask/navigate.ts +3 -1
- package/extensions/session-ask/presenter.ts +23 -0
- package/extensions/session-ask/renderer.ts +63 -0
- package/extensions/session-ask/tool-contract.ts +27 -0
- package/extensions/session-ask/view-model.ts +18 -0
- package/extensions/session-auto-title/generate.ts +18 -18
- package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +60 -53
- package/extensions/session-auto-title/model.ts +25 -17
- package/extensions/session-auto-title/retitle.ts +17 -18
- package/extensions/session-auto-title/wizard.ts +12 -11
- package/extensions/session-handoff/board-view-model.ts +179 -0
- package/extensions/session-handoff/board.ts +663 -0
- package/extensions/session-handoff/bootstrap.ts +172 -0
- package/extensions/session-handoff/extract.ts +129 -132
- package/extensions/session-handoff/install.ts +225 -0
- package/extensions/session-handoff/kickoff.ts +92 -0
- package/extensions/session-handoff/launch/backend.ts +16 -0
- package/extensions/session-handoff/launch/deferred.ts +20 -0
- package/extensions/session-handoff/launch/ghostty.ts +99 -0
- package/extensions/session-handoff/launch/resolution.ts +57 -0
- package/extensions/session-handoff/launch/shell.ts +7 -0
- package/extensions/session-handoff/launch/tmux.ts +31 -0
- package/extensions/session-handoff/launch-options.ts +61 -0
- package/extensions/session-handoff/launch-target.ts +91 -0
- package/extensions/session-handoff/metadata.ts +194 -56
- package/extensions/session-handoff/model.ts +58 -0
- package/extensions/session-handoff/query.ts +0 -1
- package/extensions/session-handoff/receipt.ts +96 -0
- package/extensions/session-handoff/review.ts +2 -28
- package/extensions/session-handoff/spawn.ts +76 -181
- package/extensions/session-handoff/strong-modal.ts +13 -0
- package/extensions/session-handoff/tool-contract.ts +12 -0
- package/extensions/session-handoff/tool-presenter.ts +35 -0
- package/extensions/session-handoff/tool-renderer.ts +66 -0
- package/extensions/session-handoff/tool-schema.ts +54 -0
- package/extensions/session-handoff/tool-view-model.ts +49 -0
- package/extensions/session-handoff/tool.ts +206 -0
- package/extensions/session-handoff/ui.ts +66 -0
- package/extensions/session-hooks/install.ts +75 -0
- package/extensions/{session-index.ts → session-index/install.ts} +9 -7
- package/extensions/session-messaging/broker/process.ts +21 -28
- package/extensions/session-messaging/broker/spawn.ts +12 -7
- package/extensions/session-messaging/install.ts +78 -0
- package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
- package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
- package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
- package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
- package/extensions/session-messaging/pi/message-contracts.ts +32 -0
- package/extensions/session-messaging/pi/renderer.ts +19 -12
- package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
- package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
- package/extensions/session-messaging/pi/service.ts +262 -81
- package/extensions/session-messaging/pi/tools.ts +184 -40
- package/extensions/session-search/extract.ts +60 -14
- package/extensions/session-search/hooks.ts +10 -13
- package/extensions/session-search/install.ts +373 -0
- package/extensions/session-search/renderer.ts +195 -0
- package/extensions/session-search/tool-contract.ts +51 -0
- package/extensions/shared/composition.ts +20 -0
- package/extensions/shared/errors.ts +3 -0
- package/extensions/shared/model-resolution.ts +73 -0
- package/extensions/shared/model-runtime.ts +33 -0
- package/extensions/shared/model.ts +7 -10
- package/extensions/shared/rendering/collapsible-text.ts +89 -0
- package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
- package/extensions/shared/rendering/theme.ts +5 -0
- package/extensions/shared/session-broker/client.ts +30 -29
- package/extensions/shared/session-broker/protocol.ts +128 -10
- package/extensions/shared/session-index/common.ts +5 -5
- package/extensions/shared/session-index/lineage.ts +0 -3
- package/extensions/shared/session-index/schema.ts +0 -1
- package/extensions/shared/session-index/scoring.ts +1 -2
- package/extensions/shared/session-index/search.ts +11 -4
- package/extensions/shared/session-index/store.ts +4 -9
- package/extensions/shared/settings.ts +83 -40
- package/extensions/shared/text.ts +5 -0
- package/extensions/shared/thinking-levels.ts +22 -0
- package/extensions/shared/tmux.ts +205 -0
- package/extensions/subagents/cancel.ts +111 -0
- package/extensions/subagents/classify.ts +58 -0
- package/extensions/subagents/install.ts +229 -0
- package/extensions/subagents/launch-target.ts +75 -0
- package/extensions/subagents/ledger.ts +217 -0
- package/extensions/subagents/reconcile.ts +386 -0
- package/extensions/subagents/report-message-presenter.ts +36 -0
- package/extensions/subagents/report-message-renderer.ts +21 -0
- package/extensions/subagents/report-message-view-model.ts +29 -0
- package/extensions/subagents/report.ts +159 -0
- package/extensions/subagents/roster.ts +262 -0
- package/extensions/subagents/settle.ts +182 -0
- package/extensions/subagents/wake.ts +173 -0
- package/images/handoff-board-subagents.png +0 -0
- package/images/handoff-board-user-sessions.png +0 -0
- package/images/handoff-subagent-report.png +0 -0
- package/images/session-handoff-tool.png +0 -0
- package/package.json +16 -18
- package/extensions/session-handoff.ts +0 -741
- package/extensions/session-hooks.ts +0 -75
- package/extensions/session-messaging/pi/message-view.ts +0 -131
- package/extensions/session-messaging.ts +0 -30
- package/extensions/session-search.ts +0 -419
- package/extensions/shared/session-broker/active.ts +0 -26
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ModelRuntimeProvider } from "../shared/model-runtime.ts";
|
|
5
|
+
import type { HandoffSettings } from "../shared/settings.ts";
|
|
6
|
+
import { generateHandoffDraftFromSessionManager } from "./extract.ts";
|
|
7
|
+
import { buildHandoffKickoffMessage, buildHandoffKickoffSource } from "./kickoff.ts";
|
|
8
|
+
import {
|
|
9
|
+
type ChildGeneratedHandoffBootstrap,
|
|
10
|
+
createHandoffSessionMetadata,
|
|
11
|
+
findPendingHandoffBootstrap,
|
|
12
|
+
getHandoffMetadataFromEntries,
|
|
13
|
+
HANDOFF_BOOTSTRAP_CONSUMED_CUSTOM_TYPE,
|
|
14
|
+
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
15
|
+
HANDOFF_STALE_SESSION_MESSAGE,
|
|
16
|
+
type HandoffBootstrapConsumedReason,
|
|
17
|
+
hasStartedConversation,
|
|
18
|
+
} from "./metadata.ts";
|
|
19
|
+
import { reviewHandoffDraftForSend } from "./review.ts";
|
|
20
|
+
import { formatHandoffError, runHandoffTaskWithLoader } from "./ui.ts";
|
|
21
|
+
|
|
22
|
+
export async function consumePendingHandoffBootstrap(
|
|
23
|
+
pi: ExtensionAPI,
|
|
24
|
+
ctx: ExtensionContext,
|
|
25
|
+
getModelRuntime: ModelRuntimeProvider,
|
|
26
|
+
destinationThinkingLevel: ThinkingLevel | undefined,
|
|
27
|
+
handoffSettings: HandoffSettings,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const scan = findPendingHandoffBootstrap(ctx.sessionManager.getBranch());
|
|
30
|
+
if (!scan) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const consumeBootstrap = (reason: HandoffBootstrapConsumedReason): void => {
|
|
35
|
+
pi.appendEntry(HANDOFF_BOOTSTRAP_CONSUMED_CUSTOM_TYPE, {
|
|
36
|
+
bootstrapEntryId: scan.entryId,
|
|
37
|
+
reason,
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (scan.kind === "invalid") {
|
|
42
|
+
consumeBootstrap("invalid");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bootstrap = scan.bootstrap;
|
|
47
|
+
if (bootstrap.sessionId !== ctx.sessionManager.getSessionId()) {
|
|
48
|
+
consumeBootstrap("invalid");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await startChildGeneratedHandoff(
|
|
53
|
+
pi,
|
|
54
|
+
ctx,
|
|
55
|
+
bootstrap,
|
|
56
|
+
scan.entryId,
|
|
57
|
+
consumeBootstrap,
|
|
58
|
+
getModelRuntime,
|
|
59
|
+
destinationThinkingLevel,
|
|
60
|
+
handoffSettings,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function startChildGeneratedHandoff(
|
|
65
|
+
pi: ExtensionAPI,
|
|
66
|
+
ctx: ExtensionContext,
|
|
67
|
+
bootstrap: ChildGeneratedHandoffBootstrap,
|
|
68
|
+
bootstrapEntryId: string,
|
|
69
|
+
consumeBootstrap: (reason: HandoffBootstrapConsumedReason) => void,
|
|
70
|
+
getModelRuntime: ModelRuntimeProvider,
|
|
71
|
+
destinationThinkingLevel: ThinkingLevel | undefined,
|
|
72
|
+
handoffSettings: HandoffSettings,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const entries = ctx.sessionManager.getEntries();
|
|
75
|
+
if (hasStartedConversation(entries)) {
|
|
76
|
+
consumeBootstrap("stale");
|
|
77
|
+
if (ctx.hasUI) {
|
|
78
|
+
ctx.ui.notify(HANDOFF_STALE_SESSION_MESSAGE, "error");
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!ctx.hasUI) {
|
|
84
|
+
if (bootstrap.bootstrapMode === "automatic") {
|
|
85
|
+
ctx.shutdown();
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A thrown error leaves the bootstrap pending so the next resume offers
|
|
91
|
+
// review again; explicit user decisions consume it.
|
|
92
|
+
try {
|
|
93
|
+
const sourceSessionManager = SessionManager.open(bootstrap.parentSessionFile);
|
|
94
|
+
const modelRuntime = await getModelRuntime(ctx.modelRegistry);
|
|
95
|
+
const generatedDraft = await runHandoffTaskWithLoader(
|
|
96
|
+
ctx,
|
|
97
|
+
"Generating handoff draft...",
|
|
98
|
+
async (signal: AbortSignal) =>
|
|
99
|
+
generateHandoffDraftFromSessionManager({
|
|
100
|
+
ctx,
|
|
101
|
+
modelRuntime,
|
|
102
|
+
sourceSessionManager,
|
|
103
|
+
sourceLeafId: bootstrap.sourceLeafId,
|
|
104
|
+
goal: bootstrap.goal,
|
|
105
|
+
settings: handoffSettings,
|
|
106
|
+
destinationThinkingLevel,
|
|
107
|
+
signal,
|
|
108
|
+
requestResponse: bootstrap.requestResponse,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
if (!generatedDraft) {
|
|
112
|
+
consumeBootstrap("cancelled");
|
|
113
|
+
ctx.ui.notify("Cancelled", "info");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (generatedDraft.debugSessionPath) {
|
|
118
|
+
ctx.ui.notify(
|
|
119
|
+
`Handoff extraction session saved to ${generatedDraft.debugSessionPath}`,
|
|
120
|
+
"info",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let prompt = generatedDraft.draft;
|
|
125
|
+
if (bootstrap.bootstrapMode === "review") {
|
|
126
|
+
const review = await reviewHandoffDraftForSend(ctx.ui, generatedDraft.draft);
|
|
127
|
+
if (review.action === "prefill") {
|
|
128
|
+
consumeBootstrap("prefilled");
|
|
129
|
+
ctx.ui.setEditorText(review.prompt);
|
|
130
|
+
ctx.ui.notify("Handoff prompt ready in editor.", "info");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (review.action === "cancel") {
|
|
134
|
+
consumeBootstrap("cancelled");
|
|
135
|
+
ctx.ui.notify("Cancelled", "info");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
prompt = review.prompt;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The tool-provided bootstrap title is authoritative; extraction does not
|
|
142
|
+
// replace it with a second generated title.
|
|
143
|
+
const metadata = createHandoffSessionMetadata(
|
|
144
|
+
bootstrap.goal,
|
|
145
|
+
prompt,
|
|
146
|
+
bootstrap.title,
|
|
147
|
+
bootstrap.launch,
|
|
148
|
+
);
|
|
149
|
+
if (!getHandoffMetadataFromEntries(ctx.sessionManager.getEntries())) {
|
|
150
|
+
pi.appendEntry(HANDOFF_METADATA_CUSTOM_TYPE, metadata);
|
|
151
|
+
}
|
|
152
|
+
pi.setSessionName(metadata.title);
|
|
153
|
+
const sourceSessionName = sourceSessionManager.getSessionName();
|
|
154
|
+
pi.sendMessage(
|
|
155
|
+
buildHandoffKickoffMessage({
|
|
156
|
+
prompt,
|
|
157
|
+
title: metadata.title,
|
|
158
|
+
source: buildHandoffKickoffSource({
|
|
159
|
+
sessionId: sourceSessionManager.getSessionId(),
|
|
160
|
+
...(sourceSessionName ? { sessionName: sourceSessionName } : {}),
|
|
161
|
+
}),
|
|
162
|
+
bootstrapEntryId,
|
|
163
|
+
}),
|
|
164
|
+
{ triggerTurn: true },
|
|
165
|
+
);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
ctx.ui.notify(formatHandoffError(error), "error");
|
|
168
|
+
if (bootstrap.bootstrapMode === "automatic") {
|
|
169
|
+
ctx.shutdown();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
ThinkingContent,
|
|
8
8
|
ToolCall,
|
|
9
9
|
} from "@earendil-works/pi-ai";
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
ModelRuntime,
|
|
13
|
+
SessionContext,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
11
15
|
import {
|
|
12
16
|
buildSessionContext,
|
|
13
17
|
convertToLlm,
|
|
@@ -19,60 +23,44 @@ import {
|
|
|
19
23
|
serializeConversation,
|
|
20
24
|
} from "@earendil-works/pi-coding-agent";
|
|
21
25
|
import { type Static, Type } from "typebox";
|
|
26
|
+
import { freshenModel } from "../shared/model-runtime.ts";
|
|
27
|
+
import { getDefaultHandoffRunsDir, type HandoffSettings } from "../shared/settings.ts";
|
|
22
28
|
import { parseTypeBoxValue } from "../shared/typebox.ts";
|
|
29
|
+
import { resolveModelOverride } from "./model.ts";
|
|
23
30
|
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
31
|
+
const MAX_HANDOFF_EXTRACTION_ATTEMPTS = 3;
|
|
32
|
+
const HANDOFF_EXTRACTION_RETRY_PROMPT =
|
|
33
|
+
"You did not call create_handoff_context. Call it exactly once now with the completed briefing.";
|
|
27
34
|
|
|
28
|
-
const HANDOFF_SYSTEM_PROMPT = `You extract context for a deliberate session handoff.
|
|
35
|
+
const HANDOFF_SYSTEM_PROMPT = `You extract supporting context for a deliberate session handoff. You are preparing a briefing for a new destination session from a snapshot of its ongoing parent session.
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
The Handoff Goal is the authoritative and exclusive task of the destination session, and will be directly passed into that session. Do not rewrite, expand the scope of, or replace the goal.
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- Prefer workspace-relative file paths when possible.
|
|
36
|
-
- title must be a short session title for the new handoff thread, 64 characters or less, without prefixes like "Handoff:" or otherwise referencing the current thread.
|
|
37
|
-
- nextTask must be the concrete next action for the new session.
|
|
38
|
-
- openQuestions should contain only unresolved items that materially affect the next task.
|
|
39
|
-
- If there are no meaningful open questions, omit openQuestions entirely.
|
|
40
|
-
- Do not write the final handoff prompt yourself.`;
|
|
39
|
+
The parent snapshot is reference material used only to provide evidence that supports that goal. Do not continue the parent conversation, respond to any questions in it, or carry forward parent tasks, unresolved work, instructions, or plans outside the goal. Only identify relevant context, files, constraints, and known decisions for the Handoff Goal.
|
|
40
|
+
|
|
41
|
+
The parent may be coordinating several parallel sessions. Do not include any references to those other sessions.`;
|
|
41
42
|
|
|
42
43
|
const HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
|
|
43
|
-
title: Type.String({
|
|
44
|
-
description: "Short display title for the new handoff session.",
|
|
45
|
-
}),
|
|
46
44
|
summary: Type.String({
|
|
47
|
-
description:
|
|
45
|
+
description:
|
|
46
|
+
"Context from the parent task that is directly relevant to the Handoff Goal. Include only details that further support the goal.",
|
|
48
47
|
}),
|
|
49
48
|
relevantFiles: Type.Array(Type.String(), {
|
|
50
|
-
description: "Relevant
|
|
51
|
-
}),
|
|
52
|
-
nextTask: Type.String({
|
|
53
|
-
description: "The concrete next task for the new session.",
|
|
49
|
+
description: "Relevant file paths.",
|
|
54
50
|
}),
|
|
55
51
|
openQuestions: Type.Optional(
|
|
56
52
|
Type.Array(Type.String(), {
|
|
57
|
-
description:
|
|
53
|
+
description:
|
|
54
|
+
"Unresolved questions that materially affect the destination task. Omit when there are none.",
|
|
58
55
|
}),
|
|
59
56
|
),
|
|
60
57
|
});
|
|
61
58
|
|
|
62
59
|
type HandoffExtractionArgs = Static<typeof HANDOFF_EXTRACTION_PARAMETERS>;
|
|
63
|
-
type RequiredHandoffExtractionArgs = Static<typeof REQUIRED_HANDOFF_EXTRACTION_PARAMETERS>;
|
|
64
|
-
|
|
65
|
-
const REQUIRED_HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
|
|
66
|
-
title: HANDOFF_EXTRACTION_PARAMETERS.properties.title,
|
|
67
|
-
summary: HANDOFF_EXTRACTION_PARAMETERS.properties.summary,
|
|
68
|
-
nextTask: HANDOFF_EXTRACTION_PARAMETERS.properties.nextTask,
|
|
69
|
-
});
|
|
70
60
|
|
|
71
61
|
export interface HandoffContext {
|
|
72
|
-
title: string;
|
|
73
62
|
summary: string;
|
|
74
63
|
relevantFiles: string[];
|
|
75
|
-
nextTask: string;
|
|
76
64
|
openQuestions: string[];
|
|
77
65
|
}
|
|
78
66
|
|
|
@@ -81,57 +69,70 @@ export interface HandoffDraftResult {
|
|
|
81
69
|
context: HandoffContext;
|
|
82
70
|
sessionId: string;
|
|
83
71
|
sessionPath?: string | undefined;
|
|
72
|
+
debugSessionPath?: string | undefined;
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (!ctx.model) {
|
|
94
|
-
throw new Error("No model is available for handoff.");
|
|
75
|
+
/** Validate an anchored source branch and return its built context. Throws a user-facing Error otherwise. */
|
|
76
|
+
export function resolveHandoffSource(
|
|
77
|
+
sessionManager: ExtensionContext["sessionManager"],
|
|
78
|
+
sourceLeafId: string,
|
|
79
|
+
): SessionContext {
|
|
80
|
+
if (!sessionManager.getEntry(sourceLeafId)) {
|
|
81
|
+
throw new Error(`Handoff source snapshot entry ${sourceLeafId} was not found.`);
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
requestResponse,
|
|
104
|
-
);
|
|
84
|
+
const sessionContext = buildSessionContext(sessionManager.getEntries(), sourceLeafId);
|
|
85
|
+
if (sessionContext.messages.length === 0) {
|
|
86
|
+
throw new Error("No conversation to hand off.");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return sessionContext;
|
|
105
90
|
}
|
|
106
91
|
|
|
107
|
-
export async function generateHandoffDraftFromSessionManager(
|
|
108
|
-
ctx
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
92
|
+
export async function generateHandoffDraftFromSessionManager({
|
|
93
|
+
ctx,
|
|
94
|
+
modelRuntime,
|
|
95
|
+
sourceSessionManager,
|
|
96
|
+
sourceLeafId,
|
|
97
|
+
goal,
|
|
98
|
+
settings,
|
|
99
|
+
destinationThinkingLevel,
|
|
100
|
+
signal,
|
|
113
101
|
requestResponse = false,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
102
|
+
}: {
|
|
103
|
+
ctx: ExtensionContext;
|
|
104
|
+
modelRuntime: ModelRuntime;
|
|
105
|
+
sourceSessionManager: ExtensionContext["sessionManager"];
|
|
106
|
+
sourceLeafId: string;
|
|
107
|
+
goal: string;
|
|
108
|
+
settings: HandoffSettings;
|
|
109
|
+
destinationThinkingLevel: ThinkingLevel | undefined;
|
|
110
|
+
signal?: AbortSignal | undefined;
|
|
111
|
+
requestResponse?: boolean | undefined;
|
|
112
|
+
}): Promise<HandoffDraftResult | undefined> {
|
|
113
|
+
const modelOverride = settings.model
|
|
114
|
+
? resolveModelOverride(modelRuntime, settings.model, settings.thinkingLevel)
|
|
115
|
+
: undefined;
|
|
116
|
+
let model = modelOverride?.model;
|
|
117
|
+
if (!model) {
|
|
118
|
+
if (!ctx.model) {
|
|
119
|
+
throw new Error("No model is available for handoff.");
|
|
120
|
+
}
|
|
121
|
+
model = freshenModel(modelRuntime, ctx.model);
|
|
126
122
|
}
|
|
123
|
+
const thinkingLevel =
|
|
124
|
+
modelOverride?.thinkingLevel ?? settings.thinkingLevel ?? destinationThinkingLevel;
|
|
125
|
+
const sessionContext = resolveHandoffSource(sourceSessionManager, sourceLeafId);
|
|
127
126
|
|
|
128
127
|
const conversationText = serializeConversation(convertToLlm(sessionContext.messages));
|
|
129
128
|
const handoffContext = await runHandoffExtractionAgent(
|
|
130
129
|
ctx,
|
|
130
|
+
modelRuntime,
|
|
131
131
|
model,
|
|
132
132
|
conversationText,
|
|
133
133
|
goal,
|
|
134
134
|
thinkingLevel,
|
|
135
|
+
settings.persistRuns,
|
|
135
136
|
signal,
|
|
136
137
|
);
|
|
137
138
|
if (!handoffContext) {
|
|
@@ -142,38 +143,51 @@ export async function generateHandoffDraftFromSessionManager(
|
|
|
142
143
|
const sessionPath = sourceSessionManager.getSessionFile();
|
|
143
144
|
|
|
144
145
|
return {
|
|
145
|
-
draft: assembleHandoffDraft(
|
|
146
|
-
|
|
146
|
+
draft: assembleHandoffDraft(
|
|
147
|
+
sessionId,
|
|
148
|
+
sessionPath,
|
|
149
|
+
handoffContext.context,
|
|
150
|
+
goal,
|
|
151
|
+
requestResponse,
|
|
152
|
+
),
|
|
153
|
+
context: handoffContext.context,
|
|
147
154
|
sessionId,
|
|
148
155
|
sessionPath,
|
|
156
|
+
...(handoffContext.debugSessionPath
|
|
157
|
+
? { debugSessionPath: handoffContext.debugSessionPath }
|
|
158
|
+
: {}),
|
|
149
159
|
};
|
|
150
160
|
}
|
|
151
161
|
|
|
152
162
|
export function buildExtractionPrompt(conversationText: string, goal: string): string {
|
|
153
163
|
return [
|
|
154
|
-
"
|
|
164
|
+
"<conversation>",
|
|
155
165
|
conversationText,
|
|
166
|
+
"</conversation>",
|
|
156
167
|
"",
|
|
157
|
-
"
|
|
168
|
+
"<handoff-goal>",
|
|
158
169
|
goal,
|
|
159
|
-
"",
|
|
160
|
-
"Call create_handoff_context exactly once.",
|
|
170
|
+
"</handoff-goal>",
|
|
161
171
|
].join("\n");
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
async function runHandoffExtractionAgent(
|
|
165
175
|
ctx: ExtensionContext,
|
|
176
|
+
modelRuntime: ModelRuntime,
|
|
166
177
|
model: Model<Api>,
|
|
167
178
|
conversationText: string,
|
|
168
179
|
goal: string,
|
|
169
180
|
thinkingLevel: ThinkingLevel | undefined,
|
|
181
|
+
persistRuns: boolean,
|
|
170
182
|
signal?: AbortSignal,
|
|
171
|
-
): Promise<HandoffContext | undefined> {
|
|
183
|
+
): Promise<{ context: HandoffContext; debugSessionPath?: string | undefined } | undefined> {
|
|
172
184
|
let capturedArguments: HandoffExtractionArgs | undefined;
|
|
185
|
+
// Pi does not inject promptSnippet or promptGuidelines when a custom system prompt is active.
|
|
173
186
|
const createHandoffContextTool = defineTool({
|
|
174
187
|
name: "create_handoff_context",
|
|
175
188
|
label: "Create handoff context",
|
|
176
|
-
description:
|
|
189
|
+
description:
|
|
190
|
+
"Submit the completed structured briefing for the destination session. Call this tool exactly once to complete extraction.",
|
|
177
191
|
parameters: HANDOFF_EXTRACTION_PARAMETERS,
|
|
178
192
|
execute: async (_toolCallId, params) => {
|
|
179
193
|
capturedArguments = params;
|
|
@@ -191,19 +205,23 @@ async function runHandoffExtractionAgent(
|
|
|
191
205
|
noExtensions: true,
|
|
192
206
|
noPromptTemplates: true,
|
|
193
207
|
noSkills: true,
|
|
194
|
-
|
|
208
|
+
systemPromptOverride: () => HANDOFF_SYSTEM_PROMPT,
|
|
195
209
|
});
|
|
196
210
|
await resourceLoader.reload();
|
|
197
211
|
|
|
212
|
+
const sessionManager = persistRuns
|
|
213
|
+
? SessionManager.create(ctx.cwd, getDefaultHandoffRunsDir())
|
|
214
|
+
: SessionManager.inMemory(ctx.cwd);
|
|
215
|
+
const debugSessionPath = persistRuns ? sessionManager.getSessionFile() : undefined;
|
|
198
216
|
const { session } = await createAgentSession({
|
|
199
217
|
cwd: ctx.cwd,
|
|
200
218
|
model,
|
|
201
|
-
|
|
219
|
+
modelRuntime,
|
|
202
220
|
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
203
|
-
tools: ["
|
|
221
|
+
tools: ["create_handoff_context"],
|
|
204
222
|
customTools: [createHandoffContextTool],
|
|
205
223
|
resourceLoader,
|
|
206
|
-
sessionManager
|
|
224
|
+
sessionManager,
|
|
207
225
|
});
|
|
208
226
|
|
|
209
227
|
const abortHandler = (): void => {
|
|
@@ -217,7 +235,19 @@ async function runHandoffExtractionAgent(
|
|
|
217
235
|
return undefined;
|
|
218
236
|
}
|
|
219
237
|
|
|
220
|
-
|
|
238
|
+
for (let attempt = 1; attempt <= MAX_HANDOFF_EXTRACTION_ATTEMPTS; attempt += 1) {
|
|
239
|
+
if (signal?.aborted) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
await session.prompt(
|
|
243
|
+
attempt === 1
|
|
244
|
+
? buildExtractionPrompt(conversationText, goal)
|
|
245
|
+
: HANDOFF_EXTRACTION_RETRY_PROMPT,
|
|
246
|
+
);
|
|
247
|
+
if (capturedArguments) {
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
221
251
|
} finally {
|
|
222
252
|
signal?.removeEventListener("abort", abortHandler);
|
|
223
253
|
session.dispose();
|
|
@@ -231,12 +261,15 @@ async function runHandoffExtractionAgent(
|
|
|
231
261
|
throw new Error("Handoff extraction did not return structured context.");
|
|
232
262
|
}
|
|
233
263
|
|
|
234
|
-
const extraction = extractHandoffContextFromArguments(capturedArguments
|
|
264
|
+
const extraction = extractHandoffContextFromArguments(capturedArguments);
|
|
235
265
|
if (!extraction.context) {
|
|
236
266
|
throw new Error(extraction.error);
|
|
237
267
|
}
|
|
238
268
|
|
|
239
|
-
return
|
|
269
|
+
return {
|
|
270
|
+
context: extraction.context,
|
|
271
|
+
...(debugSessionPath ? { debugSessionPath } : {}),
|
|
272
|
+
};
|
|
240
273
|
}
|
|
241
274
|
|
|
242
275
|
export function assembleHandoffDraft(
|
|
@@ -247,10 +280,10 @@ export function assembleHandoffDraft(
|
|
|
247
280
|
requestResponse = false,
|
|
248
281
|
): string {
|
|
249
282
|
const sections = [buildContinuityLine(sessionId, sessionPath, requestResponse)];
|
|
250
|
-
const
|
|
283
|
+
const task = goal.trim();
|
|
251
284
|
|
|
252
|
-
if (
|
|
253
|
-
sections.push(["## Task",
|
|
285
|
+
if (task) {
|
|
286
|
+
sections.push(["## Task", task].join("\n"));
|
|
254
287
|
}
|
|
255
288
|
|
|
256
289
|
if (handoffContext.relevantFiles.length > 0) {
|
|
@@ -280,24 +313,22 @@ export function assembleHandoffDraft(
|
|
|
280
313
|
|
|
281
314
|
export function extractHandoffContext(
|
|
282
315
|
response: AssistantMessage,
|
|
283
|
-
goal: string,
|
|
284
316
|
): { context: HandoffContext; error?: undefined } | { context?: undefined; error: string } {
|
|
285
317
|
const toolCall = response.content.find(isCreateHandoffContextToolCall);
|
|
286
318
|
if (!toolCall) {
|
|
287
319
|
return { error: "Handoff extraction did not return structured context." };
|
|
288
320
|
}
|
|
289
321
|
|
|
290
|
-
return extractHandoffContextFromArguments(toolCall.arguments
|
|
322
|
+
return extractHandoffContextFromArguments(toolCall.arguments);
|
|
291
323
|
}
|
|
292
324
|
|
|
293
325
|
function extractHandoffContextFromArguments(
|
|
294
326
|
argumentsValue: unknown,
|
|
295
|
-
goal: string,
|
|
296
327
|
): { context: HandoffContext; error?: undefined } | { context?: undefined; error: string } {
|
|
297
|
-
let
|
|
328
|
+
let extraction: HandoffExtractionArgs;
|
|
298
329
|
try {
|
|
299
|
-
|
|
300
|
-
|
|
330
|
+
extraction = parseTypeBoxValue(
|
|
331
|
+
HANDOFF_EXTRACTION_PARAMETERS,
|
|
301
332
|
argumentsValue,
|
|
302
333
|
"Invalid create_handoff_context arguments",
|
|
303
334
|
);
|
|
@@ -305,27 +336,16 @@ function extractHandoffContextFromArguments(
|
|
|
305
336
|
return { error: "Handoff extraction did not return structured context." };
|
|
306
337
|
}
|
|
307
338
|
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
310
|
-
return { error: "Handoff title must be 64 characters or less." };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const summary = normalizeText(requiredArguments.summary);
|
|
314
|
-
const relevantFiles = getRelevantFiles(argumentsValue);
|
|
315
|
-
const nextTask = normalizeText(requiredArguments.nextTask) || goal.trim();
|
|
316
|
-
const openQuestions = getOpenQuestions(argumentsValue);
|
|
317
|
-
|
|
318
|
-
if (!summary || !nextTask || !title) {
|
|
339
|
+
const summary = normalizeText(extraction.summary);
|
|
340
|
+
if (!summary) {
|
|
319
341
|
return { error: "Handoff extraction did not return structured context." };
|
|
320
342
|
}
|
|
321
343
|
|
|
322
344
|
return {
|
|
323
345
|
context: {
|
|
324
|
-
title,
|
|
325
346
|
summary,
|
|
326
|
-
relevantFiles,
|
|
327
|
-
|
|
328
|
-
openQuestions,
|
|
347
|
+
relevantFiles: normalizeStringArray(extraction.relevantFiles),
|
|
348
|
+
openQuestions: normalizeStringArray(extraction.openQuestions),
|
|
329
349
|
},
|
|
330
350
|
};
|
|
331
351
|
}
|
|
@@ -340,10 +360,10 @@ function buildContinuityLine(
|
|
|
340
360
|
return base;
|
|
341
361
|
}
|
|
342
362
|
|
|
343
|
-
return `${base} When this work is complete, send that session a completion report
|
|
363
|
+
return `${base} When this work is complete, send that session a completion report.`;
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
function normalizeStringArray(value: unknown
|
|
366
|
+
function normalizeStringArray(value: unknown): string[] {
|
|
347
367
|
if (!Array.isArray(value)) {
|
|
348
368
|
return [];
|
|
349
369
|
}
|
|
@@ -356,38 +376,15 @@ function normalizeStringArray(value: unknown, limit: number): string[] {
|
|
|
356
376
|
}
|
|
357
377
|
|
|
358
378
|
uniqueValues.add(normalized);
|
|
359
|
-
if (uniqueValues.size >= limit) {
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
362
379
|
}
|
|
363
380
|
|
|
364
381
|
return [...uniqueValues];
|
|
365
382
|
}
|
|
366
383
|
|
|
367
|
-
function getRelevantFiles(argumentsValue: unknown): string[] {
|
|
368
|
-
if (!isRecord(argumentsValue)) {
|
|
369
|
-
return [];
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return normalizeStringArray(argumentsValue.relevantFiles, MAX_RELEVANT_FILES);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function getOpenQuestions(argumentsValue: unknown): string[] {
|
|
376
|
-
if (!isRecord(argumentsValue)) {
|
|
377
|
-
return [];
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
return normalizeStringArray(argumentsValue.openQuestions, MAX_OPEN_QUESTIONS);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
384
|
function normalizeText(value: unknown): string {
|
|
384
385
|
return typeof value === "string" ? value.trim() : "";
|
|
385
386
|
}
|
|
386
387
|
|
|
387
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
388
|
-
return typeof value === "object" && value !== null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
388
|
function isCreateHandoffContextToolCall(
|
|
392
389
|
content: TextContent | ThinkingContent | ToolCall,
|
|
393
390
|
): content is ToolCall {
|