@oh-my-pi/pi-coding-agent 14.4.0 → 14.4.3

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 (67) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/package.json +7 -7
  3. package/src/cli.ts +0 -1
  4. package/src/config/prompt-templates.ts +1 -31
  5. package/src/config/settings-schema.ts +27 -37
  6. package/src/config/settings.ts +1 -1
  7. package/src/edit/index.ts +1 -53
  8. package/src/edit/line-hash.ts +13 -63
  9. package/src/edit/modes/atom.ts +334 -64
  10. package/src/edit/modes/hashline.ts +19 -26
  11. package/src/edit/renderer.ts +6 -8
  12. package/src/edit/streaming.ts +90 -114
  13. package/src/export/html/template.generated.ts +1 -1
  14. package/src/export/html/template.js +10 -15
  15. package/src/internal-urls/docs-index.generated.ts +1 -2
  16. package/src/lsp/defaults.json +142 -652
  17. package/src/modes/components/session-selector.ts +3 -3
  18. package/src/modes/components/settings-defs.ts +0 -5
  19. package/src/modes/components/tool-execution.ts +2 -5
  20. package/src/modes/controllers/btw-controller.ts +17 -105
  21. package/src/modes/controllers/todo-command-controller.ts +537 -0
  22. package/src/modes/interactive-mode.ts +35 -9
  23. package/src/modes/types.ts +2 -0
  24. package/src/modes/utils/ui-helpers.ts +17 -0
  25. package/src/prompts/system/irc-incoming.md +8 -0
  26. package/src/prompts/system/subagent-system-prompt.md +8 -0
  27. package/src/prompts/tools/ast-edit.md +1 -1
  28. package/src/prompts/tools/ast-grep.md +1 -0
  29. package/src/prompts/tools/atom.md +55 -53
  30. package/src/prompts/tools/bash.md +2 -2
  31. package/src/prompts/tools/grep.md +2 -5
  32. package/src/prompts/tools/irc.md +49 -0
  33. package/src/prompts/tools/job.md +11 -0
  34. package/src/prompts/tools/read.md +12 -13
  35. package/src/prompts/tools/task.md +1 -1
  36. package/src/prompts/tools/todo-write.md +14 -5
  37. package/src/registry/agent-registry.ts +139 -0
  38. package/src/sdk.ts +35 -0
  39. package/src/session/agent-session.ts +217 -5
  40. package/src/session/session-manager.ts +4 -1
  41. package/src/session/streaming-output.ts +1 -1
  42. package/src/slash-commands/builtin-registry.ts +24 -0
  43. package/src/task/executor.ts +14 -0
  44. package/src/tools/bash.ts +1 -1
  45. package/src/tools/fetch.ts +18 -6
  46. package/src/tools/fs-cache-invalidation.ts +0 -5
  47. package/src/tools/grep.ts +5 -125
  48. package/src/tools/index.ts +12 -6
  49. package/src/tools/irc.ts +258 -0
  50. package/src/tools/job.ts +489 -0
  51. package/src/tools/match-line-format.ts +8 -7
  52. package/src/tools/output-meta.ts +1 -1
  53. package/src/tools/read.ts +37 -131
  54. package/src/tools/renderers.ts +2 -0
  55. package/src/tools/todo-write.ts +243 -12
  56. package/src/tools/write.ts +2 -2
  57. package/src/utils/edit-mode.ts +1 -2
  58. package/src/utils/file-display-mode.ts +0 -3
  59. package/src/cli/read-cli.ts +0 -67
  60. package/src/commands/read.ts +0 -33
  61. package/src/edit/modes/chunk.ts +0 -832
  62. package/src/prompts/tools/cancel-job.md +0 -5
  63. package/src/prompts/tools/chunk-edit.md +0 -158
  64. package/src/prompts/tools/poll.md +0 -5
  65. package/src/prompts/tools/read-chunk.md +0 -73
  66. package/src/tools/cancel-job.ts +0 -95
  67. package/src/tools/poll-tool.ts +0 -173
@@ -12,6 +12,7 @@ import { checkPythonKernelAvailability } from "../ipy/kernel";
12
12
  import { LspTool } from "../lsp";
13
13
  import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
14
14
  import type { PlanModeState } from "../plan-mode/state";
15
+ import type { AgentRegistry } from "../registry/agent-registry";
15
16
  import type { CustomMessage } from "../session/messages";
16
17
  import type { ToolChoiceQueue } from "../session/tool-choice-queue";
