- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt. - Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`. - Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present. - Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type. - Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`. - Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins. - Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
143 lines
5.5 KiB
TypeScript
143 lines
5.5 KiB
TypeScript
/**
|
|
* Shared extension runtime wiring for print and RPC modes.
|
|
*
|
|
* Both modes initialize the extension runner with the same action handlers
|
|
* that delegate to the {@link AgentSession}. Only error reporting, shutdown
|
|
* behavior, and UI context differ between callers — those stay as
|
|
* caller-supplied hooks.
|
|
*/
|
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
import { getSessionSlashCommands } from "../extensibility/extensions/get-commands-handler";
|
|
import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types";
|
|
import type { AgentSession } from "../session/agent-session";
|
|
import { USER_INTERRUPT_LABEL } from "../session/messages";
|
|
|
|
/** Action name for an extension-originated send failure. */
|
|
export type ExtensionSendAction = "extension_send" | "extension_send_user";
|
|
|
|
export interface InitializeExtensionsOptions {
|
|
/** Reports an error thrown by an extension-initiated send. */
|
|
reportSendError: (action: ExtensionSendAction, error: Error) => void;
|
|
/** Reports a runtime error surfaced through {@link ExtensionRunner.onError}. */
|
|
reportRuntimeError: (error: ExtensionError) => void;
|
|
/** Optional shutdown hook (rpc mode signals its loop; print mode is a no-op). */
|
|
onShutdown?: () => void;
|
|
/** Optional UI context (rpc supplies one; print runs headless). */
|
|
uiContext?: ExtensionUIContext;
|
|
/** Optional lifecycle hook for extension-originated messages that can start an agent turn. */
|
|
markAgentInvokingMessage?: () => void;
|
|
/** Optional lifecycle hook for extension-originated sends whose success/failure determines turn ownership. */
|
|
trackAgentInvokingMessage?: (task: Promise<unknown>) => void;
|
|
}
|
|
|
|
/**
|
|
* Initialize the session's extension runner with the standard action set
|
|
* shared by non-interactive modes, then emit `session_start`.
|
|
*
|
|
* No-op when the session was constructed without an extension runner.
|
|
*/
|
|
export async function initializeExtensions(session: AgentSession, options: InitializeExtensionsOptions): Promise<void> {
|
|
const runner = session.extensionRunner;
|
|
if (!runner) return;
|
|
|
|
const {
|
|
reportSendError,
|
|
reportRuntimeError,
|
|
onShutdown,
|
|
uiContext,
|
|
markAgentInvokingMessage,
|
|
trackAgentInvokingMessage,
|
|
} = options;
|
|
const shutdown = onShutdown ?? (() => {});
|
|
|
|
runner.initialize(
|
|
// ExtensionActions
|
|
{
|
|
sendMessage: (message, sendOptions) => {
|
|
const sendTask = session.sendCustomMessage(message, sendOptions);
|
|
if (sendOptions?.triggerTurn) {
|
|
if (trackAgentInvokingMessage) {
|
|
trackAgentInvokingMessage(sendTask);
|
|
} else {
|
|
markAgentInvokingMessage?.();
|
|
}
|
|
}
|
|
sendTask.catch(e => {
|
|
reportSendError("extension_send", e instanceof Error ? e : new Error(String(e)));
|
|
});
|
|
},
|
|
sendUserMessage: (content, sendOptions) => {
|
|
const sendTask = session.sendUserMessage(content, sendOptions);
|
|
if (trackAgentInvokingMessage) {
|
|
trackAgentInvokingMessage(sendTask);
|
|
} else {
|
|
markAgentInvokingMessage?.();
|
|
}
|
|
sendTask.catch(e => {
|
|
reportSendError("extension_send_user", e instanceof Error ? e : new Error(String(e)));
|
|
});
|
|
},
|
|
appendEntry: (customType, data) => {
|
|
session.sessionManager.appendCustomEntry(customType, data);
|
|
},
|
|
setLabel: (targetId, label) => {
|
|
session.sessionManager.appendLabelChange(targetId, label);
|
|
},
|
|
getActiveTools: () => session.getEnabledToolNames(),
|
|
getAllTools: () => session.getAllToolNames(),
|
|
setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
|
|
getCommands: () => getSessionSlashCommands(session),
|
|
setModel: model => runExtensionSetModel(session, model),
|
|
getThinkingLevel: () => session.thinkingLevel,
|
|
setThinkingLevel: level => session.setThinkingLevel(level),
|
|
getSessionName: () => session.sessionManager.getSessionName(),
|
|
setSessionName: async name => {
|
|
await session.sessionManager.setSessionName(name, "user");
|
|
},
|
|
},
|
|
// ExtensionContextActions
|
|
{
|
|
getModel: () => session.model,
|
|
isIdle: () => !session.isStreaming,
|
|
abort: () => session.abort({ reason: USER_INTERRUPT_LABEL }),
|
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
shutdown,
|
|
getContextUsage: () => session.getContextUsage(),
|
|
getSystemPrompt: () => session.systemPrompt,
|
|
compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
|
|
},
|
|
// ExtensionCommandContextActions — commands invokable via prompt("/command")
|
|
{
|
|
getContextUsage: () => session.getContextUsage(),
|
|
waitForIdle: () => session.agent.waitForIdle(),
|
|
newSession: async newOptions => {
|
|
const success = await session.newSession({ parentSession: newOptions?.parentSession });
|
|
if (success && newOptions?.setup) {
|
|
await newOptions.setup(session.sessionManager);
|
|
}
|
|
return { cancelled: !success };
|
|
},
|
|
branch: async entryId => {
|
|
const result = await session.branch(entryId);
|
|
return { cancelled: result.cancelled };
|
|
},
|
|
navigateTree: async (targetId, navOptions) => {
|
|
const result = await session.navigateTree(targetId, { summarize: navOptions?.summarize });
|
|
return { cancelled: result.cancelled };
|
|
},
|
|
switchSession: async sessionPath => {
|
|
const success = await session.switchSession(sessionPath);
|
|
return { cancelled: !success };
|
|
},
|
|
reload: async () => {
|
|
await session.reload();
|
|
},
|
|
compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
|
|
},
|
|
uiContext,
|
|
);
|
|
|
|
runner.onError(reportRuntimeError);
|
|
await runner.emit({ type: "session_start" });
|
|
}
|