@oh-my-pi/pi-coding-agent 15.0.1 → 15.1.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/CHANGELOG.md +94 -1
- package/examples/custom-tools/README.md +11 -7
- package/examples/custom-tools/hello/index.ts +2 -2
- package/examples/extensions/README.md +19 -8
- package/examples/extensions/api-demo.ts +15 -19
- package/examples/extensions/hello.ts +5 -6
- package/examples/extensions/plan-mode.ts +1 -1
- package/examples/extensions/reload-runtime.ts +4 -3
- package/examples/extensions/with-deps/index.ts +4 -3
- package/examples/sdk/06-extensions.ts +4 -2
- package/package.json +8 -18
- package/src/autoresearch/tools/init-experiment.ts +38 -41
- package/src/autoresearch/tools/log-experiment.ts +32 -41
- package/src/autoresearch/tools/run-experiment.ts +3 -3
- package/src/autoresearch/tools/update-notes.ts +11 -11
- package/src/commands/commit.ts +10 -0
- package/src/commit/agentic/tools/analyze-file.ts +4 -4
- package/src/commit/agentic/tools/git-file-diff.ts +4 -4
- package/src/commit/agentic/tools/git-hunk.ts +5 -5
- package/src/commit/agentic/tools/git-overview.ts +4 -4
- package/src/commit/agentic/tools/propose-changelog.ts +13 -13
- package/src/commit/agentic/tools/propose-commit.ts +6 -6
- package/src/commit/agentic/tools/recent-commits.ts +3 -3
- package/src/commit/agentic/tools/schemas.ts +28 -28
- package/src/commit/agentic/tools/split-commit.ts +22 -21
- package/src/commit/analysis/summary.ts +4 -4
- package/src/commit/changelog/generate.ts +7 -11
- package/src/commit/shared-llm.ts +22 -34
- package/src/config/config-file.ts +35 -13
- package/src/config/model-registry.ts +40 -191
- package/src/config/models-config-schema.ts +166 -0
- package/src/config/settings-schema.ts +29 -0
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/edit/index.ts +2 -2
- package/src/edit/modes/apply-patch.ts +7 -6
- package/src/edit/modes/patch.ts +18 -25
- package/src/edit/modes/replace.ts +18 -20
- package/src/eval/js/shared/rewrite-imports.ts +131 -10
- package/src/eval/py/executor.ts +233 -623
- package/src/eval/py/kernel.ts +27 -2
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/exa/factory.ts +5 -4
- package/src/exa/mcp-client.ts +1 -1
- package/src/exa/researcher.ts +9 -20
- package/src/exa/search.ts +26 -52
- package/src/exa/types.ts +1 -1
- package/src/exa/websets.ts +54 -53
- package/src/exec/bash-executor.ts +2 -1
- package/src/extensibility/custom-commands/loader.ts +5 -3
- package/src/extensibility/custom-commands/types.ts +4 -2
- package/src/extensibility/custom-tools/loader.ts +5 -3
- package/src/extensibility/custom-tools/types.ts +7 -6
- package/src/extensibility/custom-tools/wrapper.ts +1 -1
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/extensions/loader.ts +7 -3
- package/src/extensibility/extensions/types.ts +9 -5
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/tool-wrapper.ts +1 -1
- package/src/extensibility/hooks/types.ts +4 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +78 -31
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/typebox.ts +391 -0
- package/src/goals/tools/goal-tool.ts +6 -12
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/hashline/types.ts +4 -4
- package/src/hindsight/state.ts +2 -2
- package/src/index.ts +0 -2
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/lsp/types.ts +30 -38
- package/src/mcp/manager.ts +1 -1
- package/src/mcp/tool-bridge.ts +1 -1
- package/src/modes/acp/acp-agent.ts +248 -50
- package/src/modes/components/session-observer-overlay.ts +12 -1
- package/src/modes/components/status-line/segments.ts +39 -4
- package/src/modes/controllers/command-controller.ts +27 -2
- package/src/modes/controllers/event-controller.ts +3 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/host-tools.ts +1 -1
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-client.ts +1 -1
- package/src/modes/rpc/rpc-mode.ts +27 -1
- package/src/modes/rpc/rpc-types.ts +58 -1
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +117 -117
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/hashline.md +22 -26
- package/src/prompts/tools/read.md +55 -37
- package/src/sdk.ts +31 -8
- package/src/session/agent-session.ts +74 -104
- package/src/session/messages.ts +16 -51
- package/src/session/session-manager.ts +22 -2
- package/src/session/streaming-output.ts +16 -6
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +210 -87
- package/src/task/index.ts +15 -11
- package/src/task/render.ts +32 -5
- package/src/task/types.ts +54 -39
- package/src/tools/ask.ts +12 -12
- package/src/tools/ast-edit.ts +11 -15
- package/src/tools/ast-grep.ts +9 -10
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash.ts +48 -38
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/browser.ts +39 -53
- package/src/tools/calculator.ts +12 -11
- package/src/tools/checkpoint.ts +7 -7
- package/src/tools/debug.ts +40 -43
- package/src/tools/eval.ts +16 -10
- package/src/tools/find.ts +10 -13
- package/src/tools/gh.ts +108 -132
- package/src/tools/hindsight-recall.ts +4 -6
- package/src/tools/hindsight-reflect.ts +5 -5
- package/src/tools/hindsight-retain.ts +15 -17
- package/src/tools/image-gen.ts +31 -81
- package/src/tools/index.ts +4 -1
- package/src/tools/inspect-image.ts +8 -9
- package/src/tools/irc.ts +15 -27
- package/src/tools/job.ts +30 -28
- package/src/tools/output-meta.ts +26 -0
- package/src/tools/read.ts +39 -12
- package/src/tools/recipe/index.ts +7 -9
- package/src/tools/render-mermaid.ts +12 -12
- package/src/tools/report-tool-issue.ts +4 -4
- package/src/tools/resolve.ts +11 -11
- package/src/tools/review.ts +14 -26
- package/src/tools/search-tool-bm25.ts +7 -9
- package/src/tools/search.ts +19 -22
- package/src/tools/ssh.ts +10 -9
- package/src/tools/todo-write.ts +26 -34
- package/src/tools/vim.ts +10 -26
- package/src/tools/write.ts +25 -5
- package/src/tools/yield.ts +100 -54
- package/src/web/search/index.ts +9 -24
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +5 -0
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
- package/src/prompts/compaction/branch-summary-context.md +0 -5
- package/src/prompts/compaction/branch-summary-preamble.md +0 -2
- package/src/prompts/compaction/branch-summary.md +0 -30
- package/src/prompts/compaction/compaction-short-summary.md +0 -9
- package/src/prompts/compaction/compaction-summary-context.md +0 -5
- package/src/prompts/compaction/compaction-summary.md +0 -38
- package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
- package/src/prompts/compaction/compaction-update-summary.md +0 -45
- package/src/prompts/system/auto-handoff-threshold-focus.md +0 -1
- package/src/prompts/system/file-operations.md +0 -10
- package/src/prompts/system/handoff-document.md +0 -49
- package/src/prompts/system/summarization-system.md +0 -3
- package/src/session/compaction/branch-summarization.ts +0 -324
- package/src/session/compaction/compaction.ts +0 -1420
- package/src/session/compaction/errors.ts +0 -31
- package/src/session/compaction/index.ts +0 -8
- package/src/session/compaction/pruning.ts +0 -91
- package/src/session/compaction/utils.ts +0 -184
|
@@ -2,7 +2,7 @@ import * as os from "node:os";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { formatDuration, formatNumber, getProjectDir, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { formatDuration, formatNumber, getProjectDir, pathIsWithin, relativePathWithinRoot } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { type ThemeColor, theme } from "../../../modes/theme/theme";
|
|
7
7
|
import { shortenPath } from "../../../tools/render-utils";
|
|
8
8
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
|
|
@@ -32,6 +32,33 @@ function normalizePremiumRequests(value: number): number {
|
|
|
32
32
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const SCRATCH_ROOTS: readonly string[] = (() => {
|
|
36
|
+
const roots = new Set<string>([os.tmpdir(), path.join(os.homedir(), "tmp")]);
|
|
37
|
+
if (process.platform === "win32") {
|
|
38
|
+
const { TEMP, TMP, SystemRoot } = process.env;
|
|
39
|
+
if (TEMP) roots.add(TEMP);
|
|
40
|
+
if (TMP) roots.add(TMP);
|
|
41
|
+
if (SystemRoot) roots.add(path.join(SystemRoot, "Temp"));
|
|
42
|
+
} else {
|
|
43
|
+
roots.add("/tmp");
|
|
44
|
+
roots.add("/var/tmp");
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
roots.add("/private/tmp");
|
|
47
|
+
roots.add("/private/var/tmp");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return [...roots];
|
|
51
|
+
})();
|
|
52
|
+
|
|
53
|
+
function classifyProjectDir(pwd: string): { scratch: boolean; relative: string | null } {
|
|
54
|
+
for (const root of SCRATCH_ROOTS) {
|
|
55
|
+
if (pathIsWithin(root, pwd)) {
|
|
56
|
+
return { scratch: true, relative: relativePathWithinRoot(root, pwd) };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { scratch: false, relative: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
63
|
// Segment Implementations
|
|
37
64
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -150,10 +177,16 @@ const pathSegment: StatusLineSegment = {
|
|
|
150
177
|
render(ctx) {
|
|
151
178
|
const opts = ctx.options.path ?? {};
|
|
152
179
|
|
|
153
|
-
|
|
180
|
+
const projectDir = getProjectDir();
|
|
181
|
+
const { scratch, relative } = classifyProjectDir(projectDir);
|
|
182
|
+
let pwd = projectDir;
|
|
154
183
|
|
|
155
184
|
if (opts.stripWorkPrefix !== false) {
|
|
156
|
-
|
|
185
|
+
if (scratch) {
|
|
186
|
+
if (relative) pwd = relative;
|
|
187
|
+
} else {
|
|
188
|
+
pwd = stripDisplayRoot(pwd);
|
|
189
|
+
}
|
|
157
190
|
}
|
|
158
191
|
if (opts.abbreviate !== false) {
|
|
159
192
|
pwd = shortenPath(pwd);
|
|
@@ -166,7 +199,9 @@ const pathSegment: StatusLineSegment = {
|
|
|
166
199
|
pwd = `${ellipsis}${pwd.slice(-sliceLen)}`;
|
|
167
200
|
}
|
|
168
201
|
|
|
169
|
-
const
|
|
202
|
+
const showScratchIcon = scratch && opts.stripWorkPrefix !== false;
|
|
203
|
+
const icon = showScratchIcon ? theme.icon.scratchFolder : theme.icon.folder;
|
|
204
|
+
const content = withIcon(icon, pwd);
|
|
170
205
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
171
206
|
},
|
|
172
207
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { CompactionCancelledError, type CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
4
5
|
import {
|
|
5
6
|
getEnvApiKey,
|
|
6
7
|
getProviderDetails,
|
|
@@ -37,7 +38,6 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
|
|
|
37
38
|
import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
|
|
38
39
|
import type { AsyncJobSnapshotItem } from "../../session/agent-session";
|
|
39
40
|
import type { AuthStorage } from "../../session/auth-storage";
|
|
40
|
-
import { CompactionCancelledError, type CompactionOutcome } from "../../session/compaction";
|
|
41
41
|
import type { NewSessionOptions } from "../../session/session-manager";
|
|
42
42
|
import { outputMeta } from "../../tools/output-meta";
|
|
43
43
|
import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
|
|
@@ -1175,8 +1175,29 @@ export class CommandController {
|
|
|
1175
1175
|
return;
|
|
1176
1176
|
}
|
|
1177
1177
|
|
|
1178
|
+
if (this.ctx.loadingAnimation) {
|
|
1179
|
+
this.ctx.loadingAnimation.stop();
|
|
1180
|
+
this.ctx.loadingAnimation = undefined;
|
|
1181
|
+
}
|
|
1182
|
+
this.ctx.statusContainer.clear();
|
|
1183
|
+
|
|
1184
|
+
const originalOnEscape = this.ctx.editor.onEscape;
|
|
1185
|
+
this.ctx.editor.onEscape = () => {
|
|
1186
|
+
this.ctx.session.abortHandoff();
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
const handoffLoader = new Loader(
|
|
1190
|
+
this.ctx.ui,
|
|
1191
|
+
spinner => theme.fg("accent", spinner),
|
|
1192
|
+
text => theme.fg("muted", text),
|
|
1193
|
+
"Generating handoff… (esc to cancel)",
|
|
1194
|
+
getSymbolTheme().spinnerFrames,
|
|
1195
|
+
);
|
|
1196
|
+
this.ctx.statusContainer.addChild(handoffLoader);
|
|
1197
|
+
this.ctx.ui.requestRender();
|
|
1198
|
+
|
|
1178
1199
|
try {
|
|
1179
|
-
//
|
|
1200
|
+
// Handoff generation runs as a oneshot request; the new session is shown after it completes.
|
|
1180
1201
|
const result = await this.ctx.session.handoff(customInstructions);
|
|
1181
1202
|
|
|
1182
1203
|
if (!result) {
|
|
@@ -1206,6 +1227,10 @@ export class CommandController {
|
|
|
1206
1227
|
} else {
|
|
1207
1228
|
this.ctx.showError(`Handoff failed: ${message}`);
|
|
1208
1229
|
}
|
|
1230
|
+
} finally {
|
|
1231
|
+
handoffLoader.stop();
|
|
1232
|
+
this.ctx.statusContainer.clear();
|
|
1233
|
+
this.ctx.editor.onEscape = originalOnEscape;
|
|
1209
1234
|
}
|
|
1210
1235
|
this.ctx.ui.requestRender();
|
|
1211
1236
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { INTENT_FIELD } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { calculatePromptTokens } from "@oh-my-pi/pi-agent-core/compaction/compaction";
|
|
2
3
|
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
4
|
import { type Component, Loader, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { settings } from "../../config/settings";
|
|
@@ -15,7 +16,6 @@ import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
|
15
16
|
import type { InteractiveModeContext, TodoPhase } from "../../modes/types";
|
|
16
17
|
import type { PlanApprovalDetails } from "../../plan-mode/approved-plan";
|
|
17
18
|
import type { AgentSessionEvent } from "../../session/agent-session";
|
|
18
|
-
import { calculatePromptTokens } from "../../session/compaction/compaction";
|
|
19
19
|
import { isSilentAbort, readPendingDisplayTag } from "../../session/messages";
|
|
20
20
|
import type { ResolveToolDetails } from "../../tools/resolve";
|
|
21
21
|
|
|
@@ -719,9 +719,8 @@ export class EventController {
|
|
|
719
719
|
|
|
720
720
|
#scheduleIdleCompaction(): void {
|
|
721
721
|
this.#cancelIdleCompaction();
|
|
722
|
-
// Don't schedule while
|
|
723
|
-
//
|
|
724
|
-
// here would fire after the session resets, trying to handoff an empty session.
|
|
722
|
+
// Don't schedule idle work while context maintenance is already running; the
|
|
723
|
+
// maintenance flow may reset the session before this timer fires.
|
|
725
724
|
if (this.ctx.session.isCompacting) return;
|
|
726
725
|
|
|
727
726
|
const idleSettings = settings.getGroup("compaction");
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SendUserMessageHandler,
|
|
17
17
|
TerminalInputHandler,
|
|
18
18
|
} from "../../extensibility/extensions";
|
|
19
|
+
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
19
20
|
import { HookEditorComponent } from "../../modes/components/hook-editor";
|
|
20
21
|
import { HookInputComponent } from "../../modes/components/hook-input";
|
|
21
22
|
import { HookSelectorComponent } from "../../modes/components/hook-selector";
|
|
@@ -109,7 +110,7 @@ export class ExtensionUiController {
|
|
|
109
110
|
},
|
|
110
111
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
111
112
|
setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
|
|
112
|
-
getCommands: () =>
|
|
113
|
+
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
|
113
114
|
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
|
114
115
|
setSessionName: name => this.#updateSessionName(name),
|
|
115
116
|
};
|
|
@@ -349,7 +350,7 @@ export class ExtensionUiController {
|
|
|
349
350
|
},
|
|
350
351
|
getThinkingLevel: () => this.ctx.session.thinkingLevel,
|
|
351
352
|
setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
|
|
352
|
-
getCommands: () =>
|
|
353
|
+
getCommands: () => getSessionSlashCommands(this.ctx.session),
|
|
353
354
|
getSessionName: () => this.ctx.sessionManager.getSessionName(),
|
|
354
355
|
setSessionName: name => this.#updateSessionName(name),
|
|
355
356
|
};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { type Agent, type AgentMessage, type AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
|
|
8
9
|
import {
|
|
9
10
|
type AssistantMessage,
|
|
10
11
|
type ImageContent,
|
|
@@ -46,7 +47,6 @@ import planModeCompactInstructionsPrompt from "../prompts/system/plan-mode-compa
|
|
|
46
47
|
type: "text",
|
|
47
48
|
};
|
|
48
49
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
49
|
-
import type { CompactionOutcome } from "../session/compaction";
|
|
50
50
|
import { HistoryStorage } from "../session/history-storage";
|
|
51
51
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
52
52
|
import { getRecentSessions } from "../session/session-manager";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { Static, TSchema } from "@oh-my-pi/pi-ai";
|
|
2
3
|
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
3
|
-
import type { Static, TSchema } from "@sinclair/typebox";
|
|
4
4
|
import { applyToolProxy } from "../../extensibility/tool-proxy";
|
|
5
5
|
import type { Theme } from "../../modes/theme/theme";
|
|
6
6
|
import type {
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Snowflake } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { InternalUrlRouter } from "../../internal-urls";
|
|
3
|
+
import type {
|
|
4
|
+
InternalResource,
|
|
5
|
+
InternalUrl,
|
|
6
|
+
ProtocolHandler,
|
|
7
|
+
ResolveContext,
|
|
8
|
+
WriteContext,
|
|
9
|
+
} from "../../internal-urls/types";
|
|
10
|
+
import type {
|
|
11
|
+
RpcHostUriCancelRequest,
|
|
12
|
+
RpcHostUriRequest,
|
|
13
|
+
RpcHostUriResult,
|
|
14
|
+
RpcHostUriSchemeDefinition,
|
|
15
|
+
} from "./rpc-types";
|
|
16
|
+
|
|
17
|
+
type RpcHostUriOutput = (frame: RpcHostUriRequest | RpcHostUriCancelRequest) => void;
|
|
18
|
+
|
|
19
|
+
type PendingUriRequest = {
|
|
20
|
+
operation: "read" | "write";
|
|
21
|
+
url: string;
|
|
22
|
+
resolve: (frame: RpcHostUriResult) => void;
|
|
23
|
+
reject: (error: Error) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Type guard for inbound `host_uri_result` frames coming from the host. */
|
|
27
|
+
export function isRpcHostUriResult(value: unknown): value is RpcHostUriResult {
|
|
28
|
+
if (!value || typeof value !== "object") return false;
|
|
29
|
+
const frame = value as { type?: unknown; id?: unknown };
|
|
30
|
+
return frame.type === "host_uri_result" && typeof frame.id === "string";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One handler instance per host-registered scheme. Delegates reads and (when
|
|
35
|
+
* the scheme was registered as writable) writes to the bridge, which serializes
|
|
36
|
+
* them over the RPC transport.
|
|
37
|
+
*/
|
|
38
|
+
class RpcHostUriProtocolHandler implements ProtocolHandler {
|
|
39
|
+
readonly scheme: string;
|
|
40
|
+
readonly immutable: boolean;
|
|
41
|
+
readonly write?: (url: InternalUrl, content: string, context?: WriteContext) => Promise<void>;
|
|
42
|
+
readonly #bridge: RpcHostUriBridge;
|
|
43
|
+
|
|
44
|
+
constructor(definition: RpcHostUriSchemeDefinition, bridge: RpcHostUriBridge) {
|
|
45
|
+
this.scheme = definition.scheme;
|
|
46
|
+
this.immutable = definition.immutable === true;
|
|
47
|
+
this.#bridge = bridge;
|
|
48
|
+
if (definition.writable === true) {
|
|
49
|
+
this.write = (url, content, context) => this.#bridge.requestWrite(this.scheme, url, content, context);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
54
|
+
return this.#bridge.requestRead(this.scheme, url, context);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Bidirectional bridge that lets the RPC host own a set of URI schemes.
|
|
60
|
+
*
|
|
61
|
+
* The host registers schemes via `set_host_uri_schemes`; the bridge installs
|
|
62
|
+
* a `RpcHostUriProtocolHandler` per scheme into the process-global
|
|
63
|
+
* {@link InternalUrlRouter}. Reads land on the read tool through the existing
|
|
64
|
+
* router; writes are intercepted by the write tool and dispatched through
|
|
65
|
+
* `requestWrite`.
|
|
66
|
+
*/
|
|
67
|
+
export class RpcHostUriBridge {
|
|
68
|
+
#output: RpcHostUriOutput;
|
|
69
|
+
#router: InternalUrlRouter;
|
|
70
|
+
#definitions = new Map<string, RpcHostUriSchemeDefinition>();
|
|
71
|
+
#pending = new Map<string, PendingUriRequest>();
|
|
72
|
+
|
|
73
|
+
constructor(output: RpcHostUriOutput, router: InternalUrlRouter = InternalUrlRouter.instance()) {
|
|
74
|
+
this.#output = output;
|
|
75
|
+
this.#router = router;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
getSchemes(): string[] {
|
|
79
|
+
return Array.from(this.#definitions.keys());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Replace the registered set of host URI schemes. Previously registered
|
|
84
|
+
* schemes that no longer appear in the new set are unregistered from the
|
|
85
|
+
* router; surviving and new schemes get fresh handler instances.
|
|
86
|
+
*/
|
|
87
|
+
setSchemes(schemes: RpcHostUriSchemeDefinition[]): string[] {
|
|
88
|
+
const normalized = new Map<string, RpcHostUriSchemeDefinition>();
|
|
89
|
+
for (const raw of schemes) {
|
|
90
|
+
const scheme = typeof raw?.scheme === "string" ? raw.scheme.trim().toLowerCase() : "";
|
|
91
|
+
if (!scheme) {
|
|
92
|
+
throw new Error("Host URI scheme must be a non-empty string");
|
|
93
|
+
}
|
|
94
|
+
if (!/^[a-z][a-z0-9+.-]*$/.test(scheme)) {
|
|
95
|
+
throw new Error(`Host URI scheme contains invalid characters: ${raw.scheme}`);
|
|
96
|
+
}
|
|
97
|
+
normalized.set(scheme, {
|
|
98
|
+
scheme,
|
|
99
|
+
description: typeof raw.description === "string" ? raw.description : undefined,
|
|
100
|
+
writable: raw.writable === true,
|
|
101
|
+
immutable: raw.immutable === true,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const previous of this.#definitions.keys()) {
|
|
106
|
+
if (!normalized.has(previous)) {
|
|
107
|
+
this.#router.unregister(previous);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const definition of normalized.values()) {
|
|
111
|
+
this.#router.register(new RpcHostUriProtocolHandler(definition, this));
|
|
112
|
+
}
|
|
113
|
+
this.#definitions = normalized;
|
|
114
|
+
return Array.from(normalized.keys());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Unregister every host scheme from the router and reject any in-flight
|
|
119
|
+
* requests. Called on RPC shutdown to keep the global router clean for
|
|
120
|
+
* subsequent sessions in the same process (used by tests).
|
|
121
|
+
*/
|
|
122
|
+
clear(message: string = "Host URI bridge shut down"): void {
|
|
123
|
+
for (const scheme of this.#definitions.keys()) {
|
|
124
|
+
this.#router.unregister(scheme);
|
|
125
|
+
}
|
|
126
|
+
this.#definitions.clear();
|
|
127
|
+
this.rejectAllPending(message);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Resolve a pending request by id; called by `rpc-mode` on inbound results. */
|
|
131
|
+
handleResult(frame: RpcHostUriResult): boolean {
|
|
132
|
+
const pending = this.#pending.get(frame.id);
|
|
133
|
+
if (!pending) return false;
|
|
134
|
+
this.#pending.delete(frame.id);
|
|
135
|
+
pending.resolve(frame);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
rejectAllPending(message: string): void {
|
|
140
|
+
const error = new Error(message);
|
|
141
|
+
const pending = Array.from(this.#pending.values());
|
|
142
|
+
this.#pending.clear();
|
|
143
|
+
for (const entry of pending) {
|
|
144
|
+
entry.reject(error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async requestRead(scheme: string, url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
149
|
+
const result = await this.#dispatch("read", url.href, undefined, context?.signal);
|
|
150
|
+
if (result.isError) {
|
|
151
|
+
throw new Error(result.error || result.content || `Host URI read failed for ${url.href}`);
|
|
152
|
+
}
|
|
153
|
+
const content = result.content ?? "";
|
|
154
|
+
const contentType = result.contentType ?? "text/plain";
|
|
155
|
+
const definition = this.#definitions.get(scheme);
|
|
156
|
+
return {
|
|
157
|
+
url: url.href,
|
|
158
|
+
content,
|
|
159
|
+
contentType,
|
|
160
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
161
|
+
notes: result.notes && result.notes.length > 0 ? [...result.notes] : undefined,
|
|
162
|
+
immutable: result.immutable ?? definition?.immutable === true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async requestWrite(_scheme: string, url: InternalUrl, content: string, context?: WriteContext): Promise<void> {
|
|
167
|
+
const result = await this.#dispatch("write", url.href, content, context?.signal);
|
|
168
|
+
if (result.isError) {
|
|
169
|
+
throw new Error(result.error || result.content || `Host URI write failed for ${url.href}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#dispatch(
|
|
174
|
+
operation: "read" | "write",
|
|
175
|
+
url: string,
|
|
176
|
+
content: string | undefined,
|
|
177
|
+
signal: AbortSignal | undefined,
|
|
178
|
+
): Promise<RpcHostUriResult> {
|
|
179
|
+
if (signal?.aborted) {
|
|
180
|
+
return Promise.reject(new Error(`Host URI ${operation} for ${url} was aborted`));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const id = Snowflake.next() as string;
|
|
184
|
+
const { promise, resolve, reject } = Promise.withResolvers<RpcHostUriResult>();
|
|
185
|
+
let settled = false;
|
|
186
|
+
|
|
187
|
+
const cleanup = () => {
|
|
188
|
+
signal?.removeEventListener("abort", onAbort);
|
|
189
|
+
this.#pending.delete(id);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const onAbort = () => {
|
|
193
|
+
if (settled) return;
|
|
194
|
+
settled = true;
|
|
195
|
+
cleanup();
|
|
196
|
+
this.#output({
|
|
197
|
+
type: "host_uri_cancel",
|
|
198
|
+
id: Snowflake.next() as string,
|
|
199
|
+
targetId: id,
|
|
200
|
+
});
|
|
201
|
+
reject(new Error(`Host URI ${operation} for ${url} was aborted`));
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
205
|
+
this.#pending.set(id, {
|
|
206
|
+
operation,
|
|
207
|
+
url,
|
|
208
|
+
resolve: frame => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
settled = true;
|
|
211
|
+
cleanup();
|
|
212
|
+
resolve(frame);
|
|
213
|
+
},
|
|
214
|
+
reject: err => {
|
|
215
|
+
if (settled) return;
|
|
216
|
+
settled = true;
|
|
217
|
+
cleanup();
|
|
218
|
+
reject(err);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const frame: RpcHostUriRequest = {
|
|
223
|
+
type: "host_uri_request",
|
|
224
|
+
id,
|
|
225
|
+
operation,
|
|
226
|
+
url,
|
|
227
|
+
};
|
|
228
|
+
if (operation === "write") {
|
|
229
|
+
frame.content = content ?? "";
|
|
230
|
+
}
|
|
231
|
+
this.#output(frame);
|
|
232
|
+
|
|
233
|
+
return promise;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
* Spawns the agent in RPC mode and provides a typed API for all operations.
|
|
5
5
|
*/
|
|
6
6
|
import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
|
+
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
7
8
|
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
8
9
|
import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import type { BashResult } from "../../exec/bash-executor";
|
|
10
11
|
import type { SessionStats } from "../../session/agent-session";
|
|
11
|
-
import type { CompactionResult } from "../../session/compaction";
|
|
12
12
|
import type {
|
|
13
13
|
RpcCommand,
|
|
14
14
|
RpcExtensionUIRequest,
|
|
@@ -21,6 +21,7 @@ import { type Theme, theme } from "../../modes/theme/theme";
|
|
|
21
21
|
import type { AgentSession } from "../../session/agent-session";
|
|
22
22
|
import { initializeExtensions } from "../runtime-init";
|
|
23
23
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
24
|
+
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
24
25
|
import type {
|
|
25
26
|
RpcCommand,
|
|
26
27
|
RpcExtensionUIRequest,
|
|
@@ -28,6 +29,8 @@ import type {
|
|
|
28
29
|
RpcHostToolCallRequest,
|
|
29
30
|
RpcHostToolCancelRequest,
|
|
30
31
|
RpcHostToolDefinition,
|
|
32
|
+
RpcHostUriCancelRequest,
|
|
33
|
+
RpcHostUriRequest,
|
|
31
34
|
RpcResponse,
|
|
32
35
|
RpcSessionState,
|
|
33
36
|
} from "./rpc-types";
|
|
@@ -41,7 +44,14 @@ export type PendingExtensionRequest = {
|
|
|
41
44
|
};
|
|
42
45
|
|
|
43
46
|
type RpcOutput = (
|
|
44
|
-
obj:
|
|
47
|
+
obj:
|
|
48
|
+
| RpcResponse
|
|
49
|
+
| RpcExtensionUIRequest
|
|
50
|
+
| RpcHostToolCallRequest
|
|
51
|
+
| RpcHostToolCancelRequest
|
|
52
|
+
| RpcHostUriRequest
|
|
53
|
+
| RpcHostUriCancelRequest
|
|
54
|
+
| object,
|
|
45
55
|
) => void;
|
|
46
56
|
|
|
47
57
|
function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
|
|
@@ -188,6 +198,7 @@ export async function runRpcMode(
|
|
|
188
198
|
|
|
189
199
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
190
200
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
201
|
+
const hostUriBridge = new RpcHostUriBridge(output);
|
|
191
202
|
|
|
192
203
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
193
204
|
const shutdownState = { requested: false };
|
|
@@ -533,6 +544,15 @@ export async function runRpcMode(
|
|
|
533
544
|
return success(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
|
|
534
545
|
}
|
|
535
546
|
|
|
547
|
+
case "set_host_uri_schemes": {
|
|
548
|
+
try {
|
|
549
|
+
const schemes = hostUriBridge.setSchemes(command.schemes);
|
|
550
|
+
return success(id, "set_host_uri_schemes", { schemes });
|
|
551
|
+
} catch (err) {
|
|
552
|
+
return error(id, "set_host_uri_schemes", err instanceof Error ? err.message : String(err));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
536
556
|
// =================================================================
|
|
537
557
|
// Model
|
|
538
558
|
// =================================================================
|
|
@@ -807,6 +827,11 @@ export async function runRpcMode(
|
|
|
807
827
|
continue;
|
|
808
828
|
}
|
|
809
829
|
|
|
830
|
+
if (isRpcHostUriResult(parsed)) {
|
|
831
|
+
hostUriBridge.handleResult(parsed);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
|
|
810
835
|
// Handle regular commands
|
|
811
836
|
const command = parsed as RpcCommand;
|
|
812
837
|
const response = await handleCommand(command);
|
|
@@ -821,5 +846,6 @@ export async function runRpcMode(
|
|
|
821
846
|
|
|
822
847
|
// stdin closed — RPC client is gone, exit cleanly
|
|
823
848
|
hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
|
|
849
|
+
hostUriBridge.clear("RPC client disconnected before host URI request completed");
|
|
824
850
|
process.exit(0);
|
|
825
851
|
}
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* Responses and events are emitted as JSON lines on stdout.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
8
|
+
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
8
9
|
import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
9
10
|
import type { BashResult } from "../../exec/bash-executor";
|
|
10
11
|
import type { ContextUsage } from "../../extensibility/extensions/types";
|
|
11
12
|
import type { SessionStats } from "../../session/agent-session";
|
|
12
|
-
import type { CompactionResult } from "../../session/compaction";
|
|
13
13
|
import type { TodoPhase } from "../../tools/todo-write";
|
|
14
14
|
|
|
15
15
|
// ============================================================================
|
|
@@ -29,6 +29,7 @@ export type RpcCommand =
|
|
|
29
29
|
| { id?: string; type: "get_state" }
|
|
30
30
|
| { id?: string; type: "set_todos"; phases: TodoPhase[] }
|
|
31
31
|
| { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
|
|
32
|
+
| { id?: string; type: "set_host_uri_schemes"; schemes: RpcHostUriSchemeDefinition[] }
|
|
32
33
|
|
|
33
34
|
// Model
|
|
34
35
|
| { id?: string; type: "set_model"; provider: string; modelId: string }
|
|
@@ -121,6 +122,7 @@ export type RpcResponse =
|
|
|
121
122
|
| { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
|
|
122
123
|
| { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
|
|
123
124
|
| { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
|
|
125
|
+
| { id?: string; type: "response"; command: "set_host_uri_schemes"; success: true; data: { schemes: string[] } }
|
|
124
126
|
|
|
125
127
|
// Model
|
|
126
128
|
| {
|
|
@@ -304,6 +306,61 @@ export interface RpcHostToolResult {
|
|
|
304
306
|
isError?: boolean;
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Host URI Frames (bidirectional)
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
export interface RpcHostUriSchemeDefinition {
|
|
314
|
+
/** URL scheme without trailing `://` (e.g. `db`, `notion`). */
|
|
315
|
+
scheme: string;
|
|
316
|
+
/** Optional human-readable description for logs/diagnostics. */
|
|
317
|
+
description?: string;
|
|
318
|
+
/** When true, the write tool is allowed to dispatch writes to this scheme. */
|
|
319
|
+
writable?: boolean;
|
|
320
|
+
/** When true, downstream callers suppress hashline anchors for resolved content. */
|
|
321
|
+
immutable?: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export type RpcHostUriOperation = "read" | "write";
|
|
325
|
+
|
|
326
|
+
/** Emitted by the RPC server when it needs the host to satisfy a URI operation. */
|
|
327
|
+
export interface RpcHostUriRequest {
|
|
328
|
+
type: "host_uri_request";
|
|
329
|
+
id: string;
|
|
330
|
+
operation: RpcHostUriOperation;
|
|
331
|
+
url: string;
|
|
332
|
+
/** Present for write operations. */
|
|
333
|
+
content?: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Emitted by the RPC server when a pending URI request should be aborted. */
|
|
337
|
+
export interface RpcHostUriCancelRequest {
|
|
338
|
+
type: "host_uri_cancel";
|
|
339
|
+
id: string;
|
|
340
|
+
targetId: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Sent by the host to complete a pending URI request. */
|
|
344
|
+
export interface RpcHostUriResult {
|
|
345
|
+
type: "host_uri_result";
|
|
346
|
+
id: string;
|
|
347
|
+
/**
|
|
348
|
+
* Required for successful `read` results. Ignored for `write` success.
|
|
349
|
+
* Set on errors when a textual explanation accompanies `isError`.
|
|
350
|
+
*/
|
|
351
|
+
content?: string;
|
|
352
|
+
/** Defaults to `text/plain` when omitted. */
|
|
353
|
+
contentType?: "text/markdown" | "application/json" | "text/plain";
|
|
354
|
+
/** Optional resolution notes propagated to the read tool. */
|
|
355
|
+
notes?: string[];
|
|
356
|
+
/** Overrides the scheme-level `immutable` flag for this single resolution. */
|
|
357
|
+
immutable?: boolean;
|
|
358
|
+
/** When true, surface the result content as an error to the caller. */
|
|
359
|
+
isError?: boolean;
|
|
360
|
+
/** Optional error message; preferred over `content` for error surfacing. */
|
|
361
|
+
error?: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
307
364
|
// ============================================================================
|
|
308
365
|
// Extension UI Commands (stdin)
|
|
309
366
|
// ============================================================================
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* caller-supplied hooks.
|
|
8
8
|
*/
|
|
9
9
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
10
|
+
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
10
11
|
import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
|
|
11
12
|
import type { AgentSession } from "../session/agent-session";
|
|
12
13
|
|
|
@@ -59,7 +60,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
|
|
|
59
60
|
getActiveTools: () => session.getActiveToolNames(),
|
|
60
61
|
getAllTools: () => session.getAllToolNames(),
|
|
61
62
|
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
|
62
|
-
getCommands: () =>
|
|
63
|
+
getCommands: () => getSessionSlashCommands(session),
|
|
63
64
|
setModel: model => runExtensionSetModel(session, model),
|
|
64
65
|
getThinkingLevel: () => session.thinkingLevel,
|
|
65
66
|
setThinkingLevel: level => session.setThinkingLevel(level),
|