Files
paseo/packages/server/src/server/agent/tools/paseo-tools.ts
T
nllptrxandMohamed Boudra a8ebd390fa feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only)

Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged.

- Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry.
- GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved.
- forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client).
- createPullRequest reports typed CLI/auth errors instead of a generic message.
- forge-resolver host/remote caches are LRU-bounded.
- Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com.
- Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology.
- Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs.

* feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals

GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals.

- threadIsResolved is part of the neutral timeline item.
- Pipeline load failures show an error instead of an empty section.
- Manual pipeline jobs render as pending.
- Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request).

* feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg)

One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts.

- CI status aggregates commit statuses and Actions runs together.
- Gitea's terminal "warning" state maps to failure on server and client.
- Gitea Actions check details are reachable from the PR pane by workflowRunId.

* refactor(forge): localize compatibility handling

* test(forge): expect normalized GitLab facts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-17 15:03:26 +08:00

2784 lines
85 KiB
TypeScript

import { z } from "zod";
import { ensureValidJson } from "../../json-utils.js";
import type { Logger } from "pino";
import type { AgentMode, AgentProvider } from "../agent-sdk-types.js";
import type { AgentManager } from "../agent-manager.js";
import {
AgentFeatureSchema,
AgentPermissionRequestPayloadSchema,
AgentListItemPayloadSchema,
AgentPermissionResponseSchema,
AgentSnapshotPayloadSchema,
} from "../../messages.js";
import type { AgentListItemPayload } from "../../messages.js";
import {
buildStoredAgentPayload,
toAgentListItemPayload,
toAgentPayload,
} from "../agent-projections.js";
import { curateAgentActivity } from "../activity-curator.js";
import { selectItemsByProjectedLimit } from "../timeline-projection.js";
import type { AgentStorage } from "../agent-storage.js";
import { ensureAgentLoaded } from "../agent-loading.js";
import { isStoredAgentProviderAvailable } from "../../persistence-hooks.js";
import {
killTerminalsForWorkspace,
type ArchiveDependencies,
} from "../../workspace-archive-service.js";
import { createAgentCommand, type CreateAgentFromMcpInput } from "../create-agent/create.js";
import type { VoiceCallerContext, VoiceSpeakHandler } from "../../voice-types.js";
import type { FirstAgentContext } from "../../messages.js";
import { expandUserPath, isSameOrDescendantPath, resolvePathFromBase } from "../../path-utils.js";
import type { TerminalManager } from "../../../terminal/terminal-manager.js";
import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js";
import type { ScheduleService } from "../../schedule/service.js";
import {
ScheduleRunSchema,
ScheduleSummarySchema,
StoredScheduleSchema,
type ScheduleCadence,
type UpdateScheduleInput,
} from "@getpaseo/protocol/schedule/types";
import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
import {
AgentModelSchema,
AgentProviderEnum,
AgentStatusEnum,
ProviderModeSchema,
ProviderSummarySchema,
parseDurationString,
resolveRequiredProviderModel,
sanitizePermissionRequest,
serializeSnapshotWithMetadata,
toScheduleSummary,
waitForAgentWithTimeout,
} from "../mcp-shared.js";
import { sendPromptToAgent, setupFinishNotification } from "../agent-prompt.js";
import { respondToAgentPermission } from "../permission-response.js";
import {
archiveAgentCommand,
cancelAgentRunCommand,
closeAgentCommand,
setAgentModeCommand,
updateAgentCommand,
} from "../lifecycle-command.js";
import type { ForgeService } from "../../../services/forge-service.js";
import type { WorkspaceGitService } from "../../workspace-git-service.js";
import type { WorkspaceRegistry } from "../../workspace-registry.js";
import { WorktreeRequestError } from "../../worktree-errors.js";
import {
archiveCommand,
type ArchiveCommandDependencies,
createPaseoWorktreeCommand,
type CreatePaseoWorktreeCommandInput,
listPaseoWorktreesCommand,
} from "../../worktree/commands.js";
import { registerBrowserTools } from "../../browser-tools/tools.js";
import type { BrowserToolsBroker } from "../../browser-tools/broker.js";
import type {
PaseoToolCatalog,
PaseoToolConfig,
PaseoToolDefinition,
PaseoToolExecutionContext,
PaseoToolResult,
} from "./types.js";
export interface PaseoToolHostDependencies {
agentManager: AgentManager;
agentStorage: AgentStorage;
terminalManager?: TerminalManager | null;
getDaemonTcpPort?: () => number | null;
scheduleService?: ScheduleService | null;
providerSnapshotManager: ProviderSnapshotManager;
github?: ForgeService;
workspaceGitService?: Pick<
WorkspaceGitService,
"getSnapshot" | "listWorktrees" | "resolveRepoRoot"
>;
findWorkspaceIdForCwd?: ArchiveDependencies["findWorkspaceIdForCwd"];
listActiveWorkspaces?: ArchiveDependencies["listActiveWorkspaces"];
archiveWorkspaceRecord?: ArchiveDependencies["archiveWorkspaceRecord"];
emitWorkspaceUpdatesForWorkspaceIds?: ArchiveDependencies["emitWorkspaceUpdatesForWorkspaceIds"];
workspaceRegistry?: Pick<WorkspaceRegistry, "get" | "upsert">;
markWorkspaceArchiving?: ArchiveDependencies["markWorkspaceArchiving"];
clearWorkspaceArchiving?: ArchiveDependencies["clearWorkspaceArchiving"];
createPaseoWorktree?: CreatePaseoWorktreeWorkflowFn;
// Mints a fresh directory workspace for a cwd and returns its id.
ensureWorkspaceForCreate?: (
cwd: string,
firstAgentContext?: FirstAgentContext,
) => Promise<string>;
browserToolsEnabled?: boolean;
browserToolsBroker?: BrowserToolsBroker | null;
paseoHome?: string;
worktreesRoot?: string;
/**
* ID of the agent that is using this tool catalog.
* Used for cwd/mode inheritance when agents spawn child agents.
*/
callerAgentId?: string;
/**
* Optional resolver for session-bound speak handlers.
* Used by hidden voice agents to narrate through daemon-managed TTS.
*/
resolveSpeakHandler?: (callerAgentId: string) => VoiceSpeakHandler | null;
resolveCallerContext?: (callerAgentId: string) => VoiceCallerContext | null;
enableVoiceTools?: boolean;
voiceOnly?: boolean;
logger: Logger;
}
function parseTimestamp(value: string | null | undefined): number {
if (!value) {
return 0;
}
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? 0 : parsed;
}
function resolveAgentListActivityTime(agent: AgentListItemPayload): number {
return Math.max(
parseTimestamp(agent.updatedAt),
parseTimestamp(agent.lastUserMessageAt),
parseTimestamp(agent.attentionTimestamp),
parseTimestamp(agent.archivedAt),
parseTimestamp(agent.createdAt),
);
}
interface ProviderSummary {
id: AgentProvider;
label: string;
description: string;
enabled: boolean;
modes: AgentMode[];
status: string;
error?: string;
}
function toProviderSummary(entry: {
provider: AgentProvider;
label?: string;
description?: string;
enabled: boolean;
modes?: AgentMode[];
status: string;
error?: string;
}): ProviderSummary {
return {
id: entry.provider,
label: entry.label ?? entry.provider,
description: entry.description ?? "",
enabled: entry.enabled,
modes: entry.modes ?? [],
status: entry.status === "ready" ? "available" : entry.status,
...(entry.error ? { error: entry.error } : {}),
};
}
function compareAgentListItems(a: AgentListItemPayload, b: AgentListItemPayload): number {
const attentionDelta =
Number(b.requiresAttention ?? false) - Number(a.requiresAttention ?? false);
if (attentionDelta !== 0) {
return attentionDelta;
}
const statusOrder = {
running: 0,
initializing: 1,
idle: 2,
error: 3,
closed: 4,
} as Record<string, number>;
const statusDelta = (statusOrder[a.status] ?? 999) - (statusOrder[b.status] ?? 999);
if (statusDelta !== 0) {
return statusDelta;
}
return resolveAgentListActivityTime(b) - resolveAgentListActivityTime(a);
}
function resolveScheduleProviderAndModel(params: {
provider?: string;
defaultProvider: AgentProvider;
}): { provider: AgentProvider; model?: string } {
const providerInput = params.provider?.trim() || params.defaultProvider;
const slashIndex = providerInput.indexOf("/");
if (slashIndex === -1) {
return { provider: providerInput };
}
const provider = providerInput.slice(0, slashIndex).trim();
const model = providerInput.slice(slashIndex + 1).trim();
if (!provider || !model) {
throw new Error("provider must be <provider> or <provider>/<model>");
}
return {
provider: provider,
model,
};
}
function resolveScheduleUpdateProviderAndModel(params: {
provider?: string;
model?: string | null;
}): { provider?: string; model?: string | null } {
const providerInput = params.provider?.trim();
const modelInput = typeof params.model === "string" ? params.model.trim() : params.model;
if (params.model !== undefined && modelInput === "") {
throw new Error("model cannot be empty");
}
if (!providerInput) {
return params.model !== undefined ? { model: modelInput } : {};
}
const slashIndex = providerInput.indexOf("/");
if (slashIndex === -1) {
return {
provider: providerInput,
...(params.model !== undefined ? { model: modelInput } : {}),
};
}
const provider = providerInput.slice(0, slashIndex).trim();
const modelFromProvider = providerInput.slice(slashIndex + 1).trim();
if (!provider || !modelFromProvider) {
throw new Error("provider must be <provider> or <provider>/<model>");
}
if (params.model === null) {
throw new Error("provider specifies a model but model is null");
}
if (typeof modelInput === "string" && modelInput !== modelFromProvider) {
throw new Error("Conflicting model values provided");
}
return {
provider,
model: modelInput ?? modelFromProvider,
};
}
interface ScheduleUpdateToolInput {
id: string;
every?: string;
cron?: string;
timezone?: string;
name?: string | null;
prompt?: string;
maxRuns?: number | null;
provider?: string;
model?: string | null;
mode?: string | null;
cwd?: string;
expiresIn?: string;
clearExpires?: boolean;
}
function normalizeScheduleCadenceArg(value: string | undefined): string | undefined {
if (value === undefined) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
return trimmed;
}
function normalizeScheduleTimeZoneArg(value: string | undefined): string | undefined {
return normalizeScheduleCadenceArg(value);
}
function resolveScheduleUpdateCadence(input: ScheduleUpdateToolInput): ScheduleCadence | undefined {
const every = normalizeScheduleCadenceArg(input.every);
const cron = normalizeScheduleCadenceArg(input.cron);
const timeZone = normalizeScheduleTimeZoneArg(input.timezone);
if (every !== undefined && cron !== undefined) {
throw new Error("Specify at most one of every or cron");
}
if (timeZone !== undefined && cron === undefined) {
throw new Error("timezone can only be used with cron");
}
if (every !== undefined) {
return { type: "every", everyMs: parseDurationString(every) };
}
if (cron !== undefined) {
return {
type: "cron",
expression: cron,
...(timeZone !== undefined ? { timezone: timeZone } : {}),
};
}
return undefined;
}
function resolveScheduleUpdateExpiresAt(input: ScheduleUpdateToolInput): string | null | undefined {
if (input.expiresIn !== undefined && input.clearExpires) {
throw new Error("Specify at most one of expiresIn or clearExpires");
}
if (input.expiresIn !== undefined) {
return new Date(Date.now() + parseDurationString(input.expiresIn)).toISOString();
}
if (input.clearExpires) {
return null;
}
return undefined;
}
function buildScheduleUpdateInput(input: ScheduleUpdateToolInput): UpdateScheduleInput {
const cadence = resolveScheduleUpdateCadence(input);
const expiresAt = resolveScheduleUpdateExpiresAt(input);
const providerModelPatch = resolveScheduleUpdateProviderAndModel({
provider: input.provider,
model: input.model,
});
const newAgentConfig = {
...(providerModelPatch.provider !== undefined ? { provider: providerModelPatch.provider } : {}),
...(providerModelPatch.model !== undefined ? { model: providerModelPatch.model } : {}),
...(input.mode !== undefined ? { modeId: input.mode } : {}),
...(input.cwd !== undefined ? { cwd: input.cwd } : {}),
};
return {
id: input.id,
...(input.name !== undefined ? { name: input.name } : {}),
...(input.prompt !== undefined ? { prompt: input.prompt } : {}),
...(cadence !== undefined ? { cadence } : {}),
...(input.maxRuns !== undefined ? { maxRuns: input.maxRuns } : {}),
...(expiresAt !== undefined ? { expiresAt } : {}),
...(Object.keys(newAgentConfig).length > 0 ? { newAgentConfig } : {}),
};
}
function resolveChildAgentCwd(params: {
parentCwd: string;
requestedCwd?: string;
lockedCwd?: string;
allowCustomCwd: boolean;
}): string {
const lockedCwd = params.lockedCwd?.trim();
if (lockedCwd) {
return expandUserPath(lockedCwd);
}
const requestedCwd = params.requestedCwd?.trim();
if (!requestedCwd || !params.allowCustomCwd) {
return params.parentCwd;
}
return resolvePathFromBase(params.parentCwd, requestedCwd);
}
const TerminalSummarySchema = z.object({
id: z.string(),
name: z.string(),
cwd: z.string(),
});
const WorktreeSummarySchema = z.object({
path: z.string(),
createdAt: z.string(),
branchName: z.string().optional(),
head: z.string().optional(),
});
function resolveTerminalKeyToken(key: string, literal: boolean): string {
if (literal) {
return key;
}
switch (key) {
case "Enter":
return "\r";
case "Tab":
return "\t";
case "Escape":
return "\u001b";
case "Space":
return " ";
case "BSpace":
return "\u007f";
case "C-c":
return "\u0003";
case "C-d":
return "\u0004";
case "C-z":
return "\u001a";
case "C-l":
return "\u000c";
case "C-a":
return "\u0001";
case "C-e":
return "\u0005";
default:
return key;
}
}
export function createPaseoToolCatalog(options: PaseoToolHostDependencies): PaseoToolCatalog {
const {
agentManager,
agentStorage,
terminalManager,
scheduleService,
providerSnapshotManager,
callerAgentId,
resolveSpeakHandler,
resolveCallerContext,
logger,
} = options;
const childLogger = logger.child({ module: "agent", component: "paseo-tool-catalog" });
const callerContext = callerAgentId ? (resolveCallerContext?.(callerAgentId) ?? null) : null;
const parseToolInput = async (tool: PaseoToolDefinition, input: unknown): Promise<unknown> => {
const inputSchema = tool.inputSchema;
if (!inputSchema) {
return input;
}
const schema =
typeof inputSchema === "object" &&
inputSchema !== null &&
typeof (inputSchema as { safeParseAsync?: unknown }).safeParseAsync === "function"
? (inputSchema as z.ZodType)
: z.object(inputSchema as z.ZodRawShape).passthrough();
return schema.parseAsync(input);
};
const tools = new Map<string, PaseoToolDefinition>();
const registerTool = (
name: string,
config: PaseoToolConfig,
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Tool handlers are schema-validated at registration boundaries.
handler: (input: any, context: PaseoToolExecutionContext) => Promise<PaseoToolResult>,
) => {
tools.set(name, {
name,
title: config.title,
description: config.description ?? name,
inputSchema: config.inputSchema,
outputSchema: config.outputSchema,
handler: handler as PaseoToolDefinition["handler"],
});
};
const toCatalog = (): PaseoToolCatalog => ({
tools,
getTool(name: string): PaseoToolDefinition | undefined {
return tools.get(name);
},
async executeTool(
name: string,
input: unknown,
context: PaseoToolExecutionContext = {},
): Promise<PaseoToolResult> {
const tool = tools.get(name);
if (!tool) {
throw new Error(`Paseo tool not found: ${name}`);
}
return tool.handler(await parseToolInput(tool, input), context);
},
});
const buildCronScheduleCadence = (input: {
cron: string | undefined;
timezone?: string;
}): ScheduleCadence => {
const expression = input.cron?.trim() ?? "";
if (!expression) {
throw new Error("cron is required");
}
const timezone = normalizeScheduleTimeZoneArg(input.timezone);
return {
type: "cron",
expression,
...(timezone !== undefined ? { timezone } : {}),
};
};
const buildScheduleExpiry = (expiresIn: string | undefined): string | undefined => {
return expiresIn === undefined
? undefined
: new Date(Date.now() + parseDurationString(expiresIn)).toISOString();
};
const resolveCallerAgent = () => {
if (!callerAgentId) {
return null;
}
const parentAgent = agentManager.getAgent(callerAgentId);
if (!parentAgent) {
throw new Error(`Parent agent ${callerAgentId} not found`);
}
return parentAgent;
};
const resolveScopedCwd = (requestedCwd?: string, opts?: { required?: boolean }): string => {
const callerAgent = resolveCallerAgent();
if (callerAgent) {
return resolveChildAgentCwd({
parentCwd: callerAgent.cwd,
requestedCwd,
lockedCwd: callerContext?.lockedCwd,
allowCustomCwd: callerContext?.allowCustomCwd ?? true,
});
}
const trimmedCwd = requestedCwd?.trim();
if (!trimmedCwd) {
if (opts?.required) {
throw new Error("cwd is required");
}
throw new Error("cwd is required outside an agent-scoped session");
}
return expandUserPath(trimmedCwd);
};
async function resolveTerminalWorkspaceId(resolvedCwd: string): Promise<string> {
// An agent-spawned terminal belongs to the caller agent's workspace. Only if
// the caller has no workspace do we mint one for the cwd.
const callerAgent = callerAgentId ? agentManager.getAgent(callerAgentId) : null;
if (callerAgent?.workspaceId) {
return callerAgent.workspaceId;
}
if (!options.ensureWorkspaceForCreate) {
throw new Error(
callerAgentId
? `Caller agent ${callerAgentId} has no workspace and workspace minting is not configured`
: "workspaceId is required outside an agent-scoped session",
);
}
return options.ensureWorkspaceForCreate(resolvedCwd);
}
function resolveWorkspaceIdForRename(requestedWorkspaceId?: string): string {
const explicitWorkspaceId = requestedWorkspaceId?.trim();
if (explicitWorkspaceId) {
return explicitWorkspaceId;
}
if (callerAgentId) {
const callerAgent = resolveCallerAgent();
if (!callerAgent?.workspaceId) {
throw new Error(`Caller agent ${callerAgentId} has no current workspace`);
}
return callerAgent.workspaceId;
}
throw new Error("workspaceId is required outside an agent-scoped session");
}
const buildCallerAgentScheduleConfigExtras = (
callerAgent: NonNullable<ReturnType<typeof resolveCallerAgent>>,
): Record<string, unknown> => {
return {
...(callerAgent.config.thinkingOptionId
? { thinkingOptionId: callerAgent.config.thinkingOptionId }
: {}),
...(callerAgent.config.approvalPolicy
? { approvalPolicy: callerAgent.config.approvalPolicy }
: {}),
...(callerAgent.config.sandboxMode ? { sandboxMode: callerAgent.config.sandboxMode } : {}),
...(typeof callerAgent.config.networkAccess === "boolean"
? { networkAccess: callerAgent.config.networkAccess }
: {}),
...(typeof callerAgent.config.webSearch === "boolean"
? { webSearch: callerAgent.config.webSearch }
: {}),
...(callerAgent.config.title ? { title: callerAgent.config.title } : {}),
...(callerAgent.config.extra ? { extra: callerAgent.config.extra } : {}),
...(callerAgent.config.featureValues
? { featureValues: callerAgent.config.featureValues }
: {}),
...(callerAgent.config.systemPrompt ? { systemPrompt: callerAgent.config.systemPrompt } : {}),
...(callerAgent.config.mcpServers ? { mcpServers: callerAgent.config.mcpServers } : {}),
};
};
const buildCallerAgentScheduleConfig = (
callerAgent: NonNullable<ReturnType<typeof resolveCallerAgent>>,
params?: { provider?: string; cwd?: string },
) => {
const hasProviderOverride = params?.provider !== undefined;
const resolvedProviderModel = hasProviderOverride
? resolveScheduleProviderAndModel({
provider: params?.provider,
defaultProvider: callerAgent.provider,
})
: null;
const resolvedProvider = resolvedProviderModel?.provider ?? callerAgent.provider;
let resolvedModel: string | undefined;
if (resolvedProviderModel?.model) {
resolvedModel = resolvedProviderModel.model;
} else if (!hasProviderOverride && callerAgent.config.model) {
resolvedModel = callerAgent.config.model;
}
return {
provider: resolvedProvider,
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : callerAgent.cwd,
...(callerAgent.currentModeId && callerAgent.provider === resolvedProvider
? {
modeId: callerAgent.currentModeId,
}
: {}),
...(resolvedModel ? { model: resolvedModel } : {}),
...buildCallerAgentScheduleConfigExtras(callerAgent),
};
};
const resolveNewAgentScheduleTarget = (params?: { provider?: string; cwd?: string }) => {
if (!params?.provider?.trim()) {
throw new Error("provider is required when target is new-agent");
}
const callerAgent = resolveCallerAgent();
if (callerAgent) {
return {
type: "new-agent" as const,
config: buildCallerAgentScheduleConfig(callerAgent, params),
};
}
const resolvedProviderModel = resolveScheduleProviderAndModel({
provider: params?.provider,
defaultProvider: params.provider,
});
return {
type: "new-agent" as const,
config: {
provider: resolvedProviderModel.provider,
cwd: params?.cwd?.trim() ? expandUserPath(params.cwd) : process.cwd(),
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
},
};
};
const ProviderModelInputSchema = AgentProviderEnum.trim()
.refine((value) => value.includes("/"), {
message: "provider must be provider/model, for example codex/gpt-5.4",
})
.refine(
(value) => {
try {
resolveRequiredProviderModel(value);
return true;
} catch {
return false;
}
},
{ message: "provider must be provider/model, for example codex/gpt-5.4" },
);
const ProviderOrProviderModelInputSchema = AgentProviderEnum.trim()
.min(1, "provider is required")
.refine(
(value) => {
if (!value.includes("/")) {
return true;
}
try {
resolveRequiredProviderModel(value);
return true;
} catch {
return false;
}
},
{ message: "provider must be provider or provider/model, for example codex/gpt-5.4" },
);
const CreateAgentSettingsInputSchema = z
.object({
modeId: z.string().optional().describe("Session mode to configure before the first run."),
thinkingOptionId: z.string().optional().describe("Thinking option ID."),
features: z
.record(z.string(), z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
})
.strict();
const UpdateAgentSettingsInputSchema = z
.object({
modeId: z.string().optional().describe("Session mode ID."),
model: z.string().nullable().optional().describe("Model ID. Pass null to clear."),
thinkingOptionId: z
.string()
.nullable()
.optional()
.describe("Thinking option ID. Pass null to clear."),
features: z
.record(z.string(), z.unknown())
.optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
})
.strict();
const InspectProviderSettingsInputSchema = z
.object({
modeId: z.string().optional().describe("Draft session mode ID."),
model: z.string().optional().describe("Draft model ID."),
thinkingOptionId: z.string().optional().describe("Draft thinking option ID."),
features: z
.record(z.string(), z.unknown())
.optional()
.describe("Draft provider feature values."),
})
.strict();
const AgentRelationshipInputSchema = z.discriminatedUnion("kind", [
z
.object({ kind: z.literal("subagent") })
.strict()
.describe("Create a child agent under this agent's subagent track."),
z
.object({ kind: z.literal("detached") })
.strict()
.describe("Create a root agent that does not appear in this agent's subagent track."),
]);
const AgentCreateWorktreeTargetInputSchema = z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("branch-off"),
worktreeSlug: z
.string()
.min(1)
.optional()
.describe("Optional worktree slug/path label. Omit to let Paseo generate one."),
branchName: z
.string()
.min(1)
.optional()
.describe("Optional git branch name. Defaults to the worktree slug."),
baseBranch: z
.string()
.min(1)
.optional()
.describe("Optional base branch. Defaults to the repository default branch."),
})
.strict()
.describe("Create a new branch in a new Paseo worktree."),
z
.object({
kind: z.literal("checkout-branch"),
branch: z.string().min(1).describe("Existing branch to check out."),
})
.strict()
.describe("Check out an existing branch in a new Paseo worktree."),
z
.object({
kind: z.literal("checkout-pr"),
githubPrNumber: z.number().int().positive().describe("GitHub pull request number."),
})
.strict()
.describe("Check out a GitHub pull request in a new Paseo worktree."),
]);
const AgentWorkspaceInputSchema = z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("current"),
cwd: z.string().optional().describe("Optional runtime cwd. Defaults to the caller's cwd."),
})
.strict()
.describe("Use the caller's current workspace."),
z
.object({
kind: z.literal("existing"),
workspaceId: z.string().min(1).describe("Existing workspace id to attach the agent to."),
cwd: z
.string()
.optional()
.describe("Optional runtime cwd. Defaults to the existing workspace cwd."),
})
.strict()
.describe("Attach the agent to an existing workspace."),
z
.object({
kind: z.literal("create"),
source: z.discriminatedUnion("kind", [
z
.object({
kind: z.literal("directory"),
path: z
.string()
.optional()
.describe("Optional directory path. Defaults to the caller's cwd."),
})
.strict(),
z
.object({
kind: z.literal("worktree"),
cwd: z
.string()
.optional()
.describe("Optional source repository. Defaults to the caller's cwd."),
target: AgentCreateWorktreeTargetInputSchema,
})
.strict(),
]),
})
.strict()
.describe("Create a new workspace for the agent."),
]);
const commonCreateAgentInputSchema = {
relationship: AgentRelationshipInputSchema.describe(
"Whether the created agent is a subagent under you or a detached root agent.",
),
workspace: AgentWorkspaceInputSchema.describe(
"Workspace ownership/location for the created agent.",
),
title: z
.string()
.trim()
.min(1, "Title is required")
.max(60, "Title must be 60 characters or fewer")
.describe("Short descriptive title (<= 60 chars) summarizing the agent's focus."),
provider: ProviderModelInputSchema.describe(
"Required provider/model pair, for example codex/gpt-5.4.",
),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
settings: CreateAgentSettingsInputSchema.optional().describe(
"Initial runtime settings for the new agent.",
),
initialPrompt: z
.string()
.trim()
.min(1, "initialPrompt is required")
.describe("Required first task to run immediately after creation."),
};
const agentToAgentInputSchema = {
...commonCreateAgentInputSchema,
notifyOnFinish: z
.boolean()
.optional()
.default(true)
.describe(
"Get notified when the created agent finishes, errors, or needs permission. Set false only for truly fire-and-forget agents.",
),
};
const canonicalTopLevelInputSchema = {
...commonCreateAgentInputSchema,
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Agent-scoped only: get notified when the created agent finishes, errors, or needs permission.",
),
};
const legacyTopLevelCreateAgentInputSchema = {
relationship: commonCreateAgentInputSchema.relationship.optional(),
workspace: commonCreateAgentInputSchema.workspace.optional(),
cwd: z
.string()
.optional()
.describe("Legacy top-level working directory. Prefer workspace.source.path."),
mode: z.string().optional().describe("Legacy session mode ID. Prefer settings.modeId."),
thinking: z
.string()
.optional()
.describe("Legacy thinking option ID. Prefer settings.thinkingOptionId."),
features: z
.record(z.string(), z.unknown())
.optional()
.describe("Legacy feature values. Prefer settings.features."),
worktreeName: z
.string()
.min(1)
.optional()
.describe("Legacy worktree slug. Prefer workspace.source.target.worktreeSlug."),
branchName: z
.string()
.min(1)
.optional()
.describe("Legacy branch name. Prefer workspace.source.target.branchName."),
baseBranch: z
.string()
.min(1)
.optional()
.describe("Legacy base branch. Prefer workspace.source.target.baseBranch."),
refName: z
.string()
.min(1)
.optional()
.describe("Legacy branch/ref to check out. Prefer workspace.source.target.branch."),
githubPrNumber: z
.number()
.int()
.positive()
.optional()
.describe("Legacy GitHub PR number. Prefer workspace.source.target.githubPrNumber."),
};
const topLevelInputSchema = {
...canonicalTopLevelInputSchema,
...legacyTopLevelCreateAgentInputSchema,
};
const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema;
const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict();
const canonicalTopLevelCreateAgentArgsSchema = z.object(canonicalTopLevelInputSchema).strict();
const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict();
const commonSendAgentPromptInputSchema = {
agentId: z.string(),
prompt: z.string(),
sessionMode: z.string().optional().describe("Optional mode to set before running the prompt."),
};
const agentToAgentSendAgentPromptInputSchema = {
...commonSendAgentPromptInputSchema,
background: z
.boolean()
.optional()
.default(true)
.describe(
"Run agent in background. Agent-scoped default is true so you can continue until the finish notification arrives. Set false only when you need a blocking response.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(true)
.describe(
"Get notified when the prompted agent finishes, errors, or needs permission. Set false only for truly fire-and-forget prompts.",
),
};
const topLevelSendAgentPromptInputSchema = {
...commonSendAgentPromptInputSchema,
background: z
.boolean()
.optional()
.default(false)
.describe(
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately.",
),
notifyOnFinish: z
.boolean()
.optional()
.default(false)
.describe(
"Agent-scoped only: get notified when the prompted agent finishes, errors, or needs permission.",
),
};
const sendAgentPromptInputSchema = callerAgentId
? agentToAgentSendAgentPromptInputSchema
: topLevelSendAgentPromptInputSchema;
const inspectProviderInputSchema = {
provider: ProviderOrProviderModelInputSchema.describe(
"Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).",
),
cwd: z
.string()
.optional()
.describe("Working directory used to resolve provider feature availability."),
settings: InspectProviderSettingsInputSchema.optional().describe(
"Draft provider settings used to compute available features.",
),
};
type AgentToAgentCreateAgentArgs = z.infer<typeof agentToAgentCreateAgentArgsSchema>;
type TopLevelCreateAgentArgs = z.infer<typeof canonicalTopLevelCreateAgentArgsSchema>;
type TopLevelCreateAgentToolArgs = z.infer<typeof topLevelCreateAgentArgsSchema>;
if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) {
registerTool(
"speak",
{
title: "Speak",
description:
"Speak text to the user via daemon-managed voice output. Blocks until playback completes.",
inputSchema: {
text: z
.string()
.trim()
.min(1, "text is required")
.max(4000, "text must be 4000 characters or fewer"),
},
outputSchema: {
ok: z.boolean(),
},
},
async (args, context) => {
if (!callerAgentId) {
throw new Error("speak is only available to agent-scoped tool sessions");
}
const handler = resolveSpeakHandler?.(callerAgentId) ?? null;
if (!handler) {
throw new Error(`No speak handler registered for your session '${callerAgentId}'`);
}
await handler({
text: args.text,
callerAgentId,
signal: context?.signal,
});
return {
content: [],
structuredContent: ensureValidJson({ ok: true }),
};
},
);
}
if (options.voiceOnly) {
return toCatalog();
}
if (options.browserToolsEnabled && options.browserToolsBroker) {
registerBrowserTools({
registerTool,
broker: options.browserToolsBroker,
callerAgentId,
resolveCallerAgent,
});
}
registerTool(
"create_agent",
{
title: "Create agent",
description:
"Create an agent. Requires relationship, workspace, provider/model (for example codex/gpt-5.4), and an initial prompt. Do not guess; call list_providers and list_models first if uncertain.",
inputSchema: createAgentInputSchema,
outputSchema: {
agentId: z.string(),
type: AgentProviderEnum,
status: AgentStatusEnum,
cwd: z.string(),
workspaceId: z.string().optional(),
currentModeId: z.string().nullable(),
availableModes: z.array(ProviderModeSchema),
lastMessage: z.string().nullable().optional(),
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
guidance: z.string().optional(),
},
},
async (args: unknown) => {
const resolvedArgs = await resolveCreateAgentToolArgs(args);
const { parsedArgs, worktree } = resolvedArgs;
let requestedBackground: boolean;
let notifyOnFinish: boolean;
let detached: boolean;
if (resolvedArgs.kind === "agent-scoped") {
requestedBackground = true;
notifyOnFinish = parsedArgs.notifyOnFinish;
detached = resolvedArgs.relationship.kind === "detached";
} else {
requestedBackground = resolvedArgs.parsedArgs.background;
notifyOnFinish = resolvedArgs.parsedArgs.notifyOnFinish ?? false;
detached = resolvedArgs.parsedArgs.relationship.kind === "detached";
}
const {
snapshot,
background: createdInBackground,
initialPromptStarted,
} = await createAgentCommand(
{
agentManager,
agentStorage,
logger: childLogger,
paseoHome: options.paseoHome,
worktreesRoot: options.worktreesRoot,
terminalManager,
providerSnapshotManager,
createPaseoWorktree: options.createPaseoWorktree,
...(options.ensureWorkspaceForCreate
? { ensureWorkspaceForCreate: options.ensureWorkspaceForCreate }
: {}),
},
{
kind: "mcp",
provider: parsedArgs.provider,
title: parsedArgs.title,
initialPrompt: parsedArgs.initialPrompt,
cwd: resolvedArgs.cwd,
workspaceId: resolvedArgs.workspaceId,
thinking: parsedArgs.settings?.thinkingOptionId,
features: parsedArgs.settings?.features,
labels: parsedArgs.labels,
mode: parsedArgs.settings?.modeId,
background: requestedBackground,
notifyOnFinish,
detached,
callerAgentId,
callerContext,
worktree,
},
);
try {
if (!createdInBackground && initialPromptStarted) {
const result = await waitForAgentWithTimeout(agentManager, snapshot.id, {
waitForActive: true,
});
const liveSnapshot = agentManager.getAgent(snapshot.id) ?? snapshot;
const responseData = {
agentId: snapshot.id,
type: snapshot.provider,
status: result.status,
cwd: liveSnapshot.cwd,
...(liveSnapshot.workspaceId ? { workspaceId: liveSnapshot.workspaceId } : {}),
currentModeId: liveSnapshot.currentModeId,
availableModes: liveSnapshot.availableModes,
lastMessage: result.lastMessage,
permission: sanitizePermissionRequest(result.permission),
};
const validJson = ensureValidJson(responseData);
const response = {
content: [],
structuredContent: validJson,
};
return response;
}
} catch (error) {
childLogger.error({ err: error, agentId: snapshot.id }, "Failed to run initial prompt");
throw error;
}
// Return immediately for async creation.
const currentSnapshot = agentManager.getAgent(snapshot.id) ?? snapshot;
const guidance =
callerAgentId && notifyOnFinish && initialPromptStarted
? "You will get notified when the created agent finishes, errors, or needs permission. Do not poll for status; continue with other work until the notification arrives."
: undefined;
const response = {
content: [],
structuredContent: ensureValidJson({
agentId: currentSnapshot.id,
type: snapshot.provider,
status: currentSnapshot.lifecycle,
cwd: currentSnapshot.cwd,
...(currentSnapshot.workspaceId ? { workspaceId: currentSnapshot.workspaceId } : {}),
currentModeId: currentSnapshot.currentModeId,
availableModes: currentSnapshot.availableModes,
lastMessage: null,
permission: null,
...(guidance ? { guidance } : {}),
}),
};
return response;
},
);
type ResolvedCreateAgentToolArgs =
| {
kind: "agent-scoped";
parsedArgs: AgentToAgentCreateAgentArgs;
relationship: AgentToAgentCreateAgentArgs["relationship"];
cwd: string | undefined;
workspaceId: string | undefined;
worktree: CreateAgentFromMcpInput["worktree"];
}
| {
kind: "top-level";
parsedArgs: TopLevelCreateAgentArgs;
cwd: string | undefined;
workspaceId: string | undefined;
worktree: CreateAgentFromMcpInput["worktree"];
};
async function resolveCreateAgentToolArgs(args: unknown): Promise<ResolvedCreateAgentToolArgs> {
if (callerAgentId) {
const parsed = agentToAgentCreateAgentArgsSchema.parse(args);
const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsed.workspace, {
prompt: parsed.initialPrompt,
});
return {
kind: "agent-scoped",
parsedArgs: parsed,
relationship: parsed.relationship,
cwd,
workspaceId,
worktree,
};
}
const parsedArgs = normalizeTopLevelCreateAgentArgs(topLevelCreateAgentArgsSchema.parse(args));
if (parsedArgs.relationship.kind === "subagent") {
throw new Error("relationship subagent requires an agent-scoped tool session");
}
const { cwd, workspaceId, worktree } = await resolveCreateAgentWorkspace(parsedArgs.workspace, {
prompt: parsedArgs.initialPrompt,
});
return {
kind: "top-level",
parsedArgs,
cwd,
workspaceId,
worktree,
};
}
function normalizeTopLevelCreateAgentArgs(
args: TopLevelCreateAgentToolArgs,
): TopLevelCreateAgentArgs {
const {
cwd,
mode,
thinking,
features,
worktreeName,
branchName,
baseBranch,
refName,
githubPrNumber,
...canonicalCandidate
} = args;
const settings = {
...canonicalCandidate.settings,
...(mode ? { modeId: mode } : {}),
...(thinking ? { thinkingOptionId: thinking } : {}),
...(features ? { features } : {}),
};
if (canonicalCandidate.relationship && canonicalCandidate.workspace) {
return canonicalTopLevelCreateAgentArgsSchema.parse({
...canonicalCandidate,
...(Object.keys(settings).length > 0 ? { settings } : {}),
});
}
if (canonicalCandidate.relationship || canonicalCandidate.workspace) {
throw new Error("relationship and workspace must be provided together");
}
if (!cwd?.trim()) {
throw new Error("cwd is required for legacy top-level create_agent calls");
}
const legacyWorktreeTarget = resolveLegacyCreateAgentWorktreeTarget({
worktreeName,
branchName,
baseBranch,
refName,
githubPrNumber,
});
const workspace = legacyWorktreeTarget
? {
kind: "create" as const,
source: {
kind: "worktree" as const,
cwd,
target: legacyWorktreeTarget,
},
}
: {
kind: "create" as const,
source: {
kind: "directory" as const,
path: cwd,
},
};
return canonicalTopLevelCreateAgentArgsSchema.parse({
...canonicalCandidate,
relationship: { kind: "detached" },
workspace,
...(Object.keys(settings).length > 0 ? { settings } : {}),
});
}
function resolveLegacyCreateAgentWorktreeTarget(input: {
worktreeName?: string;
branchName?: string;
baseBranch?: string;
refName?: string;
githubPrNumber?: number;
}): z.infer<typeof AgentCreateWorktreeTargetInputSchema> | null {
if (input.githubPrNumber !== undefined) {
return {
kind: "checkout-pr",
githubPrNumber: input.githubPrNumber,
};
}
if (input.refName) {
return {
kind: "checkout-branch",
branch: input.refName,
};
}
if (input.worktreeName || input.branchName || input.baseBranch) {
return {
kind: "branch-off",
worktreeSlug: input.worktreeName,
branchName: input.branchName,
baseBranch: input.baseBranch,
};
}
return null;
}
async function resolveCreateAgentWorkspace(
workspace: AgentToAgentCreateAgentArgs["workspace"] | TopLevelCreateAgentArgs["workspace"],
firstAgentContext: FirstAgentContext | undefined,
): Promise<{
cwd: string | undefined;
workspaceId: string | undefined;
worktree: CreateAgentFromMcpInput["worktree"];
}> {
if (workspace.kind === "current") {
if (!callerAgentId) {
throw new Error("workspace current requires an agent-scoped tool session");
}
const callerAgent = resolveCallerAgent();
if (!callerAgent?.workspaceId) {
throw new Error(`Caller agent ${callerAgentId} has no current workspace`);
}
return {
cwd: workspace.cwd,
workspaceId: callerAgent.workspaceId,
worktree: undefined,
};
}
if (workspace.kind === "existing") {
if (!options.listActiveWorkspaces) {
throw new Error("Workspace lookup is not configured");
}
const existingWorkspace = (await options.listActiveWorkspaces()).find(
(candidate) => candidate.workspaceId === workspace.workspaceId,
);
if (!existingWorkspace) {
throw new Error(`Workspace ${workspace.workspaceId} not found`);
}
const cwd = workspace.cwd
? resolveScopedCwd(workspace.cwd, { required: true })
: existingWorkspace.cwd;
const lockedCwd = callerContext?.lockedCwd?.trim();
if (lockedCwd && !isSameOrDescendantPath(expandUserPath(lockedCwd), cwd)) {
throw new Error(`Workspace ${workspace.workspaceId} is outside the allowed cwd`);
}
return {
cwd,
workspaceId: workspace.workspaceId,
worktree: undefined,
};
}
if (workspace.source.kind === "directory") {
const cwd = resolveScopedCwd(workspace.source.path, { required: true });
if (!options.ensureWorkspaceForCreate) {
throw new Error("Workspace creation is not configured");
}
return {
cwd,
workspaceId: await options.ensureWorkspaceForCreate(cwd, firstAgentContext),
worktree: undefined,
};
}
const cwd = resolveScopedCwd(workspace.source.cwd, { required: true });
return {
cwd,
workspaceId: undefined,
worktree: resolveCreateAgentWorktree(workspace.source.target),
};
}
function resolveCreateAgentWorktree(
target: z.infer<typeof AgentCreateWorktreeTargetInputSchema>,
): NonNullable<CreateAgentFromMcpInput["worktree"]> {
switch (target.kind) {
case "branch-off":
return {
action: "branch-off",
worktreeName: target.worktreeSlug,
branchName: target.branchName,
baseBranch: target.baseBranch,
};
case "checkout-branch":
return {
action: "checkout",
refName: target.branch,
};
case "checkout-pr":
return {
action: "checkout",
githubPrNumber: target.githubPrNumber,
};
default:
throw new Error("unreachable");
}
}
registerTool(
"send_agent_prompt",
{
title: "Send agent prompt",
description:
"Send a task to a running agent. Agent-scoped callers run in background by default; top-level callers wait by default.",
inputSchema: sendAgentPromptInputSchema,
outputSchema: {
success: z.boolean(),
status: AgentStatusEnum,
lastMessage: z.string().nullable().optional(),
permission: AgentPermissionRequestPayloadSchema.nullable().optional(),
guidance: z.string().optional(),
},
},
async ({
agentId,
prompt,
sessionMode,
background = Boolean(callerAgentId),
notifyOnFinish = Boolean(callerAgentId),
}) => {
const shouldNotifyOnFinish = Boolean(callerAgentId && notifyOnFinish && background);
await sendPromptToAgent({
agentManager,
agentStorage,
agentId,
prompt,
sessionMode,
logger: childLogger,
});
if (shouldNotifyOnFinish && callerAgentId) {
setupFinishNotification({
agentManager,
agentStorage,
childAgentId: agentId,
callerAgentId,
logger: childLogger,
});
}
// If not running in background, wait for completion
if (!background) {
const result = await waitForAgentWithTimeout(agentManager, agentId, {
waitForActive: true,
});
const responseData = {
success: true,
status: result.status,
lastMessage: result.lastMessage,
permission: sanitizePermissionRequest(result.permission),
};
const validJson = ensureValidJson(responseData);
const response = {
content: [],
structuredContent: validJson,
};
return response;
}
// Return immediately if background=true
// Re-fetch snapshot since the state may have changed
const currentSnapshot = agentManager.getAgent(agentId);
const responseData = {
success: true,
status: currentSnapshot?.lifecycle ?? "idle",
lastMessage: null,
permission: null,
...(shouldNotifyOnFinish
? {
guidance:
"You will get notified when the prompted agent finishes, errors, or needs permission. Do not poll for status; continue with other work until the notification arrives.",
}
: {}),
};
const validJson = ensureValidJson(responseData);
const response = {
content: [],
structuredContent: validJson,
};
return response;
},
);
registerTool(
"get_agent_status",
{
title: "Get agent status",
description:
"Return the latest snapshot for an agent, including lifecycle state, capabilities, and pending permissions.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
status: AgentStatusEnum,
snapshot: AgentSnapshotPayloadSchema,
},
},
async ({ agentId }) => {
const snapshot = agentManager.getAgent(agentId);
if (snapshot) {
const structuredSnapshot = await serializeSnapshotWithMetadata(
agentStorage,
snapshot,
childLogger,
);
return {
content: [],
structuredContent: ensureValidJson({
status: snapshot.lifecycle,
snapshot: structuredSnapshot,
}),
};
}
const record = await agentStorage.get(agentId);
if (!record || record.internal) {
throw new Error(`Agent ${agentId} not found`);
}
const structuredSnapshot = buildStoredAgentPayload(
record,
new Set(providerSnapshotManager.listRegisteredProviderIds()),
);
return {
content: [],
structuredContent: ensureValidJson({
status: structuredSnapshot.status,
snapshot: structuredSnapshot,
}),
};
},
);
registerTool(
"list_agents",
{
title: "List agents",
description: "List recent agents as compact metadata.",
inputSchema: {
includeArchived: z.boolean().optional().default(false),
cwd: z.string().optional(),
sinceHours: z
.number()
.int()
.positive()
.max(24 * 30)
.optional()
.default(48),
statuses: z.array(AgentStatusEnum).optional(),
limit: z.number().int().positive().max(200).optional().default(50),
},
outputSchema: {
agents: z.array(AgentListItemPayloadSchema),
},
},
async ({ includeArchived = false, cwd, sinceHours = 48, statuses, limit = 50 }) => {
const callerCwd = callerAgentId ? resolveCallerAgent()?.cwd : undefined;
const requestedCwd = cwd?.trim() ? expandUserPath(cwd) : callerCwd;
const statusFilter = statuses && statuses.length > 0 ? new Set(statuses) : null;
const sinceMs = Date.now() - sinceHours * 60 * 60 * 1000;
const liveSnapshots = agentManager.listAgents();
const liveAgents = await Promise.all(
liveSnapshots.map((snapshot) =>
serializeSnapshotWithMetadata(agentStorage, snapshot, childLogger),
),
);
const liveIds = new Set(liveSnapshots.map((snapshot) => snapshot.id));
const storedRecords = await agentStorage.list();
const registeredProviderIds = new Set(providerSnapshotManager.listRegisteredProviderIds());
const storedAgents = storedRecords
.filter((record) => !record.internal && !liveIds.has(record.id))
.filter((record) => includeArchived || !record.archivedAt)
.filter(
(record) =>
includeArchived || isStoredAgentProviderAvailable(record, registeredProviderIds),
)
.map((record) => buildStoredAgentPayload(record, registeredProviderIds));
const agents = [...liveAgents, ...storedAgents]
.map(toAgentListItemPayload)
.filter((agent) => !requestedCwd || isSameOrDescendantPath(requestedCwd, agent.cwd))
.filter((agent) => !statusFilter || statusFilter.has(agent.status))
.filter((agent) => !agent.archivedAt || resolveAgentListActivityTime(agent) >= sinceMs)
.sort(compareAgentListItems)
.slice(0, limit);
return {
content: [],
structuredContent: ensureValidJson({ agents }),
};
},
);
registerTool(
"cancel_agent",
{
title: "Cancel agent run",
description: "Abort the agent's current run but keep the agent alive for future tasks.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId }) => {
const { cancelled } = await cancelAgentRunCommand(
{ agentManager, logger: childLogger },
agentId,
);
return {
content: [],
structuredContent: ensureValidJson({ success: cancelled }),
};
},
);
registerTool(
"archive_agent",
{
title: "Archive agent",
description:
"Archive an agent (soft-delete). The agent is interrupted if running and removed from the active list.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId }) => {
await archiveAgentCommand(
{
agentManager,
agentStorage,
logger: childLogger,
},
agentId,
);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"kill_agent",
{
title: "Kill agent",
description: "Terminate an agent session permanently.",
inputSchema: {
agentId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId }) => {
await closeAgentCommand({ agentManager }, agentId);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"update_agent",
{
title: "Update agent",
description: "Update an agent name, labels, and/or runtime settings.",
inputSchema: {
agentId: z.string(),
name: z.string().optional(),
labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"),
settings: UpdateAgentSettingsInputSchema.optional().describe(
"Runtime settings to apply to the agent.",
),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, name, labels, settings }) => {
if (settings?.modeId !== undefined) {
await agentManager.setAgentMode(agentId, settings.modeId);
}
if (settings?.model !== undefined) {
await agentManager.setAgentModel(agentId, settings.model);
}
if (settings?.thinkingOptionId !== undefined) {
await agentManager.setAgentThinkingOption(agentId, settings.thinkingOptionId);
}
if (settings?.features) {
for (const [featureId, value] of Object.entries(settings.features)) {
await agentManager.setAgentFeature(agentId, featureId, value);
}
}
await updateAgentCommand({ agentManager }, { agentId, name, labels });
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"rename_workspace",
{
title: "Rename workspace",
description:
"Rename a workspace by setting its user-visible title. Omit workspaceId to rename your current workspace.",
inputSchema: {
workspaceId: z
.string()
.trim()
.min(1)
.optional()
.describe("Workspace id to rename. Omit to rename your current workspace."),
title: z
.string()
.trim()
.min(1, "title is required")
.describe("New user-visible workspace title."),
},
outputSchema: {
success: z.boolean(),
workspaceId: z.string(),
title: z.string(),
},
},
async ({ workspaceId: requestedWorkspaceId, title }) => {
if (!options.workspaceRegistry) {
throw new Error("Workspace registry is required to rename workspaces");
}
if (!options.emitWorkspaceUpdatesForWorkspaceIds) {
throw new Error("Workspace update emitter is required to rename workspaces");
}
const workspaceId = resolveWorkspaceIdForRename(requestedWorkspaceId);
const existing = await options.workspaceRegistry.get(workspaceId);
if (!existing) {
throw new Error(`Workspace ${workspaceId} not found`);
}
if (existing.archivedAt) {
throw new Error(`Workspace ${workspaceId} is archived`);
}
await options.workspaceRegistry.upsert({
...existing,
title,
updatedAt: new Date().toISOString(),
});
await options.emitWorkspaceUpdatesForWorkspaceIds([workspaceId]);
return {
content: [],
structuredContent: ensureValidJson({
success: true,
workspaceId,
title,
}),
};
},
);
registerTool(
"list_terminals",
{
title: "List terminals",
description: "List terminals for a working directory or across all working directories.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional working directory. Defaults to your current working directory."),
all: z.boolean().optional().describe("List terminals across all working directories."),
},
outputSchema: {
terminals: z.array(TerminalSummarySchema),
},
},
async ({ cwd, all }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminals = all
? (
await Promise.all(
terminalManager.listDirectories().map(async (directory) =>
(await terminalManager.getTerminals(directory)).map((terminal) => ({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
})),
),
)
).flat()
: (await terminalManager.getTerminals(resolveScopedCwd(cwd, { required: true }))).map(
(terminal) => ({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
}),
);
return {
content: [],
structuredContent: ensureValidJson({ terminals }),
};
},
);
registerTool(
"create_terminal",
{
title: "Create terminal",
description: "Create a terminal session for a working directory.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional working directory. Defaults to your current working directory."),
name: z.string().optional().describe("Optional terminal name."),
},
outputSchema: TerminalSummarySchema.shape,
},
async ({ cwd, name }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
const workspaceId = await resolveTerminalWorkspaceId(resolvedCwd);
const terminal = await terminalManager.createTerminal({
cwd: resolvedCwd,
workspaceId,
...(name?.trim() ? { name: name.trim() } : {}),
});
return {
content: [],
structuredContent: ensureValidJson({
id: terminal.id,
name: terminal.name,
cwd: terminal.cwd,
}),
};
},
);
registerTool(
"kill_terminal",
{
title: "Kill terminal",
description: "Kill an existing terminal session.",
inputSchema: {
terminalId: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ terminalId }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminal = terminalManager.getTerminal(terminalId);
if (!terminal) {
throw new Error(`Terminal ${terminalId} not found`);
}
terminal.kill();
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"capture_terminal",
{
title: "Capture terminal",
description: "Capture plain-text terminal output lines from a terminal session.",
inputSchema: {
terminalId: z.string(),
start: z.number().optional(),
end: z.number().optional(),
scrollback: z.boolean().optional(),
stripAnsi: z.boolean().optional().default(true),
},
outputSchema: {
terminalId: z.string(),
lines: z.array(z.string()),
totalLines: z.number().int().nonnegative(),
},
},
async ({ terminalId, start, end, scrollback, stripAnsi = true }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
if (!terminalManager.getTerminal(terminalId)) {
throw new Error(`Terminal ${terminalId} not found`);
}
const capture = await terminalManager.captureTerminal(terminalId, {
start: scrollback ? 0 : start,
end,
stripAnsi,
});
return {
content: [],
structuredContent: ensureValidJson({
terminalId,
lines: capture.lines,
totalLines: capture.totalLines,
}),
};
},
);
registerTool(
"send_terminal_keys",
{
title: "Send terminal keys",
description: "Send literal text or special key tokens to a terminal session.",
inputSchema: {
terminalId: z.string(),
keys: z.string(),
literal: z.boolean().optional(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ terminalId, keys, literal = false }) => {
if (!terminalManager) {
throw new Error("Terminal manager is not configured");
}
const terminal = terminalManager.getTerminal(terminalId);
if (!terminal) {
throw new Error(`Terminal ${terminalId} not found`);
}
terminal.send({
type: "input",
data: resolveTerminalKeyToken(keys, literal),
});
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"create_schedule",
{
title: "Create schedule",
description: "Create a recurring schedule that starts a new agent on a cron cadence.",
inputSchema: {
prompt: z.string().trim().min(1, "prompt is required"),
cron: z.string().trim().min(1, "cron is required"),
timezone: z
.string()
.trim()
.min(1)
.optional()
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
name: z.string().optional(),
provider: AgentProviderEnum.optional().describe(
"Provider, or provider/model (for example: codex or codex/gpt-5.4).",
),
cwd: z.string().optional(),
maxRuns: z.number().int().positive().optional(),
expiresIn: z.string().optional(),
},
outputSchema: ScheduleSummarySchema.shape,
},
async ({ prompt, cron, timezone, name, provider, cwd, maxRuns, expiresIn }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const expiresAt = buildScheduleExpiry(expiresIn);
const schedule = await scheduleService.createOrReplace({
prompt: prompt.trim(),
cadence: buildCronScheduleCadence({
cron,
...(timezone !== undefined ? { timezone } : {}),
}),
target: resolveNewAgentScheduleTarget({ provider, cwd }),
...(name?.trim() ? { name: name.trim() } : {}),
...(maxRuns === undefined ? {} : { maxRuns }),
...(expiresAt === undefined ? {} : { expiresAt }),
});
return {
content: [],
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
};
},
);
registerTool(
"create_heartbeat",
{
title: "Create heartbeat",
description: "Create a recurring heartbeat that sends you a prompt on a cron cadence.",
inputSchema: {
prompt: z.string().trim().min(1, "prompt is required"),
cron: z.string().trim().min(1, "cron is required"),
timezone: z
.string()
.trim()
.min(1)
.optional()
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
name: z.string().optional(),
maxRuns: z.number().int().positive().optional(),
expiresIn: z.string().optional(),
},
outputSchema: ScheduleSummarySchema.shape,
},
async ({ prompt, cron, timezone, name, maxRuns, expiresIn }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
if (!callerAgentId) {
throw new Error("create_heartbeat requires an agent-scoped session");
}
resolveCallerAgent();
const expiresAt = buildScheduleExpiry(expiresIn);
const schedule = await scheduleService.createOrReplace({
prompt: prompt.trim(),
cadence: buildCronScheduleCadence({
cron,
...(timezone !== undefined ? { timezone } : {}),
}),
target: { type: "agent", agentId: callerAgentId },
...(name?.trim() ? { name: name.trim() } : {}),
...(maxRuns === undefined ? {} : { maxRuns }),
...(expiresAt === undefined ? {} : { expiresAt }),
});
return {
content: [],
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
};
},
);
registerTool(
"list_schedules",
{
title: "List schedules",
description: "List all schedules managed by the daemon.",
inputSchema: {},
outputSchema: {
schedules: z.array(ScheduleSummarySchema),
},
},
async () => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const schedules = (await scheduleService.list()).map((schedule) =>
toScheduleSummary(schedule),
);
return {
content: [],
structuredContent: ensureValidJson({ schedules }),
};
},
);
registerTool(
"inspect_schedule",
{
title: "Inspect schedule",
description: "Inspect a schedule and its run history.",
inputSchema: {
id: z.string(),
},
outputSchema: StoredScheduleSchema.shape,
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const schedule = await scheduleService.inspect(id);
return {
content: [],
structuredContent: ensureValidJson(schedule),
};
},
);
registerTool(
"pause_schedule",
{
title: "Pause schedule",
description: "Pause an active schedule.",
inputSchema: {
id: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
await scheduleService.pause(id);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"resume_schedule",
{
title: "Resume schedule",
description: "Resume a paused schedule.",
inputSchema: {
id: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
await scheduleService.resume(id);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"delete_schedule",
{
title: "Delete schedule",
description: "Delete a schedule permanently.",
inputSchema: {
id: z.string(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
await scheduleService.delete(id);
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"update_schedule",
{
title: "Update schedule",
description:
"Update an existing schedule. Only provided fields are changed; omitted fields remain unchanged.",
inputSchema: {
id: z.string(),
every: z.string().optional().describe("New interval duration string (e.g. 5m, 1h)."),
cron: z.string().optional().describe("New cron expression."),
timezone: z
.string()
.trim()
.min(1)
.optional()
.describe(
"IANA time zone for cron cadence; requires cron. For example: America/New_York.",
),
name: z.string().nullable().optional().describe("New name (null to clear)."),
prompt: z.string().trim().min(1).optional().describe("New prompt text."),
maxRuns: z
.number()
.int()
.positive()
.nullable()
.optional()
.describe("New max runs limit (null to clear)."),
provider: z
.string()
.trim()
.min(1)
.optional()
.describe("New provider for new-agent target."),
model: z
.string()
.trim()
.min(1)
.nullable()
.optional()
.describe("New model for new-agent target (null to clear)."),
mode: z
.string()
.trim()
.min(1)
.nullable()
.optional()
.describe("New mode for new-agent target (null to clear)."),
cwd: z.string().trim().min(1).optional().describe("New cwd for new-agent target."),
expiresIn: z
.string()
.optional()
.describe("New relative expiry duration (for example: 1h, 2d)."),
clearExpires: z.boolean().optional().describe("Clear any schedule expiry."),
},
outputSchema: StoredScheduleSchema.shape,
},
async (input) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const schedule = await scheduleService.update(buildScheduleUpdateInput(input));
return {
content: [],
structuredContent: ensureValidJson(schedule),
};
},
);
registerTool(
"schedule_logs",
{
title: "Schedule logs",
description: "Get the run history (logs) for a schedule.",
inputSchema: {
id: z.string(),
},
outputSchema: {
runs: z.array(ScheduleRunSchema),
},
},
async ({ id }) => {
if (!scheduleService) {
throw new Error("Schedule service is not configured");
}
const runs = await scheduleService.logs(id);
return {
content: [],
structuredContent: ensureValidJson({ runs }),
};
},
);
registerTool(
"list_providers",
{
title: "List providers",
description: "List configured agent providers, availability, and their modes.",
inputSchema: {},
outputSchema: {
providers: z.array(ProviderSummarySchema),
},
},
async () => {
const providers = (await providerSnapshotManager.listProviders({ wait: true })).map(
toProviderSummary,
);
return {
content: [],
structuredContent: ensureValidJson({ providers }),
};
},
);
registerTool(
"list_models",
{
title: "List models",
description: "List models for an agent provider.",
inputSchema: {
provider: AgentProviderEnum,
},
outputSchema: {
provider: z.string(),
models: z.array(AgentModelSchema),
},
},
async ({ provider }) => {
const models = await providerSnapshotManager.listModels({
provider,
wait: true,
});
return {
content: [],
structuredContent: ensureValidJson({
provider,
models,
}),
};
},
);
registerTool(
"inspect_provider",
{
title: "Inspect provider",
description:
"Inspect compact provider capabilities for orchestration, including modes and draft feature settings. Use list_models for the full model list.",
inputSchema: inspectProviderInputSchema,
outputSchema: {
provider: AgentProviderEnum,
label: z.string().nullable().optional(),
description: z.string().nullable().optional(),
enabled: z.boolean(),
status: z.string(),
modes: z.array(ProviderModeSchema).nullish(),
selectedModel: z.string().nullable(),
features: z.array(AgentFeatureSchema),
},
},
async ({ provider, cwd, settings }) => {
const resolvedProviderModel = resolveScheduleProviderAndModel({
provider,
defaultProvider: provider,
});
const providerId = resolvedProviderModel.provider;
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
const entry = await providerSnapshotManager.getProvider({
cwd: resolvedCwd,
provider: providerId,
wait: true,
});
const summary = toProviderSummary(entry);
if (!entry.enabled) {
throw new Error(`Provider '${providerId}' is disabled`);
}
if (entry.status !== "ready") {
throw new Error(entry.error ?? `Provider '${providerId}' is unavailable`);
}
const selectedModel = settings?.model ?? resolvedProviderModel.model;
const features = await agentManager.listDraftFeatures({
provider: providerId,
cwd: resolvedCwd,
...(settings?.modeId ? { modeId: settings.modeId } : {}),
...(selectedModel ? { model: selectedModel } : {}),
...(settings?.thinkingOptionId ? { thinkingOptionId: settings.thinkingOptionId } : {}),
...(settings?.features ? { featureValues: settings.features } : {}),
});
return {
content: [],
structuredContent: ensureValidJson({
provider: providerId,
label: summary.label,
description: summary.description,
enabled: summary.enabled,
status: summary.status,
modes: summary.modes,
selectedModel: selectedModel ?? null,
features,
}),
};
},
);
registerTool(
"list_worktrees",
{
title: "List worktrees",
description: "List Paseo-managed git worktrees for a repository.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional repository cwd. Defaults to your current working directory."),
},
outputSchema: {
worktrees: z.array(WorktreeSummarySchema),
},
},
async ({ cwd }) => {
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
if (!options.workspaceGitService) {
throw new Error("WorkspaceGitService is required to list worktrees");
}
const worktrees = await listPaseoWorktreesCommand(
{ workspaceGitService: options.workspaceGitService },
{
cwd: resolvedCwd,
reason: "mcp:list-worktrees",
},
);
return {
content: [],
structuredContent: ensureValidJson({ worktrees }),
};
},
);
registerTool(
"create_worktree",
{
title: "Create worktree",
description:
"Create a Paseo-managed git worktree. Branch off a new branch, check out an existing branch, or check out a GitHub PR.",
inputSchema: {
cwd: z.string().optional().describe("Repository directory. Defaults to the agent's cwd."),
target: AgentCreateWorktreeTargetInputSchema.describe("What the worktree should contain."),
},
outputSchema: {
branchName: z.string(),
worktreePath: z.string(),
workspaceId: z.string(),
},
},
async ({ cwd, target }) => {
const repoRoot = resolveScopedCwd(cwd, { required: true });
const commandResult = await createPaseoWorktreeCommand(
{
paseoHome: options.paseoHome,
worktreesRoot: options.worktreesRoot,
createPaseoWorktreeWorkflow: options.createPaseoWorktree,
},
createMcpWorktreeCommandInput(repoRoot, target),
);
if (!commandResult.ok) {
throw new WorktreeRequestError(commandResult.error);
}
const { worktree, workspace } = commandResult.createdWorktree;
await options.workspaceGitService?.listWorktrees?.(repoRoot, {
force: true,
reason: "mcp:create-worktree",
});
return {
content: [],
structuredContent: ensureValidJson({
branchName: worktree.branchName,
worktreePath: worktree.worktreePath,
workspaceId: workspace.workspaceId,
}),
};
},
);
registerTool(
"archive_worktree",
{
title: "Archive worktree",
description: "Delete a Paseo-managed git worktree.",
inputSchema: {
cwd: z
.string()
.optional()
.describe("Optional repository cwd. Defaults to your current working directory."),
worktreePath: z.string().optional(),
worktreeSlug: z.string().optional(),
},
outputSchema: {
success: z.boolean(),
},
},
async ({ cwd, worktreePath, worktreeSlug }) => {
const resolvedCwd = resolveScopedCwd(cwd, { required: true });
if (!worktreePath && !worktreeSlug) {
throw new Error("worktreePath or worktreeSlug is required");
}
if (!options.workspaceGitService) {
throw new Error("WorkspaceGitService is required to archive worktrees");
}
const repoRoot = await options.workspaceGitService.resolveRepoRoot(resolvedCwd);
const result = await archiveCommand(
archiveWorktreeDependencies(options, {
agentManager,
agentStorage,
terminalManager: terminalManager ?? null,
logger: childLogger,
}),
{
requestId: "mcp:archive_worktree",
repoRoot,
worktreePath,
worktreeSlug,
// This tool archives every workspace on the directory, then removes the
// directory. Disk removal is derived from scope + last-reference.
scope: "worktree",
},
);
if (!result.ok) {
throw new Error(result.message);
}
await options.workspaceGitService.listWorktrees(repoRoot, {
force: true,
reason: "mcp:archive-worktree",
});
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
registerTool(
"get_agent_activity",
{
title: "Get agent activity",
description: "Return recent agent timeline entries as a curated summary.",
inputSchema: {
agentId: z.string(),
limit: z
.number()
.optional()
.describe("Optional limit for number of activities to include (most recent first)."),
},
outputSchema: {
agentId: z.string(),
updateCount: z.number(),
currentModeId: z.string().nullable(),
content: z.string(),
},
},
async ({ agentId, limit }) => {
await ensureAgentLoaded(agentId, {
agentManager,
agentStorage,
logger: childLogger,
});
const timeline = agentManager.getTimeline(agentId);
const snapshot = agentManager.getAgent(agentId);
const selection = selectItemsByProjectedLimit({
items: timeline,
direction: "tail",
limit: limit ?? 0,
});
const curatedContent = curateAgentActivity(selection.items);
const { totalProjected, shownProjected } = selection;
const noun = totalProjected === 1 ? "activity" : "activities";
const countHeader =
limit && shownProjected < totalProjected
? `Showing ${shownProjected} of ${totalProjected} ${noun} (limited to ${limit})`
: `Showing all ${totalProjected} ${noun}`;
const contentWithCount = `${countHeader}\n\n${curatedContent}`;
return {
content: [],
structuredContent: ensureValidJson({
agentId,
updateCount: timeline.length,
currentModeId: snapshot?.currentModeId ?? null,
content: contentWithCount,
}),
};
},
);
registerTool(
"set_agent_mode",
{
title: "Set agent session mode",
description:
"Switch the agent's session mode (plan, bypassPermissions, read-only, auto, etc.).",
inputSchema: {
agentId: z.string(),
modeId: z.string(),
},
outputSchema: {
success: z.boolean(),
newMode: z.string(),
},
},
async ({ agentId, modeId }) => {
const result = await setAgentModeCommand({ agentManager }, { agentId, modeId });
return {
content: [],
structuredContent: ensureValidJson({ success: true, newMode: result.modeId }),
};
},
);
registerTool(
"list_pending_permissions",
{
title: "List pending permissions",
description:
"Return all pending permission requests across all agents with the normalized payloads.",
inputSchema: {},
outputSchema: {
permissions: z.array(
z.object({
agentId: z.string(),
status: AgentStatusEnum,
request: AgentPermissionRequestPayloadSchema,
}),
),
},
},
async () => {
const permissions = agentManager.listAgents().flatMap((agent) => {
const payload = toAgentPayload(agent);
return payload.pendingPermissions.map((request) => ({
agentId: agent.id,
status: payload.status,
request: sanitizePermissionRequest(request),
}));
});
return {
content: [],
structuredContent: ensureValidJson({ permissions }),
};
},
);
registerTool(
"respond_to_permission",
{
title: "Respond to permission",
description:
"Approve or deny a pending permission request with an AgentManager-compatible response payload.",
inputSchema: {
agentId: z.string(),
requestId: z.string(),
response: AgentPermissionResponseSchema,
},
outputSchema: {
success: z.boolean(),
},
},
async ({ agentId, requestId, response }) => {
await respondToAgentPermission({
agentManager,
agentId,
requestId,
response,
logger: childLogger,
});
return {
content: [],
structuredContent: ensureValidJson({ success: true }),
};
},
);
return toCatalog();
}
type McpCreateWorktreeTarget =
| { kind: "branch-off"; worktreeSlug?: string; branchName?: string; baseBranch?: string }
| { kind: "checkout-branch"; branch: string }
| { kind: "checkout-pr"; githubPrNumber: number };
interface ArchiveWorktreeCommandContext {
agentManager: AgentManager;
agentStorage: AgentStorage;
terminalManager: TerminalManager | null;
logger: Logger;
}
function archiveWorktreeDependencies(
options: PaseoToolHostDependencies,
context: ArchiveWorktreeCommandContext,
): ArchiveCommandDependencies {
if (!options.github) {
throw new Error("GitHub service is required to archive worktrees");
}
if (!options.workspaceGitService) {
throw new Error("WorkspaceGitService is required to archive worktrees");
}
if (!options.archiveWorkspaceRecord) {
throw new Error("Workspace registry archiver is required to archive worktrees");
}
if (!options.findWorkspaceIdForCwd) {
throw new Error("Workspace resolver is required to archive worktrees");
}
if (!options.listActiveWorkspaces) {
throw new Error("Active workspace lister is required to archive worktrees");
}
if (!options.emitWorkspaceUpdatesForWorkspaceIds) {
throw new Error("Workspace update emitter is required to archive worktrees");
}
if (!options.markWorkspaceArchiving) {
throw new Error("Workspace archiving marker is required to archive worktrees");
}
if (!options.clearWorkspaceArchiving) {
throw new Error("Workspace archiving clearer is required to archive worktrees");
}
return {
paseoHome: options.paseoHome,
paseoWorktreesBaseRoot: options.worktreesRoot,
github: options.github,
workspaceGitService: options.workspaceGitService,
agentManager: context.agentManager,
agentStorage: context.agentStorage,
findWorkspaceIdForCwd: options.findWorkspaceIdForCwd,
listActiveWorkspaces: options.listActiveWorkspaces,
archiveWorkspaceRecord: options.archiveWorkspaceRecord,
emitWorkspaceUpdatesForWorkspaceIds: options.emitWorkspaceUpdatesForWorkspaceIds,
markWorkspaceArchiving: options.markWorkspaceArchiving,
clearWorkspaceArchiving: options.clearWorkspaceArchiving,
killTerminalsForWorkspace: (workspaceId: string) =>
killTerminalsForWorkspace(
{
terminalManager: context.terminalManager,
sessionLogger: context.logger,
},
workspaceId,
),
sessionLogger: context.logger,
};
}
function createMcpWorktreeCommandInput(
repoRoot: string,
target: McpCreateWorktreeTarget,
): CreatePaseoWorktreeCommandInput {
const base = { cwd: repoRoot } as const;
switch (target.kind) {
case "branch-off":
return {
...base,
worktreeSlug: target.worktreeSlug,
branchName: target.branchName,
action: "branch-off",
...(target.baseBranch ? { refName: target.baseBranch } : {}),
};
case "checkout-branch":
return { ...base, action: "checkout", refName: target.branch };
case "checkout-pr":
return { ...base, action: "checkout", githubPrNumber: target.githubPrNumber };
default:
throw new Error("unreachable");
}
}