@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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.
Files changed (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -0,0 +1,186 @@
1
+ import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import { Snowflake } from "@oh-my-pi/pi-utils";
3
+ import type { Static, TSchema } from "@sinclair/typebox";
4
+ import { applyToolProxy } from "../../extensibility/tool-proxy";
5
+ import type { Theme } from "../../modes/theme/theme";
6
+ import type {
7
+ RpcHostToolCallRequest,
8
+ RpcHostToolCancelRequest,
9
+ RpcHostToolDefinition,
10
+ RpcHostToolResult,
11
+ RpcHostToolUpdate,
12
+ } from "./rpc-types";
13
+
14
+ type RpcHostToolOutput = (frame: RpcHostToolCallRequest | RpcHostToolCancelRequest) => void;
15
+
16
+ type PendingHostToolCall = {
17
+ resolve: (result: AgentToolResult<unknown>) => void;
18
+ reject: (error: Error) => void;
19
+ onUpdate?: AgentToolUpdateCallback<unknown>;
20
+ };
21
+
22
+ function isAgentToolResult(value: unknown): value is AgentToolResult<unknown> {
23
+ if (!value || typeof value !== "object") return false;
24
+ const content = (value as { content?: unknown }).content;
25
+ return Array.isArray(content);
26
+ }
27
+
28
+ export function isRpcHostToolResult(value: unknown): value is RpcHostToolResult {
29
+ if (!value || typeof value !== "object") return false;
30
+ const frame = value as { type?: unknown; id?: unknown; result?: unknown };
31
+ return frame.type === "host_tool_result" && typeof frame.id === "string" && isAgentToolResult(frame.result);
32
+ }
33
+
34
+ export function isRpcHostToolUpdate(value: unknown): value is RpcHostToolUpdate {
35
+ if (!value || typeof value !== "object") return false;
36
+ const frame = value as { type?: unknown; id?: unknown; partialResult?: unknown };
37
+ return frame.type === "host_tool_update" && typeof frame.id === "string" && isAgentToolResult(frame.partialResult);
38
+ }
39
+
40
+ class RpcHostToolAdapter<TParams extends TSchema = TSchema, TTheme extends Theme = Theme>
41
+ implements AgentTool<TParams, unknown, TTheme>
42
+ {
43
+ declare name: string;
44
+ declare label: string;
45
+ declare description: string;
46
+ declare parameters: TParams;
47
+ readonly strict = true;
48
+ concurrency: "shared" | "exclusive" = "shared";
49
+ #bridge: RpcHostToolBridge;
50
+ #definition: RpcHostToolDefinition;
51
+
52
+ constructor(definition: RpcHostToolDefinition, bridge: RpcHostToolBridge) {
53
+ this.#definition = definition;
54
+ this.#bridge = bridge;
55
+ applyToolProxy(definition, this);
56
+ }
57
+
58
+ execute(
59
+ toolCallId: string,
60
+ params: Static<TParams>,
61
+ signal?: AbortSignal,
62
+ onUpdate?: AgentToolUpdateCallback<unknown>,
63
+ ): Promise<AgentToolResult<unknown>> {
64
+ return this.#bridge.requestExecution(
65
+ this.#definition,
66
+ toolCallId,
67
+ params as Record<string, unknown>,
68
+ signal,
69
+ onUpdate,
70
+ );
71
+ }
72
+ }
73
+
74
+ export class RpcHostToolBridge {
75
+ #output: RpcHostToolOutput;
76
+ #definitions = new Map<string, RpcHostToolDefinition>();
77
+ #pendingCalls = new Map<string, PendingHostToolCall>();
78
+
79
+ constructor(output: RpcHostToolOutput) {
80
+ this.#output = output;
81
+ }
82
+
83
+ getToolNames(): string[] {
84
+ return Array.from(this.#definitions.keys());
85
+ }
86
+
87
+ setTools(tools: RpcHostToolDefinition[]): AgentTool[] {
88
+ this.#definitions = new Map(tools.map(tool => [tool.name, tool]));
89
+ return tools.map(tool => new RpcHostToolAdapter(tool, this));
90
+ }
91
+
92
+ handleResult(frame: RpcHostToolResult): boolean {
93
+ const pending = this.#pendingCalls.get(frame.id);
94
+ if (!pending) return false;
95
+ this.#pendingCalls.delete(frame.id);
96
+ if (frame.isError) {
97
+ const text = frame.result.content
98
+ .filter(
99
+ (item): item is { type: "text"; text: string } => item.type === "text" && typeof item.text === "string",
100
+ )
101
+ .map(item => item.text)
102
+ .join("\n")
103
+ .trim();
104
+ pending.reject(new Error(text || "Host tool execution failed"));
105
+ return true;
106
+ }
107
+ pending.resolve(frame.result);
108
+ return true;
109
+ }
110
+
111
+ handleUpdate(frame: RpcHostToolUpdate): boolean {
112
+ const pending = this.#pendingCalls.get(frame.id);
113
+ if (!pending) return false;
114
+ pending.onUpdate?.(frame.partialResult);
115
+ return true;
116
+ }
117
+
118
+ requestExecution(
119
+ definition: RpcHostToolDefinition,
120
+ toolCallId: string,
121
+ args: Record<string, unknown>,
122
+ signal?: AbortSignal,
123
+ onUpdate?: AgentToolUpdateCallback<unknown>,
124
+ ): Promise<AgentToolResult<unknown>> {
125
+ if (signal?.aborted) {
126
+ return Promise.reject(new Error(`Host tool "${definition.name}" was aborted`));
127
+ }
128
+
129
+ const id = Snowflake.next() as string;
130
+ const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
131
+ let settled = false;
132
+
133
+ const cleanup = () => {
134
+ signal?.removeEventListener("abort", onAbort);
135
+ this.#pendingCalls.delete(id);
136
+ };
137
+
138
+ const onAbort = () => {
139
+ if (settled) return;
140
+ settled = true;
141
+ cleanup();
142
+ this.#output({
143
+ type: "host_tool_cancel",
144
+ id: Snowflake.next() as string,
145
+ targetId: id,
146
+ });
147
+ reject(new Error(`Host tool "${definition.name}" was aborted`));
148
+ };
149
+
150
+ signal?.addEventListener("abort", onAbort, { once: true });
151
+ this.#pendingCalls.set(id, {
152
+ resolve: result => {
153
+ if (settled) return;
154
+ settled = true;
155
+ cleanup();
156
+ resolve(result);
157
+ },
158
+ reject: error => {
159
+ if (settled) return;
160
+ settled = true;
161
+ cleanup();
162
+ reject(error);
163
+ },
164
+ onUpdate,
165
+ });
166
+
167
+ this.#output({
168
+ type: "host_tool_call",
169
+ id,
170
+ toolCallId,
171
+ toolName: definition.name,
172
+ arguments: args,
173
+ });
174
+
175
+ return promise;
176
+ }
177
+
178
+ rejectAllPending(message: string): void {
179
+ const error = new Error(message);
180
+ const pendingCalls = Array.from(this.#pendingCalls.values());
181
+ this.#pendingCalls.clear();
182
+ for (const pending of pendingCalls) {
183
+ pending.reject(error);
184
+ }
185
+ }
186
+ }
@@ -3,13 +3,22 @@
3
3
  *
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
- import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
6
+ import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
7
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
8
8
  import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";
