Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 142 additions & 3 deletions .storybook/mocks/orpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Creates a client that matches the AppRouter interface with configurable mock data.
*/
import type { APIClient } from "@/browser/contexts/API";
import type { AgentDefinitionDescriptor, AgentDefinitionPackage } from "@/common/types/agentDefinition";
import type { FrontendWorkspaceMetadata } from "@/common/types/workspace";
import type { ProjectConfig } from "@/node/config";
import type {
Expand All @@ -21,6 +22,11 @@ import {
type SubagentAiDefaults,
type TaskSettings,
} from "@/common/types/tasks";
import {
normalizeModeAiDefaults,
type ModeAiDefaults,
} from "@/common/types/modeAiDefaults";
import { normalizeAgentAiDefaults, type AgentAiDefaults } from "@/common/types/agentAiDefaults";
import { createAsyncMessageQueue } from "@/common/utils/asyncMessageQueue";
import { isWorkspaceArchived } from "@/common/utils/archive";

Expand Down Expand Up @@ -57,6 +63,12 @@ export interface MockORPCClientOptions {
workspaces?: FrontendWorkspaceMetadata[];
/** Initial task settings for config.getConfig (e.g., Settings → Tasks section) */
taskSettings?: Partial<TaskSettings>;
/** Initial mode AI defaults for config.getConfig (e.g., Settings → Modes section) */
modeAiDefaults?: ModeAiDefaults;
/** Initial unified AI defaults for agents (plan/exec/compact + subagents) */
agentAiDefaults?: AgentAiDefaults;
/** Agent definitions to expose via agents.list */
agentDefinitions?: AgentDefinitionDescriptor[];
/** Initial per-subagent AI defaults for config.getConfig (e.g., Settings → Tasks section) */
subagentAiDefaults?: SubagentAiDefaults;
/** Per-workspace chat callback. Return messages to emit, or use the callback for streaming. */
Expand Down Expand Up @@ -140,7 +152,10 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
mcpOverrides = new Map(),
mcpTestResults = new Map(),
taskSettings: initialTaskSettings,
modeAiDefaults: initialModeAiDefaults,
subagentAiDefaults: initialSubagentAiDefaults,
agentAiDefaults: initialAgentAiDefaults,
agentDefinitions: initialAgentDefinitions,
} = options;

// Feature flags
Expand All @@ -158,8 +173,78 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
};

const workspaceMap = new Map(workspaces.map((w) => [w.id, w]));

const agentDefinitions: AgentDefinitionDescriptor[] =
initialAgentDefinitions ??
([
{
id: "plan",
scope: "built-in",
name: "Plan",
description: "Create a plan before coding",
uiSelectable: true,
subagentRunnable: false,
policyBase: "plan",
},
{
id: "exec",
scope: "built-in",
name: "Exec",
description: "Implement changes in the repository",
uiSelectable: true,
subagentRunnable: true,
policyBase: "exec",
},
{
id: "compact",
scope: "built-in",
name: "Compact",
description: "History compaction (internal)",
uiSelectable: false,
subagentRunnable: false,
policyBase: "compact",
},
{
id: "explore",
scope: "built-in",
name: "Explore",
description: "Read-only repository exploration",
uiSelectable: false,
subagentRunnable: true,
policyBase: "exec",
},
] satisfies AgentDefinitionDescriptor[]);

let taskSettings = normalizeTaskSettings(initialTaskSettings ?? DEFAULT_TASK_SETTINGS);
let subagentAiDefaults = normalizeSubagentAiDefaults(initialSubagentAiDefaults ?? {});

let agentAiDefaults = normalizeAgentAiDefaults(
initialAgentAiDefaults ??
({
...(initialSubagentAiDefaults ?? {}),
...(initialModeAiDefaults ?? {}),
} as const)
);

const deriveModeAiDefaults = () =>
normalizeModeAiDefaults({
plan: agentAiDefaults.plan,
exec: agentAiDefaults.exec,
compact: agentAiDefaults.compact,
});

const deriveSubagentAiDefaults = () => {
const raw: Record<string, unknown> = {};
for (const [agentId, entry] of Object.entries(agentAiDefaults)) {
if (agentId === "plan" || agentId === "exec" || agentId === "compact") {
continue;
}
raw[agentId] = entry;
}
return normalizeSubagentAiDefaults(raw);
};

let modeAiDefaults = deriveModeAiDefaults();
let subagentAiDefaults = deriveSubagentAiDefaults();

