pim-agent 0.10.0 → 0.12.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/package.json +3 -4
- package/packages/core/src/extensions/apply-patch/model.ts +1 -12
- package/packages/core/src/extensions/bash/index.ts +2 -1
- package/packages/core/src/extensions/bash/run.ts +8 -2
- package/packages/core/src/extensions/edit/index.ts +1 -1
- package/packages/core/src/extensions/subagent/index.ts +2 -1
- package/packages/core/src/extensions/subagent/subagent.ts +10 -1
- package/packages/core/src/extensions/system-prompt/index.ts +33 -24
- package/packages/core/src/extensions/system-prompt/prompt.ts +43 -67
- package/packages/core/src/session/SessionHost.ts +35 -9
- package/packages/core/src/session/SessionRegistry.ts +22 -1
- package/packages/core/src/session/SessionUi.ts +127 -0
- package/packages/core/src/shared/Directories.ts +39 -5
- package/packages/core/src/shared/ExtensionToggles.ts +8 -0
- package/packages/core/src/shared/PiExtensions.ts +228 -0
- package/packages/core/src/shared/Tools.ts +3 -3
- package/packages/protocol/src/Command.ts +22 -1
- package/packages/protocol/src/ServerEvent.ts +34 -0
- package/packages/server/src/ProbeClient.ts +21 -0
- package/packages/server/src/SessionStream.ts +228 -2
- package/packages/server/src/WsGateway.ts +153 -16
- package/packages/server/src/probe.ts +16 -0
- package/packages/web/dist/client/assets/CommitMonoV143-VF-ytizKI8U.woff2 +0 -0
- package/packages/web/dist/client/assets/{core-CA0aSzPu.js → core-DKI-ZG3V.js} +1 -1
- package/packages/web/dist/client/assets/index-BCSESkyh.css +1 -0
- package/packages/web/dist/client/assets/index-BWUZzdeu.js +45 -0
- package/packages/web/dist/client/index.html +2 -2
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/index-B9be-i9d.css +0 -1
- package/packages/web/dist/client/assets/index-CpinM5NK.js +0 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pim-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A batteries-included distro of Pi, accessible in your terminal, browser and Telegram.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
"web:dev": "cd packages/web && vite"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@fontsource/commit-mono": "^5.3.0",
|
|
60
59
|
"@happy-dom/global-registrator": "^20.14.0",
|
|
61
60
|
"@iconify-json/griddy-icons": "^1.2.0",
|
|
62
61
|
"@iconify-json/solar": "^1.2.10",
|
|
@@ -76,8 +75,8 @@
|
|
|
76
75
|
"vite": "^8.2.2"
|
|
77
76
|
},
|
|
78
77
|
"dependencies": {
|
|
79
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
80
|
-
"@earendil-works/pi-server": "0.
|
|
78
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
79
|
+
"@earendil-works/pi-server": "0.87.1",
|
|
81
80
|
"diff": "^9.0.0",
|
|
82
81
|
"fzf": "^0.5.2",
|
|
83
82
|
"grammy": "^1.46.0",
|
|
@@ -4,8 +4,6 @@ type ModelLike = {
|
|
|
4
4
|
readonly id?: string;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
const CLAUDE_APPLY_PATCH_MAJOR = 5;
|
|
8
|
-
|
|
9
7
|
// Never match a GPT-named non-OpenAI model (`eleutherai/gpt-neo`): a false positive hands it a tool it was not trained on.
|
|
10
8
|
function isGptModel(model: ModelLike | undefined): boolean {
|
|
11
9
|
const provider = (model?.provider ?? "").toLowerCase();
|
|
@@ -25,15 +23,6 @@ function isGptModel(model: ModelLike | undefined): boolean {
|
|
|
25
23
|
);
|
|
26
24
|
}
|
|
27
25
|
|
|
28
|
-
function claudeMajorVersion(id: string): number | undefined {
|
|
29
|
-
const match = /(?:^|[/.])claude-[a-z]+-(\d+)/.exec(id);
|
|
30
|
-
return match ? Number(match[1]) : undefined;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
26
|
export function prefersApplyPatch(model: ModelLike | undefined): boolean {
|
|
34
|
-
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
const major = claudeMajorVersion((model?.id ?? "").toLowerCase());
|
|
38
|
-
return major !== undefined && major >= CLAUDE_APPLY_PATCH_MAJOR;
|
|
27
|
+
return isGptModel(model);
|
|
39
28
|
}
|
|
@@ -42,7 +42,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
42
42
|
name: "bash",
|
|
43
43
|
label: "bash",
|
|
44
44
|
description:
|
|
45
|
-
"Execute a bash command in the cwd
|
|
45
|
+
"Execute a bash command in the cwd from <environment>. " +
|
|
46
|
+
"Never prefix `cd <cwd> &&`; pass paths relative to cwd or absolute. " +
|
|
46
47
|
"Returns exit code, signal (if any), and stdout/stderr captured separately. " +
|
|
47
48
|
"Stdout that is a png/jpeg/gif/webp is returned as a picture. " +
|
|
48
49
|
"Prefer commands that emit only what you need; keep output as small as possible.",
|
|
@@ -14,6 +14,12 @@ type Reader = ReadableStreamDefaultReader<Uint8Array>;
|
|
|
14
14
|
|
|
15
15
|
const activePids = new Set<number>();
|
|
16
16
|
|
|
17
|
+
const COMMAND_VAR = "PIM_BASH_COMMAND";
|
|
18
|
+
// argv is what `pkill -f`/`pgrep -f` match, so a command naming its own target would
|
|
19
|
+
// signal this shell before its target. The env hands it over out of argv's reach, and
|
|
20
|
+
// the unset keeps it out of every child's environment too.
|
|
21
|
+
const RUNNER = `__pim_command=$${COMMAND_VAR}; unset ${COMMAND_VAR}; eval "$__pim_command"`;
|
|
22
|
+
|
|
17
23
|
export function killAllActiveBashGroups(sig: NodeJS.Signals = "SIGTERM"): void {
|
|
18
24
|
for (const pid of activePids) {
|
|
19
25
|
killGroup(pid, sig);
|
|
@@ -97,11 +103,11 @@ export async function runBashCommand(
|
|
|
97
103
|
|
|
98
104
|
// setsid gives the tree its own process group (pgid == proc.pid) so the whole tree can be signalled.
|
|
99
105
|
const proc = Bun.spawn({
|
|
100
|
-
cmd: ["setsid", "bash", "-lc",
|
|
106
|
+
cmd: ["setsid", "bash", "-lc", RUNNER],
|
|
101
107
|
cwd,
|
|
102
108
|
stdout: "pipe",
|
|
103
109
|
stderr: "pipe",
|
|
104
|
-
env: { ...process.env },
|
|
110
|
+
env: { ...process.env, [COMMAND_VAR]: command },
|
|
105
111
|
});
|
|
106
112
|
if (proc.pid !== undefined) {
|
|
107
113
|
activePids.add(proc.pid);
|
|
@@ -10,7 +10,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
10
10
|
name: "edit",
|
|
11
11
|
label: "edit",
|
|
12
12
|
description:
|
|
13
|
-
"Replace strings in a UTF-8 text file
|
|
13
|
+
"Replace strings in a UTF-8 text file; batch several replacements in one call via `edits`. " +
|
|
14
14
|
"Prefer edit over write for changes to existing files.",
|
|
15
15
|
parameters: editSchema,
|
|
16
16
|
renderShell: "self",
|
|
@@ -12,7 +12,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
12
12
|
label: "subagent",
|
|
13
13
|
description:
|
|
14
14
|
"Run a task in an isolated subagent with a fresh context. " +
|
|
15
|
-
"The subagent inherits the
|
|
15
|
+
"The subagent inherits the current model, thinking level and active tools, except subagent itself. " +
|
|
16
16
|
"Multiple subagent calls in one turn run in parallel. " +
|
|
17
17
|
"Subagent output returned to the main agent is capped at 32KB.",
|
|
18
18
|
parameters: subagentSchema,
|
|
@@ -26,6 +26,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
26
26
|
signal,
|
|
27
27
|
onUpdate,
|
|
28
28
|
activeToolNames: pi.getActiveTools(),
|
|
29
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
29
30
|
});
|
|
30
31
|
},
|
|
31
32
|
toViewModel: subagentView,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
3
|
import type {
|
|
3
4
|
AgentSessionEvent,
|
|
4
5
|
AgentToolResult,
|
|
@@ -66,6 +67,8 @@ export type SubagentSession = {
|
|
|
66
67
|
|
|
67
68
|
export type SubagentSessionSpec = {
|
|
68
69
|
readonly activeToolNames?: readonly string[];
|
|
70
|
+
/** The parent's live level; left unset, pi falls back to settings, not to the parent. */
|
|
71
|
+
readonly thinkingLevel?: ThinkingLevel;
|
|
69
72
|
/** The parent's tool call id, which names the child's log on disk. */
|
|
70
73
|
readonly callId?: string;
|
|
71
74
|
};
|
|
@@ -111,6 +114,7 @@ export async function createSdkSubagentSession(
|
|
|
111
114
|
cwd: parentCtx.cwd,
|
|
112
115
|
agentDir: getAgentDir(),
|
|
113
116
|
model: parentCtx.model,
|
|
117
|
+
thinkingLevel: spec.thinkingLevel,
|
|
114
118
|
sessionManager: await childSessionManager(parentCtx, spec.callId),
|
|
115
119
|
resourceLoader: loader,
|
|
116
120
|
tools: spec.activeToolNames
|
|
@@ -144,6 +148,7 @@ export async function runSubagent(
|
|
|
144
148
|
onUpdate,
|
|
145
149
|
createSession = createSdkSubagentSession,
|
|
146
150
|
activeToolNames,
|
|
151
|
+
thinkingLevel,
|
|
147
152
|
callId,
|
|
148
153
|
} = run;
|
|
149
154
|
|
|
@@ -175,7 +180,11 @@ export async function runSubagent(
|
|
|
175
180
|
};
|
|
176
181
|
|
|
177
182
|
try {
|
|
178
|
-
session = await createSession(parentCtx, {
|
|
183
|
+
session = await createSession(parentCtx, {
|
|
184
|
+
activeToolNames,
|
|
185
|
+
thinkingLevel,
|
|
186
|
+
callId,
|
|
187
|
+
});
|
|
179
188
|
capture.noteSessionId(session.sessionId);
|
|
180
189
|
session.subscribe((event) => capture.handle(event));
|
|
181
190
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
@@ -2,35 +2,44 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionFactory,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { formatSkillsForPrompt } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
|
|
7
5
|
import type { Surface } from "../../shared/Surface";
|
|
8
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
buildEnvironment,
|
|
8
|
+
buildInstructions,
|
|
9
|
+
formatDatetime,
|
|
10
|
+
leadWithSystemPrompt,
|
|
11
|
+
} from "./prompt";
|
|
12
|
+
|
|
13
|
+
const DATETIME_MESSAGE_TYPE = "pim-datetime";
|
|
9
14
|
|
|
10
15
|
export default function (surface?: Surface): ExtensionFactory {
|
|
11
16
|
return (pi: ExtensionAPI): void => {
|
|
17
|
+
// On submit rather than in `before_agent_start`, which can only append
|
|
18
|
+
// behind the user's message.
|
|
19
|
+
pi.on("input", () => {
|
|
20
|
+
pi.sendMessage(
|
|
21
|
+
{
|
|
22
|
+
customType: DATETIME_MESSAGE_TYPE,
|
|
23
|
+
content: `<datetime>${formatDatetime(new Date())}</datetime>`,
|
|
24
|
+
display: false,
|
|
25
|
+
},
|
|
26
|
+
{ triggerTurn: false }
|
|
27
|
+
);
|
|
28
|
+
return { action: "continue" };
|
|
29
|
+
});
|
|
12
30
|
pi.on("before_agent_start", (event, ctx) => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
contextFiles: contextFiles ?? [],
|
|
26
|
-
skillsBlock:
|
|
27
|
-
skills && skills.length > 0 ? formatSkillsForPrompt(skills) : "",
|
|
28
|
-
toolGuidelines: promptGuidelines ?? [],
|
|
29
|
-
appendSystemPrompt,
|
|
30
|
-
customPrompt,
|
|
31
|
-
...(surface === undefined ? {} : { surface }),
|
|
32
|
-
}),
|
|
33
|
-
};
|
|
31
|
+
const options = event.systemPromptOptions;
|
|
32
|
+
if (!options.customPrompt?.trim()) {
|
|
33
|
+
options.customPrompt = buildInstructions(options);
|
|
34
|
+
}
|
|
35
|
+
options.sections.environment = buildEnvironment({
|
|
36
|
+
model: ctx.model,
|
|
37
|
+
surface,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
pi.on("context_with_system", (event) => {
|
|
41
|
+
const messages = leadWithSystemPrompt(event.messages);
|
|
42
|
+
return messages ? { messages } : undefined;
|
|
34
43
|
});
|
|
35
44
|
};
|
|
36
45
|
}
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { NormalizedBuildSystemPromptOptions } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import type { Surface } from "../../shared/Surface";
|
|
4
4
|
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
readonly skillsBlock: string;
|
|
13
|
-
readonly toolGuidelines: ReadonlyArray<string>;
|
|
14
|
-
readonly appendSystemPrompt?: string;
|
|
15
|
-
readonly customPrompt?: string;
|
|
5
|
+
type GuidelineOptions = Pick<
|
|
6
|
+
NormalizedBuildSystemPromptOptions,
|
|
7
|
+
"selectedTools" | "toolGuidelines" | "promptGuidelines"
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
type EnvironmentOptions = {
|
|
11
|
+
readonly model?: { readonly id: string; readonly provider: string };
|
|
16
12
|
readonly os?: string;
|
|
17
13
|
readonly surface?: Surface;
|
|
18
14
|
};
|
|
@@ -34,57 +30,45 @@ function dynamicGuidelines(): ReadonlyArray<string> {
|
|
|
34
30
|
return guidelines;
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
export function
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
33
|
+
export function buildInstructions(options: GuidelineOptions): string {
|
|
34
|
+
const guidelines = new Set([
|
|
35
|
+
...options.selectedTools.flatMap(
|
|
36
|
+
(name) => options.toolGuidelines[name] ?? []
|
|
37
|
+
),
|
|
38
|
+
...options.promptGuidelines,
|
|
39
|
+
...dynamicGuidelines(),
|
|
40
|
+
]);
|
|
41
|
+
return [
|
|
42
|
+
"<system_instructions>",
|
|
43
|
+
"You are Pim (Pi IMproved), a batteries-included agent built on the Pi harness.",
|
|
44
|
+
...[...guidelines].map((g) => `- ${g}`),
|
|
45
|
+
"</system_instructions>",
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function leadWithSystemPrompt<T extends { readonly role: string }>(
|
|
50
|
+
messages: ReadonlyArray<T>
|
|
51
|
+
): T[] | undefined {
|
|
52
|
+
const first = messages.findIndex((message) => message.role === "system");
|
|
53
|
+
if (first <= 0) {
|
|
54
|
+
return undefined;
|
|
52
55
|
}
|
|
56
|
+
return [
|
|
57
|
+
messages[first]!,
|
|
58
|
+
...messages.slice(0, first),
|
|
59
|
+
...messages.slice(first + 1),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
53
62
|
|
|
63
|
+
export function buildEnvironment(opts: EnvironmentOptions): string {
|
|
54
64
|
const model = opts.model
|
|
55
65
|
? `${opts.model.id} via ${opts.model.provider}`
|
|
56
66
|
: "unknown";
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`- model: ${model}`,
|
|
63
|
-
...(opts.surface ? [`- surface: ${opts.surface}`] : []),
|
|
64
|
-
`- datetime: ${formatDatetime(new Date())}`,
|
|
65
|
-
"</environment>",
|
|
66
|
-
].join("\n")
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
if (opts.contextFiles.length > 0) {
|
|
70
|
-
const files = opts.contextFiles
|
|
71
|
-
.map(
|
|
72
|
-
({ path, content }) =>
|
|
73
|
-
`<file path="${escapeXmlAttr(path)}">\n${content}\n</file>`
|
|
74
|
-
)
|
|
75
|
-
.join("\n");
|
|
76
|
-
sections.push(`<project_instructions>\n${files}\n</project_instructions>`);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (opts.skillsBlock) {
|
|
80
|
-
sections.push(opts.skillsBlock.trimStart());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (opts.appendSystemPrompt && opts.appendSystemPrompt.trim().length > 0) {
|
|
84
|
-
sections.push(opts.appendSystemPrompt);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return sections.join("\n\n");
|
|
67
|
+
return [
|
|
68
|
+
`- os: ${opts.os ?? describeOs()}`,
|
|
69
|
+
`- model: ${model}`,
|
|
70
|
+
...(opts.surface ? [`- surface: ${opts.surface}`] : []),
|
|
71
|
+
].join("\n");
|
|
88
72
|
}
|
|
89
73
|
|
|
90
74
|
export function describeOs(options: OsDescriptionOptions = {}): string {
|
|
@@ -210,7 +194,7 @@ function unquoteValue(value: string): string {
|
|
|
210
194
|
return quote === "'" ? unquoted : unquoted.replace(/\\(["\\$`])/g, "$1");
|
|
211
195
|
}
|
|
212
196
|
|
|
213
|
-
function formatDatetime(d: Date): string {
|
|
197
|
+
export function formatDatetime(d: Date): string {
|
|
214
198
|
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
215
199
|
const offsetMinutes = -d.getTimezoneOffset();
|
|
216
200
|
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
@@ -222,11 +206,3 @@ function formatDatetime(d: Date): string {
|
|
|
222
206
|
const day = d.toLocaleDateString("en-US", { weekday: "long" });
|
|
223
207
|
return `${iso} (${day})`;
|
|
224
208
|
}
|
|
225
|
-
|
|
226
|
-
function escapeXmlAttr(value: string): string {
|
|
227
|
-
return value
|
|
228
|
-
.replace(/&/g, "&")
|
|
229
|
-
.replace(/"/g, """)
|
|
230
|
-
.replace(/</g, "<")
|
|
231
|
-
.replace(/>/g, ">");
|
|
232
|
-
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
SettingsManager,
|
|
16
16
|
type AgentSessionEvent,
|
|
17
17
|
type CompactionResult,
|
|
18
|
+
type ExtensionError,
|
|
18
19
|
type ToolDefinition,
|
|
19
20
|
} from "@earendil-works/pi-coding-agent";
|
|
20
21
|
import { unlink } from "node:fs/promises";
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
type LeaseRecord,
|
|
31
32
|
} from "./SessionLease";
|
|
32
33
|
import { SessionName } from "./SessionName";
|
|
34
|
+
import { adaptSessionUi, type SessionUi } from "./SessionUi";
|
|
33
35
|
import { WriteMark } from "./WriteMark";
|
|
34
36
|
|
|
35
37
|
/** What the agent is doing right now. */
|
|
@@ -102,6 +104,13 @@ export type SessionHostDeps = {
|
|
|
102
104
|
) => readonly ToolDefinition[];
|
|
103
105
|
/** Runs after the agent is disposed, before the session file is forgotten. */
|
|
104
106
|
readonly onRetire?: (sessionPath: string) => Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Where this host's extensions say things, resolved per call so an agent
|
|
109
|
+
* rebuilt under the host follows its live target. Omit to leave pi's no-op
|
|
110
|
+
* UI in place: pi freezes the mode at `bindExtensions`, so this is a dep
|
|
111
|
+
* rather than something a caller can turn on once a session exists.
|
|
112
|
+
*/
|
|
113
|
+
readonly ui?: () => SessionUi | undefined;
|
|
105
114
|
};
|
|
106
115
|
|
|
107
116
|
/** What a frontend's own tools are built against. */
|
|
@@ -770,16 +779,33 @@ export class SessionHost {
|
|
|
770
779
|
],
|
|
771
780
|
});
|
|
772
781
|
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
782
|
+
const onError = (err: ExtensionError) => {
|
|
783
|
+
console.warn(
|
|
784
|
+
`[${this.label}] extension ${err.extensionPath} (${err.event}):`,
|
|
785
|
+
err.error
|
|
786
|
+
);
|
|
787
|
+
// Pi answers a command handler that threw as if it had succeeded, so
|
|
788
|
+
// without this the user's message vanishes and only stderr says why.
|
|
789
|
+
this.deps
|
|
790
|
+
.ui?.()
|
|
791
|
+
?.notify(
|
|
792
|
+
`${err.extensionPath} (${err.event}) failed: ${err.error}`,
|
|
793
|
+
"error"
|
|
780
794
|
);
|
|
781
|
-
|
|
782
|
-
|
|
795
|
+
};
|
|
796
|
+
const ui = this.deps.ui;
|
|
797
|
+
// Emits session_start; without it extension tools are registered but never usable.
|
|
798
|
+
await agent.bindExtensions(
|
|
799
|
+
ui
|
|
800
|
+
? // Any context at all makes pi's `hasUI()` true (`uiContext !== noOpUIContext`,
|
|
801
|
+
// runner.js:318); "rpc" is its own name for dialog-capable but not a terminal.
|
|
802
|
+
{
|
|
803
|
+
uiContext: adaptSessionUi(ui),
|
|
804
|
+
mode: "rpc",
|
|
805
|
+
onError,
|
|
806
|
+
}
|
|
807
|
+
: { mode: "print", onError }
|
|
808
|
+
);
|
|
783
809
|
|
|
784
810
|
return { agent, cwd };
|
|
785
811
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./SessionHost";
|
|
19
19
|
import { SessionLease } from "./SessionLease";
|
|
20
20
|
import { SessionName } from "./SessionName";
|
|
21
|
+
import type { SessionUi } from "./SessionUi";
|
|
21
22
|
|
|
22
23
|
/** A session as pi stores it: one JSONL file under a cwd-encoded directory. */
|
|
23
24
|
export type SessionSummary = {
|
|
@@ -58,6 +59,7 @@ export class SessionRegistry {
|
|
|
58
59
|
private readonly deps: SessionRegistryDeps;
|
|
59
60
|
private readonly runtime: AgentRuntime;
|
|
60
61
|
private readonly hosts: SessionCache<SessionHost>;
|
|
62
|
+
private ui: ((host: SessionHost) => SessionUi | undefined) | undefined;
|
|
61
63
|
|
|
62
64
|
public constructor(deps: SessionRegistryDeps) {
|
|
63
65
|
this.deps = deps;
|
|
@@ -65,10 +67,24 @@ export class SessionRegistry {
|
|
|
65
67
|
this.hosts = new SessionCache(deps.capacity);
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Where the extensions of every host built from here speak. It belongs to the
|
|
72
|
+
* registry rather than each caller because `create` binds an agent eagerly,
|
|
73
|
+
* and pi freezes a session's UI mode at that bind. Resolved per call: a
|
|
74
|
+
* host outlives the stream that speaks for it.
|
|
75
|
+
*/
|
|
76
|
+
public setUi(ui: (host: SessionHost) => SessionUi | undefined): void {
|
|
77
|
+
this.ui = ui;
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
public get sessionsRoot(): string {
|
|
69
81
|
return join(this.runtime.agentDir, "sessions");
|
|
70
82
|
}
|
|
71
83
|
|
|
84
|
+
public get agentDir(): string {
|
|
85
|
+
return this.runtime.agentDir;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
public async init(): Promise<void> {
|
|
73
89
|
await this.runtime.init();
|
|
74
90
|
}
|
|
@@ -207,7 +223,10 @@ export class SessionRegistry {
|
|
|
207
223
|
}
|
|
208
224
|
|
|
209
225
|
private buildHost(label: string, settings: HostSettings): SessionHost {
|
|
210
|
-
|
|
226
|
+
const ui = this.ui;
|
|
227
|
+
// Named so the sink can reach back for the host it belongs to; the closure
|
|
228
|
+
// only ever runs once construction has returned.
|
|
229
|
+
const host: SessionHost = new SessionHost({
|
|
211
230
|
label: `session ${label}`,
|
|
212
231
|
settings,
|
|
213
232
|
defaults: this.deps.defaults,
|
|
@@ -225,7 +244,9 @@ export class SessionRegistry {
|
|
|
225
244
|
...(this.deps.surface === undefined
|
|
226
245
|
? {}
|
|
227
246
|
: { surface: this.deps.surface }),
|
|
247
|
+
...(ui === undefined ? {} : { ui: () => ui(host) }),
|
|
228
248
|
});
|
|
249
|
+
return host;
|
|
229
250
|
}
|
|
230
251
|
}
|
|
231
252
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionUIContext,
|
|
3
|
+
Theme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
import type { NoticeSeverity } from "../view/ViewBlock";
|
|
7
|
+
|
|
8
|
+
/** The slice of pi's dialog options a surface without a terminal can honour; the rest is countdown chrome. */
|
|
9
|
+
export type UiAsk = {
|
|
10
|
+
readonly timeout?: number;
|
|
11
|
+
readonly signal?: AbortSignal;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Where an extension's own words go. Four of pi's thirty methods: the ones that address a human rather than a terminal. */
|
|
15
|
+
export type SessionUi = {
|
|
16
|
+
readonly notify: (text: string, severity: NoticeSeverity) => void;
|
|
17
|
+
readonly select: (
|
|
18
|
+
title: string,
|
|
19
|
+
options: readonly string[],
|
|
20
|
+
opts?: UiAsk
|
|
21
|
+
) => Promise<string | undefined>;
|
|
22
|
+
readonly confirm: (
|
|
23
|
+
title: string,
|
|
24
|
+
message: string,
|
|
25
|
+
opts?: UiAsk
|
|
26
|
+
) => Promise<boolean>;
|
|
27
|
+
readonly input: (
|
|
28
|
+
title: string,
|
|
29
|
+
placeholder?: string,
|
|
30
|
+
opts?: UiAsk
|
|
31
|
+
) => Promise<string | undefined>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Everything off the terminal paints with; also what pi's border getters hand back. */
|
|
35
|
+
const IDENTITY = (text: string): string => text;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* pi keeps its own `theme` instance to itself, and nothing off the terminal has
|
|
39
|
+
* colours to paint with. `satisfies` covers the whole public surface — `keyof`
|
|
40
|
+
* skips the private fields the cast is for — so a method pi adds is a type
|
|
41
|
+
* error here rather than a throw inside somebody's event handler.
|
|
42
|
+
*/
|
|
43
|
+
const PLAIN_THEME = {
|
|
44
|
+
fg: (_color: string, text: string) => text,
|
|
45
|
+
bg: (_color: string, text: string) => text,
|
|
46
|
+
bold: IDENTITY,
|
|
47
|
+
italic: IDENTITY,
|
|
48
|
+
underline: IDENTITY,
|
|
49
|
+
inverse: IDENTITY,
|
|
50
|
+
strikethrough: IDENTITY,
|
|
51
|
+
getFgAnsi: () => "",
|
|
52
|
+
getBgAnsi: () => "",
|
|
53
|
+
getColorMode: () => "truecolor",
|
|
54
|
+
getThinkingBorderColor: () => IDENTITY,
|
|
55
|
+
getBashModeBorderColor: () => IDENTITY,
|
|
56
|
+
} satisfies { [K in keyof Theme]: Theme[K] } as unknown as Theme;
|
|
57
|
+
|
|
58
|
+
function severityOf(
|
|
59
|
+
kind: Parameters<ExtensionUIContext["notify"]>[1]
|
|
60
|
+
): NoticeSeverity {
|
|
61
|
+
return kind === "warning" ? "warn" : (kind ?? "info");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A dialog an extension awaits goes unanswered rather than ever throwing at it. */
|
|
65
|
+
async function settle<T>(
|
|
66
|
+
ask: () => Promise<T> | undefined
|
|
67
|
+
): Promise<T | undefined> {
|
|
68
|
+
try {
|
|
69
|
+
return await ask();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.warn("[SessionUi] dialog sink failed:", err);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Widens the sink onto pi's `ExtensionUIContext`, no-opping everything a
|
|
78
|
+
* terminal owns exactly as pi's own RPC mode does. `sink` is read per call, so
|
|
79
|
+
* an agent rebuilt under a host keeps whatever its host was last handed.
|
|
80
|
+
*
|
|
81
|
+
* Every method has to be an *own* property: pi re-wraps what it is given with
|
|
82
|
+
* `{ ...ui }` (`runner.js:274`), which copies own enumerable keys only. A class
|
|
83
|
+
* instance would arrive with `notify` and the rest of the prototype missing.
|
|
84
|
+
*/
|
|
85
|
+
export function adaptSessionUi(
|
|
86
|
+
sink: () => SessionUi | undefined
|
|
87
|
+
): ExtensionUIContext {
|
|
88
|
+
return {
|
|
89
|
+
select: (title, options, opts) =>
|
|
90
|
+
settle(() => sink()?.select(title, options, opts)),
|
|
91
|
+
confirm: async (title, message, opts) =>
|
|
92
|
+
(await settle(() => sink()?.confirm(title, message, opts))) === true,
|
|
93
|
+
input: (title, placeholder, opts) =>
|
|
94
|
+
settle(() => sink()?.input(title, placeholder, opts)),
|
|
95
|
+
notify: (message, kind) => {
|
|
96
|
+
try {
|
|
97
|
+
sink()?.notify(message, severityOf(kind));
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.warn("[SessionUi] notify sink failed:", err);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onTerminalInput: () => () => {},
|
|
103
|
+
setStatus: () => {},
|
|
104
|
+
setWorkingMessage: () => {},
|
|
105
|
+
setWorkingVisible: () => {},
|
|
106
|
+
setWorkingIndicator: () => {},
|
|
107
|
+
setHiddenThinkingLabel: () => {},
|
|
108
|
+
setWidget: () => {},
|
|
109
|
+
setFooter: () => {},
|
|
110
|
+
setHeader: () => {},
|
|
111
|
+
setTitle: () => {},
|
|
112
|
+
custom: async <T>() => undefined as T,
|
|
113
|
+
pasteToEditor: () => {},
|
|
114
|
+
setEditorText: () => {},
|
|
115
|
+
getEditorText: () => "",
|
|
116
|
+
editor: async () => undefined,
|
|
117
|
+
addAutocompleteProvider: () => {},
|
|
118
|
+
setEditorComponent: () => {},
|
|
119
|
+
getEditorComponent: () => undefined,
|
|
120
|
+
theme: PLAIN_THEME,
|
|
121
|
+
getAllThemes: () => [],
|
|
122
|
+
getTheme: () => undefined,
|
|
123
|
+
setTheme: () => ({ success: false, error: "UI not available" }),
|
|
124
|
+
getToolsExpanded: () => false,
|
|
125
|
+
setToolsExpanded: () => {},
|
|
126
|
+
};
|
|
127
|
+
}
|