17
18
  import { TaskTool } from "../task";
@@ -24,7 +25,6 @@ import { AstGrepTool } from "./ast-grep";
24
25
  import { BashTool } from "./bash";
25
26
  import { BrowserTool } from "./browser";
26
27
  import { CalculatorTool } from "./calculator";
27
- import { CancelJobTool } from "./cancel-job";
28
28
  import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
29
29
  import { DebugTool } from "./debug";
30
30
  import { ExitPlanModeTool } from "./exit-plan-mode";
@@ -32,9 +32,10 @@ import { FindTool } from "./find";
32
32
  import { GithubTool } from "./gh";
33
33
  import { GrepTool } from "./grep";
34
34
  import { InspectImageTool } from "./inspect-image";
35
+ import { IrcTool } from "./irc";
36
+ import { JobTool } from "./job";
35
37
  import { NotebookTool } from "./notebook";
36
38
  import { wrapToolWithMetaNotice } from "./output-meta";
37
- import { PollTool } from "./poll-tool";
38
39
  import { PythonTool } from "./python";
39
40
  import { ReadTool } from "./read";
40
41
  import { RenderMermaidTool } from "./render-mermaid";
@@ -62,7 +63,6 @@ export * from "./ast-grep";
62
63
  export * from "./bash";
63
64
  export * from "./browser";
64
65
  export * from "./calculator";
65
- export * from "./cancel-job";
66
66
  export * from "./checkpoint";
67
67
  export * from "./debug";
68
68
  export * from "./exit-plan-mode";
@@ -71,8 +71,9 @@ export * from "./gh";
71
71
  export * from "./grep";
72
72
  export * from "./image-gen";
73
73
  export * from "./inspect-image";
74
+ export * from "./irc";
75
+ export * from "./job";
74
76
  export * from "./notebook";
75
- export * from "./poll-tool";
76
77
  export * from "./python";
77
78
  export * from "./read";
78
79
  export * from "./render-mermaid";