12
- import type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc-types";
12
+ import type {
13
+ RpcCommand,
14
+ RpcHostToolCallRequest,
15
+ RpcHostToolCancelRequest,
16
+ RpcHostToolDefinition,
17
+ RpcHostToolResult,
18
+ RpcHostToolUpdate,
19
+ RpcResponse,
20
+ RpcSessionState,
21
+ } from "./rpc-types";
13
22
 
14
23
  /** Distributive Omit that works with union types */
15
24
  type DistributiveOmit<T, K extends keyof T> = T extends unknown ? Omit<T, K> : never;
@@ -32,12 +41,40 @@ export interface RpcClientOptions {
32
41
  sessionDir?: string;
33
42
  /** Additional CLI arguments */
34
43
  args?: string[];
44
+ /** Custom tools owned by the embedding host and exposed over the RPC transport */
45
+ customTools?: RpcClientCustomTool[];
35
46
  }
36
47
 
37
48
  export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
38
49
 
39
50
  export type RpcEventListener = (event: AgentEvent) => void;
40
51
 
52
+ export interface RpcClientToolContext<TDetails = unknown> {
53
+ toolCallId: string;
54
+ signal: AbortSignal;
55
+ sendUpdate(partialResult: RpcClientToolResult<TDetails>): void;
56
+ }
57
+
58
+ export type RpcClientToolResult<TDetails = unknown> = AgentToolResult<TDetails> | string;
59
+
60
+ export interface RpcClientCustomTool<
61
+ TParams extends Record<string, unknown> = Record<string, unknown>,
62
+ TDetails = unknown,
63
+ > extends Omit<RpcHostToolDefinition, "parameters"> {
64
+ parameters: Record<string, unknown>;
65
+ execute(
66
+ params: TParams,
67
+ context: RpcClientToolContext<TDetails>,
68
+ ): Promise<RpcClientToolResult<TDetails>> | RpcClientToolResult<TDetails>;
69
+ }
70
+
71
+ export function defineRpcClientTool<
72
+ TParams extends Record<string, unknown> = Record<string, unknown>,
73
+ TDetails = unknown,
74
+ >(tool: RpcClientCustomTool<TParams, TDetails>): RpcClientCustomTool<TParams, TDetails> {
75
+ return tool;
76
+ }
77
+
41
78
  const agentEventTypes = new Set<AgentEvent["type"]>([
42
79
  "agent_start",
43
80
  "agent_end",
@@ -70,6 +107,31 @@ function isAgentEvent(value: unknown): value is AgentEvent {
70
107
  return agentEventTypes.has(type as AgentEvent["type"]);
71
108
  }
72
109
 
110
+ function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
111
+ if (!isRecord(value)) return false;
112
+ return (
113
+ value.type === "host_tool_call" &&
114
+ typeof value.id === "string" &&
115
+ typeof value.toolCallId === "string" &&
116
+ typeof value.toolName === "string" &&
117
+ isRecord(value.arguments)
118
+ );
119
+ }
120
+
121
+ function isRpcHostToolCancelRequest(value: unknown): value is RpcHostToolCancelRequest {
122
+ if (!isRecord(value)) return false;
123
+ return value.type === "host_tool_cancel" && typeof value.id === "string" && typeof value.targetId === "string";
124
+ }
125
+
126
+ function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
127
+ if (typeof result === "string") {
128
+ return {
129
+ content: [{ type: "text", text: result }],
130
+ };
131
+ }
132
+ return result;
133
+ }
134
+
73
135
  // ============================================================================
