pi-sessions 0.7.2 → 0.9.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 +71 -37
- 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} +67 -46
- package/extensions/session-auto-title/model.ts +25 -17
- package/extensions/session-auto-title/retitle.ts +26 -12
- package/extensions/session-auto-title/wizard.ts +12 -6
- package/extensions/session-handoff/board-view-model.ts +181 -0
- package/extensions/session-handoff/board.ts +663 -0
- package/extensions/session-handoff/bootstrap.ts +174 -0
- package/extensions/session-handoff/extract.ts +101 -122
- 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 +152 -63
- 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/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 +80 -38
- 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 +52 -0
- package/extensions/subagents/install.ts +344 -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/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 +10 -16
- 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
package/README.md
CHANGED
|
@@ -8,7 +8,21 @@
|
|
|
8
8
|
|
|
9
9
|

|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Handoff board
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### Subagent report
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
### Session handoff
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
### Handoff prompt review
|
|
12
26
|
|
|
13
27
|

|
|
14
28
|
|
|
@@ -20,7 +34,7 @@
|
|
|
20
34
|
|
|
21
35
|
## Install
|
|
22
36
|
|
|
23
|
-
Requires Pi `0.80.
|
|
37
|
+
Requires Pi `0.80.10` or newer and Node `>=24 <26`.
|
|
24
38
|
|
|
25
39
|
**From npm** (recommended):
|
|
26
40
|
|
|
@@ -46,20 +60,36 @@ What session did I implement the db layer?
|
|
|
46
60
|
```
|
|
47
61
|
|
|
48
62
|
```text
|
|
49
|
-
|
|
63
|
+
Open the frontend implementation task in a session to the right.
|
|
50
64
|
```
|
|
51
65
|
|
|
52
66
|
## Features
|
|
53
67
|
|
|
54
|
-
| Extension | Surface
|
|
55
|
-
| ------------------ |
|
|
56
|
-
| Session Search | `session_search` pi tool
|
|
57
|
-
| Session Ask | `session_ask` pi tool
|
|
58
|
-
| Session Handoff |
|
|
59
|
-
| Session Messaging | `session_send_message` pi
|
|
60
|
-
| Session Picker | `Alt+O`
|
|
61
|
-
| Session Index | `/session-index` slash command
|
|
62
|
-
| Session Auto Title | in background, `/title` slash command
|
|
68
|
+
| Extension | Surface | What it does |
|
|
69
|
+
| ------------------ | ------------------------------------------------- | ------------------------------------------------------- |
|
|
70
|
+
| Session Search | `session_search` pi tool | Search through old sessions |
|
|
71
|
+
| Session Ask | `session_ask` pi tool | Ask questions about old sessions |
|
|
72
|
+
| Session Handoff | `session_handoff` pi tool, `/handoff` board | Start and manage focused child sessions |
|
|
73
|
+
| Session Messaging | `session_send_message`, `session_cancel` pi tools | Coordinate between live Pi sessions and own subagents |
|
|
74
|
+
| Session Picker | `Alt+O` | Reference old sessions in your prompt |
|
|
75
|
+
| Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
|
|
76
|
+
| Session Auto Title | in background, `/title` slash command | Give sessions titles |
|
|
77
|
+
|
|
78
|
+
Every feature is on by default. Turn one off with `enable: false` under its own settings namespace, which unregisters its tools and hooks.
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"sessions": {
|
|
83
|
+
"messaging": { "enable": true },
|
|
84
|
+
"subagents": { "enable": true },
|
|
85
|
+
"handoff": { "enable": true },
|
|
86
|
+
"search": { "enable": true },
|
|
87
|
+
"ask": { "enable": true },
|
|
88
|
+
"autoTitle": { "enable": true },
|
|
89
|
+
"hooks": { "enable": true }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
63
93
|
|
|
64
94
|
## Session Search
|
|
65
95
|
|
|
@@ -72,40 +102,25 @@ File filters distinguish read-or-write evidence from write-only evidence:
|
|
|
72
102
|
- `files.touched`: sessions that read or changed a path
|
|
73
103
|
- `files.changed`: sessions that changed a path
|
|
74
104
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`/handoff <goal>` starts a focused new session. Give pi a goal, and it will generate a prompt for you to review before kicking it off.
|
|
78
|
-
|
|
79
|
-
You can either start a new session directly in your current one, or if you have Ghostty on macOS, you can spawn a new one in a split-pane and continue in your current:
|
|
80
|
-
|
|
81
|
-
- `/handoff --left <goal>`
|
|
82
|
-
- `/handoff --right <goal>`
|
|
83
|
-
- `/handoff --up <goal>`
|
|
84
|
-
- `/handoff --down <goal>`
|
|
85
|
-
|
|
86
|
-
The flag indicates the Ghostty split direction.
|
|
105
|
+
Use `kind: "user"` or `kind: "subagent"` to filter by session type across the whole index. Use `relationScope: "branch"` to search subagents launched from the current conversation branch, or `relationScope: "tree"` to include subagents launched from abandoned branches as well.
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- run `/handoff [--<direction>] <goal>`
|
|
91
|
-
- review the generated prompt preview
|
|
92
|
-
- optionally edit the prompt
|
|
93
|
-
- start the new session
|
|
107
|
+
## Session Handoff
|
|
94
108
|
|
|
95
|
-
|
|
109
|
+
The `session_handoff` tool lets the agent create a child session with a self-contained task. Ask for a direction when you want a visible split, ask for a deferred handoff when you only want the prepared session, or let the agent delegate suitable independent work to a background subagent.
|
|
96
110
|
|
|
97
|
-
|
|
111
|
+
Directional launches use tmux when the current terminal is inside tmux, or Ghostty on macOS. Deferred launches create the child without starting it and copy its resume command to the clipboard. A background subagent runs in a detached tmux window and reports back when finished.
|
|
98
112
|
|
|
99
|
-
|
|
113
|
+
Run `/handoff` to open the **Handoffs** board. The Subagents and User sessions tabs show status, age, launch details, and the actions currently available: stop, copy an observation command, or copy a resume command.
|
|
100
114
|
|
|
101
115
|
## Session Messaging
|
|
102
116
|
|
|
103
|
-
Agents can coordinate with
|
|
117
|
+
Agents can coordinate with live Pi sessions and their own subagents:
|
|
104
118
|
|
|
105
119
|
- `session_search` with `live: true` lists live sessions
|
|
106
|
-
- `session_send_message` sends a message to
|
|
120
|
+
- `session_send_message` sends a message to a live session or own subagent
|
|
121
|
+
- `session_cancel` aborts another live session's current turn
|
|
107
122
|
|
|
108
|
-
Incoming messages start the recipient agent when idle and steer it when already running.
|
|
123
|
+
Incoming messages start the recipient agent when idle and steer it when already running. Messaging a dormant owned subagent resumes it automatically; other inactive sessions cannot receive messages, but you can still use `session_search` and `session_ask` with them.
|
|
109
124
|
|
|
110
125
|
## Session picker
|
|
111
126
|
|
|
@@ -124,7 +139,24 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
|
|
|
124
139
|
{
|
|
125
140
|
"sessions": {
|
|
126
141
|
"handoff": {
|
|
127
|
-
"pickerShortcut": "alt+p"
|
|
142
|
+
"pickerShortcut": "alt+p",
|
|
143
|
+
"deferred": {
|
|
144
|
+
"copyToClipboard": true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`deferred.copyToClipboard` (default `true`) controls whether deferred handoffs copy the resume command to the clipboard. When off, the resume command is only shown in the tool call.
|
|
152
|
+
|
|
153
|
+
Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`):
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"sessions": {
|
|
158
|
+
"subagents": {
|
|
159
|
+
"maxDepth": 2
|
|
128
160
|
}
|
|
129
161
|
}
|
|
130
162
|
}
|
|
@@ -190,7 +222,9 @@ To change auto-titling settings, edit `~/.pi/agent/settings.json`:
|
|
|
190
222
|
"sessions": {
|
|
191
223
|
"autoTitle": {
|
|
192
224
|
"refreshTurns": 4,
|
|
225
|
+
"timeoutSecs": 15,
|
|
193
226
|
"model": "anthropic/claude-haiku-4-5",
|
|
227
|
+
"thinkingLevel": "off",
|
|
194
228
|
"prompt": "Custom prompt that overrides the default."
|
|
195
229
|
}
|
|
196
230
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ModelRuntime,
|
|
4
|
+
SessionManager,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { installAsk } from "./session-ask/install.ts";
|
|
7
|
+
import { installAutoTitle } from "./session-auto-title/install.ts";
|
|
8
|
+
import { installHandoff } from "./session-handoff/install.ts";
|
|
9
|
+
import { installHooks } from "./session-hooks/install.ts";
|
|
10
|
+
import { installIndex } from "./session-index/install.ts";
|
|
11
|
+
import { installMessaging } from "./session-messaging/install.ts";
|
|
12
|
+
import {
|
|
13
|
+
createSessionCancelTool,
|
|
14
|
+
createSessionSendMessageTool,
|
|
15
|
+
} from "./session-messaging/pi/tools.ts";
|
|
16
|
+
import { installSearch } from "./session-search/install.ts";
|
|
17
|
+
import type { SessionLifecycle } from "./shared/composition.ts";
|
|
18
|
+
import { createSessionModelRuntime, type ModelRuntimeProvider } from "./shared/model-runtime.ts";
|
|
19
|
+
import { loadSettings } from "./shared/settings.ts";
|
|
20
|
+
import { installSubagents } from "./subagents/install.ts";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The single advertised entrypoint. It loads settings once, constructs each feature in
|
|
24
|
+
* dependency order, and wires them by constructor parameters.
|
|
25
|
+
* It also owns the single `session_start`/`session_shutdown` subscription so lifecycle order
|
|
26
|
+
* is deterministic: broker registration (messaging) resolves before any other feature hook.
|
|
27
|
+
*/
|
|
28
|
+
export default function piSessions(pi: ExtensionAPI): void {
|
|
29
|
+
const settings = loadSettings();
|
|
30
|
+
let sessionEpoch = 0;
|
|
31
|
+
|
|
32
|
+
// The mirrored ModelRuntime is expensive to build and only changes across session
|
|
33
|
+
// boundaries, so cache it per epoch (the root's own invalidation signal) instead of
|
|
34
|
+
// rebuilding it on every auto-title, /title, session_ask, and handoff draft.
|
|
35
|
+
let cachedModelRuntime: { epoch: number; runtime: Promise<ModelRuntime> } | undefined;
|
|
36
|
+
const getModelRuntime: ModelRuntimeProvider = (modelRegistry) => {
|
|
37
|
+
if (cachedModelRuntime?.epoch !== sessionEpoch) {
|
|
38
|
+
cachedModelRuntime = {
|
|
39
|
+
epoch: sessionEpoch,
|
|
40
|
+
runtime: createSessionModelRuntime(modelRegistry),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return cachedModelRuntime.runtime;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const index = installIndex(pi, { settings });
|
|
47
|
+
const messaging = settings.features.messaging
|
|
48
|
+
? installMessaging(pi, { settings, index })
|
|
49
|
+
: undefined;
|
|
50
|
+
|
|
51
|
+
const lifecycles: SessionLifecycle[] = [];
|
|
52
|
+
if (messaging) {
|
|
53
|
+
lifecycles.push(messaging);
|
|
54
|
+
}
|
|
55
|
+
const subagents =
|
|
56
|
+
settings.features.subagents && messaging
|
|
57
|
+
? installSubagents(pi, { settings, messaging })
|
|
58
|
+
: undefined;
|
|
59
|
+
if (subagents) {
|
|
60
|
+
lifecycles.push(subagents);
|
|
61
|
+
}
|
|
62
|
+
if (messaging) {
|
|
63
|
+
pi.registerTool(
|
|
64
|
+
createSessionSendMessageTool(subagents ?? messaging, {
|
|
65
|
+
wakeCapable: Boolean(subagents),
|
|
66
|
+
getCachedRelationTo: messaging.getCachedRelationTo,
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
pi.registerTool(createSessionCancelTool(subagents ?? messaging));
|
|
70
|
+
}
|
|
71
|
+
if (settings.features.handoff) {
|
|
72
|
+
const board = {
|
|
73
|
+
roster: subagents?.roster,
|
|
74
|
+
cancelSubagent: subagents
|
|
75
|
+
? (sessionId: string) => subagents.cancelSession(sessionId)
|
|
76
|
+
: undefined,
|
|
77
|
+
listLiveSessions: messaging ? () => messaging.listSessions() : undefined,
|
|
78
|
+
readSessionEntries: (sessionFile: string) => SessionManager.open(sessionFile).getEntries(),
|
|
79
|
+
};
|
|
80
|
+
lifecycles.push(
|
|
81
|
+
installHandoff(pi, {
|
|
82
|
+
settings,
|
|
83
|
+
index,
|
|
84
|
+
getModelRuntime,
|
|
85
|
+
...(subagents ? { getLaunchTargets: () => subagents.getLaunchTargets() } : {}),
|
|
86
|
+
board,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (settings.features.search) {
|
|
91
|
+
installSearch(pi, { settings, index, messaging, roster: subagents?.roster });
|
|
92
|
+
}
|
|
93
|
+
if (settings.features.ask) {
|
|
94
|
+
installAsk(pi, { settings, index, getModelRuntime });
|
|
95
|
+
}
|
|
96
|
+
if (settings.features.autoTitle) {
|
|
97
|
+
lifecycles.push(
|
|
98
|
+
installAutoTitle(pi, {
|
|
99
|
+
settings,
|
|
100
|
+
getModelRuntime,
|
|
101
|
+
getSessionEpoch: () => sessionEpoch,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (settings.features.hooks) {
|
|
106
|
+
lifecycles.push(installHooks(pi, { settings, index }));
|
|
107
|
+
}
|
|
108
|
+
// Messaging leads every session_start so the broker connection is registered before any
|
|
109
|
+
// other feature hook runs. A side effect is that messaging's first relation snapshot can
|
|
110
|
+
// predate hooks' index sync for this session; getCachedRelationTo self-heals on miss, so
|
|
111
|
+
// this ordering is intentional — do not reorder to "fix" it.
|
|
112
|
+
pi.on("session_start", async (event, ctx) => {
|
|
113
|
+
sessionEpoch += 1;
|
|
114
|
+
for (const lifecycle of lifecycles) {
|
|
115
|
+
await lifecycle.onSessionStart?.(event, ctx);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
120
|
+
sessionEpoch += 1;
|
|
121
|
+
for (let index = lifecycles.length - 1; index >= 0; index -= 1) {
|
|
122
|
+
await lifecycles[index]?.onSessionShutdown?.(event, ctx);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtensionContext, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import {
|
|
4
4
|
createAgentSession,
|
|
5
5
|
DefaultResourceLoader,
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
SessionManager,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { type Static, Type } from "typebox";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveAuthenticatedModel } from "../shared/model-resolution.ts";
|
|
12
|
+
import { freshenModel } from "../shared/model-runtime.ts";
|
|
12
13
|
import {
|
|
13
14
|
type SessionLineageRow,
|
|
14
15
|
searchSessionChunks,
|
|
@@ -122,6 +123,7 @@ export interface SessionAskAgentResult {
|
|
|
122
123
|
|
|
123
124
|
export async function runSessionAskAgent(params: {
|
|
124
125
|
ctx: ExtensionContext;
|
|
126
|
+
modelRuntime: ModelRuntime;
|
|
125
127
|
target: SessionLineageRow;
|
|
126
128
|
question: string;
|
|
127
129
|
indexPath: string;
|
|
@@ -129,13 +131,22 @@ export async function runSessionAskAgent(params: {
|
|
|
129
131
|
thinkingLevel: ThinkingLevel | undefined;
|
|
130
132
|
signal?: AbortSignal | undefined;
|
|
131
133
|
}): Promise<SessionAskAgentResult | undefined> {
|
|
134
|
+
params.signal?.throwIfAborted();
|
|
132
135
|
const navigationData = loadSessionNavigationData(params.target.sessionPath);
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
const configured = params.askSettings?.model
|
|
137
|
+
? resolveAuthenticatedModel({
|
|
138
|
+
modelRuntime: params.modelRuntime,
|
|
139
|
+
modelPattern: params.askSettings.model,
|
|
140
|
+
})
|
|
141
|
+
: undefined;
|
|
142
|
+
const currentModel = params.ctx.model
|
|
143
|
+
? freshenModel(params.modelRuntime, params.ctx.model)
|
|
144
|
+
: undefined;
|
|
145
|
+
const model = (configured?.ok ? configured.model : undefined) ?? currentModel;
|
|
136
146
|
if (!model) {
|
|
137
147
|
throw new Error("No active model is available for session_ask.");
|
|
138
148
|
}
|
|
149
|
+
const configuredThinkingLevel = configured?.ok ? configured.thinkingLevel : undefined;
|
|
139
150
|
|
|
140
151
|
let capturedArguments: ProvideResultsArgs | undefined;
|
|
141
152
|
const searchSessionTool = defineTool({
|
|
@@ -234,7 +245,7 @@ export async function runSessionAskAgent(params: {
|
|
|
234
245
|
description: "Return the final answer for session_ask.",
|
|
235
246
|
promptSnippet: "Provide session ask result",
|
|
236
247
|
promptGuidelines: [
|
|
237
|
-
"Call provide_results exactly once when you are ready to answer. It is the final tool call and ends the session_ask
|
|
248
|
+
"Call provide_results exactly once when you are ready to answer. It is the final tool call and ends the session_ask subagent turn.",
|
|
238
249
|
],
|
|
239
250
|
parameters: PROVIDE_RESULTS_PARAMETERS,
|
|
240
251
|
execute: async (_toolCallId, toolParams: ProvideResultsArgs) => {
|
|
@@ -261,9 +272,12 @@ export async function runSessionAskAgent(params: {
|
|
|
261
272
|
noSkills: true,
|
|
262
273
|
appendSystemPromptOverride: (base) => [...base, SESSION_ASK_NAVIGATION_SYSTEM_PROMPT],
|
|
263
274
|
});
|
|
275
|
+
params.signal?.throwIfAborted();
|
|
264
276
|
await resourceLoader.reload();
|
|
277
|
+
params.signal?.throwIfAborted();
|
|
265
278
|
|
|
266
|
-
const thinkingLevel =
|
|
279
|
+
const thinkingLevel =
|
|
280
|
+
params.askSettings?.thinkingLevel ?? configuredThinkingLevel ?? params.thinkingLevel;
|
|
267
281
|
const sessionManager = params.askSettings?.persistRuns
|
|
268
282
|
? SessionManager.create(cwd, getDefaultSessionAskRunsDir())
|
|
269
283
|
: SessionManager.inMemory(cwd);
|
|
@@ -273,7 +287,7 @@ export async function runSessionAskAgent(params: {
|
|
|
273
287
|
const { session } = await createAgentSession({
|
|
274
288
|
cwd,
|
|
275
289
|
model,
|
|
276
|
-
|
|
290
|
+
modelRuntime: params.modelRuntime,
|
|
277
291
|
...(thinkingLevel ? { thinkingLevel } : {}),
|
|
278
292
|
tools: SESSION_ASK_AGENT_TOOLS,
|
|
279
293
|
customTools: [searchSessionTool, sessionReadTool, provideResultsTool],
|
|
@@ -281,29 +295,35 @@ export async function runSessionAskAgent(params: {
|
|
|
281
295
|
sessionManager,
|
|
282
296
|
});
|
|
283
297
|
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
// Exactly one session.abort() runs no matter how many abort paths fire.
|
|
299
|
+
let nestedAbort: Promise<void> | undefined;
|
|
300
|
+
const startNestedAbort = (): void => {
|
|
301
|
+
nestedAbort ??= Promise.resolve(session.abort()).catch(() => {});
|
|
286
302
|
};
|
|
287
303
|
|
|
288
304
|
try {
|
|
289
|
-
params.signal?.addEventListener("abort",
|
|
305
|
+
params.signal?.addEventListener("abort", startNestedAbort, { once: true });
|
|
290
306
|
if (params.signal?.aborted) {
|
|
291
|
-
|
|
292
|
-
return undefined;
|
|
307
|
+
startNestedAbort();
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
for (let attempt = 1; attempt <= MAX_SESSION_ASK_ATTEMPTS; attempt += 1) {
|
|
311
|
+
params.signal?.throwIfAborted();
|
|
296
312
|
const prompt =
|
|
297
313
|
attempt === 1
|
|
298
314
|
? buildNavigationPrompt(navigationData, params.question)
|
|
299
315
|
: `You did not call ${PROVIDE_RESULTS_TOOL_NAME}. Continue from your prior work and call ${PROVIDE_RESULTS_TOOL_NAME} exactly once with the final markdown answer.`;
|
|
300
316
|
await session.prompt(prompt);
|
|
317
|
+
params.signal?.throwIfAborted();
|
|
301
318
|
if (capturedArguments) {
|
|
302
319
|
break;
|
|
303
320
|
}
|
|
304
321
|
}
|
|
305
322
|
} finally {
|
|
306
|
-
params.signal?.removeEventListener("abort",
|
|
323
|
+
params.signal?.removeEventListener("abort", startNestedAbort);
|
|
324
|
+
if (nestedAbort) {
|
|
325
|
+
await nestedAbort;
|
|
326
|
+
}
|
|
307
327
|
session.dispose();
|
|
308
328
|
}
|
|
309
329
|
|
|
@@ -1,39 +1,38 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
|
|
1
|
+
import { type ExtensionAPI, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import { Type } from "typebox";
|
|
4
|
-
import {
|
|
3
|
+
import { isSessionStarting } from "../session-handoff/metadata.ts";
|
|
4
|
+
import type { IndexHandle } from "../shared/composition.ts";
|
|
5
|
+
import type { ModelRuntimeProvider } from "../shared/model-runtime.ts";
|
|
5
6
|
import {
|
|
6
7
|
getSessionById,
|
|
7
8
|
type SessionLineageRow,
|
|
8
9
|
withSessionIndex,
|
|
9
|
-
} from "
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
} from "../shared/session-index/index.ts";
|
|
11
|
+
import { isExactSessionId } from "../shared/session-ui.ts";
|
|
12
|
+
import type { SessionSettings } from "../shared/settings.ts";
|
|
13
|
+
import { runSessionAskAgent } from "./agent.ts";
|
|
14
|
+
import { renderSessionAskResult } from "./renderer.ts";
|
|
15
|
+
import type {
|
|
16
|
+
SessionAskProgressDetails,
|
|
17
|
+
SessionAskRelevantFile,
|
|
18
|
+
SessionAskResultDetails,
|
|
19
|
+
} from "./tool-contract.ts";
|
|
14
20
|
|
|
15
21
|
interface SessionAskToolParams {
|
|
16
22
|
session: string;
|
|
17
23
|
question: string;
|
|
18
24
|
}
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
sessionId?: string | undefined;
|
|
31
|
-
sessionName?: string | undefined;
|
|
32
|
-
sessionPath?: string | undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
36
|
-
const settings = loadSettings();
|
|
26
|
+
export function installAsk(
|
|
27
|
+
pi: ExtensionAPI,
|
|
28
|
+
deps: {
|
|
29
|
+
settings: SessionSettings;
|
|
30
|
+
index: IndexHandle;
|
|
31
|
+
getModelRuntime: ModelRuntimeProvider;
|
|
32
|
+
},
|
|
33
|
+
): void {
|
|
34
|
+
const { settings } = deps;
|
|
35
|
+
const indexPath = deps.index.path;
|
|
37
36
|
|
|
38
37
|
pi.registerTool({
|
|
39
38
|
name: "session_ask",
|
|
@@ -60,12 +59,40 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
|
60
59
|
throw new Error("session_ask requires a question.");
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
const resolvedTarget = resolveSessionAskTarget(sessionId,
|
|
62
|
+
const resolvedTarget = resolveSessionAskTarget(sessionId, indexPath);
|
|
64
63
|
if (!resolvedTarget.resolved) {
|
|
65
64
|
throw new Error(resolvedTarget.error ?? "Unable to resolve session id.");
|
|
66
65
|
}
|
|
67
66
|
|
|
68
|
-
const
|
|
67
|
+
const startingAnswer = getStartingSessionAnswer(resolvedTarget.resolved.sessionPath);
|
|
68
|
+
if (startingAnswer) {
|
|
69
|
+
const details: SessionAskResultDetails = {
|
|
70
|
+
answer: startingAnswer,
|
|
71
|
+
relevantFiles: [],
|
|
72
|
+
sessionId: resolvedTarget.resolved.sessionId,
|
|
73
|
+
sessionName: resolvedTarget.resolved.sessionName,
|
|
74
|
+
sessionPath: resolvedTarget.resolved.sessionPath,
|
|
75
|
+
question,
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: [
|
|
82
|
+
formatSessionAskHeader(
|
|
83
|
+
resolvedTarget.resolved.sessionId,
|
|
84
|
+
resolvedTarget.resolved.sessionName,
|
|
85
|
+
question,
|
|
86
|
+
),
|
|
87
|
+
startingAnswer,
|
|
88
|
+
].join("\n\n"),
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
details,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const progressDetails: SessionAskProgressDetails = {
|
|
69
96
|
question,
|
|
70
97
|
sessionId: resolvedTarget.resolved.sessionId,
|
|
71
98
|
sessionName: resolvedTarget.resolved.sessionName,
|
|
@@ -90,26 +117,26 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
|
90
117
|
details: progressDetails,
|
|
91
118
|
});
|
|
92
119
|
|
|
120
|
+
const modelRuntime = await deps.getModelRuntime(ctx.modelRegistry);
|
|
93
121
|
const agentResult = await runSessionAskAgent({
|
|
94
122
|
ctx,
|
|
123
|
+
modelRuntime,
|
|
95
124
|
target: resolvedTarget.resolved,
|
|
96
125
|
question,
|
|
97
|
-
indexPath
|
|
126
|
+
indexPath,
|
|
98
127
|
askSettings: settings.ask,
|
|
99
128
|
thinkingLevel: pi.getThinkingLevel(),
|
|
100
129
|
signal,
|
|
101
130
|
});
|
|
102
131
|
|
|
103
|
-
if (signal?.aborted) {
|
|
104
|
-
throw new Error("Session ask was cancelled.");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
132
|
const answer = agentResult?.answer || "Could not determine an answer from the session.";
|
|
108
133
|
const relevantFiles = agentResult?.relevantFiles ?? [];
|
|
109
134
|
|
|
110
|
-
const details:
|
|
135
|
+
const details: SessionAskResultDetails = {
|
|
111
136
|
answer,
|
|
112
|
-
|
|
137
|
+
...(agentResult?.debugSessionPath
|
|
138
|
+
? { debugSessionPath: agentResult.debugSessionPath }
|
|
139
|
+
: {}),
|
|
113
140
|
relevantFiles,
|
|
114
141
|
sessionId: resolvedTarget.resolved.sessionId,
|
|
115
142
|
sessionName: resolvedTarget.resolved.sessionName,
|
|
@@ -133,42 +160,21 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
|
|
|
133
160
|
details,
|
|
134
161
|
};
|
|
135
162
|
},
|
|
136
|
-
renderResult
|
|
137
|
-
const details = result.details as SessionAskToolDetails | undefined;
|
|
138
|
-
const content = result.content[0];
|
|
139
|
-
if (content?.type !== "text") {
|
|
140
|
-
return new Text(theme.fg("error", "No session output"), 0, 0);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (context.isError) {
|
|
144
|
-
return new Text(theme.fg("error", content.text), 0, 0);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (isPartial) {
|
|
148
|
-
const lines = [theme.bold(theme.fg("warning", "Reading session..."))];
|
|
149
|
-
if (details?.sessionId || details?.sessionName) {
|
|
150
|
-
const identity = formatSessionTitleOrShortId(details.sessionName, details.sessionId);
|
|
151
|
-
lines.push(`title: ${theme.fg("accent", identity)}`);
|
|
152
|
-
}
|
|
153
|
-
if (details?.question) {
|
|
154
|
-
lines.push(theme.fg("muted", `prompt: ${details.question}`));
|
|
155
|
-
}
|
|
156
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const answer = (details?.answer ?? "").trim() || "No answer generated.";
|
|
160
|
-
const identity = formatSessionTitleOrShortId(details?.sessionName, details?.sessionId);
|
|
161
|
-
const lines = [`title: ${theme.bold(identity)}`];
|
|
162
|
-
if (details?.question) {
|
|
163
|
-
lines.push(theme.fg("muted", `prompt: ${details.question}`));
|
|
164
|
-
lines.push("");
|
|
165
|
-
}
|
|
166
|
-
lines.push(...formatSessionAskAnswerPreview(answer, expanded, theme));
|
|
167
|
-
return new Text(lines.join("\n"), 0, 0);
|
|
168
|
-
},
|
|
163
|
+
renderResult: renderSessionAskResult,
|
|
169
164
|
});
|
|
170
165
|
}
|
|
171
166
|
|
|
167
|
+
function getStartingSessionAnswer(sessionPath: string): string | undefined {
|
|
168
|
+
try {
|
|
169
|
+
if (isSessionStarting(SessionManager.open(sessionPath).getBranch())) {
|
|
170
|
+
return "This session is still starting: a handoff is in progress and it has not received its kickoff. Retry once the session is no longer starting.";
|
|
171
|
+
}
|
|
172
|
+
} catch {
|
|
173
|
+
// The navigation agent preserves the existing failure behavior for unreadable sessions.
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
172
178
|
function resolveSessionAskTarget(
|
|
173
179
|
sessionId: string,
|
|
174
180
|
indexPath: string,
|
|
@@ -220,26 +226,3 @@ function formatSessionAskAnswer(
|
|
|
220
226
|
}
|
|
221
227
|
return lines.join("\n");
|
|
222
228
|
}
|
|
223
|
-
|
|
224
|
-
function formatSessionAskAnswerPreview(answer: string, expanded: boolean, theme: Theme): string[] {
|
|
225
|
-
const lines = answer.split(/\r?\n/);
|
|
226
|
-
if (expanded || lines.length <= COLLAPSED_ANSWER_PREVIEW_ROWS) {
|
|
227
|
-
return lines;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return [
|
|
231
|
-
...lines.slice(0, COLLAPSED_ANSWER_PREVIEW_ROWS),
|
|
232
|
-
formatOverflowHint(lines.length - COLLAPSED_ANSWER_PREVIEW_ROWS, lines.length, theme),
|
|
233
|
-
];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function formatOverflowHint(remaining: number, total: number, theme: Theme): string {
|
|
237
|
-
return `${theme.fg("muted", `... (${remaining} more lines, ${total} total,`)} ${theme.fg(
|
|
238
|
-
"dim",
|
|
239
|
-
formatKeyHint("app.tools.expand"),
|
|
240
|
-
)}${theme.fg("muted", " to expand)")}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function formatKeyHint(keybinding: Keybinding): string {
|
|
244
|
-
return getKeybindings().getKeys(keybinding).join("/");
|
|
245
|
-
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SessionManager,
|
|
8
8
|
type SessionMessageEntry,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { HANDOFF_KICKOFF_CUSTOM_TYPE } from "../session-handoff/kickoff.ts";
|
|
10
11
|
import { contentToText, isRecord, truncateBlock, truncateInline } from "../shared/text.ts";
|
|
11
12
|
|
|
12
13
|
const MAX_TOOL_RESULT_PREVIEW_CHARS = 300;
|
|
@@ -726,7 +727,8 @@ function renderEntryContent(entry: SessionEntry, body: SessionReadBodyMode = "fu
|
|
|
726
727
|
return renderMessageEntry(entry, body);
|
|
727
728
|
}
|
|
728
729
|
if (entry.type === "custom_message") {
|
|
729
|
-
|
|
730
|
+
const label = entry.customType === HANDOFF_KICKOFF_CUSTOM_TYPE ? "Handoff kickoff" : "Custom";
|
|
731
|
+
return `[${label}]: ${String(entry.content ?? "")}`;
|
|
730
732
|
}
|
|
731
733
|
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
732
734
|
return String(entry.summary ?? "");
|