mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-10 04:11:15 +02:00
529 lines
22 KiB
Markdown
529 lines
22 KiB
Markdown
# Orchestrator MCP Server
|
|
|
|
## Purpose
|
|
|
|
T3 exposes V2 orchestration through its app-owned MCP endpoint. A provider
|
|
agent can use this endpoint to:
|
|
|
|
- create an app-owned sub-agent on any supported provider instance;
|
|
- wait for or poll the sub-agent's durable result;
|
|
- cancel an active delegated task; and
|
|
- create one or more ordinary top-level T3 threads;
|
|
- list a project's threads and incrementally read any thread;
|
|
- rename threads, regenerate titles, and link or unlink pull requests;
|
|
- send or steer follow-up messages; and
|
|
- wait for or interrupt ordinary thread runs.
|
|
|
|
These are T3 orchestration operations, not provider-native sub-agent APIs.
|
|
Delegated tasks always create a T3 child thread and run. The child receives
|
|
only the supplied task prompt, plus an optional role instruction supplied in
|
|
the same tool call. Parent conversation history is not copied into the child.
|
|
|
|
`ThreadManagementService` is the shared server application boundary for V2
|
|
WebSocket commands and MCP. It owns thread lookup, listing, send-mode
|
|
selection, durable send postconditions, wait polling, and interrupt selection;
|
|
`OrchestratorV2` remains the lower-level command/event processor. Transport
|
|
adapters only authenticate, resolve transport-specific inputs, and shape
|
|
responses.
|
|
|
|
## Transport And Authentication
|
|
|
|
The orchestration tools share the existing authenticated HTTP MCP endpoint:
|
|
|
|
```text
|
|
http://127.0.0.1:<server-port>/mcp
|
|
```
|
|
|
|
The provider-visible server key is `t3-code`. The endpoint registers both the
|
|
preview toolkit and the orchestration toolkit.
|
|
|
|
Before `ProviderSessionManager` opens a new V2 provider session, it asks
|
|
`McpSessionRegistry` for a credential scoped to:
|
|
|
|
- the T3 environment;
|
|
- the parent T3 thread;
|
|
- the concrete provider instance; and
|
|
- the provider session.
|
|
|
|
The credential grants `preview` and `orchestration` capabilities. Credentials
|
|
expire after a maximum lifetime, expire when idle, and are revoked when the
|
|
provider session is released. The raw token is not persisted in orchestration
|
|
state.
|
|
|
|
The MCP HTTP server resolves the bearer token and supplies the resulting
|
|
`McpInvocationScope` to tool handlers. Orchestration handlers additionally
|
|
check the `orchestration` capability before reading or mutating state.
|
|
|
|
## Provider Injection
|
|
|
|
### Codex V2
|
|
|
|
Codex app-server receives the remote MCP server through command-line config
|
|
overrides:
|
|
|
|
```text
|
|
-c mcp_servers.t3-code.url=http://127.0.0.1:<port>/mcp
|
|
-c mcp_servers.t3-code.bearer_token_env_var="T3_MCP_BEARER_TOKEN"
|
|
```
|
|
|
|
The provider-session token is placed in `T3_MCP_BEARER_TOKEN`. Both the
|
|
production Codex launcher and the injectable test launcher use the same
|
|
projection helper.
|
|
|
|
### Claude Agent SDK V2
|
|
|
|
Claude receives an HTTP MCP server in its query options:
|
|
|
|
```ts
|
|
{
|
|
mcpServers: {
|
|
"t3-code": {
|
|
type: "http",
|
|
url: "http://127.0.0.1:<port>/mcp",
|
|
headers: {
|
|
Authorization: "Bearer <provider-session-token>",
|
|
},
|
|
},
|
|
},
|
|
allowedTools: [
|
|
// existing allowed tools
|
|
"mcp__t3-code__*",
|
|
],
|
|
}
|
|
```
|
|
|
|
The adapter logs only whether MCP configuration exists; it does not log the
|
|
server headers or token.
|
|
|
|
### Cursor Agent SDK V2
|
|
|
|
Cursor receives the same authenticated HTTP MCP endpoint through the SDK's
|
|
`mcpServers` agent and send options. The adapter passes the authorization header
|
|
to the SDK but projects only redacted option metadata into protocol diagnostics.
|
|
|
|
### Grok ACP V2
|
|
|
|
Grok receives the authenticated HTTP MCP endpoint through the ACP
|
|
`session/new`, `session/load`, and `session/fork` `mcpServers` field. The shared
|
|
ACP adapter owns standard protocol behavior; the Grok flavor adds xAI extension
|
|
requests such as structured user questions.
|
|
|
|
ACP does not define native subagents or active steering. Grok therefore uses
|
|
orchestrator-owned child threads and implements steering through
|
|
cancel-and-restart. Its current driver also lacks `session/fork`, so app forks
|
|
use portable context transfer. These are orchestrator policies, not
|
|
provider-specific MCP tools.
|
|
|
|
### ACP Registry V2
|
|
|
|
The `acpRegistry` driver is the generic flavor of the same shared ACP adapter.
|
|
Each provider instance names an agent from the official ACP Registry. Settings
|
|
searches the registry through the connected server, then prepares a compatible
|
|
distribution before persisting the provider instance. Binary distributions use
|
|
a managed, versioned cache; declared checksums are verified when present.
|
|
Version-pinned `npx` packages install globally through `npm`; `uvx` packages use
|
|
`uv tool install`. ACP launches the resulting global command directly, so the
|
|
same command is available for terminal authentication. A local executable may
|
|
override the installed command without changing the registry-declared arguments
|
|
or environment.
|
|
|
|
Search, preparation, provider status, and session startup share one
|
|
server-scoped catalog service. This keeps platform selection and registry
|
|
validation identical across settings and runtime use. Catalog inspection never
|
|
starts an ACP process, probes models, or performs authentication. The managed
|
|
provider snapshot creates a disposable `session/new` through the normal provider
|
|
refresh lifecycle, using success as the authentication-readiness proof and
|
|
projecting advertised models into the snapshot. Terminal-only login remains a
|
|
manual operation on the connected server.
|
|
|
|
Capabilities such as session loading, session forking, models, modes, and MCP
|
|
transport are enabled only when the selected agent advertises them. Missing
|
|
features degrade through V2 policy: steering uses interrupt-and-restart,
|
|
forking uses portable context when native `session/fork` is unavailable, and
|
|
subagents use orchestrator-owned child threads. Registry agents do not receive
|
|
provider-specific extensions; those remain in flavors such as Grok.
|
|
|
|
### Pi V2
|
|
|
|
T3 keeps a provider-session HTTP bridge even when Pi supports native MCP.
|
|
`mcp.json` entries override native registrations, and native MCP's default
|
|
60-second request timeout can interrupt long-running T3 tools. The bridge owns
|
|
the injected endpoint and credential and forwards Pi's cancellation signal.
|
|
|
|
When a provider session credential exists, the adapter writes a T3-owned
|
|
extension into the server cache and spawns
|
|
`pi --mode rpc --extension <cache>/pi-t3-mcp-extension.ts` with:
|
|
|
|
```text
|
|
T3_MCP_URL=http://127.0.0.1:<port>/mcp
|
|
T3_MCP_BEARER_TOKEN=<provider-session-token>
|
|
```
|
|
|
|
The extension preserves public names under `mcp__t3-code__` for saved loadouts
|
|
and tool selectors. Modern Pi also receives hidden `mcp__t3_code__` aliases,
|
|
which reserve the normalized namespace against configured MCP servers without
|
|
adding declarations or search results. On Pi 0.99+,
|
|
`orchestrator_capabilities`, `delegate_task`, and `task_status` remain directly
|
|
available; optional tools are discovered through Pi's builtin `tool_search`.
|
|
On older Pi or without builtin search, all tools remain directly available.
|
|
The bridge calls the original MCP tool name over HTTP. Follow-up requests send
|
|
`mcp-protocol-version: 2025-06-18`; Effect's MCP transport returns 400
|
|
without it. The first turn of a session also receives the shared T3
|
|
orchestration instructions.
|
|
|
|
Pi keeps ownership of native extension discovery. T3 does not replace Pi's
|
|
`subagent` tool or reproduce Pi's package and project-trust loader. Durable
|
|
delegation goes through the namespaced T3 MCP `delegate_task` tool and the
|
|
shared orchestration child-thread lifecycle. When Pi's example `subagent`
|
|
extension is installed, the adapter observes its documented `details.results`
|
|
shape and projects task cards with no child thread id. Unknown result shapes
|
|
remain ordinary dynamic tool output.
|
|
|
|
### Provider Support
|
|
|
|
A provider instance can run child tasks when its live `ProviderInstance`
|
|
exposes a V2 `orchestrationAdapter` — the same registration the orchestrator
|
|
resolves when a `delegated_task.request` executes. That covers Codex, Claude
|
|
Agent SDK, Cursor Agent SDK, Grok, generic registry agents over ACP, OpenCode,
|
|
OpenCode 2, Pi, Antigravity, and any future driver that builds an adapter.
|
|
Capability discovery still reports other registered provider instances, but
|
|
marks them unavailable for orchestration when no adapter resolves. This keeps
|
|
provider selection model-visible without allowing a request that cannot run.
|
|
|
|
## Tool Surface
|
|
|
|
The server exposes eleven orchestration tools.
|
|
|
|
### `orchestrator_capabilities`
|
|
|
|
Returns:
|
|
|
|
- the inherited provider instance and model;
|
|
- the parent runtime and interaction modes;
|
|
- registered provider instances and advertised models;
|
|
- whether each provider can run a child task; and
|
|
- feature flags for polling, cancellation, and batch thread creation.
|
|
|
|
Unavailable providers include model-visible constraints such as missing V2
|
|
adapter support, disabled state, missing executable, or missing authentication.
|
|
|
|
### `delegate_task`
|
|
|
|
Creates a T3-owned child thread and immediately dispatches the supplied task
|
|
prompt.
|
|
|
|
```ts
|
|
type DelegateTaskInput = {
|
|
task: string;
|
|
target?: {
|
|
providerInstanceId?: string;
|
|
driverKind?: string;
|
|
model?: string;
|
|
};
|
|
title?: string;
|
|
role?: "implementation" | "research" | "review" | "design" | "test" | "general";
|
|
mode?: "async" | "wait";
|
|
timeoutMs?: number;
|
|
clientRequestId?: string;
|
|
runtimeMode?: "inherit" | "approval-required" | "auto-accept-edits" | "full-access";
|
|
interactionMode?: "inherit" | "plan" | "default";
|
|
};
|
|
```
|
|
|
|
Provider, model, runtime mode, and interaction mode inherit from the parent
|
|
when omitted. A driver-only target inherits the parent's provider instance
|
|
when it can run child tasks, and otherwise selects an available instance of
|
|
that driver; an explicit `providerInstanceId` is honored exactly and fails
|
|
when unavailable. Selecting a different provider without a model uses that
|
|
provider's first advertised model.
|
|
|
|
Each delegated review round uses a new `delegate_task` call with the original brief,
|
|
prior findings, responses, and unresolved objections. Track each round by its own `taskId` and use
|
|
a distinct `clientRequestId` per round, stable across retries of that round.
|
|
`childThreadId` is backing storage, not a target for another review round through
|
|
`t3_thread_send`. Ordinary thread messaging remains available for user-requested
|
|
conversations; it does not reopen a completed task. There is no task-level follow-up
|
|
API for preserving the same reviewer session.
|
|
|
|
Delegation requires an active parent run owned by the MCP credential's
|
|
provider session. The request becomes the V2 command
|
|
`delegated_task.request`.
|
|
|
|
`mode: "async"` returns the current durable state immediately.
|
|
`mode: "wait"` waits for the task result, including nested work and completion follow-ups, or until
|
|
the timeout expires. A wait timeout does not cancel the child; the result sets
|
|
`waitTimedOut: true`, and the caller can continue with `task_status`.
|
|
|
|
```ts
|
|
type DelegateTaskResult = {
|
|
taskId: string;
|
|
childThreadId: string;
|
|
childRunId: string | null;
|
|
childNodeId: string;
|
|
status: "queued" | "running" | "waiting" | "completed" | "failed" | "cancelled" | "interrupted";
|
|
workState: "working" | "waiting_for_children" | "result_available";
|
|
hasPendingChildRuns: boolean;
|
|
providerInstanceId: string;
|
|
model: string | null;
|
|
summary: string | null;
|
|
resultContextTransferId: string | null;
|
|
latestTerminalRunId: string | null;
|
|
latestTerminalStatus: "completed" | "failed" | "cancelled" | "interrupted" | null;
|
|
latestTerminalSummary: string | null;
|
|
latestTerminalResultContextTransferId: string | null;
|
|
waitTimedOut: boolean;
|
|
};
|
|
```
|
|
|
|
### `task_status`
|
|
|
|
Reads a delegated task from the parent thread's durable projection. A task ID
|
|
from another parent thread is rejected. `childRunId` identifies the original
|
|
run. `workState` distinguishes active work, a finished turn waiting for children,
|
|
and an available result. The task remains nonterminal until its known work
|
|
finishes. Its published `summary` and result transfer then remain stable across
|
|
later follow-ups. `hasPendingChildRuns` reports later queued or executing turns;
|
|
`latestTerminal*` exposes later executed, non-monitor results without replacing
|
|
the published task result.
|
|
|
|
### `task_cancel`
|
|
|
|
Stops the child thread with the internal `thread.stop` command, then stops every
|
|
task the child delegated, and disposes automatic parent delivery. Like a user Stop,
|
|
`thread.stop` interrupts the running turn, holds queued turns, and ends pull request
|
|
watches. A nonterminal task with no interruptible run is rejected. A terminal task
|
|
returns its existing status, and its child thread still stops, including later
|
|
runs and watch wakes. Published task results remain available. It accepts an
|
|
optional cancellation reason.
|
|
|
|
### `create_threads`
|
|
|
|
Creates between one and twenty ordinary top-level T3 threads:
|
|
|
|
```ts
|
|
type CreateThreadsInput = {
|
|
threads: Array<{
|
|
prompt?: string;
|
|
title?: string;
|
|
target?: {
|
|
providerInstanceId?: string;
|
|
driverKind?: string;
|
|
model?: string;
|
|
};
|
|
runtimeMode?: "inherit" | "approval-required" | "auto-accept-edits" | "full-access";
|
|
interactionMode?: "inherit" | "plan" | "default";
|
|
}>;
|
|
clientRequestId?: string;
|
|
};
|
|
```
|
|
|
|
Each entry independently resolves provider, model, and modes. The new threads
|
|
inherit the parent's project, branch, and worktree path, but they have no
|
|
sub-agent lineage. Entries with a prompt immediately dispatch a run; entries
|
|
without a prompt remain idle.
|
|
|
|
### `t3_thread_launch`
|
|
|
|
Launches one ordinary top-level thread through the app's launch service. Use an
|
|
explicit `workspaceStrategy` to create a new worktree (`worktree` with `baseRef`),
|
|
attach an existing checkout (`existing_worktree` with `worktreePath`), or use the
|
|
project root (`root`, also the default). The thread is bound to that workspace
|
|
before the agent starts. Creating a worktree in the task prompt does not update
|
|
this binding.
|
|
|
|
Pass the task in `message`. Project, model, and modes inherit when omitted;
|
|
workspace does not. `scratch: true` launches without a project, in a folder of
|
|
its own under the environment's Scratch project. For stacked PRs, use the parent branch as `baseRef` with
|
|
`startFromOrigin: false`. The new thread may not run with broader runtime or
|
|
interaction modes than the caller. Launch has no retry key, so inspect existing threads after a failed or lost response before
|
|
launching again. `create_threads` remains the batch option for a shared checkout.
|
|
|
|
### `t3_thread_list`
|
|
|
|
Lists durable thread shells in one project, newest first: `projectId` when
|
|
given, else the calling thread's project. Callers can filter by title, run
|
|
status, and whether app-owned sub-agent threads are included. Results are
|
|
bounded and offset-paginated. Deleted threads are never listed.
|
|
|
|
### `t3_thread_read`
|
|
|
|
Reads the durable state, recent runs, and visible timeline of any thread in
|
|
the environment by thread ID. A deleted thread returns `thread_not_found`. The
|
|
default `messages` view returns user messages, assistant
|
|
messages, and proposed plans. The `activity` view also returns summarized tool,
|
|
reasoning, checkpoint, handoff, and runtime-request items. Large item text is
|
|
bounded and reports whether it was truncated. `afterPosition` and
|
|
`nextPosition` support incremental reads.
|
|
|
|
Thread and message results include required `createdBy` and `creationSource`
|
|
provenance. MCP-created threads and user-role messages use `createdBy: "agent"`
|
|
and `creationSource: "mcp"`; provider output uses `creationSource: "provider"`.
|
|
Actor and ingress are separate so agent-authored user-role messages remain
|
|
distinguishable from human-authored messages.
|
|
|
|
Agents mention another thread as `[title](t3-thread://v1/<threadId>)`. The
|
|
link carries only the id, which resolves in the environment of the message that
|
|
holds it. Clients show the thread's current title rather than the label, so a
|
|
rename never leaves a stale link.
|
|
|
|
List and read results report `snoozed` and `snoozedUntil`, and
|
|
`t3_thread_list` filters on `snoozed`. The server's `isSnoozed` follows the
|
|
client's `effectiveSnoozed`, so agents and the sidebar agree: a snoozed thread
|
|
wakes early when it has a pending request, fails, or completes after the snooze.
|
|
|
|
### `t3_thread_update`
|
|
|
|
Updates metadata for the calling thread or any other thread in the environment.
|
|
The typed actions are `rename`, `regenerate_title`, `link_pull_request`, and
|
|
`unlink_pull_request`. A link input supplies the repository, number, and URL;
|
|
the server records the target thread's project ID. Branch and workspace changes
|
|
are outside this tool.
|
|
|
|
The result includes the command ID and durable event sequence together with the
|
|
resultant title, title-regeneration marker, and linked pull request. Reusing a
|
|
`clientRequestId` for the same action and thread replays the same command
|
|
receipt. Thread list and read results expose the linked pull request, and thread
|
|
detail also exposes an in-flight title regeneration.
|
|
|
|
### `t3_thread_send`
|
|
|
|
Sends a message to any ordinary or delegated thread in the environment:
|
|
|
|
- `auto` starts an idle thread, steers a fully active turn, or queues behind a
|
|
turn that is not yet steerable;
|
|
- `queue` creates a separate follow-up run after active work;
|
|
- `steer` requires a steerable active provider turn; and
|
|
- `restart` requires an active provider turn and uses the orchestrator's
|
|
interrupt-and-restart path.
|
|
|
|
The target runtime and interaction modes may not be broader than the caller's.
|
|
Stable command and message IDs are derived from `clientRequestId` for
|
|
idempotent retries.
|
|
|
|
### `t3_thread_wait`
|
|
|
|
Waits for a selected run to become `completed`, `failed`, `cancelled`,
|
|
`interrupted`, or `rolled_back`. Without `runId`, it pins the latest run at call
|
|
time; an idle thread returns immediately. A timeout reports the latest durable
|
|
status and does not cancel work.
|
|
|
|
### `t3_thread_interrupt`
|
|
|
|
Interrupts a selected active run through the normal V2 `run.interrupt` command.
|
|
Without `runId`, it selects the newest interruptible run. A terminal run is
|
|
returned unchanged, and a thread with no active provider turn returns
|
|
`no_active_run`.
|
|
|
|
## Delegated Task Lifecycle
|
|
|
|
The MCP server is a command ingress into V2. It does not call provider adapters
|
|
directly.
|
|
|
|
```text
|
|
provider model
|
|
-> MCP tools/call delegate_task
|
|
-> authenticated OrchestratorMcpService
|
|
-> shared ThreadManagementService
|
|
-> V2 delegated_task.request command
|
|
-> child thread + child run
|
|
-> parent app_owned subagent projection
|
|
-> parent/child execution nodes
|
|
-> consumed subagent_spawn context transfer
|
|
-> normal provider effect and runtime ingestion
|
|
-> child run reaches a terminal state
|
|
-> parent subagent/node/turn item finalized
|
|
-> consumed subagent_result context transfer
|
|
-> wait result or later task_status result
|
|
```
|
|
|
|
The child thread has lineage relationship `subagent` and points back to the
|
|
parent node. The parent gets an `app_owned` sub-agent projection and a
|
|
sub-agent turn item so the existing debug UI can render progress.
|
|
|
|
Terminal provider events trigger finalization. The event stream first replays
|
|
persisted events and then follows live events, so finalization also runs after
|
|
a server restart. An existing `subagent_result` transfer makes finalization
|
|
idempotent.
|
|
|
|
A failed run exposes its provider error before any progress text. Successful
|
|
results use the latest assistant content from the final work turn.
|
|
|
|
## Policy And Idempotency
|
|
|
|
- A child runtime mode may stay equal to or become narrower than the parent
|
|
mode. It may not escalate privileges.
|
|
- A child interaction mode may stay equal to or narrow from `default` to
|
|
`plan`. It may not escalate from `plan` to `default`.
|
|
- Thread tools take any thread in the environment as a target. For a thread
|
|
caller, list and search cover one project: its own unless `projectId` is given.
|
|
- A tool that changes another thread needs the calling thread's live run, and
|
|
the target's runtime and interaction modes may not be broader than the
|
|
caller's. This is the same privilege ceiling as child creation.
|
|
- Provider instances must be enabled, installed, available, authenticated, and
|
|
backed by a V2 adapter.
|
|
- A requested model must be advertised by the selected provider when the
|
|
provider publishes a model list.
|
|
- `clientRequestId` derives stable command, thread, and message IDs within the
|
|
provider session. Retrying the same call returns the same durable work.
|
|
- Calls without `clientRequestId` receive a generated request key and create
|
|
new work.
|
|
|
|
Expected denials use the typed `OrchestratorMcpFailure` result:
|
|
|
|
```text
|
|
capability_denied
|
|
parent_not_active
|
|
provider_unavailable
|
|
model_unavailable
|
|
runtime_mode_escalation_denied
|
|
interaction_mode_escalation_denied
|
|
task_not_found
|
|
task_not_cancellable
|
|
thread_not_found
|
|
run_not_found
|
|
thread_not_sendable
|
|
thread_not_interruptible
|
|
invalid_request
|
|
orchestration_error
|
|
```
|
|
|
|
## Code Ownership
|
|
|
|
- Shared schemas: `packages/contracts/src/orchestratorMcp.ts` and
|
|
`packages/contracts/src/threadMetadataMcp.ts`
|
|
- MCP services: `apps/server/src/mcp/OrchestratorMcpService.ts` and the focused
|
|
`apps/server/src/mcp/ThreadMetadataMcpService.ts`
|
|
- Tool definitions and handlers:
|
|
`apps/server/src/mcp/toolkits/orchestrator/`
|
|
- HTTP registration and authentication:
|
|
`apps/server/src/mcp/McpHttpServer.ts`
|
|
- Credential lifecycle: `apps/server/src/mcp/McpSessionRegistry.ts`
|
|
- Provider injection:
|
|
`apps/server/src/orchestration-v2/ProviderSessionManager.ts` and V2 adapters
|
|
- Durable delegated-task command and finalization:
|
|
`apps/server/src/orchestration-v2/Orchestrator.ts`
|
|
|
|
## Verification
|
|
|
|
The integration test uses the real MCP toolkit registration, V2 orchestrator,
|
|
SQL persistence, event ingestion, projections, and checkpoints. Only the
|
|
external provider adapters are deterministic test implementations.
|
|
|
|
Coverage includes:
|
|
|
|
- capability discovery;
|
|
- cross-provider delegated completion;
|
|
- prompt-only child context;
|
|
- parent and child lineage projections;
|
|
- spawn and result context transfers;
|
|
- async status polling;
|
|
- cancellation;
|
|
- batch ordinary-thread creation;
|
|
- thread listing and timeline reads, including another project's threads;
|
|
- ordinary-thread send, wait, steering, and interruption;
|
|
- inheritance and per-thread provider overrides; and
|
|
- idempotent retries.
|
|
|
|
Provider adapter tests separately verify Codex, Claude, Cursor, Grok, and ACP
|
|
Registry behavior and MCP injection. The provider-session manager test verifies
|
|
that credentials exist before an adapter opens and are revoked when it closes.
|