74
136
  // RPC Client
75
137
  // ============================================================================
@@ -79,10 +141,14 @@ export class RpcClient {
79
141
  #eventListeners: RpcEventListener[] = [];
80
142
  #pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
81
143
  new Map();
144
+ #customTools: RpcClientCustomTool[] = [];
145
+ #pendingHostToolCalls = new Map<string, { controller: AbortController }>();
82
146
  #requestId = 0;
83
147
  #abortController = new AbortController();
84
148
 
85
- constructor(private options: RpcClientOptions = {}) {}
149
+ constructor(private options: RpcClientOptions = {}) {
150
+ this.#customTools = [...(options.customTools ?? [])];
151
+ }
86
152
 
87
153
  /**
88
154
  * Start the RPC agent process.
@@ -162,6 +228,9 @@ export class RpcClient {
162
228
 
163
229
  try {
164
230
  await readyPromise;
231
+ if (this.#customTools.length > 0) {
232
+ await this.setCustomTools(this.#customTools);
233
+ }
165
234
  } finally {
166
235
  clearTimeout(readyTimeout);
167
236
  }
@@ -177,6 +246,10 @@ export class RpcClient {
177
246
  this.#abortController.abort();
178
247
  this.#process = null;
179
248
  this.#pendingRequests.clear();
249
+ for (const pendingCall of this.#pendingHostToolCalls.values()) {
250
+ pendingCall.controller.abort();
251
+ }
252
+ this.#pendingHostToolCalls.clear();
180
253
  }
181
254
 
182
255
  /**
@@ -434,6 +507,26 @@ export class RpcClient {
434
507
  return this.#getData<{ messages: AgentMessage[] }>(response).messages;
435
508
  }
436
509
 
510
+ /**
511
+ * Replace the host-owned custom tools exposed to the RPC session.
512
+ * Changes take effect before the next model call.
513
+ */
514
+ async setCustomTools(tools: RpcClientCustomTool[]): Promise<string[]> {
515
+ this.#customTools = [...tools];
516
+ if (!this.#process) {
517
+ return this.#customTools.map(tool => tool.name);
518
+ }
519
+ const definitions: RpcHostToolDefinition[] = this.#customTools.map(tool => ({
520
+ name: tool.name,
521
+ label: tool.label,
522
+ description: tool.description,
523
+ parameters: tool.parameters,
524
+ hidden: tool.hidden,
525
+ }));
526
+ const response = await this.#send({ type: "set_host_tools", tools: definitions });
527
+ return this.#getData<{ toolNames: string[] }>(response).toolNames;
528
+ }
529
+
437
530
  // =========================================================================
