pi-sessions 0.16.0 → 0.17.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
CHANGED
|
@@ -74,6 +74,8 @@ Open the frontend implementation task in a session to the right.
|
|
|
74
74
|
| Session Index | `/session-index` slash command | Shows index status and rebuilds the local session index |
|
|
75
75
|
| Session Auto Title | in background, `/title` slash command | Give sessions titles |
|
|
76
76
|
|
|
77
|
+
Settings live under `sessions` in `~/.pi/agent/settings.json`. A project's `.pi/settings.json` overrides them the way pi merges its own settings: nested objects merge key by key, and the project wins.
|
|
78
|
+
|
|
77
79
|
Every feature is on by default. Turn one off with `enable: false` under its own settings namespace. Index recovery and startup/switch sync always run, including when search is disabled. `hooks.enable` controls turn, tree, and compaction sync, not startup recovery.
|
|
78
80
|
|
|
79
81
|
```json
|
|
@@ -199,6 +201,7 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
|
|
|
199
201
|
"thinkingLevel": "low",
|
|
200
202
|
"roster": ["anthropic/*", "openai-codex/gpt-5.6-terra:high"],
|
|
201
203
|
"deferred": {
|
|
204
|
+
"enable": true,
|
|
202
205
|
"copyToClipboard": true
|
|
203
206
|
}
|
|
204
207
|
}
|
|
@@ -210,6 +213,8 @@ If you want to override the shortcut, put this in your `~/.pi/agent/settings.jso
|
|
|
210
213
|
|
|
211
214
|
`roster` limits which models a handoff may launch a child session on. Defaults to pi's own `enabledModels` scoping, then to all configured models. May optionally include a thinking level; listing a model more than once adds to the levels it allows.
|
|
212
215
|
|
|
216
|
+
`deferred.enable` (default `true`) offers the `deferred` launch. Turn it off, along with `sessions.subagents.enable`, to leave only host and split launches.
|
|
217
|
+
|
|
213
218
|
`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.
|
|
214
219
|
|
|
215
220
|
Subagents require the handoff and messaging features. Limit recursive delegation depth with `sessions.subagents.maxDepth` (default `2`), or cap the context a subagent runs in with `sessions.subagents.contextLimit`:
|
|
@@ -28,7 +28,11 @@ import {
|
|
|
28
28
|
} from "./tool.ts";
|
|
29
29
|
import { HANDOFF_TOOL_DETAILS_SCHEMA } from "./tool-contract.ts";
|
|
30
30
|
import { HandoffToolComponent } from "./tool-renderer.ts";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
buildHandoffLaunchSchema,
|
|
33
|
+
buildHandoffPromptGuidelines,
|
|
34
|
+
offersSubagentLaunch,
|
|
35
|
+
} from "./tool-schema.ts";
|
|
32
36
|
import { buildHandoffToolView } from "./tool-view-model.ts";
|
|
33
37
|
import { formatHandoffError } from "./ui.ts";
|
|
34
38
|
|
|
@@ -66,14 +70,16 @@ export function installHandoff(
|
|
|
66
70
|
launchTargets: readonly HandoffLaunchTarget[],
|
|
67
71
|
roster: HandoffRoster | undefined,
|
|
68
72
|
): void {
|
|
73
|
+
const offersSubagent = offersSubagentLaunch(launchTargets);
|
|
69
74
|
pi.registerTool({
|
|
70
75
|
name: "session_handoff",
|
|
71
76
|
label: "Session Handoff",
|
|
72
77
|
description: "Start a new Pi session with a self-contained task.",
|
|
73
78
|
// Nested calls write no toolResult entry, so the board would never see the launch receipt.
|
|
74
79
|
exposure: "model-only",
|
|
75
|
-
promptSnippet:
|
|
76
|
-
"Delegate bounded work to a background subagent or hand off context to another Pi session"
|
|
80
|
+
promptSnippet: offersSubagent
|
|
81
|
+
? "Delegate bounded work to a background subagent or hand off context to another Pi session"
|
|
82
|
+
: "Hand off context to another Pi session",
|
|
77
83
|
promptGuidelines: buildHandoffPromptGuidelines(launchTargets, models, roster),
|
|
78
84
|
parameters: Type.Object({
|
|
79
85
|
goal: Type.String({
|
|
@@ -93,8 +99,7 @@ export function installHandoff(
|
|
|
93
99
|
),
|
|
94
100
|
requestResponse: Type.Optional(
|
|
95
101
|
Type.Boolean({
|
|
96
|
-
description:
|
|
97
|
-
"Whether the child session should report completion/results of its task back to this session. Defaults to true for subagent launches and false otherwise.",
|
|
102
|
+
description: `Whether the child session should report completion/results of its task back to this session. ${offersSubagent ? "Defaults to true for subagent launches and false otherwise." : "Defaults to false."}`,
|
|
98
103
|
}),
|
|
99
104
|
),
|
|
100
105
|
provider: Type.Optional(
|
|
@@ -111,8 +116,9 @@ export function installHandoff(
|
|
|
111
116
|
Type.Union(
|
|
112
117
|
THINKING_LEVELS.map((level) => Type.Literal(level)),
|
|
113
118
|
{
|
|
114
|
-
description:
|
|
115
|
-
"Thinking level for the child session. For
|
|
119
|
+
description: offersSubagent
|
|
120
|
+
? "Thinking level for the child session. For launches other than subagent, override only when the user requests it."
|
|
121
|
+
: "Thinking level for the child session. Override only when the user requests it.",
|
|
116
122
|
},
|
|
117
123
|
),
|
|
118
124
|
),
|
|
@@ -215,7 +221,7 @@ export function installHandoff(
|
|
|
215
221
|
createHandoffLaunchTargets({
|
|
216
222
|
pi,
|
|
217
223
|
splitBackend,
|
|
218
|
-
|
|
224
|
+
deferred: settings.handoff.deferred,
|
|
219
225
|
additionalTargets: deps.getLaunchTargets?.() ?? [],
|
|
220
226
|
hosts: deps.getHosts?.() ?? [],
|
|
221
227
|
}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Host } from "../hosts/contract.ts";
|
|
3
|
+
import type { HandoffSettings } from "../shared/settings.ts";
|
|
3
4
|
import { createDeferredLaunchBackend } from "./launch/deferred.ts";
|
|
4
5
|
import type { SplitLaunchBackend } from "./launch/resolution.ts";
|
|
5
6
|
import {
|
|
@@ -18,7 +19,7 @@ export interface ResolvedHandoffLaunchTarget {
|
|
|
18
19
|
export function createHandoffLaunchTargets(options: {
|
|
19
20
|
pi: ExtensionAPI;
|
|
20
21
|
splitBackend: SplitLaunchBackend | undefined;
|
|
21
|
-
|
|
22
|
+
deferred: HandoffSettings["deferred"];
|
|
22
23
|
additionalTargets: readonly HandoffLaunchTarget[];
|
|
23
24
|
hosts?: readonly Host[];
|
|
24
25
|
}): HandoffLaunchTarget[] {
|
|
@@ -35,11 +36,15 @@ export function createHandoffLaunchTargets(options: {
|
|
|
35
36
|
),
|
|
36
37
|
)
|
|
37
38
|
: [];
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const deferredTargets = options.deferred.enable
|
|
40
|
+
? [
|
|
41
|
+
createBackendLaunchTarget(
|
|
42
|
+
DEFERRED_LAUNCH,
|
|
43
|
+
createDeferredLaunchBackend({ copyToClipboard: options.deferred.copyToClipboard }),
|
|
44
|
+
"'deferred' creates the session and returns its resume command without opening anything.",
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
: [];
|
|
43
48
|
return [
|
|
44
49
|
...hosts.map((host) =>
|
|
45
50
|
createBackendLaunchTarget(
|
|
@@ -50,7 +55,7 @@ export function createHandoffLaunchTargets(options: {
|
|
|
50
55
|
),
|
|
51
56
|
),
|
|
52
57
|
...splitTargets,
|
|
53
|
-
|
|
58
|
+
...deferredTargets,
|
|
54
59
|
...options.additionalTargets,
|
|
55
60
|
];
|
|
56
61
|
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEFERRED_LAUNCH,
|
|
5
|
+
type HandoffLaunchTarget,
|
|
6
|
+
LAUNCH_DIRECTIONS,
|
|
7
|
+
SUBAGENT_LAUNCH,
|
|
8
|
+
} from "./launch-target.ts";
|
|
4
9
|
import { formatRosterEntry, type HandoffRoster } from "./roster.ts";
|
|
5
10
|
|
|
6
11
|
export function buildHandoffLaunchSchema(targets: readonly HandoffLaunchTarget[]) {
|
|
@@ -23,9 +28,6 @@ const SUBAGENT_GUIDELINES = [
|
|
|
23
28
|
"After launching a subagent, do not sleep, poll, or repeatedly check its progress; continue useful work or finish the current turn and wait for its report.",
|
|
24
29
|
];
|
|
25
30
|
|
|
26
|
-
const LAUNCH_TARGET_GUIDELINE =
|
|
27
|
-
"Use session_handoff directional or deferred launches only when the user requests one.";
|
|
28
|
-
|
|
29
31
|
const MODEL_INHERITANCE_GUIDELINES = [
|
|
30
32
|
"Leave provider and model unset to run the handoff on this session's current model.",
|
|
31
33
|
"To run the handoff on a different model, set both provider and model together (both are required).",
|
|
@@ -35,7 +37,7 @@ const RESTRICTED_THINKING_GUIDELINE =
|
|
|
35
37
|
'A ":level" suffix on a listed model limits it to those thinking levels; set thinkingLevel to one of them, or leave it unset when only one is listed.';
|
|
36
38
|
|
|
37
39
|
const SUBAGENT_MODEL_OVERRIDE_GUIDELINE =
|
|
38
|
-
"For subagents, choose the model that best fits the delegated task; for
|
|
40
|
+
"For subagents, choose the model that best fits the delegated task; for other launches, only override the model when the task clearly warrants it. When unsure, ask the user for their preference.";
|
|
39
41
|
|
|
40
42
|
const DEFAULT_MODEL_OVERRIDE_GUIDELINE =
|
|
41
43
|
"Only override the model when the task clearly warrants it.";
|
|
@@ -45,20 +47,15 @@ export function buildHandoffPromptGuidelines(
|
|
|
45
47
|
models: readonly Model<Api>[],
|
|
46
48
|
roster: HandoffRoster | undefined,
|
|
47
49
|
): string[] {
|
|
48
|
-
const hasSubagent = targets
|
|
49
|
-
const
|
|
50
|
-
(target) =>
|
|
51
|
-
target.value !== "subagent" &&
|
|
52
|
-
target.value !== "deferred" &&
|
|
53
|
-
!LAUNCH_DIRECTIONS.some((direction) => direction === target.value),
|
|
54
|
-
);
|
|
50
|
+
const hasSubagent = offersSubagentLaunch(targets);
|
|
51
|
+
const userFacingKinds = describeUserFacingLaunchKinds(targets);
|
|
55
52
|
const selectable =
|
|
56
53
|
roster?.map(formatRosterEntry) ?? models.map((model) => `${model.provider}/${model.id}`);
|
|
57
54
|
return [
|
|
58
55
|
...(hasSubagent ? SUBAGENT_GUIDELINES : []),
|
|
59
|
-
|
|
60
|
-
?
|
|
61
|
-
:
|
|
56
|
+
...(userFacingKinds
|
|
57
|
+
? [`Use session_handoff ${userFacingKinds} launches only when the user requests one.`]
|
|
58
|
+
: []),
|
|
62
59
|
...MODEL_INHERITANCE_GUIDELINES,
|
|
63
60
|
hasSubagent ? SUBAGENT_MODEL_OVERRIDE_GUIDELINE : DEFAULT_MODEL_OVERRIDE_GUIDELINE,
|
|
64
61
|
...(selectable.length > 0
|
|
@@ -67,3 +64,24 @@ export function buildHandoffPromptGuidelines(
|
|
|
67
64
|
...(roster?.some((entry) => entry.thinkingLevels) ? [RESTRICTED_THINKING_GUIDELINE] : []),
|
|
68
65
|
];
|
|
69
66
|
}
|
|
67
|
+
|
|
68
|
+
export function offersSubagentLaunch(targets: readonly HandoffLaunchTarget[]): boolean {
|
|
69
|
+
return targets.some((target) => target.value === SUBAGENT_LAUNCH);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function describeUserFacingLaunchKinds(targets: readonly HandoffLaunchTarget[]): string {
|
|
73
|
+
const isDirection = (value: string) => LAUNCH_DIRECTIONS.some((direction) => direction === value);
|
|
74
|
+
const kinds = [
|
|
75
|
+
targets.some((target) => isDirection(target.value)) ? "directional" : undefined,
|
|
76
|
+
targets.some(
|
|
77
|
+
(target) =>
|
|
78
|
+
target.value !== SUBAGENT_LAUNCH &&
|
|
79
|
+
target.value !== DEFERRED_LAUNCH &&
|
|
80
|
+
!isDirection(target.value),
|
|
81
|
+
)
|
|
82
|
+
? "host"
|
|
83
|
+
: undefined,
|
|
84
|
+
targets.some((target) => target.value === DEFERRED_LAUNCH) ? "deferred" : undefined,
|
|
85
|
+
];
|
|
86
|
+
return kinds.filter((kind) => kind !== undefined).join(" or ");
|
|
87
|
+
}
|
|
@@ -35,6 +35,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
35
35
|
roster: Type.Optional(Type.Array(Type.String())),
|
|
36
36
|
deferred: Type.Optional(
|
|
37
37
|
Type.Object({
|
|
38
|
+
enable: Type.Optional(Type.Boolean()),
|
|
38
39
|
copyToClipboard: Type.Optional(Type.Boolean()),
|
|
39
40
|
}),
|
|
40
41
|
),
|
|
@@ -103,6 +104,7 @@ export interface HandoffSettings extends AgentModelSettings {
|
|
|
103
104
|
/** Patterns naming the models a handoff may launch a child on. Empty means unrestricted. */
|
|
104
105
|
roster: readonly ModelSelection[];
|
|
105
106
|
deferred: {
|
|
107
|
+
enable: boolean;
|
|
106
108
|
copyToClipboard: boolean;
|
|
107
109
|
};
|
|
108
110
|
}
|
|
@@ -228,9 +230,14 @@ function normalizeAutoTitlePrompt(value: string | undefined): string {
|
|
|
228
230
|
return trimmed ? trimmed : DEFAULT_AUTO_TITLE_PROMPT;
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
/**
|
|
234
|
+
* pi's merged view: project `.pi/settings.json` deep-merged over the global file.
|
|
235
|
+
* Extensions load before `pi.getSettings()` is bound, and the trust decision it
|
|
236
|
+
* honors is not visible at load time, so project settings apply regardless of trust.
|
|
237
|
+
*/
|
|
231
238
|
function loadSessionFileSettings(): SessionFileSettings {
|
|
232
|
-
const
|
|
233
|
-
const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA,
|
|
239
|
+
const mergedSettings = SettingsManager.create(process.cwd()).getSettings();
|
|
240
|
+
const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, mergedSettings, "Invalid settings");
|
|
234
241
|
return parsed.sessions ?? {};
|
|
235
242
|
}
|
|
236
243
|
|
|
@@ -263,6 +270,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
|
|
|
263
270
|
persistRuns: fileSettings.handoff?.persistRuns ?? false,
|
|
264
271
|
roster: normalizeRoster(fileSettings.handoff?.roster),
|
|
265
272
|
deferred: {
|
|
273
|
+
enable: fileSettings.handoff?.deferred?.enable ?? true,
|
|
266
274
|
copyToClipboard: fileSettings.handoff?.deferred?.copyToClipboard ?? true,
|
|
267
275
|
},
|
|
268
276
|
},
|