@oh-my-pi/pi-coding-agent 15.10.11 → 15.10.12
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 +44 -0
- package/dist/cli.js +5349 -5328
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli-commands.d.ts +12 -0
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
- package/dist/types/eval/py/executor.d.ts +5 -0
- package/dist/types/eval/py/kernel.d.ts +6 -1
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/runtime.d.ts +4 -0
- package/dist/types/memory-backend/types.d.ts +66 -1
- package/dist/types/modes/index.d.ts +3 -3
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/oauth-manual-input.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
- package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
- package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
- package/dist/types/modes/setup-wizard/index.d.ts +5 -1
- package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/secrets/index.d.ts +1 -1
- package/dist/types/secrets/obfuscator.d.ts +8 -2
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/streaming-output.d.ts +23 -0
- package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
- package/dist/types/slash-commands/types.d.ts +1 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/index.d.ts +2 -2
- package/dist/types/task/types.d.ts +8 -0
- package/dist/types/thinking.d.ts +4 -0
- package/dist/types/tiny/title-client.d.ts +11 -0
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/utils/git.d.ts +15 -2
- package/dist/types/utils/title-generator.d.ts +3 -2
- package/package.json +10 -10
- package/src/auto-thinking/classifier.ts +1 -0
- package/src/cli/args.ts +3 -0
- package/src/cli-commands.ts +29 -0
- package/src/cli.ts +8 -9
- package/src/commands/launch.ts +4 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/api-key-resolver.ts +8 -6
- package/src/config/model-registry.ts +97 -30
- package/src/config/model-resolver.ts +60 -0
- package/src/config/settings-schema.ts +43 -15
- package/src/config/settings.ts +61 -3
- package/src/edit/hashline/execute.ts +39 -2
- package/src/edit/hashline/noop-loop-guard.ts +99 -0
- package/src/eval/completion-bridge.ts +1 -0
- package/src/eval/py/executor.ts +29 -7
- package/src/eval/py/index.ts +6 -1
- package/src/eval/py/kernel.ts +31 -11
- package/src/eval/py/runtime.ts +37 -0
- package/src/exec/bash-executor.ts +82 -3
- package/src/extensibility/extensions/get-commands-handler.ts +2 -1
- package/src/extensibility/extensions/runner.ts +6 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/hindsight/bank.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/main.ts +18 -6
- package/src/memories/index.ts +2 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/local-backend.ts +9 -0
- package/src/memory-backend/off-backend.ts +9 -0
- package/src/memory-backend/runtime.ts +66 -0
- package/src/memory-backend/types.ts +81 -1
- package/src/mnemopi/backend.ts +151 -4
- package/src/modes/acp/acp-agent.ts +119 -11
- package/src/modes/components/assistant-message.ts +19 -21
- package/src/modes/components/footer.ts +3 -1
- package/src/modes/components/status-line/component.ts +118 -34
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/mcp-command-controller.ts +38 -3
- package/src/modes/index.ts +3 -21
- package/src/modes/interactive-mode.ts +39 -9
- package/src/modes/oauth-manual-input.ts +30 -3
- package/src/modes/rpc/rpc-client.ts +154 -3
- package/src/modes/rpc/rpc-mode.ts +97 -12
- package/src/modes/rpc/rpc-subagents.ts +265 -0
- package/src/modes/rpc/rpc-types.ts +81 -1
- package/src/modes/setup-wizard/index.ts +12 -2
- package/src/modes/setup-wizard/lazy.ts +16 -0
- package/src/modes/types.ts +2 -0
- package/src/sdk.ts +8 -1
- package/src/secrets/index.ts +8 -1
- package/src/secrets/obfuscator.ts +39 -18
- package/src/session/agent-session.ts +179 -54
- package/src/session/streaming-output.ts +166 -10
- package/src/slash-commands/acp-builtins.ts +24 -0
- package/src/slash-commands/builtin-registry.ts +20 -0
- package/src/slash-commands/types.ts +1 -1
- package/src/system-prompt.ts +14 -0
- package/src/task/executor.ts +13 -12
- package/src/task/index.ts +9 -8
- package/src/task/render.ts +18 -3
- package/src/task/types.ts +9 -0
- package/src/thinking.ts +7 -0
- package/src/tiny/title-client.ts +34 -5
- package/src/tiny/title-protocol.ts +1 -1
- package/src/tiny/worker.ts +6 -4
- package/src/tools/bash.ts +46 -5
- package/src/tools/image-gen.ts +11 -4
- package/src/tools/index.ts +13 -1
- package/src/tools/inspect-image.ts +1 -0
- package/src/utils/commit-message-generator.ts +1 -0
- package/src/utils/git.ts +267 -13
- package/src/utils/title-generator.ts +24 -5
package/src/modes/index.ts
CHANGED
|
@@ -8,27 +8,9 @@ import { postmortem } from "@oh-my-pi/pi-utils";
|
|
|
8
8
|
* barrel does not pull print, RPC server, or ACP server mode into the normal
|
|
9
9
|
* TUI graph.
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
type ModelInfo,
|
|
15
|
-
RpcClient,
|
|
16
|
-
type RpcClientCustomTool,
|
|
17
|
-
type RpcClientOptions,
|
|
18
|
-
type RpcClientToolContext,
|
|
19
|
-
type RpcClientToolResult,
|
|
20
|
-
type RpcEventListener,
|
|
21
|
-
} from "./rpc/rpc-client";
|
|
22
|
-
export type {
|
|
23
|
-
RpcCommand,
|
|
24
|
-
RpcHostToolCallRequest,
|
|
25
|
-
RpcHostToolCancelRequest,
|
|
26
|
-
RpcHostToolDefinition,
|
|
27
|
-
RpcHostToolResult,
|
|
28
|
-
RpcHostToolUpdate,
|
|
29
|
-
RpcResponse,
|
|
30
|
-
RpcSessionState,
|
|
31
|
-
} from "./rpc/rpc-types";
|
|
11
|
+
export * from "./interactive-mode";
|
|
12
|
+
export * from "./rpc/rpc-client";
|
|
13
|
+
export * from "./rpc/rpc-types";
|
|
32
14
|
|
|
33
15
|
postmortem.register("terminal-restore", () => {
|
|
34
16
|
emergencyTerminalRestore();
|
|
@@ -59,6 +59,7 @@ import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slas
|
|
|
59
59
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
60
60
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
61
61
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
62
|
+
import type { MCPManager } from "../mcp";
|
|
62
63
|
import {
|
|
63
64
|
humanizePlanTitle,
|
|
64
65
|
type PlanApprovalDetails,
|
|
@@ -74,8 +75,10 @@ import { HistoryStorage } from "../session/history-storage";
|
|
|
74
75
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
75
76
|
import { getRecentSessions } from "../session/session-manager";
|
|
76
77
|
import type { ShakeMode } from "../session/shake-types";
|
|
78
|
+
import { BUILTIN_SLASH_COMMAND_RESERVED_NAMES } from "../slash-commands/builtin-registry";
|
|
77
79
|
import { formatDuration } from "../slash-commands/helpers/format";
|
|
78
80
|
import { STTController, type SttState } from "../stt";
|
|
81
|
+
import { discoverTitleSystemPromptFile, resolvePromptInput } from "../system-prompt";
|
|
79
82
|
import type { LspStartupServerInfo } from "../tools";
|
|
80
83
|
import { normalizeLocalScheme } from "../tools/path-utils";
|
|
81
84
|
import { setAutoQaConsentHandler } from "../tools/report-tool-issue";
|
|
@@ -123,6 +126,7 @@ import {
|
|
|
123
126
|
} from "./loop-limit";
|
|
124
127
|
import { OAuthManualInputManager } from "./oauth-manual-input";
|
|
125
128
|
import { SessionObserverRegistry } from "./session-observer-registry";
|
|
129
|
+
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
|
126
130
|
import { interruptHint } from "./shared";
|
|
127
131
|
import { type ShimmerPalette, shimmerEnabled, shimmerSegments, shimmerText } from "./theme/shimmer";
|
|
128
132
|
import type { Theme } from "./theme/theme";
|
|
@@ -260,6 +264,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
260
264
|
keybindings: KeybindingsManager;
|
|
261
265
|
agent: Agent;
|
|
262
266
|
historyStorage?: HistoryStorage;
|
|
267
|
+
titleSystemPrompt?: string;
|
|
263
268
|
|
|
264
269
|
ui: TUI;
|
|
265
270
|
chatContainer: TranscriptContainer;
|
|
@@ -349,7 +354,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
349
354
|
#planReviewOverlay: PlanReviewOverlay | undefined;
|
|
350
355
|
#planReviewOverlayHandle: OverlayHandle | undefined;
|
|
351
356
|
readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
|
|
352
|
-
mcpManager?:
|
|
357
|
+
mcpManager?: MCPManager;
|
|
353
358
|
readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
|
|
354
359
|
|
|
355
360
|
readonly #btwController: BtwController;
|
|
@@ -380,8 +385,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
380
385
|
changelogMarkdown: string | undefined = undefined,
|
|
381
386
|
setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
|
|
382
387
|
lspServers: LspStartupServerInfo[] | undefined = undefined,
|
|
383
|
-
mcpManager?:
|
|
388
|
+
mcpManager?: MCPManager,
|
|
384
389
|
eventBus?: EventBus,
|
|
390
|
+
titleSystemPrompt?: string,
|
|
385
391
|
) {
|
|
386
392
|
this.session = session;
|
|
387
393
|
this.sessionManager = session.sessionManager;
|
|
@@ -394,6 +400,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
394
400
|
this.lspServers = lspServers;
|
|
395
401
|
this.mcpManager = mcpManager;
|
|
396
402
|
this.#eventBus = eventBus;
|
|
403
|
+
this.titleSystemPrompt = titleSystemPrompt;
|
|
397
404
|
if (eventBus) {
|
|
398
405
|
this.#eventBusUnsubscribers.push(
|
|
399
406
|
eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
|
|
@@ -447,9 +454,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
447
454
|
|
|
448
455
|
this.hideThinkingBlock = settings.get("hideThinkingBlock");
|
|
449
456
|
|
|
450
|
-
const builtinCommandNames = new Set(BUILTIN_SLASH_COMMANDS.map(c => c.name));
|
|
451
457
|
const hookCommands: SlashCommand[] = (
|
|
452
|
-
this.session.extensionRunner?.getRegisteredCommands(
|
|
458
|
+
this.session.extensionRunner?.getRegisteredCommands(BUILTIN_SLASH_COMMAND_RESERVED_NAMES) ?? []
|
|
453
459
|
).map(cmd => ({
|
|
454
460
|
name: cmd.name,
|
|
455
461
|
description: cmd.description ?? "(hook command)",
|
|
@@ -679,6 +685,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
679
685
|
this.updateEditorTopBorder();
|
|
680
686
|
}
|
|
681
687
|
|
|
688
|
+
/** Reload the title-generation system prompt override for the provided working directory. */
|
|
689
|
+
async refreshTitleSystemPrompt(cwd?: string): Promise<void> {
|
|
690
|
+
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
691
|
+
const titleSystemPromptSource = discoverTitleSystemPromptFile(basePath);
|
|
692
|
+
this.titleSystemPrompt = await resolvePromptInput(titleSystemPromptSource, "title system prompt");
|
|
693
|
+
}
|
|
694
|
+
|
|
682
695
|
/** Reload slash commands and autocomplete for the provided working directory. */
|
|
683
696
|
async refreshSlashCommandState(cwd?: string): Promise<void> {
|
|
684
697
|
const basePath = cwd ?? this.sessionManager.getCwd();
|
|
@@ -713,6 +726,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
713
726
|
// Re-warm plugin roots, capabilities, slash commands, and the ssh tool so
|
|
714
727
|
// the next prompt sees everything scoped to the new project directory.
|
|
715
728
|
clearClaudePluginRootsCache();
|
|
729
|
+
await this.refreshTitleSystemPrompt(newCwd);
|
|
716
730
|
resetCapabilities();
|
|
717
731
|
await this.refreshSlashCommandState(newCwd);
|
|
718
732
|
await this.session.refreshSshTool({ activateIfAvailable: true });
|
|
@@ -1694,6 +1708,15 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1694
1708
|
}
|
|
1695
1709
|
}
|
|
1696
1710
|
|
|
1711
|
+
async #hasPlanModeDraftContent(planFilePath: string): Promise<boolean> {
|
|
1712
|
+
const candidates = new Set<string>([planFilePath, ...(await this.#listLocalPlanFiles())]);
|
|
1713
|
+
for (const candidate of candidates) {
|
|
1714
|
+
const content = await this.#readPlanFile(candidate);
|
|
1715
|
+
if (content !== null && content.trim().length > 0) return true;
|
|
1716
|
+
}
|
|
1717
|
+
return false;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1697
1720
|
/** `local://` URLs of plan files in the session-local root, newest first.
|
|
1698
1721
|
* A fallback for `resolveApprovedPlan` when the agent dropped `extra.title`,
|
|
1699
1722
|
* so the plan it wrote is still found by scanning recent `*-plan.md` files. */
|
|
@@ -1998,11 +2021,14 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1998
2021
|
return;
|
|
1999
2022
|
}
|
|
2000
2023
|
if (this.planModeEnabled) {
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2024
|
+
const planFilePath = this.planModePlanFilePath ?? (await this.#getPlanFilePath());
|
|
2025
|
+
if (await this.#hasPlanModeDraftContent(planFilePath)) {
|
|
2026
|
+
const confirmed = await this.showHookConfirm(
|
|
2027
|
+
"Exit plan mode?",
|
|
2028
|
+
"This exits plan mode without approving a plan.",
|
|
2029
|
+
);
|
|
2030
|
+
if (!confirmed) return;
|
|
2031
|
+
}
|
|
2006
2032
|
await this.#exitPlanMode({ paused: true });
|
|
2007
2033
|
return;
|
|
2008
2034
|
}
|
|
@@ -3153,6 +3179,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3153
3179
|
return this.#selectorController.showOAuthSelector(mode, providerId);
|
|
3154
3180
|
}
|
|
3155
3181
|
|
|
3182
|
+
showProviderSetup(): Promise<void> {
|
|
3183
|
+
return runProviderSetupWizard(this);
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3156
3186
|
showHookConfirm(title: string, message: string): Promise<boolean> {
|
|
3157
3187
|
return this.#extensionUiController.showHookConfirm(title, message);
|
|
3158
3188
|
}
|
|
@@ -3,6 +3,10 @@ type PendingInput = {
|
|
|
3
3
|
resolve: (value: string) => void;
|
|
4
4
|
reject: (error: Error) => void;
|
|
5
5
|
};
|
|
6
|
+
type ClaimedInput = {
|
|
7
|
+
promise: Promise<string>;
|
|
8
|
+
clear: (reason?: string) => void;
|
|
9
|
+
};
|
|
6
10
|
|
|
7
11
|
export class OAuthManualInputManager {
|
|
8
12
|
#pending?: PendingInput;
|
|
@@ -12,9 +16,27 @@ export class OAuthManualInputManager {
|
|
|
12
16
|
this.clear("Manual OAuth input superseded by a new login");
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
const
|
|
16
|
-
this.#pending =
|
|
17
|
-
return promise;
|
|
19
|
+
const pending = this.#createPending(providerId);
|
|
20
|
+
this.#pending = pending;
|
|
21
|
+
return pending.promise;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
tryWaitForInput(providerId: string): Promise<string> | undefined {
|
|
25
|
+
if (this.#pending) return undefined;
|
|
26
|
+
return this.waitForInput(providerId);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
tryClaimInput(providerId: string): ClaimedInput | undefined {
|
|
30
|
+
if (this.#pending) return undefined;
|
|
31
|
+
const pending = this.#createPending(providerId);
|
|
32
|
+
this.#pending = pending;
|
|
33
|
+
return {
|
|
34
|
+
promise: pending.promise,
|
|
35
|
+
clear: (reason?: string) => {
|
|
36
|
+
if (this.#pending !== pending) return;
|
|
37
|
+
this.clear(reason);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
submit(input: string): boolean {
|
|
@@ -39,4 +61,9 @@ export class OAuthManualInputManager {
|
|
|
39
61
|
get pendingProviderId(): string | undefined {
|
|
40
62
|
return this.#pending?.providerId;
|
|
41
63
|
}
|
|
64
|
+
|
|
65
|
+
#createPending(providerId: string): PendingInput & { promise: Promise<string> } {
|
|
66
|
+
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
|
67
|
+
return { providerId, resolve, reject, promise };
|
|
68
|
+
}
|
|
42
69
|
}
|
|
@@ -9,8 +9,9 @@ import type { AgentEvent, AgentMessage, AgentToolResult, ThinkingLevel } from "@
|
|
|
9
9
|
import type { CompactionResult } from "@oh-my-pi/pi-agent-core/compaction";
|
|
10
10
|
import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
|
|
12
|
+
import type { FileSink } from "bun";
|
|
12
13
|
import type { BashResult } from "../../exec/bash-executor";
|
|
13
|
-
import type { SessionStats } from "../../session/agent-session";
|
|
14
|
+
import type { AgentSessionEvent, SessionStats } from "../../session/agent-session";
|
|
14
15
|
import type {
|
|
15
16
|
RpcCommand,
|
|
16
17
|
RpcExtensionUIRequest,
|
|
@@ -22,6 +23,12 @@ import type {
|
|
|
22
23
|
RpcHostToolUpdate,
|
|
23
24
|
RpcResponse,
|
|
24
25
|
RpcSessionState,
|
|
26
|
+
RpcSubagentEventFrame,
|
|
27
|
+
RpcSubagentLifecycleFrame,
|
|
28
|
+
RpcSubagentMessagesResult,
|
|
29
|
+
RpcSubagentProgressFrame,
|
|
30
|
+
RpcSubagentSnapshot,
|
|
31
|
+
RpcSubagentSubscriptionLevel,
|
|
25
32
|
} from "./rpc-types";
|
|
26
33
|
|
|
27
34
|
/** Distributive Omit that works with union types */
|
|
@@ -52,6 +59,10 @@ export interface RpcClientOptions {
|
|
|
52
59
|
export type ModelInfo = Pick<Model, "provider" | "id" | "contextWindow" | "reasoning" | "thinking">;
|
|
53
60
|
|
|
54
61
|
export type RpcEventListener = (event: AgentEvent) => void;
|
|
62
|
+
export type RpcSessionEventListener = (event: AgentSessionEvent) => void;
|
|
63
|
+
export type RpcSubagentLifecycleListener = (payload: RpcSubagentLifecycleFrame["payload"]) => void;
|
|
64
|
+
export type RpcSubagentProgressListener = (payload: RpcSubagentProgressFrame["payload"]) => void;
|
|
65
|
+
export type RpcSubagentEventListener = (payload: RpcSubagentEventFrame["payload"]) => void;
|
|
55
66
|
|
|
56
67
|
export interface RpcClientToolContext<TDetails = unknown> {
|
|
57
68
|
toolCallId: string;
|
|
@@ -92,6 +103,23 @@ const agentEventTypes = new Set<AgentEvent["type"]>([
|
|
|
92
103
|
"tool_execution_end",
|
|
93
104
|
]);
|
|
94
105
|
|
|
106
|
+
const sessionEventTypes = new Set<AgentSessionEvent["type"]>([
|
|
107
|
+
...agentEventTypes,
|
|
108
|
+
"auto_compaction_start",
|
|
109
|
+
"auto_compaction_end",
|
|
110
|
+
"auto_retry_start",
|
|
111
|
+
"auto_retry_end",
|
|
112
|
+
"retry_fallback_applied",
|
|
113
|
+
"retry_fallback_succeeded",
|
|
114
|
+
"ttsr_triggered",
|
|
115
|
+
"todo_reminder",
|
|
116
|
+
"todo_auto_clear",
|
|
117
|
+
"irc_message",
|
|
118
|
+
"notice",
|
|
119
|
+
"thinking_level_changed",
|
|
120
|
+
"goal_updated",
|
|
121
|
+
]);
|
|
122
|
+
|
|
95
123
|
function isRpcResponse(value: unknown): value is RpcResponse {
|
|
96
124
|
if (!isRecord(value)) return false;
|
|
97
125
|
if (value.type !== "response") return false;
|
|
@@ -111,6 +139,28 @@ function isAgentEvent(value: unknown): value is AgentEvent {
|
|
|
111
139
|
return agentEventTypes.has(type as AgentEvent["type"]);
|
|
112
140
|
}
|
|
113
141
|
|
|
142
|
+
function isAgentSessionEvent(value: unknown): value is AgentSessionEvent {
|
|
143
|
+
if (!isRecord(value)) return false;
|
|
144
|
+
const type = value.type;
|
|
145
|
+
if (typeof type !== "string") return false;
|
|
146
|
+
return sessionEventTypes.has(type as AgentSessionEvent["type"]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isRpcSubagentLifecycleFrame(value: unknown): value is RpcSubagentLifecycleFrame {
|
|
150
|
+
if (!isRecord(value)) return false;
|
|
151
|
+
return value.type === "subagent_lifecycle" && isRecord(value.payload);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isRpcSubagentProgressFrame(value: unknown): value is RpcSubagentProgressFrame {
|
|
155
|
+
if (!isRecord(value)) return false;
|
|
156
|
+
return value.type === "subagent_progress" && isRecord(value.payload);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isRpcSubagentEventFrame(value: unknown): value is RpcSubagentEventFrame {
|
|
160
|
+
if (!isRecord(value)) return false;
|
|
161
|
+
return value.type === "subagent_event" && isRecord(value.payload);
|
|
162
|
+
}
|
|
163
|
+
|
|
114
164
|
function isRpcHostToolCallRequest(value: unknown): value is RpcHostToolCallRequest {
|
|
115
165
|
if (!isRecord(value)) return false;
|
|
116
166
|
return (
|
|
@@ -148,6 +198,10 @@ function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): A
|
|
|
148
198
|
export class RpcClient {
|
|
149
199
|
#process: ptree.ChildProcess | null = null;
|
|
150
200
|
#eventListeners: RpcEventListener[] = [];
|
|
201
|
+
#sessionEventListeners: RpcSessionEventListener[] = [];
|
|
202
|
+
#subagentLifecycleListeners = new Set<RpcSubagentLifecycleListener>();
|
|
203
|
+
#subagentProgressListeners = new Set<RpcSubagentProgressListener>();
|
|
204
|
+
#subagentEventListeners = new Set<RpcSubagentEventListener>();
|
|
151
205
|
#pendingRequests: Map<string, { resolve: (response: RpcResponse) => void; reject: (error: Error) => void }> =
|
|
152
206
|
new Map();
|
|
153
207
|
#customTools: RpcClientCustomTool[] = [];
|
|
@@ -286,6 +340,43 @@ export class RpcClient {
|
|
|
286
340
|
};
|
|
287
341
|
}
|
|
288
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Subscribe to all top-level session events, including non-core session state events.
|
|
345
|
+
*/
|
|
346
|
+
onSessionEvent(listener: RpcSessionEventListener): () => void {
|
|
347
|
+
this.#sessionEventListeners.push(listener);
|
|
348
|
+
return () => {
|
|
349
|
+
const index = this.#sessionEventListeners.indexOf(listener);
|
|
350
|
+
if (index !== -1) {
|
|
351
|
+
this.#sessionEventListeners.splice(index, 1);
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Subscribe to subagent lifecycle frames after setSubagentSubscription("progress" | "events").
|
|
358
|
+
*/
|
|
359
|
+
onSubagentLifecycle(listener: RpcSubagentLifecycleListener): () => void {
|
|
360
|
+
this.#subagentLifecycleListeners.add(listener);
|
|
361
|
+
return () => this.#subagentLifecycleListeners.delete(listener);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Subscribe to aggregated subagent progress frames after setSubagentSubscription("progress" | "events").
|
|
366
|
+
*/
|
|
367
|
+
onSubagentProgress(listener: RpcSubagentProgressListener): () => void {
|
|
368
|
+
this.#subagentProgressListeners.add(listener);
|
|
369
|
+
return () => this.#subagentProgressListeners.delete(listener);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Subscribe to raw subagent session events. Call setSubagentSubscription(\"events\") to enable them server-side.
|
|
374
|
+
*/
|
|
375
|
+
onSubagentEvent(listener: RpcSubagentEventListener): () => void {
|
|
376
|
+
this.#subagentEventListeners.add(listener);
|
|
377
|
+
return () => this.#subagentEventListeners.delete(listener);
|
|
378
|
+
}
|
|
379
|
+
|
|
289
380
|
/**
|
|
290
381
|
* Get collected stderr output (useful for debugging).
|
|
291
382
|
*/
|
|
@@ -358,6 +449,40 @@ export class RpcClient {
|
|
|
358
449
|
return this.#getData(response);
|
|
359
450
|
}
|
|
360
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Configure subagent frames emitted by the RPC server. Servers default to "off".
|
|
454
|
+
* "progress" emits lifecycle/progress frames; "events" additionally emits raw subagent session events.
|
|
455
|
+
*/
|
|
456
|
+
async setSubagentSubscription(level: RpcSubagentSubscriptionLevel): Promise<RpcSubagentSubscriptionLevel> {
|
|
457
|
+
const response = await this.#send({ type: "set_subagent_subscription", level });
|
|
458
|
+
return this.#getData<{ level: RpcSubagentSubscriptionLevel }>(response).level;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Return the RPC server's current subagent snapshot.
|
|
463
|
+
*/
|
|
464
|
+
async getSubagents(): Promise<RpcSubagentSnapshot[]> {
|
|
465
|
+
const response = await this.#send({ type: "get_subagents" });
|
|
466
|
+
return this.#getData<{ subagents: RpcSubagentSnapshot[] }>(response).subagents;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Read persisted transcript entries for a tracked subagent session.
|
|
471
|
+
*/
|
|
472
|
+
async getSubagentMessages(selector: {
|
|
473
|
+
subagentId?: string;
|
|
474
|
+
sessionFile?: string;
|
|
475
|
+
fromByte?: number;
|
|
476
|
+
}): Promise<RpcSubagentMessagesResult> {
|
|
477
|
+
const response = await this.#send({
|
|
478
|
+
type: "get_subagent_messages",
|
|
479
|
+
subagentId: selector.subagentId,
|
|
480
|
+
sessionFile: selector.sessionFile,
|
|
481
|
+
fromByte: selector.fromByte,
|
|
482
|
+
});
|
|
483
|
+
return this.#getData<RpcSubagentMessagesResult>(response);
|
|
484
|
+
}
|
|
485
|
+
|
|
361
486
|
/**
|
|
362
487
|
* Set model by provider and ID.
|
|
363
488
|
*/
|
|
@@ -679,9 +804,35 @@ export class RpcClient {
|
|
|
679
804
|
return;
|
|
680
805
|
}
|
|
681
806
|
|
|
807
|
+
if (isRpcSubagentLifecycleFrame(data)) {
|
|
808
|
+
for (const listener of this.#subagentLifecycleListeners) {
|
|
809
|
+
listener(data.payload);
|
|
810
|
+
}
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (isRpcSubagentProgressFrame(data)) {
|
|
815
|
+
for (const listener of this.#subagentProgressListeners) {
|
|
816
|
+
listener(data.payload);
|
|
817
|
+
}
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (isRpcSubagentEventFrame(data)) {
|
|
822
|
+
for (const listener of this.#subagentEventListeners) {
|
|
823
|
+
listener(data.payload);
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (!isAgentSessionEvent(data)) return;
|
|
829
|
+
|
|
830
|
+
for (const listener of this.#sessionEventListeners) {
|
|
831
|
+
listener(data);
|
|
832
|
+
}
|
|
833
|
+
|
|
682
834
|
if (!isAgentEvent(data)) return;
|
|
683
835
|
|
|
684
|
-
// Otherwise it's an event
|
|
685
836
|
for (const listener of this.#eventListeners) {
|
|
686
837
|
listener(data);
|
|
687
838
|
}
|
|
@@ -789,7 +940,7 @@ export class RpcClient {
|
|
|
789
940
|
if (!this.#process?.stdin) {
|
|
790
941
|
throw new Error("Client not started");
|
|
791
942
|
}
|
|
792
|
-
const stdin = this.#process.stdin as
|
|
943
|
+
const stdin = this.#process.stdin as FileSink;
|
|
793
944
|
stdin.write(`${JSON.stringify(frame)}\n`);
|
|
794
945
|
const flushResult = stdin.flush();
|
|
795
946
|
if (isPromise(flushResult)) {
|
|
@@ -21,9 +21,11 @@ import {
|
|
|
21
21
|
} from "../../extensibility/extensions";
|
|
22
22
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
23
23
|
import type { AgentSession } from "../../session/agent-session";
|
|
24
|
+
import type { EventBus } from "../../utils/event-bus";
|
|
24
25
|
import { initializeExtensions } from "../runtime-init";
|
|
25
26
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
26
27
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
28
|
+
import { RpcSubagentRegistry, readRpcSubagentTranscript } from "./rpc-subagents";
|
|
27
29
|
import type {
|
|
28
30
|
RpcCommand,
|
|
29
31
|
RpcExtensionUIRequest,
|
|
@@ -35,6 +37,7 @@ import type {
|
|
|
35
37
|
RpcHostUriRequest,
|
|
36
38
|
RpcResponse,
|
|
37
39
|
RpcSessionState,
|
|
40
|
+
RpcSubagentSubscriptionLevel,
|
|
38
41
|
} from "./rpc-types";
|
|
39
42
|
|
|
40
43
|
// Re-export types for consumers
|
|
@@ -56,6 +59,47 @@ type RpcOutput = (
|
|
|
56
59
|
| object,
|
|
57
60
|
) => void;
|
|
58
61
|
|
|
62
|
+
export type RpcSessionChangeCommand = Extract<
|
|
63
|
+
RpcCommand,
|
|
64
|
+
{ type: "new_session" } | { type: "switch_session" } | { type: "branch" }
|
|
65
|
+
>;
|
|
66
|
+
|
|
67
|
+
export type RpcSessionChangeResult =
|
|
68
|
+
| { type: "new_session"; data: { cancelled: boolean } }
|
|
69
|
+
| { type: "switch_session"; data: { cancelled: boolean } }
|
|
70
|
+
| { type: "branch"; data: { text: string; cancelled: boolean } };
|
|
71
|
+
|
|
72
|
+
export type RpcSessionChangeSession = Pick<AgentSession, "newSession" | "switchSession" | "branch">;
|
|
73
|
+
export type RpcSubagentResetRegistry = Pick<RpcSubagentRegistry, "clear">;
|
|
74
|
+
|
|
75
|
+
export async function handleRpcSessionChange(
|
|
76
|
+
session: RpcSessionChangeSession,
|
|
77
|
+
command: RpcSessionChangeCommand,
|
|
78
|
+
subagentRegistry?: RpcSubagentResetRegistry,
|
|
79
|
+
): Promise<RpcSessionChangeResult> {
|
|
80
|
+
switch (command.type) {
|
|
81
|
+
case "new_session": {
|
|
82
|
+
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
|
|
83
|
+
const cancelled = !(await session.newSession(options));
|
|
84
|
+
if (!cancelled) subagentRegistry?.clear();
|
|
85
|
+
return { type: "new_session", data: { cancelled } };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "switch_session": {
|
|
89
|
+
const cancelled = !(await session.switchSession(command.sessionPath));
|
|
90
|
+
if (!cancelled) subagentRegistry?.clear();
|
|
91
|
+
return { type: "switch_session", data: { cancelled } };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "branch": {
|
|
95
|
+
const result = await session.branch(command.entryId);
|
|
96
|
+
if (!result.cancelled) subagentRegistry?.clear();
|
|
97
|
+
return { type: "branch", data: { text: result.selectedText, cancelled: result.cancelled } };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
throw new Error("Unsupported RPC session change command");
|
|
101
|
+
}
|
|
102
|
+
|
|
59
103
|
function normalizeHostToolDefinitions(tools: RpcHostToolDefinition[]): RpcHostToolDefinition[] {
|
|
60
104
|
return tools.map((tool, index) => {
|
|
61
105
|
const name = typeof tool.name === "string" ? tool.name.trim() : "";
|
|
@@ -99,6 +143,10 @@ function shouldEmitRpcTitles(): boolean {
|
|
|
99
143
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
100
144
|
}
|
|
101
145
|
|
|
146
|
+
function isSubagentSubscriptionLevel(value: unknown): value is RpcSubagentSubscriptionLevel {
|
|
147
|
+
return value === "off" || value === "progress" || value === "events";
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
export function requestRpcEditor(
|
|
103
151
|
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
104
152
|
output: RpcOutput,
|
|
@@ -169,6 +217,7 @@ export function requestRpcEditor(
|
|
|
169
217
|
export async function runRpcMode(
|
|
170
218
|
session: AgentSession,
|
|
171
219
|
setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
220
|
+
eventBus?: EventBus,
|
|
172
221
|
): Promise<never> {
|
|
173
222
|
// Signal to RPC clients that the server is ready to accept commands
|
|
174
223
|
// Suppress terminal notifications: they write \x07 (BEL) or OSC sequences directly to
|
|
@@ -201,6 +250,7 @@ export async function runRpcMode(
|
|
|
201
250
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
202
251
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
203
252
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
253
|
+
const subagentRegistry = eventBus ? new RpcSubagentRegistry(eventBus, output) : undefined;
|
|
204
254
|
|
|
205
255
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
206
256
|
const shutdownState = { requested: false };
|
|
@@ -507,9 +557,8 @@ export async function runRpcMode(
|
|
|
507
557
|
}
|
|
508
558
|
|
|
509
559
|
case "new_session": {
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
return success(id, "new_session", { cancelled });
|
|
560
|
+
const result = await handleRpcSessionChange(session, command, subagentRegistry);
|
|
561
|
+
return success(id, result.type, result.data);
|
|
513
562
|
}
|
|
514
563
|
|
|
515
564
|
// =================================================================
|
|
@@ -564,6 +613,44 @@ export async function runRpcMode(
|
|
|
564
613
|
}
|
|
565
614
|
}
|
|
566
615
|
|
|
616
|
+
case "set_subagent_subscription": {
|
|
617
|
+
if (!subagentRegistry) {
|
|
618
|
+
return error(id, "set_subagent_subscription", "Subagent event bus is unavailable");
|
|
619
|
+
}
|
|
620
|
+
if (!isSubagentSubscriptionLevel(command.level)) {
|
|
621
|
+
return error(
|
|
622
|
+
id,
|
|
623
|
+
"set_subagent_subscription",
|
|
624
|
+
`Invalid subagent subscription level: ${String(command.level)}`,
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
subagentRegistry.setSubscriptionLevel(command.level);
|
|
628
|
+
return success(id, "set_subagent_subscription", { level: subagentRegistry.getSubscriptionLevel() });
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
case "get_subagents": {
|
|
632
|
+
if (!subagentRegistry) {
|
|
633
|
+
return error(id, "get_subagents", "Subagent event bus is unavailable");
|
|
634
|
+
}
|
|
635
|
+
return success(id, "get_subagents", { subagents: subagentRegistry.getSubagents() });
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
case "get_subagent_messages": {
|
|
639
|
+
if (!subagentRegistry) {
|
|
640
|
+
return error(id, "get_subagent_messages", "Subagent event bus is unavailable");
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
if (command.fromByte !== undefined && !Number.isFinite(command.fromByte)) {
|
|
644
|
+
return error(id, "get_subagent_messages", "fromByte must be a finite number");
|
|
645
|
+
}
|
|
646
|
+
const sessionFile = subagentRegistry.resolveSessionFile(command);
|
|
647
|
+
const transcript = await readRpcSubagentTranscript(sessionFile, command.fromByte);
|
|
648
|
+
return success(id, "get_subagent_messages", transcript);
|
|
649
|
+
} catch (err) {
|
|
650
|
+
return error(id, "get_subagent_messages", err instanceof Error ? err.message : String(err));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
567
654
|
// =================================================================
|
|
568
655
|
// Model
|
|
569
656
|
// =================================================================
|
|
@@ -683,14 +770,10 @@ export async function runRpcMode(
|
|
|
683
770
|
return success(id, "export_html", { path });
|
|
684
771
|
}
|
|
685
772
|
|
|
686
|
-
case "switch_session":
|
|
687
|
-
const cancelled = !(await session.switchSession(command.sessionPath));
|
|
688
|
-
return success(id, "switch_session", { cancelled });
|
|
689
|
-
}
|
|
690
|
-
|
|
773
|
+
case "switch_session":
|
|
691
774
|
case "branch": {
|
|
692
|
-
const result = await session
|
|
693
|
-
return success(id,
|
|
775
|
+
const result = await handleRpcSessionChange(session, command, subagentRegistry);
|
|
776
|
+
return success(id, result.type, result.data);
|
|
694
777
|
}
|
|
695
778
|
|
|
696
779
|
case "get_branch_messages": {
|
|
@@ -850,13 +933,15 @@ export async function runRpcMode(
|
|
|
850
933
|
|
|
851
934
|
// Check for deferred shutdown request (idle between commands)
|
|
852
935
|
await checkShutdownRequested();
|
|
853
|
-
} catch (e:
|
|
854
|
-
|
|
936
|
+
} catch (e: unknown) {
|
|
937
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
938
|
+
output(error(undefined, "parse", `Failed to parse command: ${message}`));
|
|
855
939
|
}
|
|
856
940
|
}
|
|
857
941
|
|
|
858
942
|
// stdin closed — RPC client is gone, exit cleanly
|
|
859
943
|
hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
|
|
860
944
|
hostUriBridge.clear("RPC client disconnected before host URI request completed");
|
|
945
|
+
subagentRegistry?.dispose();
|
|
861
946
|
process.exit(0);
|
|
862
947
|
}
|