438
531
  // Helpers
439
532
  // =========================================================================
@@ -514,6 +607,16 @@ export class RpcClient {
514
607
  }
515
608
  }
516
609
 
610
+ if (isRpcHostToolCallRequest(data)) {
611
+ void this.#handleHostToolCall(data);
612
+ return;
613
+ }
614
+
615
+ if (isRpcHostToolCancelRequest(data)) {
616
+ this.#pendingHostToolCalls.get(data.targetId)?.controller.abort();
617
+ return;
618
+ }
619
+
517
620
  if (!isAgentEvent(data)) return;
518
621
 
519
622
  // Otherwise it's an event
@@ -555,21 +658,83 @@ export class RpcClient {
555
658
  },
556
659
  });
557
660
 
558
- // Write to stdin after registering the handler
559
- const stdin = this.#process!.stdin as import("bun").FileSink;
560
- stdin.write(`${JSON.stringify(fullCommand)}\n`);
561
- // flush() returns number | Promise<number> - handle both cases
661
+ this.#writeFrame(fullCommand, err => {
662
+ this.#pendingRequests.delete(id);
663
+ if (settled) return;
664
+ settled = true;
665
+ clearTimeout(timeoutId);
666
+ reject(err);
667
+ });
668
+ return promise;
669
+ }
670
+
671
+ async #handleHostToolCall(request: RpcHostToolCallRequest): Promise<void> {
672
+ const tool = this.#customTools.find(candidate => candidate.name === request.toolName);
673
+ if (!tool) {
674
+ this.#writeFrame({
675
+ type: "host_tool_result",
676
+ id: request.id,
677
+ result: {
678
+ content: [{ type: "text", text: `Host tool "${request.toolName}" is not registered` }],
679
+ details: {},
680
+ },
681
+ isError: true,
682
+ } satisfies RpcHostToolResult);
683
+ return;
684
+ }
685
+
686
+ const controller = new AbortController();
687
+ this.#pendingHostToolCalls.set(request.id, { controller });
688
+
689
+ const sendUpdate = (partialResult: RpcClientToolResult<unknown>): void => {
690
+ if (controller.signal.aborted) return;
691
+ this.#writeFrame({
692
+ type: "host_tool_update",
693
+ id: request.id,
694
+ partialResult: normalizeToolResult(partialResult),
695
+ } satisfies RpcHostToolUpdate);
696
+ };
697
+
698
+ try {
699
+ const result = await tool.execute(request.arguments, {
700
+ toolCallId: request.toolCallId,
701
+ signal: controller.signal,
702
+ sendUpdate,
703
+ });
704
+ if (controller.signal.aborted) return;
705
+ this.#writeFrame({
706
+ type: "host_tool_result",
707
+ id: request.id,
708
+ result: normalizeToolResult(result),
709
+ } satisfies RpcHostToolResult);
710
+ } catch (error) {
711
+ if (controller.signal.aborted) return;
712
+ this.#writeFrame({
713
+ type: "host_tool_result",
714
+ id: request.id,
715
+ result: {
716
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
717
+ details: {},
718
+ },
719
+ isError: true,
720
+ } satisfies RpcHostToolResult);
721
+ } finally {
722
+ this.#pendingHostToolCalls.delete(request.id);
723
+ }
724
+ }
725
+
726
+ #writeFrame(frame: RpcCommand | RpcHostToolResult | RpcHostToolUpdate, onError?: (error: Error) => void): void {
727
+ if (!this.#process?.stdin) {
728
+ throw new Error("Client not started");
729
+ }
730
+ const stdin = this.#process.stdin as import("bun").FileSink;
731
+ stdin.write(`${JSON.stringify(frame)}\n`);
562
732
  const flushResult = stdin.flush();