const mockStats: ChatStats = {
consumers: [],
Expand Down Expand Up @@ -193,15 +278,69 @@ export function createMockORPCClient(options: MockORPCClientOptions = {}): APICl
setSshHost: async () => undefined,
},
config: {
getConfig: async () => ({ taskSettings, subagentAiDefaults }),
saveConfig: async (input: { taskSettings: unknown; subagentAiDefaults?: unknown }) => {
getConfig: async () => ({ taskSettings, agentAiDefaults, subagentAiDefaults, modeAiDefaults }),
saveConfig: async (input: {
taskSettings: unknown;
agentAiDefaults?: unknown;
subagentAiDefaults?: unknown;
}) => {
taskSettings = normalizeTaskSettings(input.taskSettings);

if (input.agentAiDefaults !== undefined) {
agentAiDefaults = normalizeAgentAiDefaults(input.agentAiDefaults);
modeAiDefaults = deriveModeAiDefaults();
subagentAiDefaults = deriveSubagentAiDefaults();
}

if (input.subagentAiDefaults !== undefined) {
subagentAiDefaults = normalizeSubagentAiDefaults(input.subagentAiDefaults);

const nextAgentAiDefaults: Record<string, unknown> = { ...agentAiDefaults };
for (const [agentType, entry] of Object.entries(subagentAiDefaults)) {
nextAgentAiDefaults[agentType] = entry;
}

agentAiDefaults = normalizeAgentAiDefaults(nextAgentAiDefaults);
modeAiDefaults = deriveModeAiDefaults();
}

return undefined;
},
updateAgentAiDefaults: async (input: { agentAiDefaults: unknown }) => {
agentAiDefaults = normalizeAgentAiDefaults(input.agentAiDefaults);
modeAiDefaults = deriveModeAiDefaults();
subagentAiDefaults = deriveSubagentAiDefaults();
return undefined;
},
updateModeAiDefaults: async (input: { modeAiDefaults: unknown }) => {
modeAiDefaults = normalizeModeAiDefaults(input.modeAiDefaults);
agentAiDefaults = normalizeAgentAiDefaults({ ...agentAiDefaults, ...modeAiDefaults });
modeAiDefaults = deriveModeAiDefaults();
subagentAiDefaults = deriveSubagentAiDefaults();
return undefined;
},
},
agents: {
list: async (_input: { workspaceId: string }) => agentDefinitions,
get: async (input: { workspaceId: string; agentId: string }) => {
const descriptor =
agentDefinitions.find((agent) => agent.id === input.agentId) ?? agentDefinitions[0];

return {
id: descriptor.id,
scope: descriptor.scope,
frontmatter: {
name: descriptor.name,
description: descriptor.description,
ui: { selectable: descriptor.uiSelectable },
subagent: { runnable: descriptor.subagentRunnable },
ai: descriptor.aiDefaults,
policy: { base: descriptor.policyBase, tools: descriptor.toolFilter },
},
body: "",
} satisfies AgentDefinitionPackage;
},
},
providers: {
list: async () => providersList,
getConfig: async () => providersConfig,
Expand Down
9 changes: 5 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ dev: node_modules/.installed build-main ## Start development server (Vite + node
# https://github.com/oven-sh/bun/issues/18275
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) NODE_OPTIONS="--max-old-space-size=4096" npm x concurrently -k --raw \
"bun x nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --banner:js='import { createRequire } from \"module\"; const require = createRequire(import.meta.url);' --watch" \
"vite"
else
dev: node_modules/.installed build-main build-preload ## Start development server (Vite + tsgo watcher for 10x faster type checking)
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --banner:js='import { createRequire } from \"module\"; const require = createRequire(import.meta.url);' --watch" \
"vite"
endif

Expand All @@ -159,7 +159,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
@# On Windows, use npm run because bunx doesn't correctly pass arguments
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) npmx concurrently -k \
"npmx nodemon --watch src --watch tsconfig.main.json --watch tsconfig.json --ext ts,tsx,json --ignore dist --ignore node_modules --exec node scripts/build-main-watch.js" \
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"npx esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --banner:js='import { createRequire } from \"module\"; const require = createRequire(import.meta.url);' --watch" \
"npmx nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec \"node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)\"" \
"$(SHELL) -lc \"MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) MUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite\""
else
Expand All @@ -171,7 +171,7 @@ dev-server: node_modules/.installed build-main ## Start server mode with hot rel
@echo "For remote access: make dev-server VITE_HOST=0.0.0.0 BACKEND_HOST=0.0.0.0"
@MUX_DISABLE_TELEMETRY=$(if $(MUX_ENABLE_TELEMETRY_IN_DEV),,$(or $(MUX_DISABLE_TELEMETRY),1)) bun x concurrently -k \
"bun x concurrently \"$(TSGO) -w -p tsconfig.main.json\" \"bun x tsc-alias -w -p tsconfig.main.json\"" \
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --watch" \
"bun x esbuild src/cli/api.ts --bundle --format=esm --platform=node --target=node20 --outfile=dist/cli/api.mjs --external:zod --external:commander --external:@trpc/server --banner:js='import { createRequire } from \"module\"; const require = createRequire(import.meta.url);' --watch" \
"bun x nodemon --watch dist/cli/index.js --watch dist/cli/server.js --delay 500ms --exec 'NODE_ENV=development node dist/cli/index.js server --host $(or $(BACKEND_HOST),localhost) --port $(or $(BACKEND_PORT),3000)'" \
"MUX_VITE_HOST=$(or $(VITE_HOST),127.0.0.1) MUX_VITE_PORT=$(or $(VITE_PORT),5173) VITE_BACKEND_URL=http://$(or $(BACKEND_HOST),localhost):$(or $(BACKEND_PORT),3000) vite"
endif
Expand Down Expand Up @@ -200,6 +200,7 @@ dist/cli/api.mjs: src/cli/api.ts src/cli/proxifyOrpc.ts $(TS_SOURCES)
--platform=node \
--target=node20 \
--outfile=dist/cli/api.mjs \
--banner:js='import { createRequire } from "module"; const require = createRequire(import.meta.url);' \
--external:zod \
--external:commander \
--external:@trpc/server
Expand Down
122 changes: 122 additions & 0 deletions docs/agents.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
title: Agents
description: Define custom agents (modes + subagents) with Markdown files
---