@@ -135,6 +136,10 @@ export interface ToolSession {
135
136
  trackPythonExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
136
137
  /** Get session ID */
137
138
  getSessionId?: () => string | null;
139
+ /** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
140
+ getAgentId?: () => string | null;
141
+ /** Agent registry for IRC routing across live sessions. */
142
+ agentRegistry?: AgentRegistry;
138
143
  /** Get artifacts directory for artifact:// URLs */
139
144
  getArtifactsDir?: () => string | null;
140
145
  /** Allocate a new artifact path and ID for session-scoped truncated output. */
@@ -218,8 +223,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
218
223
  checkpoint: CheckpointTool.createIf,
219
224
  rewind: RewindTool.createIf,
220
225
  task: TaskTool.create,
221
- cancel_job: CancelJobTool.createIf,
222
- poll: PollTool.createIf,
226
+ job: JobTool.createIf,
227
+ irc: IrcTool.createIf,
223
228
  todo_write: s => new TodoWriteTool(s),
224
229
  web_search: s => new SearchTool(s),
225
230
  search_tool_bm25: SearchToolBm25Tool.createIf,
@@ -386,6 +391,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
386
391
  if (name === "calc") return session.settings.get("calc.enabled");
387
392
  if (name === "browser") return session.settings.get("browser.enabled");
388
393
  if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
394
+ if (name === "irc") return session.settings.get("irc.enabled");
389
395
  if (name === "task") {
390
396
  const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
391
397
  const currentDepth = session.taskDepth ?? 0;
@@ -0,0 +1,258 @@
1
+ /**
2
+ * IRC tool — agent-to-agent messaging.
3
+ *
4
+ * Lets any live agent send a short prose message to any other live agent in
5
+ * this process and (optionally) get a prose reply.
6
+ *
7
+ * Routing happens via the global AgentRegistry. Replies are produced by an
8
+ * ephemeral side-channel call (`AgentSession.respondAsBackground`) that
9
+ * mirrors `/btw`: the recipient's current model, system prompt, and message
10
+ * history are used to compute a reply without persisting it through the
11
+ * normal stream path. After the reply is generated, both the incoming
12
+ * message and the auto-reply are queued for injection into the recipient's
13
+ * persisted history (deferred until the recipient is idle), so the model
14
+ * sees the exchange on its next turn.
15
+ *
16
+ * This avoids the deadlock that arises when the recipient is blocked on a
17
+ * long-running tool call: the side-channel call does not depend on the
18
+ * recipient's main agent loop being free.
19
+ */
20
+
21
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
22
+ import { prompt } from "@oh-my-pi/pi-utils";
23
+ import { type Static, Type } from "@sinclair/typebox";
24
+ import ircDescription from "../prompts/tools/irc.md" with { type: "text" };
25
+ import type { AgentRef, AgentRegistry } from "../registry/agent-registry";
26
+ import type { ToolSession } from ".";
27
+
28
+ const ircSchema = Type.Object({
29
+ op: Type.Union(
30
+ [
31
+ Type.Literal("send", { description: "Send a message to one peer or to all peers" }),
32
+ Type.Literal("list", { description: "List currently visible peers" }),
33
+ ],
34
+ { description: "IRC operation" },
35
+ ),
36
+ to: Type.Optional(
37
+ Type.String({
38
+ description: 'Recipient agent id (e.g. "0-Main", "0-AuthLoader") or "all" to broadcast',
39
+ examples: ["0-Main", "all"],
40
+ }),
41
+ ),
42
+ message: Type.Optional(
43
+ Type.String({
44
+ description: "Message body to deliver",
45
+ examples: ["Should we use JWT or session cookies?"],
46
+ }),
47
+ ),
48
+ awaitReply: Type.Optional(
49
+ Type.Boolean({
50
+ description: "Wait for the recipient's prose reply (default: true for DM, false for broadcast)",
51
+ }),
52
+ ),
53
+ });
54
+
55
+ type IrcParams = Static<typeof ircSchema>;
56
+
57
+ interface IrcReply {
58
+ from: string;
59
+ text: string;
60
+ }
61
+
62
+ export interface IrcDetails {
63
+ op: "send" | "list";
64
+ from?: string;
65
+ to?: string;
66
+ delivered?: string[];
67
+ replies?: IrcReply[];
68
+ failed?: Array<{ id: string; error: string }>;
69
+ notFound?: string[];
70
+ peers?: Array<{ id: string; displayName: string; kind: string; status: string; parentId?: string }>;
71
+ channels?: string[];
72
+ }
73
+
74
+ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
75
+ readonly name = "irc";
76
+ readonly label = "IRC";
77
+ readonly description: string;
78
+ readonly parameters = ircSchema;
79
+ readonly strict = true;
80
+
81
+ constructor(private readonly session: ToolSession) {
82
+ this.description = prompt.render(ircDescription);
83
+ }
84
+
85
+ static createIf(session: ToolSession): IrcTool | null {
86
+ if (!session.settings.get("irc.enabled")) return null;
87
+ if (!session.agentRegistry || !session.getAgentId) return null;
88
+ return new IrcTool(session);
89
+ }
90
+
91
+ async execute(
92
+ _toolCallId: string,
93
+ params: IrcParams,
94
+ signal?: AbortSignal,
95
+ _onUpdate?: AgentToolUpdateCallback<IrcDetails>,
96
+ _context?: AgentToolContext,
97
+ ): Promise<AgentToolResult<IrcDetails>> {
98
+ const registry = this.session.agentRegistry;
99
+ const senderId = this.session.getAgentId?.() ?? null;
100
+ if (!registry) {
101
+ return errorResult("IRC is unavailable in this session.", { op: params.op });
102
+ }
103
+ if (!senderId) {
104
+ return errorResult("IRC is unavailable: caller has no agent id.", { op: params.op });
105
+ }
106
+
107
+ if (params.op === "list") {
108
+ return this.#executeList(registry, senderId);
109
+ }
110
+ if (params.op === "send") {
111
+ return this.#executeSend(registry, senderId, params, signal);
112
+ }
113
+ return errorResult("Unknown irc op.", { op: params.op as "send" | "list" });
114
+ }
115
+
116
+ #executeList(registry: AgentRegistry, senderId: string): AgentToolResult<IrcDetails> {
117
+ const peers = registry.listVisibleTo(senderId);
118
+ const lines: string[] = [];
119
+ if (peers.length === 0) {
120
+ lines.push("No other live agents.");
121
+ } else {
122
+ lines.push(`${peers.length} peer(s):`);
123
+ for (const peer of peers) {
124
+ lines.push(`- ${peer.id} [${peer.displayName} · ${peer.kind} · ${peer.status}]`);
125
+ }
126
+ }
127
+ const channels = ["all", ...peers.map(p => p.id)];
128
+ return {
129
+ content: [{ type: "text", text: lines.join("\n") }],
130
+ details: {
131
+ op: "list",
132
+ from: senderId,
133
+ peers: peers.map(p => ({
134
+ id: p.id,
135
+ displayName: p.displayName,
136
+ kind: p.kind,
137
+ status: p.status,
138
+ parentId: p.parentId,
139
+ })),
140
+ channels,
141
+ },
142
+ };
143
+ }
144
+
145
+ async #executeSend(
146
+ registry: AgentRegistry,
147
+ senderId: string,
148
+ params: IrcParams,
149
+ signal?: AbortSignal,
150
+ ): Promise<AgentToolResult<IrcDetails>> {
151
+ const to = params.to?.trim();
152
+ const message = params.message?.trim();
153
+ if (!to) {
154
+ return errorResult('`to` is required for op="send".', { op: "send", from: senderId });
155
+ }
156
+ if (!message) {
157
+ return errorResult('`message` is required for op="send".', { op: "send", from: senderId });
158
+ }
159
+
160
+ // Resolve target peers.
161
+ let targets: AgentRef[];
162
+ const notFound: string[] = [];
163
+ const isBroadcast = to === "all";
164
+ if (isBroadcast) {
165
+ targets = registry.listVisibleTo(senderId);
166
+ } else {
167
+ const ref = registry.get(to);
168
+ if (!ref || ref.id === senderId) {
169
+ notFound.push(to);
170
+ targets = [];
171
+ } else if (ref.status !== "running" && ref.status !== "idle") {
172
+ notFound.push(to);
173
+ targets = [];
174
+ } else {
175
+ targets = [ref];
176
+ }
177
+ }
178
+
179
+ const awaitReply = params.awaitReply ?? !isBroadcast;
180
+
181
+ const delivered: string[] = [];
182
+ const replies: IrcReply[] = [];
183
+ const failed: Array<{ id: string; error: string }> = [];
184
+
185
+ // Dispatch to each target in parallel via the recipient's ephemeral
186
+ // side-channel. Independent calls so a slow recipient cannot stall the
187
+ // others. The recipient's main loop never has to be unblocked: the
188
+ // side-channel runs alongside any in-flight tool call.
189
+ const dispatches = targets.map(async target => {
190
+ const targetSession = target.session;
191
+ if (!targetSession) {
192
+ notFound.push(target.id);
193
+ return;
194
+ }
195
+ try {
196
+ const result = await targetSession.respondAsBackground({
197
+ from: senderId,
198
+ message,
199
+ awaitReply,
200
+ signal,
201
+ });
202
+ delivered.push(target.id);
203
+ if (awaitReply && result.replyText) {
204
+ replies.push({ from: target.id, text: result.replyText });
205
+ }
206
+ } catch (err) {
207
+ failed.push({ id: target.id, error: err instanceof Error ? err.message : String(err) });
208
+ }
209
+ });
210
+ await Promise.all(dispatches);
211
+
212
+ const lines: string[] = [];
213
+ if (delivered.length === 0) {
214
+ lines.push("No recipients received the message.");
215
+ } else {
216
+ lines.push(`Delivered to ${delivered.length} peer(s): ${delivered.join(", ")}`);
217
+ }
218
+ if (replies.length > 0) {
219
+ lines.push("");
220
+ lines.push("## Replies");
221
+ for (const reply of replies) {
222
+ lines.push(`### ${reply.from}`);
223
+ lines.push(reply.text);
224
+ }
225
+ }
226
+ if (failed.length > 0) {
227
+ lines.push("");
228
+ lines.push("## Failed");
229
+ for (const f of failed) {
230
+ lines.push(`- ${f.id}: ${f.error}`);
231
+ }
232
+ }
233
+ if (notFound.length > 0) {
234
+ lines.push("");
235
+ lines.push(`Unknown / unavailable peers: ${notFound.join(", ")}`);
236
+ }
237
+
238
+ return {
239
+ content: [{ type: "text", text: lines.join("\n") }],
240
+ details: {
241
+ op: "send",
242
+ from: senderId,
243
+ to,
244
+ delivered,
245
+ ...(replies.length > 0 ? { replies } : {}),
246
+ ...(failed.length > 0 ? { failed } : {}),
247
+ ...(notFound.length > 0 ? { notFound } : {}),
248
+ },
249
+ };
250
+ }
251
+ }
252
+
253
+ function errorResult(text: string, details: IrcDetails): AgentToolResult<IrcDetails> {
254
+ return {
255
+ content: [{ type: "text", text }],
256
+ details,
257
+ };
258
+ }