563
733
  if (flushResult instanceof Promise) {
564
734
  flushResult.catch((err: Error) => {
565
- this.#pendingRequests.delete(id);
566
- if (settled) return;
567
- settled = true;
568
- clearTimeout(timeoutId);
569
- reject(err);
735
+ onError?.(err);
570
736
  });
571
737
  }
572
- return promise;
573
738
  }
574
739
 
575
740
  #getData<T>(response: RpcResponse): T {
@@ -10,7 +10,7 @@
10
10
  * - Events: AgentSessionEvent objects streamed as they occur
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
- import { readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
13
+ import { $env, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
14
14
  import type {
15
15
  ExtensionUIContext,
16
16
  ExtensionUIDialogOptions,
@@ -18,10 +18,14 @@ import type {
18
18
  } from "../../extensibility/extensions";
19
19
  import { type Theme, theme } from "../../modes/theme/theme";
20
20
  import type { AgentSession } from "../../session/agent-session";
21
+ import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
21
22
  import type {
22
23
  RpcCommand,
23
24
  RpcExtensionUIRequest,
24
25
  RpcExtensionUIResponse,
26
+ RpcHostToolCallRequest,
27
+ RpcHostToolCancelRequest,
28
+ RpcHostToolDefinition,
25
29
  RpcResponse,
26
30
  RpcSessionState,
27
31
  } from "./rpc-types";
@@ -34,7 +38,40 @@ export type PendingExtensionRequest = {
34
38
  reject: (error: Error) => void;
35
39
  };
36
40
 
37
- type RpcOutput = (obj: RpcResponse | RpcExtensionUIRequest | object) => void;
41
+ type RpcOutput = (
42
+ obj: RpcResponse | RpcExtensionUIRequest | RpcHostToolCallRequest | RpcHostToolCancelRequest | object,
43
+ ) => void;
44
+
45
+ function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
46
+ return tools.map((tool, index) => {
47
+ const name = typeof tool.name === "string" ? tool.name.trim() : "";
48
+ if (!name) {
49
+ throw new Error(`Host tool at index ${index} must provide a non-empty name`);
50
+ }
51
+ const description = typeof tool.description === "string" ? tool.description.trim() : "";
52
+ if (!description) {
53
+ throw new Error(`Host tool "${name}" must provide a non-empty description`);
54
+ }
55
+ if (!tool.parameters || typeof tool.parameters !== "object" || Array.isArray(tool.parameters)) {
56
+ throw new Error(`Host tool "${name}" must provide a JSON Schema object`);
57
+ }
58
+ const label = typeof tool.label === "string" && tool.label.trim() ? tool.label.trim() : name;
59
+ return {
60
+ name,
61
+ label,
62
+ description,
63
+ parameters: tool.parameters,
64
+ hidden: tool.hidden === true,
65
+ };
66
+ });
67
+ }
68
+
69
+ function shouldEmitRpcTitles(): boolean {
70
+ const raw = $env.PI_RPC_EMIT_TITLE;
71
+ if (!raw) return false;
72
+ const normalized = raw.trim().toLowerCase();
73
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
74
+ }
38
75
 
39
76
  export function requestRpcEditor(
40
77
  pendingRequests: Map<string, PendingExtensionRequest>,
@@ -110,6 +147,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
110
147
  const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
111
148
  process.stdout.write(`${JSON.stringify(obj)}\n`);
112
149
  };
150
+ const emitRpcTitles = shouldEmitRpcTitles();
113
151
 
114
152
  const success = <T extends RpcCommand["type"]>(
115
153
  id: string | undefined,
@@ -127,6 +165,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
127
165
  };
128
166
 
129
167
  const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
168
+ const hostToolBridge = new RpcHostToolBridge(output);
130
169
 
131
170
  // Shutdown request flag (wrapped in object to allow mutation with const)
132
171
  const shutdownState = { requested: false };
@@ -291,7 +330,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
291
330
  }
292
331
 
293
332
  setTitle(title: string): void {
294
- // Fire and forget - host can implement terminal title control
333
+ // Title updates are low-value noise for most RPC hosts; opt in via PI_RPC_EMIT_TITLE=1.
334
+ if (!emitRpcTitles) return;
295
335
  this.output({
296
336
  type: "extension_ui_request",
297
337
  id: Snowflake.next() as string,
@@ -544,10 +584,29 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
544
584
  autoCompactionEnabled: session.autoCompactionEnabled,
545
585
  messageCount: session.messages.length,
546
586
  queuedMessageCount: session.queuedMessageCount,
587
+ todoPhases: session.getTodoPhases(),
588
+ systemPrompt: session.systemPrompt,
589
+ dumpTools: session.agent.state.tools.map(tool => ({
590
+ name: tool.name,
591
+ description: tool.description,
592
+ parameters: tool.parameters,
593
+ })),
547
594
  };
548
595
  return success(id, "get_state", state);
549
596
  }
550
597
 
598
+ case "set_todos": {
599
+ session.setTodoPhases(command.phases);
600
+ return success(id, "set_todos", { todoPhases: session.getTodoPhases() });
601
+ }
602
+
603
+ case "set_host_tools": {
604
+ const tools = normalizeHostToolDefinitions(command.tools);
605
+ const rpcTools = hostToolBridge.setTools(tools);
606
+ await session.refreshRpcHostTools(rpcTools);
607
+ return success(id, "set_host_tools", { toolNames: tools.map(tool => tool.name) });
608
+ }
609
+
551
610
  // =================================================================
552
611
  // Model
553
612
  // =================================================================
@@ -738,6 +797,16 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
738
797
  continue;
739
798
  }
740
799
 
800
+ if (isRpcHostToolResult(parsed)) {
801
+ hostToolBridge.handleResult(parsed);
802
+ continue;
803
+ }
804
+
805
+ if (isRpcHostToolUpdate(parsed)) {
806
+ hostToolBridge.handleUpdate(parsed);
807
+ continue;
808
+ }
809
+
741
810
  // Handle regular commands
742
811
  const command = parsed as RpcCommand;
743
812
  const response = await handleCommand(command);
@@ -751,5 +820,6 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
751
820
  }