## Overview

mux uses **agents** to control the model’s:

- **System prompt** (what the assistant “is”)
- **Tool access policy** (which tools it can call)

This unifies two older concepts:

- **UI modes** (Plan/Exec/Compact)
- **Subagents** (the presets used by the `task` tool)

An **Agent Definition** is a Markdown file:

- The **YAML frontmatter** defines metadata + policy.
- The **Markdown body** becomes the agent’s system prompt (layered with mux’s base prelude).

## Discovery + precedence

mux discovers agent definitions from (non-recursive):

1. `<projectRoot>/.mux/agents/*.md`
2. `~/.mux/agents/*.md`
3. Built-ins (shipped with mux)

If multiple definitions share the same **agent id**, the higher-precedence one wins:

`project` overrides `global` overrides `built-in`.

### Agent IDs

The **agent id** is derived from the filename:

- `review.md` → `agentId = "review"`

Agent ids are lowercase and should be simple (letters/numbers with `-`/`_`).

## File format

Example:

```md
---
name: Review
description: Terse reviewer-style feedback

ui:
selectable: true

subagent:
runnable: true

policy:
base: exec
tools:
deny: ["file_edit_insert", "file_edit_replace_string"]
---

You are a code reviewer.

- Focus on correctness, risks, and test coverage.
- Prefer short, actionable comments.
```

### Frontmatter fields (v1)

- `name` (required): UI label
- `description` (optional): UI help text
- `ui.selectable` (optional, default `false`): show in the main agent selector
- `subagent.runnable` (optional, default `false`): allow `task({ agentId: ... })`
- `policy.base` (optional, default `exec`): `plan | exec | compact`
- `policy.tools` (optional): restrict tools from the base policy
- `deny: ["toolA", ...]` — disable a subset
- `only: ["toolA", ...]` — disable everything except this allowlist

Notes:

- `policy.tools` must specify **exactly one** of `deny` or `only`.
- Tool names are matched against mux’s tool registry. Unknown names are effectively no-ops.

## Tool policy semantics

- `policy.base: plan` enables Plan-mode behaviors (e.g. `ask_user_question`).
- `policy.base: exec` is the normal coding workflow.
- `policy.base: compact` is an internal no-tools flow.

Even if an agent definition tries to allow them, mux applies **hard denies** in child workspaces
(subagents) so tasks cannot:

- spawn more tasks (no recursive `task`/`task_*`)
- call `propose_plan`
- call `ask_user_question`

## Using agents

### Main agent

Use the agent selector in the chat input to switch agents.

- `Cmd+Shift+M` (mac) / `Ctrl+Shift+M` (win/linux) toggles between the last two UI-selectable agents.

### Subagents (task tool)

Spawn a subagent workspace with:

```ts
task({
agentId: "explore",
title: "Find the callsites",
prompt: "Locate where X is computed and report back",
});
```

## Related docs

- Scoped instructions in `AGENTS.md`: see **Instruction Files**.
- Built-in skills (`agent_skill_read`): see **Agent Skills**.
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
},
"context-management",
"instruction-files",
"agents",
"agent-skills",
"mcp-servers",
{
Expand Down
Loading