pi-sessions 0.9.0 → 0.11.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 +29 -20
- package/dist/session-messaging/broker/client-frame.js +62 -0
- package/dist/session-messaging/broker/process.js +234 -0
- package/dist/shared/session-broker/framing.js +59 -0
- package/dist/shared/session-broker/socket-path.js +19 -0
- package/extensions/pi-sessions.ts +14 -1
- package/extensions/session-auto-title/generate.ts +56 -66
- package/extensions/session-auto-title/install.ts +2 -0
- package/extensions/session-auto-title/runs.ts +50 -0
- package/extensions/session-handoff/board-view-model.ts +2 -4
- package/extensions/session-handoff/bootstrap.ts +14 -16
- package/extensions/session-handoff/extract.ts +34 -16
- package/extensions/session-handoff/install.ts +1 -1
- package/extensions/session-handoff/kickoff.ts +4 -3
- package/extensions/session-handoff/launch-target.ts +1 -1
- package/extensions/session-handoff/metadata.ts +72 -23
- package/extensions/session-messaging/broker/client-frame.ts +113 -0
- package/extensions/session-messaging/broker/process.ts +12 -21
- package/extensions/session-messaging/broker/spawn.ts +12 -7
- package/extensions/session-messaging/pi/reachable-contract.ts +59 -0
- package/extensions/session-messaging/pi/reachable-presenter.ts +49 -0
- package/extensions/session-messaging/pi/reachable-tool.ts +190 -0
- package/extensions/session-messaging/pi/reachable-view-model.ts +59 -0
- package/extensions/session-messaging/pi/renderer.ts +1 -1
- package/extensions/session-messaging/pi/tools.ts +8 -8
- package/extensions/session-search/hooks.ts +45 -52
- package/extensions/session-search/install.ts +3 -158
- package/extensions/session-search/reindex.ts +3 -3
- package/extensions/session-search/renderer.ts +1 -32
- package/extensions/session-search/tool-contract.ts +1 -21
- package/extensions/shared/model-runtime.ts +8 -1
- package/extensions/shared/session-broker/client.ts +3 -4
- package/extensions/shared/session-broker/framing.ts +6 -7
- package/extensions/shared/session-index/lineage.ts +6 -2
- package/extensions/shared/session-index/sqlite.ts +44 -17
- package/extensions/shared/settings.ts +25 -9
- package/extensions/shared/tmux.ts +22 -0
- package/extensions/subagents/classify.ts +6 -0
- package/extensions/subagents/install.ts +22 -137
- package/extensions/subagents/report-message-renderer.ts +2 -2
- package/extensions/subagents/settle.ts +182 -0
- package/package.json +18 -26
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
2
|
import type { Api, Model, TextContent, UserMessage } from "@earendil-works/pi-ai";
|
|
6
3
|
import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
7
4
|
import type { AutoTitleContext } from "./context.ts";
|
|
5
|
+
import { type AutoTitleRun, startAutoTitleRun } from "./runs.ts";
|
|
8
6
|
import type { AutoTitleTrigger } from "./state.ts";
|
|
9
7
|
|
|
10
|
-
const AUTO_TITLE_MAX_TOKENS = 64;
|
|
11
8
|
const AUTO_TITLE_CHAR_MAX = 80;
|
|
12
9
|
|
|
13
10
|
export interface AutoTitleFailure {
|
|
@@ -20,7 +17,9 @@ export interface AutoTitleFailure {
|
|
|
20
17
|
export interface AutoTitleGeneration {
|
|
21
18
|
systemPrompt: string;
|
|
22
19
|
timeoutMs: number;
|
|
20
|
+
tokenBudget: number;
|
|
23
21
|
thinkingLevel: ThinkingLevel | undefined;
|
|
22
|
+
persistRuns: boolean;
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export type AutoTitleGenerationResult =
|
|
@@ -66,35 +65,47 @@ export async function generateAutoTitle(
|
|
|
66
65
|
const abortController = new AbortController();
|
|
67
66
|
const timeoutId = setTimeout(() => abortController.abort(), generation.timeoutMs);
|
|
68
67
|
const thinkingLevel = generation.thinkingLevel;
|
|
68
|
+
const run = generation.persistRuns
|
|
69
|
+
? startAutoTitleRun({
|
|
70
|
+
cwd: context.cwd ?? process.cwd(),
|
|
71
|
+
model,
|
|
72
|
+
trigger,
|
|
73
|
+
systemPrompt: resolvedSystemPrompt,
|
|
74
|
+
tokenBudget: generation.tokenBudget,
|
|
75
|
+
thinkingLevel,
|
|
76
|
+
message,
|
|
77
|
+
})
|
|
78
|
+
: undefined;
|
|
69
79
|
|
|
70
80
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
const response = await modelRuntime.completeSimple(
|
|
82
|
+
model,
|
|
83
|
+
{
|
|
84
|
+
systemPrompt: resolvedSystemPrompt,
|
|
85
|
+
messages: [message],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
maxTokens: generation.tokenBudget,
|
|
89
|
+
...(thinkingLevel && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
|
|
90
|
+
signal: abortController.signal,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
run?.recordResponse(response);
|
|
82
94
|
|
|
83
95
|
if (response.stopReason === "error" || response.stopReason === "aborted") {
|
|
84
96
|
const fallbackMessage =
|
|
85
97
|
response.stopReason === "aborted" ? "Request was aborted." : "Provider returned an error.";
|
|
86
|
-
return
|
|
87
|
-
ok: false,
|
|
88
|
-
failure: createAutoTitleFailure(trigger, model, response.errorMessage || fallbackMessage),
|
|
89
|
-
};
|
|
98
|
+
return failGeneration(run, trigger, model, response.errorMessage || fallbackMessage);
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
const normalizedTitle = normalizeGeneratedAutoTitle(extractResponseText(response.content));
|
|
93
102
|
if (!normalizedTitle) {
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
return failGeneration(
|
|
104
|
+
run,
|
|
105
|
+
trigger,
|
|
106
|
+
model,
|
|
107
|
+
describeEmptyTitle(response.stopReason === "length", generation.tokenBudget),
|
|
108
|
+
);
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
return {
|
|
@@ -102,10 +113,7 @@ export async function generateAutoTitle(
|
|
|
102
113
|
title: normalizedTitle,
|
|
103
114
|
};
|
|
104
115
|
} catch (error) {
|
|
105
|
-
return
|
|
106
|
-
ok: false,
|
|
107
|
-
failure: createAutoTitleFailure(trigger, model, extractErrorMessage(error)),
|
|
108
|
-
};
|
|
116
|
+
return failGeneration(run, trigger, model, extractErrorMessage(error));
|
|
109
117
|
} finally {
|
|
110
118
|
clearTimeout(timeoutId);
|
|
111
119
|
}
|
|
@@ -124,6 +132,27 @@ export function createAutoTitleFailure(
|
|
|
124
132
|
};
|
|
125
133
|
}
|
|
126
134
|
|
|
135
|
+
function failGeneration(
|
|
136
|
+
run: AutoTitleRun | undefined,
|
|
137
|
+
trigger: AutoTitleTrigger,
|
|
138
|
+
model: Model<Api>,
|
|
139
|
+
message: string,
|
|
140
|
+
): AutoTitleGenerationResult {
|
|
141
|
+
run?.recordFailure(message);
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
failure: createAutoTitleFailure(trigger, model, message),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function describeEmptyTitle(hitTokenBudget: boolean, tokenBudget: number): string {
|
|
149
|
+
if (!hitTokenBudget) {
|
|
150
|
+
return "Model returned an empty title.";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `Model spent its ${tokenBudget}-token budget without producing a title. Raise sessions.autoTitle.tokenBudget if the model reasons before answering.`;
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
function buildAutoTitleSystemPrompt(systemPrompt: string, shouldPreserveTitle: boolean): string {
|
|
128
157
|
if (!shouldPreserveTitle) {
|
|
129
158
|
return systemPrompt;
|
|
@@ -170,45 +199,6 @@ function normalizeGeneratedAutoTitle(value: string): string | undefined {
|
|
|
170
199
|
return truncated || undefined;
|
|
171
200
|
}
|
|
172
201
|
|
|
173
|
-
function writeAutoTitleDebugRequest(
|
|
174
|
-
model: Model<Api>,
|
|
175
|
-
trigger: AutoTitleTrigger,
|
|
176
|
-
request: unknown,
|
|
177
|
-
): void {
|
|
178
|
-
writeAutoTitleDebugEntry({
|
|
179
|
-
phase: "request",
|
|
180
|
-
trigger,
|
|
181
|
-
model: formatModelLabel(model),
|
|
182
|
-
request,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function writeAutoTitleDebugResponse(
|
|
187
|
-
model: Model<Api>,
|
|
188
|
-
trigger: AutoTitleTrigger,
|
|
189
|
-
response: unknown,
|
|
190
|
-
): void {
|
|
191
|
-
writeAutoTitleDebugEntry({
|
|
192
|
-
phase: "response",
|
|
193
|
-
trigger,
|
|
194
|
-
model: formatModelLabel(model),
|
|
195
|
-
response,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function writeAutoTitleDebugEntry(entry: Record<string, unknown>): void {
|
|
200
|
-
try {
|
|
201
|
-
const dir = join(homedir(), ".pi", "agent", "pi-sessions");
|
|
202
|
-
mkdirSync(dir, { recursive: true });
|
|
203
|
-
appendFileSync(
|
|
204
|
-
join(dir, "auto-title-debug.jsonl"),
|
|
205
|
-
`${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`,
|
|
206
|
-
);
|
|
207
|
-
} catch {
|
|
208
|
-
// Debug logging must not affect title generation.
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
202
|
function extractResponseText(content: unknown[]): string {
|
|
213
203
|
return content
|
|
214
204
|
.filter(
|
|
@@ -53,7 +53,9 @@ export function installAutoTitle(
|
|
|
53
53
|
): AutoTitleGeneration => ({
|
|
54
54
|
systemPrompt: settings.autoTitle.prompt,
|
|
55
55
|
timeoutMs: settings.autoTitle.timeoutMs,
|
|
56
|
+
tokenBudget: settings.autoTitle.tokenBudget,
|
|
56
57
|
thinkingLevel: settings.autoTitle.thinkingLevel ?? resolution?.thinkingLevel,
|
|
58
|
+
persistRuns: settings.autoTitle.persistRuns,
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
pi.registerCommand("title", {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Api, AssistantMessage, Model, UserMessage } from "@earendil-works/pi-ai";
|
|
3
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { getDefaultAutoTitleRunsDir } from "../shared/settings.ts";
|
|
5
|
+
import type { AutoTitleTrigger } from "./state.ts";
|
|
6
|
+
|
|
7
|
+
export const AUTO_TITLE_RUN_REQUEST_CUSTOM_TYPE = "pi-sessions.auto-title-request";
|
|
8
|
+
export const AUTO_TITLE_RUN_FAILURE_CUSTOM_TYPE = "pi-sessions.auto-title-failure";
|
|
9
|
+
|
|
10
|
+
export interface AutoTitleRunRequest {
|
|
11
|
+
cwd: string;
|
|
12
|
+
model: Model<Api>;
|
|
13
|
+
trigger: AutoTitleTrigger;
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
tokenBudget: number;
|
|
16
|
+
thinkingLevel: ThinkingLevel | undefined;
|
|
17
|
+
message: UserMessage;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AutoTitleRun {
|
|
21
|
+
sessionPath: string | undefined;
|
|
22
|
+
recordResponse(response: AssistantMessage): void;
|
|
23
|
+
recordFailure(message: string): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Records one auto-title request as a standalone Pi session so the exact prompt can be replayed. */
|
|
27
|
+
export function startAutoTitleRun(request: AutoTitleRunRequest): AutoTitleRun {
|
|
28
|
+
const sessionManager = SessionManager.create(request.cwd, getDefaultAutoTitleRunsDir());
|
|
29
|
+
sessionManager.appendSessionInfo(`auto-title (${request.trigger})`);
|
|
30
|
+
sessionManager.appendModelChange(request.model.provider, request.model.id);
|
|
31
|
+
if (request.thinkingLevel) {
|
|
32
|
+
sessionManager.appendThinkingLevelChange(request.thinkingLevel);
|
|
33
|
+
}
|
|
34
|
+
sessionManager.appendCustomEntry(AUTO_TITLE_RUN_REQUEST_CUSTOM_TYPE, {
|
|
35
|
+
trigger: request.trigger,
|
|
36
|
+
systemPrompt: request.systemPrompt,
|
|
37
|
+
tokenBudget: request.tokenBudget,
|
|
38
|
+
});
|
|
39
|
+
sessionManager.appendMessage(request.message);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
sessionPath: sessionManager.getSessionFile(),
|
|
43
|
+
recordResponse(response) {
|
|
44
|
+
sessionManager.appendMessage(response);
|
|
45
|
+
},
|
|
46
|
+
recordFailure(message) {
|
|
47
|
+
sessionManager.appendCustomEntry(AUTO_TITLE_RUN_FAILURE_CUSTOM_TYPE, { message });
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { tmuxSessionName } from "../shared/tmux.ts";
|
|
2
|
-
import type
|
|
2
|
+
import { isRunningSubagentState, type SubagentState } from "../subagents/classify.ts";
|
|
3
3
|
import type { SubagentRosterEntry } from "../subagents/roster.ts";
|
|
4
4
|
import { shellQuote } from "./launch/shell.ts";
|
|
5
5
|
import type { HandoffLaunchReceipt } from "./receipt.ts";
|
|
6
6
|
|
|
7
|
-
const RUNNING_SUBAGENT_STATES: ReadonlySet<SubagentState> = new Set(["starting", "busy", "active"]);
|
|
8
|
-
|
|
9
7
|
export type HandoffBoardTab = "subagents" | "user-sessions";
|
|
10
8
|
export type UserSessionStatus = "live" | "ready" | "starting" | "closed" | "unknown";
|
|
11
9
|
|
|
@@ -169,7 +167,7 @@ function buildUserSessionAction(
|
|
|
169
167
|
function buildSubagentAction(entry: SubagentRosterEntry, insideTmux: boolean): HandoffBoardAction {
|
|
170
168
|
return {
|
|
171
169
|
subagent: entry,
|
|
172
|
-
canStop:
|
|
170
|
+
canStop: isRunningSubagentState(entry.state),
|
|
173
171
|
...(entry.tmuxWindowId ? { observeCommand: buildObserveCommand(entry, insideTmux) } : {}),
|
|
174
172
|
...(!entry.managedLive ? { resumeCommand: entry.resumeCommand } : {}),
|
|
175
173
|
};
|
|
@@ -2,9 +2,9 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { ModelRuntimeProvider } from "../shared/model-runtime.ts";
|
|
5
|
+
import type { HandoffSettings } from "../shared/settings.ts";
|
|
5
6
|
import { generateHandoffDraftFromSessionManager } from "./extract.ts";
|
|
6
7
|
import { buildHandoffKickoffMessage, buildHandoffKickoffSource } from "./kickoff.ts";
|
|
7
|
-
import { SUBAGENT_LAUNCH } from "./launch-target.ts";
|
|
8
8
|
import {
|
|
9
9
|
type ChildGeneratedHandoffBootstrap,
|
|
10
10
|
createHandoffSessionMetadata,
|
|
@@ -23,8 +23,8 @@ export async function consumePendingHandoffBootstrap(
|
|
|
23
23
|
pi: ExtensionAPI,
|
|
24
24
|
ctx: ExtensionContext,
|
|
25
25
|
getModelRuntime: ModelRuntimeProvider,
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
destinationThinkingLevel: ThinkingLevel | undefined,
|
|
27
|
+
handoffSettings: HandoffSettings,
|
|
28
28
|
): Promise<void> {
|
|
29
29
|
const scan = findPendingHandoffBootstrap(ctx.sessionManager.getBranch());
|
|
30
30
|
if (!scan) {
|
|
@@ -56,8 +56,8 @@ export async function consumePendingHandoffBootstrap(
|
|
|
56
56
|
scan.entryId,
|
|
57
57
|
consumeBootstrap,
|
|
58
58
|
getModelRuntime,
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
destinationThinkingLevel,
|
|
60
|
+
handoffSettings,
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -68,8 +68,8 @@ async function startChildGeneratedHandoff(
|
|
|
68
68
|
bootstrapEntryId: string,
|
|
69
69
|
consumeBootstrap: (reason: HandoffBootstrapConsumedReason) => void,
|
|
70
70
|
getModelRuntime: ModelRuntimeProvider,
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
destinationThinkingLevel: ThinkingLevel | undefined,
|
|
72
|
+
handoffSettings: HandoffSettings,
|
|
73
73
|
): Promise<void> {
|
|
74
74
|
const entries = ctx.sessionManager.getEntries();
|
|
75
75
|
if (hasStartedConversation(entries)) {
|
|
@@ -96,17 +96,17 @@ async function startChildGeneratedHandoff(
|
|
|
96
96
|
ctx,
|
|
97
97
|
"Generating handoff draft...",
|
|
98
98
|
async (signal: AbortSignal) =>
|
|
99
|
-
generateHandoffDraftFromSessionManager(
|
|
99
|
+
generateHandoffDraftFromSessionManager({
|
|
100
100
|
ctx,
|
|
101
101
|
modelRuntime,
|
|
102
102
|
sourceSessionManager,
|
|
103
|
-
bootstrap.sourceLeafId,
|
|
104
|
-
bootstrap.goal,
|
|
105
|
-
|
|
106
|
-
|
|
103
|
+
sourceLeafId: bootstrap.sourceLeafId,
|
|
104
|
+
goal: bootstrap.goal,
|
|
105
|
+
settings: handoffSettings,
|
|
106
|
+
destinationThinkingLevel,
|
|
107
107
|
signal,
|
|
108
|
-
bootstrap.requestResponse,
|
|
109
|
-
),
|
|
108
|
+
requestResponse: bootstrap.requestResponse,
|
|
109
|
+
}),
|
|
110
110
|
);
|
|
111
111
|
if (!generatedDraft) {
|
|
112
112
|
consumeBootstrap("cancelled");
|
|
@@ -140,13 +140,11 @@ async function startChildGeneratedHandoff(
|
|
|
140
140
|
|
|
141
141
|
// The tool-provided bootstrap title is authoritative; extraction does not
|
|
142
142
|
// replace it with a second generated title.
|
|
143
|
-
const subagent = bootstrap.launch === SUBAGENT_LAUNCH ? bootstrap.subagent : undefined;
|
|
144
143
|
const metadata = createHandoffSessionMetadata(
|
|
145
144
|
bootstrap.goal,
|
|
146
145
|
prompt,
|
|
147
146
|
bootstrap.title,
|
|
148
147
|
bootstrap.launch,
|
|
149
|
-
subagent,
|
|
150
148
|
);
|
|
151
149
|
if (!getHandoffMetadataFromEntries(ctx.sessionManager.getEntries())) {
|
|
152
150
|
pi.appendEntry(HANDOFF_METADATA_CUSTOM_TYPE, metadata);
|
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
} from "@earendil-works/pi-coding-agent";
|
|
25
25
|
import { type Static, Type } from "typebox";
|
|
26
26
|
import { freshenModel } from "../shared/model-runtime.ts";
|
|
27
|
-
import { getDefaultHandoffRunsDir } from "../shared/settings.ts";
|
|
27
|
+
import { getDefaultHandoffRunsDir, type HandoffSettings } from "../shared/settings.ts";
|
|
28
28
|
import { parseTypeBoxValue } from "../shared/typebox.ts";
|
|
29
|
+
import { resolveModelOverride } from "./model.ts";
|
|
29
30
|
|
|
30
31
|
const MAX_HANDOFF_EXTRACTION_ATTEMPTS = 3;
|
|
31
32
|
const HANDOFF_EXTRACTION_RETRY_PROMPT =
|
|
@@ -88,22 +89,39 @@ export function resolveHandoffSource(
|
|
|
88
89
|
return sessionContext;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
export async function generateHandoffDraftFromSessionManager(
|
|
92
|
-
ctx
|
|
93
|
-
modelRuntime
|
|
94
|
-
sourceSessionManager
|
|
95
|
-
sourceLeafId
|
|
96
|
-
goal
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
signal
|
|
92
|
+
export async function generateHandoffDraftFromSessionManager({
|
|
93
|
+
ctx,
|
|
94
|
+
modelRuntime,
|
|
95
|
+
sourceSessionManager,
|
|
96
|
+
sourceLeafId,
|
|
97
|
+
goal,
|
|
98
|
+
settings,
|
|
99
|
+
destinationThinkingLevel,
|
|
100
|
+
signal,
|
|
100
101
|
requestResponse = false,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
|
|
123
|
+
const thinkingLevel =
|
|
124
|
+
modelOverride?.thinkingLevel ?? settings.thinkingLevel ?? destinationThinkingLevel;
|
|
107
125
|
const sessionContext = resolveHandoffSource(sourceSessionManager, sourceLeafId);
|
|
108
126
|
|
|
109
127
|
const conversationText = serializeConversation(convertToLlm(sessionContext.messages));
|
|
@@ -114,7 +132,7 @@ export async function generateHandoffDraftFromSessionManager(
|
|
|
114
132
|
conversationText,
|
|
115
133
|
goal,
|
|
116
134
|
thinkingLevel,
|
|
117
|
-
persistRuns,
|
|
135
|
+
settings.persistRuns,
|
|
118
136
|
signal,
|
|
119
137
|
);
|
|
120
138
|
if (!handoffContext) {
|
|
@@ -55,22 +55,23 @@ export function buildHandoffKickoffSource(source: HandoffKickoffSource): Handoff
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export const renderHandoffKickoffMessage: MessageRenderer = (message,
|
|
58
|
+
export const renderHandoffKickoffMessage: MessageRenderer = (message, options, theme) => {
|
|
59
59
|
const details = safeParseTypeBoxValue(HANDOFF_KICKOFF_DETAILS_SCHEMA, message.details);
|
|
60
60
|
if (!details) {
|
|
61
61
|
return undefined;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const prompt = typeof message.content === "string" ? message.content : "";
|
|
65
|
-
return createHandoffKickoffComponent(details, prompt, theme);
|
|
65
|
+
return createHandoffKickoffComponent(details, prompt, options.outputPad, theme);
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
export function createHandoffKickoffComponent(
|
|
69
69
|
details: HandoffKickoffDetails,
|
|
70
70
|
prompt: string,
|
|
71
|
+
outputPad: number,
|
|
71
72
|
theme: RenderTheme,
|
|
72
73
|
): Component {
|
|
73
|
-
const box = new Box(
|
|
74
|
+
const box = new Box(outputPad, 1, (text: string) => theme.bg("customMessageBg", text));
|
|
74
75
|
box.addChild(new Text(renderHandoffKickoffView(details, prompt, theme), 0, 0));
|
|
75
76
|
return box;
|
|
76
77
|
}
|
|
@@ -59,7 +59,7 @@ export interface HandoffLaunchTarget {
|
|
|
59
59
|
description?: string | undefined;
|
|
60
60
|
requestResponseDefault: boolean;
|
|
61
61
|
bootstrapMode: "review" | "automatic";
|
|
62
|
-
/** Subagent launches stamp their child's identity into the
|
|
62
|
+
/** Subagent launches stamp their child's identity into the child bootstrap. */
|
|
63
63
|
describeSubagentChild?(input: {
|
|
64
64
|
childSessionId: string;
|
|
65
65
|
ownerSessionId: string;
|
|
@@ -3,6 +3,7 @@ import { type Static, Type } from "typebox";
|
|
|
3
3
|
import { safeParseTypeBoxValue } from "../shared/typebox.ts";
|
|
4
4
|
import { HANDOFF_KICKOFF_CUSTOM_TYPE, HANDOFF_KICKOFF_DETAILS_SCHEMA } from "./kickoff.ts";
|
|
5
5
|
import {
|
|
6
|
+
HANDOFF_LAUNCH_VALUE_SCHEMA,
|
|
6
7
|
HANDOFF_NON_SUBAGENT_LAUNCH_SCHEMA,
|
|
7
8
|
type HandoffLaunchValue,
|
|
8
9
|
SUBAGENT_LAUNCH,
|
|
@@ -16,7 +17,7 @@ export const HANDOFF_STALE_SESSION_MESSAGE =
|
|
|
16
17
|
export const SESSION_STARTING_MESSAGE =
|
|
17
18
|
"Target session is still starting. wait for it to show up in session_search, then resend.";
|
|
18
19
|
|
|
19
|
-
// A subagent's self-knowledge, stamped into
|
|
20
|
+
// A subagent's self-knowledge, stamped into its child-local bootstrap so the
|
|
20
21
|
// child never opens the parent transcript to learn who it is.
|
|
21
22
|
export const HANDOFF_SUBAGENT_SCHEMA = Type.Object({
|
|
22
23
|
childSessionId: Type.String(),
|
|
@@ -32,18 +33,10 @@ const HANDOFF_METADATA_BASE = {
|
|
|
32
33
|
initial_prompt: Type.String(),
|
|
33
34
|
};
|
|
34
35
|
|
|
35
|
-
export const HANDOFF_SESSION_METADATA_SCHEMA = Type.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
subagent: HANDOFF_SUBAGENT_SCHEMA,
|
|
40
|
-
}),
|
|
41
|
-
Type.Object({
|
|
42
|
-
...HANDOFF_METADATA_BASE,
|
|
43
|
-
launch: HANDOFF_NON_SUBAGENT_LAUNCH_SCHEMA,
|
|
44
|
-
subagent: Type.Optional(Type.Never()),
|
|
45
|
-
}),
|
|
46
|
-
]);
|
|
36
|
+
export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Object({
|
|
37
|
+
...HANDOFF_METADATA_BASE,
|
|
38
|
+
launch: HANDOFF_LAUNCH_VALUE_SCHEMA,
|
|
39
|
+
});
|
|
47
40
|
|
|
48
41
|
const HANDOFF_BOOTSTRAP_BASE = {
|
|
49
42
|
mode: Type.Literal("generate"),
|
|
@@ -91,21 +84,14 @@ export function createHandoffSessionMetadata(
|
|
|
91
84
|
initialPrompt: string,
|
|
92
85
|
title: string,
|
|
93
86
|
launch: HandoffLaunchValue,
|
|
94
|
-
subagent: HandoffSubagent | undefined,
|
|
95
87
|
): HandoffSessionMetadata {
|
|
96
|
-
|
|
97
|
-
origin: "handoff"
|
|
88
|
+
return {
|
|
89
|
+
origin: "handoff",
|
|
98
90
|
goal: goal.trim(),
|
|
99
91
|
title,
|
|
100
92
|
initial_prompt: initialPrompt.trim(),
|
|
93
|
+
launch,
|
|
101
94
|
};
|
|
102
|
-
if (launch === SUBAGENT_LAUNCH) {
|
|
103
|
-
if (!subagent) {
|
|
104
|
-
throw new Error("A subagent handoff requires a subagent identity block.");
|
|
105
|
-
}
|
|
106
|
-
return { ...base, launch, subagent };
|
|
107
|
-
}
|
|
108
|
-
return { ...base, launch };
|
|
109
95
|
}
|
|
110
96
|
|
|
111
97
|
export function createChildGeneratedHandoffBootstrap(options: {
|
|
@@ -195,6 +181,69 @@ export function isSessionStarting(branch: readonly SessionEntry[]): boolean {
|
|
|
195
181
|
return findPendingHandoffBootstrap(branch)?.kind === "pending";
|
|
196
182
|
}
|
|
197
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Folds the bootstrap lifecycle in one transcript pass. The bootstrap owns its
|
|
186
|
+
* data; kickoff and consumed entries only establish whether it started or ended.
|
|
187
|
+
*/
|
|
188
|
+
export function findCurrentHandoffBootstrap(
|
|
189
|
+
branch: readonly SessionEntry[],
|
|
190
|
+
): HandoffBootstrap | undefined {
|
|
191
|
+
const bootstrapsById = new Map<string, HandoffBootstrap>();
|
|
192
|
+
const bootstrapIds: string[] = [];
|
|
193
|
+
const startedBootstrapIds: string[] = [];
|
|
194
|
+
const endedBootstrapIds = new Set<string>();
|
|
195
|
+
let conversationStarted = false;
|
|
196
|
+
|
|
197
|
+
for (const entry of branch) {
|
|
198
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
199
|
+
conversationStarted = true;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (entry.type === "custom_message" && entry.customType === HANDOFF_KICKOFF_CUSTOM_TYPE) {
|
|
203
|
+
conversationStarted = true;
|
|
204
|
+
const details = safeParseTypeBoxValue(HANDOFF_KICKOFF_DETAILS_SCHEMA, entry.details);
|
|
205
|
+
if (details?.bootstrapEntryId) {
|
|
206
|
+
startedBootstrapIds.push(details.bootstrapEntryId);
|
|
207
|
+
}
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (entry.type !== "custom") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (entry.customType === HANDOFF_BOOTSTRAP_PENDING_CUSTOM_TYPE) {
|
|
214
|
+
const bootstrap = safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_SCHEMA, entry.data);
|
|
215
|
+
if (bootstrap) {
|
|
216
|
+
bootstrapsById.set(entry.id, bootstrap);
|
|
217
|
+
bootstrapIds.push(entry.id);
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (entry.customType === HANDOFF_BOOTSTRAP_CONSUMED_CUSTOM_TYPE) {
|
|
222
|
+
const consumed = safeParseTypeBoxValue(HANDOFF_BOOTSTRAP_CONSUMED_SCHEMA, entry.data);
|
|
223
|
+
if (consumed) {
|
|
224
|
+
endedBootstrapIds.add(consumed.bootstrapEntryId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
for (let index = startedBootstrapIds.length - 1; index >= 0; index -= 1) {
|
|
230
|
+
const bootstrap = bootstrapsById.get(startedBootstrapIds[index] ?? "");
|
|
231
|
+
if (bootstrap) {
|
|
232
|
+
return bootstrap;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (conversationStarted) {
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
for (let index = bootstrapIds.length - 1; index >= 0; index -= 1) {
|
|
239
|
+
const bootstrapId = bootstrapIds[index];
|
|
240
|
+
if (bootstrapId && !endedBootstrapIds.has(bootstrapId)) {
|
|
241
|
+
return bootstrapsById.get(bootstrapId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
198
247
|
export function parseHandoffSessionMetadata(value: unknown): HandoffSessionMetadata | undefined {
|
|
199
248
|
return safeParseTypeBoxValue(HANDOFF_SESSION_METADATA_SCHEMA, value);
|
|
200
249
|
}
|