752
821
 
753
822
  // stdin closed — RPC client is gone, exit cleanly
823
+ hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
754
824
  process.exit(0);
755
825
  }
@@ -4,11 +4,12 @@
4
4
  * Commands are sent as JSON lines on stdin.
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
- import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
+ import type { AgentMessage, AgentToolResult, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { Effort, ImageContent, Model } from "@oh-my-pi/pi-ai";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";
12
+ import type { TodoPhase } from "../../tools/todo-write";
12
13
 
13
14
  // ============================================================================
14
15
  // RPC Commands (stdin)
@@ -25,6 +26,8 @@ export type RpcCommand =
25
26
 
26
27
  // State
27
28
  | { id?: string; type: "get_state" }
29
+ | { id?: string; type: "set_todos"; phases: TodoPhase[] }
30
+ | { id?: string; type: "set_host_tools"; tools: RpcHostToolDefinition[] }
28
31
 
29
32
  // Model
30
33
  | { id?: string; type: "set_model"; provider: string; modelId: string }
@@ -82,6 +85,10 @@ export interface RpcSessionState {
82
85
  autoCompactionEnabled: boolean;
83
86
  messageCount: number;
84
87
  queuedMessageCount: number;
88
+ todoPhases: TodoPhase[];
89
+ /** For session dump / export (plain-text parity with /dump). */
90
+ systemPrompt?: string;
91
+ dumpTools?: Array<{ name: string; description: string; parameters: unknown }>;
85
92
  }
86
93
 
87
94
  // ============================================================================
@@ -100,6 +107,8 @@ export type RpcResponse =
100
107
 
101
108
  // State
102
109
  | { id?: string; type: "response"; command: "get_state"; success: true; data: RpcSessionState }
110
+ | { id?: string; type: "response"; command: "set_todos"; success: true; data: { todoPhases: TodoPhase[] } }
111
+ | { id?: string; type: "response"; command: "set_host_tools"; success: true; data: { toolNames: string[] } }
103
112
 
104
113
  // Model
105
114
  | {
@@ -228,6 +237,49 @@ export type RpcExtensionUIRequest =
228
237
  | { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
229
238
  | { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string };
230
239
 
240
+ // ============================================================================
241
+ // Host Tool Frames (bidirectional)
242
+ // ============================================================================
243
+
244
+ export interface RpcHostToolDefinition {
245
+ name: string;
246
+ label?: string;
247
+ description: string;
248
+ parameters: Record<string, unknown>;
249
+ hidden?: boolean;
250
+ }
251
+
252
+ /** Emitted by the RPC server when it needs the host to execute a registered tool. */
253
+ export interface RpcHostToolCallRequest {
254
+ type: "host_tool_call";
255
+ id: string;
256
+ toolCallId: string;
257
+ toolName: string;
258
+ arguments: Record<string, unknown>;
259
+ }
260
+
261
+ /** Emitted by the RPC server when a pending host tool call should be aborted. */
262
+ export interface RpcHostToolCancelRequest {
263
+ type: "host_tool_cancel";
264
+ id: string;
265
+ targetId: string;
266
+ }
267
+
268
+ /** Sent by the host to stream partial tool updates back to the RPC server. */
269
+ export interface RpcHostToolUpdate {
270
+ type: "host_tool_update";
271
+ id: string;
272
+ partialResult: AgentToolResult<unknown>;
273
+ }
274
+
275
+ /** Sent by the host to complete a pending tool call. */
276
+ export interface RpcHostToolResult {
277
+ type: "host_tool_result";
278
+ id: string;
279
+ result: AgentToolResult<unknown>;
280
+ isError?: boolean;
281
+ }
282
+
231
283
  // ============================================================================
232
284
  // Extension UI Commands (stdin)
233
285
  // ============================================================================