* feat(server): let clients create internal agents The daemon's own branch-name and commit-message generators run as internal agents: never persisted, listed, flagged, announced, or seen by plugin lifecycle hooks. Nothing outside the daemon could ask for one, because the wire config schema strips `internal` and the agent.create transform re-pins it. A plugin that runs a one-turn helper per event therefore left an archived session in History every time. Add an optional `internal` flag to `create_agent_request` (inherited by `agent.create.request`), copy it onto the config in the session's create path, and skip provider session persistence for it like the generators do. The client SDK forwards the flag on both creation paths and gates it once on `server_info.features.internalAgents`. Two daemon paths assumed every agent is public and had to follow: `resolveAgentIdentifier` now accepts a live agent by exact id, since the id set it matched against came from listings that exclude internal agents and left `wait_for_finish` answering "Agent not found"; and archiving an internal agent now closes it without writing the record that `registry.applySnapshot` used to leave behind, so `autoArchive` does not accumulate hidden files on disk. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(server): keep internal agents whole through failure, archive, and wait Review follow-ups for internal agents: - A create that fails after the runtime is registered now closes the internal agent before rethrowing, so a hidden runtime cannot outlive the failed request. Public agents keep today's behaviour. - Archiving an internal agent cascades to attached children like any parent. cascadeArchiveChildren takes the parent record instead of reading it back from storage, where an internal agent never is. - The archived snapshot and last message of an internal agent stay readable by exact id for ten minutes, so create-then-waitForFinish returns the finished turn whether the wait lands before or after an auto-archive. Storage plays that role for public agents. - The internal-agent e2e scenarios live in the existing create_agent_request lifecycle suite rather than a new file. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(server): cascade to live internal children and keep a failed internal create's worktree until it closes Internal children never reach storage, so the parent archive cascade scans live agents for them as well. A failed internal create now clears the created id only after the runtime is archived, so worktree cleanup cannot delete the directory under a runtime that would not close. Stored agent payloads carry lastError, so a retained failed helper reports its error through wait_for_finish instead of the generic message. * feat(cli,mcp): expose internal agents and document --no-wait paseo run gains --internal and create_agent gains internal, both forwarded to the create request. --no-wait becomes the documented spelling for returning immediately, matching paseo send; --background and --detach stay as hidden aliases under COMPAT tags. * test: follow the --no-wait rename and stub getRetiredInternalAgent * feat: list internal agents on request and add internal workspaces Internal agents stay hidden by default, but a caller can now ask for them: `fetch_agents_request` takes `filter.includeInternal`, snapshots carry `internal: true`, and `paseo ls --internal` shows them in an INTERNAL column. The app never passes the flag. Workspaces get the same flag. `workspace.create.request` with `internal: true` persists a hidden workspace: listings, update subscriptions, sync reads, empty-project counting, open-by-path, MCP `list_workspaces`, and plugin workspace hooks all skip it unless the caller opts in with `filter.includeInternal`. Every agent created inside an internal workspace is internal and unpersisted; AgentManager applies that from the workspace id before the plugin hook so creators cannot opt out. Archive and shutdown enumerate internal agents so nothing inside an internal workspace outlives it. Surfaces: `paseo workspace create --internal`, `paseo workspace ls --internal`, `paseo run --internal --new-workspace`, MCP `create_workspace({ internal })`, SDK `workspaces.create({ internal })` and `list({ filter: { includeInternal } })`. Clients gate on `features.internalWorkspaces`. Co-Authored-By: Claude <noreply@anthropic.com> * feat(workspaces): add background visibility with inherited creation defaults * fix(cli): resolve workspace callers on the selected daemon * test(workspaces): keep caller integration coverage in server suite * test(cli): exercise workspace callers through CLI integration harness * test(mcp): remove obsolete execution flags from browser fixtures --------- Co-authored-by: marvin-ambrozio <marvin@gustavo.eng.br> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
6.9 KiB
title, description, nav, order, category
| title | description | nav | order | category |
|---|---|---|---|---|
| Agents with the SDK | Create, run, find, reuse, parent, and archive coding agents from TypeScript. | Agents | 52 | TypeScript SDK |
Agents with the SDK
An agent handle keeps a stable agent ID and exposes the turn lifecycle without exposing daemon RPCs.
Run an initial prompt
const agent = await client.agents.create({
config: {
provider: "claude/claude-sonnet-5",
},
cwd: "/Users/me/dev/storefront",
prompt: "Review the checkout flow and propose one focused fix.",
labels: { source: "checkout-review" },
});
const result = await agent.waitForFinish();
console.log(result.status, result.lastMessage);
waitForFinish() returns one of four statuses:
| Status | Meaning |
|---|---|
idle |
The turn completed and the agent can accept another prompt. |
permission |
The agent needs a person to answer a permission request in Paseo. |
error |
The provider ended the turn with an error. |
timeout |
The wait deadline elapsed; the agent may still be running. |
A timeout does not cancel the agent.
Keep a session alive for follow-ups
Create an idle session when prompts arrive later:
const reviewer = await client.agents.create({
config: {
provider: "codex/gpt-5.5",
},
cwd: "/Users/me/dev/storefront",
title: "Checkout reviewer",
});
const first = await reviewer.run("Review the current diff.");
if (first.status === "idle") {
const second = await reviewer.run("Now focus on failure recovery.");
console.log(second.lastMessage);
}
Use send() for fire-and-forget delivery. Use run() when the caller needs the outcome of that turn.
Find agents by label
Set labels at creation, then filter on them. The daemon does the matching:
const page = await client.agents.list({
filter: { labels: { "issue-provider": "my-tracker" } },
});
for (const { agent } of page.entries) {
console.log(agent.id, agent.title, agent.status);
}
Continue an agent by ID
const agent = client.agents.ref("agent_01H8X...");
const result = await agent.run("Now write the fix.");
console.log(result.lastMessage);
ref() does not contact the daemon. Call refresh() first when you need to know whether the agent still exists; it returns null if it does not.
Create a subagent
Create a child through its workspace. The handle owns placement, so the caller does not repeat its directory:
if (!parent.workspaceId) throw new Error("Parent has no workspace");
const workspace = client.workspaces.ref(parent.workspaceId);
const child = await workspace.agents.create({
config: {
provider: "codex/gpt-5.5",
},
parent,
title: "Implement checkout fix",
prompt: "Implement the accepted checkout plan and run focused tests.",
});
parent establishes parentage. Archiving a parent cascade-archives its children. Call detach() first when a child should continue independently.
Run work in a background workspace
Create a background workspace and its first agent in one call. Put background on the
workspace options and the agent's configuration under agent:
const cwd = "/Users/me/dev/storefront";
const workspace = await client.workspaces.create({
source: { kind: "directory", path: cwd },
background: true,
agent: {
cwd,
config: { provider: "codex/gpt-5.5" },
prompt: "Review the checkout flow.",
},
});
The call returns a workspace handle and starts its first agent with the supplied prompt.
Background agents keep their history and survive daemon restarts. Default discovery hides them;
use client.agents.list({ filter: { includeBackground: true } }) to include them.
Exact-ID handles work normally. See workspace creation
for defaults and caller inheritance.
Request structured output
const schema = {
type: "object",
properties: {
summary: { type: "string" },
risk: { type: "string", enum: ["low", "medium", "high"] },
},
required: ["summary", "risk"],
additionalProperties: false,
};
const agent = await client.agents.create({
config: {
provider: "codex/gpt-5.5",
},
cwd: "/Users/me/dev/storefront",
outputSchema: schema,
prompt: "Assess the release risk of the current diff.",
});
const result = await agent.waitForFinish();
if (result.status !== "idle" || !result.lastMessage) {
throw new Error(result.error ?? "Agent returned no structured output");
}
const assessment = JSON.parse(result.lastMessage) as {
summary: string;
risk: "low" | "medium" | "high";
};
Validate the parsed value in your application before using it as trusted input.
Read the current state of an agent
const agent = client.agents.ref("agent_01H8X...");
await agent.refresh();
if (agent.pendingPermissions?.length) {
console.log(`Waiting on ${agent.pendingPermissions.length} permission request(s)`);
}
console.log(agent.lastUsage?.totalCostUsd, agent.runtimeInfo?.sessionId);
The handle exposes status, capabilities, availableModes, pendingPermissions, activeTurn, lastUsage, lastError, features, runtimeInfo, archivedAt, workspaceId, and cwd as properties. All of them read the last snapshot the handle observed and never fetch.
A handle from ref() has observed nothing, so every one of them is null until refresh(), run(), waitForFinish(), a timeline refetch, or subscribe() delivers a snapshot. Optional values in an observed snapshot also read as null. Use current() when you need the whole snapshot or need to distinguish those states.
subscribe() is a local listener. An owned agent-directory observation supplies its updates:
const unsubscribe = agent.subscribe(() => {
if (agent.status === "error") console.error(agent.lastError);
});
const directory = await client.agents.list({ subscribe: {} });
// When this view closes:
unsubscribe();
await directory.subscription.release();
List the commands a session loaded
const { commands, error } = await agent.commands();
if (error) throw new Error(error);
const skills = commands.filter((command) => command.kind === "skill");
The answer comes from the running session, not from a directory scan, so it includes commands and skills built into the provider that never appear on disk. kind is the provider's own classification and is optional; treat a missing kind as unclassified rather than assuming "command".
A provider that cannot produce a list reports that in error and returns an empty commands array. The call does not reject.
Archive or detach
await agent.archive(); // Soft-deletes the agent and closes its runtime.
await child.detach(); // Keeps the child alive but removes its parent relationship.
Closing the SDK connection does not archive agents. Archive temporary agents explicitly, preferably in finally.