Publish the TypeScript SDK and integration guides (#3141)
* feat(client): publish the TypeScript SDK surface Make the high-level client API usable for external integrations and document complete agent, workspace, provider, event, and recipe flows. * fix(client): correlate provider snapshots by daemon cwd Return the daemon's canonical snapshot identity so waitForReady can match updates for relative, tilde, and otherwise non-canonical paths without duplicating filesystem rules in the SDK. * test(server): make snapshot cwd assertion portable * fix(client): gate canonical provider snapshot waits Older daemons do not expose the canonical snapshot identity needed to correlate workspace updates safely. Require the advertised capability and fail with an update-host message instead of guessing across workspaces.
This commit is contained in:
@@ -1,12 +1,39 @@
|
||||
# @getpaseo/client
|
||||
|
||||
Paseo's JavaScript/TypeScript client package.
|
||||
TypeScript SDK for building integrations on top of a Paseo daemon.
|
||||
|
||||
```bash
|
||||
npm install @getpaseo/client
|
||||
```
|
||||
|
||||
```ts
|
||||
import { createPaseoClient } from "@getpaseo/client";
|
||||
|
||||
const client = createPaseoClient({ url: "ws://127.0.0.1:6767/ws" });
|
||||
await client.connect();
|
||||
|
||||
const agent = await client.agents.create({
|
||||
config: { provider: "codex/gpt-5.5" },
|
||||
cwd: "/Users/me/dev/storefront",
|
||||
prompt: "Review the current diff and name the riskiest change.",
|
||||
});
|
||||
|
||||
const result = await agent.waitForFinish();
|
||||
console.log(result.lastMessage);
|
||||
|
||||
await client.close();
|
||||
```
|
||||
|
||||
The public API is the package root. Imports under `@getpaseo/client/internal/*` are unsupported implementation details used by Paseo's own packages.
|
||||
|
||||
Read the [SDK documentation](https://paseo.sh/docs/sdk) for agents, workspaces, provider discovery, events, recipes, and the API reference. Runnable TypeScript patterns also live in [`examples/`](./examples/README.md).
|
||||
|
||||
## Runtime
|
||||
|
||||
The client needs a WebSocket implementation. Modern browsers and Node.js 22 provide one globally.
|
||||
|
||||
Use a WebSocket URL ending in `/ws`, such as `ws://127.0.0.1:6767/ws`. Pass `password` when the daemon requires authentication.
|
||||
|
||||
## Stability
|
||||
|
||||
This package is public so Paseo's published packages can depend on it cleanly.
|
||||
It is not a stable public SDK yet.
|
||||
|
||||
APIs, exports, runtime behavior, and types may change or disappear in any
|
||||
release without advance notice. Use it outside Paseo at your own risk until the
|
||||
package is explicitly documented as stable.
|
||||
The high-level API exported from `@getpaseo/client` is the supported SDK surface. The SDK and daemon remain protocol-compatible across versions, but newly added capabilities can require a newer daemon.
|
||||
|
||||
@@ -6,13 +6,16 @@ These examples use only the public SDK root:
|
||||
import { createPaseoClient, type PaseoClient } from "@getpaseo/client";
|
||||
```
|
||||
|
||||
Pass the daemon WebSocket URL into the exported functions. In worktree dev, read it
|
||||
Each example takes the daemon WebSocket URL as an argument. In worktree dev, read it
|
||||
from the portless banner or `portless get daemon`; for the desktop-managed daemon,
|
||||
use the URL for that daemon.
|
||||
|
||||
- `workspaces.ts` covers creating, opening, refetching, and archiving a workspace.
|
||||
- `agents-and-providers.ts` covers creating agents and choosing providers with `client.providers.*`.
|
||||
- `quickstart.ts` runs one agent and prints its reply. It is a standalone script with the URL at the top; the rest export functions.
|
||||
- `workspaces.ts` covers creating a fresh workspace, opening by directory, refreshing, and archiving.
|
||||
- `agents-and-providers.ts` covers provider discovery, creating agents, and waiting for turns.
|
||||
- `events-and-timeline.ts` covers subscribing to workspace, agent, and timeline events, plus refetching a timeline page.
|
||||
- `issue-to-agent.ts` turns an issue record into a visible Paseo workspace and agent.
|
||||
- `parallel-review.ts` launches several reviewers concurrently and cleans them up.
|
||||
- `provider-settings.ts` covers provider settings that are currently daemon config-backed.
|
||||
|
||||
Provider profiles, provider env vars, custom binaries, and additional models are still raw daemon config behavior. The SDK exposes them through `client.config.get()` and `client.config.patch()` until first-class provider settings RPCs exist.
|
||||
|
||||
@@ -14,13 +14,11 @@ export async function createCodexAgent(url: string, cwd: string): Promise<string
|
||||
|
||||
const agent = await client.agents.create({
|
||||
config: {
|
||||
...client.providers.codex({
|
||||
model: "gpt-5.2",
|
||||
modeId: "full-auto",
|
||||
}),
|
||||
cwd,
|
||||
provider: "codex/gpt-5.5",
|
||||
modeId: "full-access",
|
||||
},
|
||||
initialPrompt: "Inspect this repository and summarize the next useful task.",
|
||||
cwd,
|
||||
prompt: "Inspect this repository and summarize the next useful task.",
|
||||
});
|
||||
|
||||
return agent.id;
|
||||
@@ -35,18 +33,18 @@ export async function chooseProviderFromSnapshot(url: string, cwd: string): Prom
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
const snapshot = await client.providers.snapshot({ cwd });
|
||||
const snapshot = await client.providers.waitForReady({ cwd });
|
||||
const readyProvider = snapshot.entries.find((provider) => provider.status === "ready");
|
||||
const providerConfig = readyProvider
|
||||
? client.providers.config(readyProvider.provider)
|
||||
: client.providers.claude();
|
||||
const model =
|
||||
readyProvider?.models?.find((candidate) => candidate.isDefault) ?? readyProvider?.models?.[0];
|
||||
if (!readyProvider || !model) throw new Error("No provider model is ready");
|
||||
|
||||
const agent = await client.agents.create({
|
||||
config: {
|
||||
...providerConfig,
|
||||
cwd,
|
||||
provider: `${readyProvider.provider}/${model.id}`,
|
||||
},
|
||||
initialPrompt: "Start with a quick repository map.",
|
||||
cwd,
|
||||
prompt: "Start with a quick repository map.",
|
||||
});
|
||||
|
||||
return agent.id;
|
||||
@@ -54,3 +52,16 @@ export async function chooseProviderFromSnapshot(url: string, cwd: string): Prom
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runFollowUp(url: string, agentId: string): Promise<string | null> {
|
||||
const client = createClient(url);
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
const result = await client.agents.ref(agentId).run("Summarize your progress and next step.");
|
||||
if (result.status !== "idle") throw new Error(result.error ?? result.status);
|
||||
return result.lastMessage;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createPaseoClient, type PaseoClient } from "@getpaseo/client";
|
||||
|
||||
interface Issue {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
repositoryPath: string;
|
||||
}
|
||||
|
||||
export async function startIssue(client: PaseoClient, issue: Issue) {
|
||||
const workspace = await client.workspaces.open(issue.repositoryPath);
|
||||
const agent = await workspace.agents.create({
|
||||
config: {
|
||||
provider: "codex/gpt-5.5",
|
||||
},
|
||||
title: issue.title,
|
||||
labels: {
|
||||
"issue-provider": "my-tracker",
|
||||
"issue-id": issue.id,
|
||||
},
|
||||
prompt: [
|
||||
`Implement issue ${issue.id}: ${issue.title}`,
|
||||
"",
|
||||
issue.description,
|
||||
"",
|
||||
"Run focused tests and summarize the result.",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
return { workspaceId: workspace.id, agentId: agent.id };
|
||||
}
|
||||
|
||||
export function createClient(url: string): PaseoClient {
|
||||
return createPaseoClient({ url });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { PaseoAgentHandle, PaseoClient } from "@getpaseo/client";
|
||||
|
||||
export async function reviewInParallel(client: PaseoClient, cwd: string): Promise<string[]> {
|
||||
const agents: PaseoAgentHandle[] = [];
|
||||
|
||||
try {
|
||||
agents.push(
|
||||
...(await Promise.all([
|
||||
client.agents.create({
|
||||
config: {
|
||||
provider: "codex/gpt-5.5",
|
||||
},
|
||||
cwd,
|
||||
title: "Correctness review",
|
||||
prompt: "Review the current diff for correctness and missed edge cases.",
|
||||
}),
|
||||
client.agents.create({
|
||||
config: {
|
||||
provider: "claude/claude-sonnet-5",
|
||||
},
|
||||
cwd,
|
||||
title: "Security review",
|
||||
prompt: "Review the current diff for security and unsafe input handling.",
|
||||
}),
|
||||
])),
|
||||
);
|
||||
|
||||
const results = await Promise.all(agents.map((agent) => agent.waitForFinish()));
|
||||
|
||||
return results.map((result) => {
|
||||
if (result.status !== "idle") throw new Error(result.error ?? result.status);
|
||||
return result.lastMessage ?? "";
|
||||
});
|
||||
} finally {
|
||||
await Promise.allSettled(agents.map((agent) => agent.archive()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createPaseoClient } from "@getpaseo/client";
|
||||
|
||||
const client = createPaseoClient({ url: "ws://127.0.0.1:6767/ws" });
|
||||
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
const agent = await client.agents.create({
|
||||
config: {
|
||||
provider: "codex/gpt-5.5",
|
||||
},
|
||||
cwd: process.cwd(),
|
||||
prompt: "Inspect this repository and name the next useful task.",
|
||||
});
|
||||
|
||||
const result = await agent.waitForFinish();
|
||||
if (result.status !== "idle") {
|
||||
throw new Error(result.error ?? `Agent stopped with status: ${result.status}`);
|
||||
}
|
||||
|
||||
console.log(result.lastMessage);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
@@ -12,18 +12,14 @@ export async function createOpenAndArchiveWorkspace(url: string, cwd: string): P
|
||||
try {
|
||||
await client.connect();
|
||||
|
||||
const created = await client.workspaces.create(cwd);
|
||||
if (!created.workspace) {
|
||||
throw new Error(created.error ?? "Workspace creation failed");
|
||||
}
|
||||
|
||||
const created = await client.workspaces.create({
|
||||
source: { kind: "directory", path: cwd },
|
||||
title: "Fresh SDK workspace",
|
||||
});
|
||||
const opened = await client.workspaces.open(cwd);
|
||||
if (!opened.workspace) {
|
||||
throw new Error(opened.error ?? "Workspace open failed");
|
||||
}
|
||||
|
||||
await opened.workspace.refetch();
|
||||
await opened.workspace.archive();
|
||||
await opened.refresh();
|
||||
await created.archive();
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { createPaseoClient } from "./index.js";
|
||||
import type { PaseoAgent, PaseoClient, PaseoProviderConfig, PaseoWorkspace } from "./index.js";
|
||||
import type { PaseoAgent, PaseoClient, PaseoWorkspace } from "./index.js";
|
||||
|
||||
type FakeWebSocketHandler = (...args: unknown[]) => void;
|
||||
|
||||
@@ -80,7 +80,9 @@ function parseSentFrame(
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
async function connectClient(): Promise<{ client: PaseoClient; ws: FakeWebSocket }> {
|
||||
async function connectClient(
|
||||
features: Record<string, boolean> = { providersSnapshotCwd: true },
|
||||
): Promise<{ client: PaseoClient; ws: FakeWebSocket }> {
|
||||
vi.stubGlobal("WebSocket", FakeWebSocket);
|
||||
const client = createPaseoClient({
|
||||
url: "ws://daemon.test",
|
||||
@@ -105,6 +107,7 @@ async function connectClient(): Promise<{ client: PaseoClient; ws: FakeWebSocket
|
||||
serverId: "srv_sdk_test",
|
||||
hostname: null,
|
||||
version: null,
|
||||
features,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -211,6 +214,59 @@ test("createPaseoClient exposes workspace list through the daemon client", async
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent actions list the daemon directory without exposing the low-level client", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const listedAgent = createAgent({ title: "Planner" });
|
||||
|
||||
const listPromise = client.agents.list({
|
||||
filter: { includeArchived: false },
|
||||
page: { limit: 10 },
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
type: "fetch_agents_request",
|
||||
filter: { includeArchived: false },
|
||||
page: { limit: 10 },
|
||||
});
|
||||
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "fetch_agents_response",
|
||||
payload: {
|
||||
requestId: request.requestId,
|
||||
entries: [
|
||||
{
|
||||
agent: listedAgent,
|
||||
project: {
|
||||
projectKey: "project_sdk",
|
||||
projectName: "SDK",
|
||||
workspaceName: "main",
|
||||
checkout: {
|
||||
cwd: "/repo/sdk",
|
||||
isGit: false,
|
||||
currentBranch: null,
|
||||
remoteUrl: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
prevCursor: null,
|
||||
hasMore: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(listPromise).resolves.toMatchObject({
|
||||
entries: [{ agent: { id: "agent_sdk", title: "Planner" } }],
|
||||
});
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("workspace handles keep identity and refresh snapshots through existing driver calls", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const openedWorkspace = createWorkspace();
|
||||
@@ -232,17 +288,18 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
}),
|
||||
);
|
||||
|
||||
const opened = await openPromise;
|
||||
const workspace = opened.workspace;
|
||||
expect(workspace?.id).toBe("workspace_sdk");
|
||||
expect(workspace?.latest()).toEqual(openedWorkspace);
|
||||
const workspace = await openPromise;
|
||||
expect(workspace.id).toBe("workspace_sdk");
|
||||
expect(workspace.projectId).toBe("project_sdk");
|
||||
expect(workspace.directory).toBe("/repo/sdk");
|
||||
expect(workspace.current()).toEqual(openedWorkspace);
|
||||
|
||||
const refreshedWorkspace = createWorkspace({ name: "sdk refreshed" });
|
||||
const refetchPromise = workspace?.refetch({ requestId: "workspace-refetch-request" });
|
||||
const refetchPromise = workspace.refresh({ requestId: "workspace-refetch-request" });
|
||||
expect(parseSentSessionMessage(ws.sent.at(-1))).toMatchObject({
|
||||
type: "fetch_workspaces_request",
|
||||
requestId: "workspace-refetch-request",
|
||||
page: { limit: 25 },
|
||||
page: { limit: 200 },
|
||||
});
|
||||
|
||||
ws.message(
|
||||
@@ -250,6 +307,27 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
requestId: "workspace-refetch-request",
|
||||
entries: [],
|
||||
pageInfo: {
|
||||
nextCursor: "workspace-page-2",
|
||||
prevCursor: null,
|
||||
hasMore: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const secondPageRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(secondPageRequest).toMatchObject({
|
||||
type: "fetch_workspaces_request",
|
||||
page: { limit: 200, cursor: "workspace-page-2" },
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "fetch_workspaces_response",
|
||||
payload: {
|
||||
requestId: secondPageRequest.requestId,
|
||||
entries: [refreshedWorkspace],
|
||||
pageInfo: {
|
||||
nextCursor: null,
|
||||
@@ -261,10 +339,10 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
);
|
||||
|
||||
await expect(refetchPromise).resolves.toEqual(refreshedWorkspace);
|
||||
expect(workspace?.latest()).toEqual(refreshedWorkspace);
|
||||
expect(workspace.current()).toEqual(refreshedWorkspace);
|
||||
|
||||
const updates: string[] = [];
|
||||
const unsubscribe = workspace?.subscribe((update) => {
|
||||
const unsubscribe = workspace.subscribe((update) => {
|
||||
if (update.kind === "upsert") {
|
||||
updates.push(update.workspace.name);
|
||||
}
|
||||
@@ -280,9 +358,9 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
}),
|
||||
);
|
||||
expect(updates).toEqual(["sdk pushed"]);
|
||||
expect(workspace?.latest()).toEqual(pushedWorkspace);
|
||||
expect(workspace.current()).toEqual(pushedWorkspace);
|
||||
|
||||
unsubscribe?.();
|
||||
unsubscribe();
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "workspace_update",
|
||||
@@ -297,20 +375,79 @@ test("workspace handles keep identity and refresh snapshots through existing dri
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("workspace create is fresh and workspace-scoped agent create owns placement", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const createdWorkspace = createWorkspace({ id: "workspace_fresh", name: "Issue 42" });
|
||||
|
||||
const workspacePromise = client.workspaces.create({
|
||||
source: { kind: "directory", path: "/repo/sdk", projectId: "project_sdk" },
|
||||
title: "Issue 42",
|
||||
});
|
||||
const workspaceRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(workspaceRequest).toMatchObject({
|
||||
type: "workspace.create.request",
|
||||
source: { kind: "directory", path: "/repo/sdk", projectId: "project_sdk" },
|
||||
title: "Issue 42",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "workspace.create.response",
|
||||
payload: {
|
||||
requestId: workspaceRequest.requestId,
|
||||
workspace: createdWorkspace,
|
||||
setupTerminalId: null,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const workspace = await workspacePromise;
|
||||
const agentPromise = workspace.agents.create({
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
parent: client.agents.ref("parent_sdk"),
|
||||
prompt: "Implement issue 42.",
|
||||
});
|
||||
const agentRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(agentRequest).toMatchObject({
|
||||
type: "create_agent_request",
|
||||
config: { provider: "codex", model: "gpt-5.4", cwd: "/repo/sdk" },
|
||||
workspaceId: "workspace_fresh",
|
||||
callerAgentId: "parent_sdk",
|
||||
initialPrompt: "Implement issue 42.",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "status",
|
||||
payload: {
|
||||
status: "agent_created",
|
||||
requestId: agentRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
agent: createAgent({ workspaceId: "workspace_fresh" }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const agent = await agentPromise;
|
||||
expect(agent.workspaceId).toBe("workspace_fresh");
|
||||
expect(agent.cwd).toBe("/repo/sdk");
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent handles delegate create, send, timeline refetch, archive, and local updates", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const createdAgent = createAgent();
|
||||
|
||||
const createPromise = client.agents.create({
|
||||
provider: "codex",
|
||||
config: { provider: "codex/gpt-5.4" },
|
||||
cwd: "/repo/sdk",
|
||||
initialPrompt: "ship it",
|
||||
prompt: "ship it",
|
||||
});
|
||||
const createRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(createRequest).toMatchObject({
|
||||
type: "create_agent_request",
|
||||
config: {
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
cwd: "/repo/sdk",
|
||||
},
|
||||
initialPrompt: "ship it",
|
||||
@@ -330,7 +467,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
|
||||
const agent = await createPromise;
|
||||
expect(agent.id).toBe("agent_sdk");
|
||||
expect(agent.latest()).toEqual(createdAgent);
|
||||
expect(agent.current()).toEqual(createdAgent);
|
||||
|
||||
const updatedAgents: string[] = [];
|
||||
const unsubscribe = agent.subscribe((update) => {
|
||||
@@ -350,7 +487,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
}),
|
||||
);
|
||||
expect(updatedAgents).toEqual(["Updated"]);
|
||||
expect(agent.latest()).toEqual(updatedAgent);
|
||||
expect(agent.current()).toEqual(updatedAgent);
|
||||
|
||||
const sendPromise = agent.send("hello", { messageId: "message-sdk" });
|
||||
const sendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
@@ -374,6 +511,74 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
);
|
||||
await sendPromise;
|
||||
|
||||
const runPromise = agent.run("finish the task", {
|
||||
messageId: "run-message-sdk",
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
const runSendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(runSendRequest).toMatchObject({
|
||||
type: "send_agent_message_request",
|
||||
agentId: "agent_sdk",
|
||||
text: "finish the task",
|
||||
messageId: "run-message-sdk",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "send_agent_message_response",
|
||||
payload: {
|
||||
requestId: runSendRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
accepted: true,
|
||||
error: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const waitRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(waitRequest).toMatchObject({
|
||||
type: "wait_for_finish_request",
|
||||
agentId: "agent_sdk",
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
const finishedAgent = createAgent({ title: "Finished" });
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "wait_for_finish_response",
|
||||
payload: {
|
||||
requestId: waitRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
status: "idle",
|
||||
final: finishedAgent,
|
||||
error: null,
|
||||
lastMessage: "DONE",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(runPromise).resolves.toMatchObject({ status: "idle", lastMessage: "DONE" });
|
||||
expect(agent.current()).toEqual(finishedAgent);
|
||||
|
||||
const defaultWaitPromise = agent.waitForFinish();
|
||||
const defaultWaitRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(defaultWaitRequest).toMatchObject({
|
||||
type: "wait_for_finish_request",
|
||||
agentId: "agent_sdk",
|
||||
timeoutMs: 10 * 60_000,
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "wait_for_finish_response",
|
||||
payload: {
|
||||
requestId: defaultWaitRequest.requestId,
|
||||
agentId: "agent_sdk",
|
||||
status: "idle",
|
||||
final: finishedAgent,
|
||||
error: null,
|
||||
lastMessage: "DONE",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(defaultWaitPromise).resolves.toMatchObject({ status: "idle" });
|
||||
|
||||
const timelineAgent = createAgent({ title: "Timeline" });
|
||||
const timelinePromise = agent.timeline.refetch({ limit: 5 });
|
||||
const timelineRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
@@ -410,7 +615,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
}),
|
||||
);
|
||||
await timelinePromise;
|
||||
expect(agent.latest()).toEqual(timelineAgent);
|
||||
expect(agent.current()).toEqual(timelineAgent);
|
||||
|
||||
const archivePromise = agent.archive();
|
||||
const archiveRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
@@ -431,7 +636,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
||||
await expect(archivePromise).resolves.toEqual({
|
||||
archivedAt: "2026-05-16T01:00:00.000Z",
|
||||
});
|
||||
expect(agent.latest()?.archivedAt).toBe("2026-05-16T01:00:00.000Z");
|
||||
expect(agent.current()?.archivedAt).toBe("2026-05-16T01:00:00.000Z");
|
||||
|
||||
unsubscribe();
|
||||
await client.close();
|
||||
@@ -496,10 +701,9 @@ test("provider actions delegate to existing provider RPCs and local snapshot upd
|
||||
|
||||
const featuresPromise = client.providers.listFeatures(
|
||||
{
|
||||
provider: "codex",
|
||||
provider: "codex/gpt-5.4",
|
||||
cwd: "/repo/sdk",
|
||||
modeId: "full-access",
|
||||
model: "gpt-5.4",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
},
|
||||
@@ -579,6 +783,81 @@ test("provider actions delegate to existing provider RPCs and local snapshot upd
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
});
|
||||
|
||||
const readyPromise = client.providers.waitForReady({ cwd: "/repo/sdk", timeoutMs: 5_000 });
|
||||
const readyRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(readyRequest).toMatchObject({
|
||||
type: "get_providers_snapshot_request",
|
||||
cwd: "/repo/sdk",
|
||||
});
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
requestId: readyRequest.requestId,
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "loading", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:00.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd: "/repo/other",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:30.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:01.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(readyPromise).resolves.toMatchObject({
|
||||
requestId: readyRequest.requestId,
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:01.000Z",
|
||||
});
|
||||
|
||||
const canonicalReadyPromise = client.providers.waitForReady({
|
||||
cwd: "/repo/./sdk",
|
||||
timeoutMs: 5_000,
|
||||
});
|
||||
const canonicalReadyRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "providers_snapshot_update",
|
||||
payload: {
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:02.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
ws.message(
|
||||
sessionMessage({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
requestId: canonicalReadyRequest.requestId,
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "loading", enabled: true }],
|
||||
generatedAt: "2026-05-16T00:00:01.000Z",
|
||||
},
|
||||
}),
|
||||
);
|
||||
await expect(canonicalReadyPromise).resolves.toMatchObject({
|
||||
requestId: canonicalReadyRequest.requestId,
|
||||
cwd: "/repo/sdk",
|
||||
entries: [{ provider: "codex", status: "ready", enabled: true }],
|
||||
});
|
||||
|
||||
const refreshPromise = client.providers.refresh({
|
||||
cwd: "/repo/sdk",
|
||||
providers: ["codex"],
|
||||
@@ -665,6 +944,18 @@ test("provider actions delegate to existing provider RPCs and local snapshot upd
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("waitForReady requires canonical provider snapshot identity from the host", async () => {
|
||||
const { client, ws } = await connectClient({});
|
||||
const sentBeforeWait = ws.sent.length;
|
||||
|
||||
await expect(client.providers.waitForReady({ cwd: "/repo/./sdk" })).rejects.toThrow(
|
||||
"Update the host to wait for provider discovery.",
|
||||
);
|
||||
expect(ws.sent).toHaveLength(sentBeforeWait);
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("config actions delegate to existing daemon config RPCs", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
|
||||
@@ -757,34 +1048,29 @@ test("config actions delegate to existing daemon config RPCs", async () => {
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("provider config builders shape existing create-agent config fields", async () => {
|
||||
test("agent config maps provider/model and provider-native options to the daemon", async () => {
|
||||
const { client, ws } = await connectClient();
|
||||
const provider = client.providers.codex({
|
||||
model: "gpt-5.4",
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
});
|
||||
const expectedProviderConfig = {
|
||||
provider: "codex",
|
||||
model: "gpt-5.4",
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
} satisfies PaseoProviderConfig;
|
||||
|
||||
expect(provider).toEqual(expectedProviderConfig);
|
||||
|
||||
const createdAgent = createAgent({
|
||||
model: "gpt-5.4",
|
||||
currentModeId: "full-access",
|
||||
});
|
||||
const createPromise = client.agents.create({
|
||||
config: {
|
||||
...provider,
|
||||
cwd: "/repo/sdk",
|
||||
provider: "codex/gpt-5.4",
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
options: {
|
||||
approval_policy: "never",
|
||||
sandbox_mode: "workspace-write",
|
||||
sandbox_workspace_write: {
|
||||
writable_roots: ["/repo/sdk"],
|
||||
network_access: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialPrompt: "use configured provider",
|
||||
cwd: "/repo/sdk",
|
||||
prompt: "use configured provider",
|
||||
});
|
||||
const request = parseSentSessionMessage(ws.sent.at(-1));
|
||||
expect(request).toMatchObject({
|
||||
@@ -796,6 +1082,14 @@ test("provider config builders shape existing create-agent config fields", async
|
||||
modeId: "full-access",
|
||||
thinkingOptionId: "high",
|
||||
featureValues: { webSearch: true },
|
||||
providerOptions: {
|
||||
approval_policy: "never",
|
||||
sandbox_mode: "workspace-write",
|
||||
sandbox_workspace_write: {
|
||||
writable_roots: ["/repo/sdk"],
|
||||
network_access: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
initialPrompt: "use configured provider",
|
||||
});
|
||||
@@ -813,7 +1107,20 @@ test("provider config builders shape existing create-agent config fields", async
|
||||
);
|
||||
|
||||
const agent = await createPromise;
|
||||
expect(agent.latest()).toEqual(createdAgent);
|
||||
expect(agent.current()).toEqual(createdAgent);
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
test("agent config requires provider/model syntax", async () => {
|
||||
const { client } = await connectClient();
|
||||
|
||||
await expect(
|
||||
client.agents.create({
|
||||
config: { provider: "codex" },
|
||||
cwd: "/repo/sdk",
|
||||
}),
|
||||
).rejects.toThrow('Expected config.provider in "provider/model" format');
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
+326
-121
@@ -17,24 +17,25 @@ import type {
|
||||
SendAgentMessageRequest,
|
||||
SessionOutboundMessage,
|
||||
WorkspaceDescriptorPayload,
|
||||
WorkspaceCreateRequest,
|
||||
} from "@getpaseo/protocol/messages";
|
||||
import { DaemonClient } from "./daemon-client.js";
|
||||
import type {
|
||||
FetchAgentsEntry,
|
||||
FetchAgentsOptions,
|
||||
FetchAgentsPageInfo,
|
||||
FetchAgentTimelineCursor,
|
||||
FetchAgentTimelineDirection,
|
||||
FetchAgentTimelinePayload,
|
||||
FetchAgentTimelineProjection,
|
||||
WaitForFinishResult,
|
||||
} from "./daemon-client.js";
|
||||
|
||||
export { DaemonClient };
|
||||
export type {
|
||||
DaemonClientConfig,
|
||||
DaemonEvent,
|
||||
BrowserAutomationExecuteRequestMessage,
|
||||
BrowserAutomationExecuteResponseMessage,
|
||||
WebSocketFactory,
|
||||
WebSocketLike,
|
||||
} from "./daemon-client.js";
|
||||
/**
|
||||
* Coding turns routinely run for minutes, so the handle waits far longer than
|
||||
* the transport's own conservative default.
|
||||
*/
|
||||
const DEFAULT_WAIT_FOR_FINISH_MS = 10 * 60_000;
|
||||
|
||||
export type ConnectionState =
|
||||
| { status: "idle" }
|
||||
@@ -75,6 +76,14 @@ export interface PaseoClientConfig {
|
||||
|
||||
export type PaseoWorkspace = WorkspaceDescriptorPayload;
|
||||
export type PaseoAgent = AgentSnapshotPayload;
|
||||
export type PaseoAgentListOptions = FetchAgentsOptions;
|
||||
|
||||
export interface PaseoAgentListResult {
|
||||
requestId: string;
|
||||
subscriptionId?: string | null;
|
||||
entries: FetchAgentsEntry[];
|
||||
pageInfo: FetchAgentsPageInfo;
|
||||
}
|
||||
export type PaseoWorkspaceListOptions = Omit<
|
||||
FetchWorkspacesRequestMessage,
|
||||
"type" | "requestId"
|
||||
@@ -94,11 +103,9 @@ export interface PaseoWorkspaceOpenOptions {
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface PaseoWorkspaceOpenResult {
|
||||
requestId: string;
|
||||
workspace: PaseoWorkspaceHandle | null;
|
||||
error: string | null;
|
||||
}
|
||||
export type PaseoWorkspaceCreateOptions = Omit<WorkspaceCreateRequest, "type" | "requestId"> & {
|
||||
requestId?: string;
|
||||
};
|
||||
|
||||
export interface PaseoWorkspaceArchiveResult {
|
||||
requestId: string;
|
||||
@@ -114,19 +121,17 @@ export type PaseoWorkspaceUpdate = Extract<
|
||||
|
||||
export type PaseoWorkspaceUpdateHandler = (update: PaseoWorkspaceUpdate) => void;
|
||||
|
||||
/**
|
||||
* A handle is a stable typed reference to a daemon resource. Its identity is the
|
||||
* daemon id, and `latest()` only returns the most recent snapshot this handle has
|
||||
* seen through construction, `refetch()`, or this handle's local subscription.
|
||||
*/
|
||||
export interface PaseoWorkspaceHandle {
|
||||
readonly id: string;
|
||||
latest(): PaseoWorkspace | null;
|
||||
/**
|
||||
* Fetches a fresh workspace snapshot through the existing workspace list RPC,
|
||||
* exact-matches this handle id from the result, and updates `latest()`.
|
||||
*/
|
||||
refetch(options?: { requestId?: string }): Promise<PaseoWorkspace | null>;
|
||||
readonly projectId: string | null;
|
||||
readonly directory: string | null;
|
||||
readonly name: string | null;
|
||||
readonly status: PaseoWorkspace["status"] | null;
|
||||
readonly agents: {
|
||||
create(options: PaseoWorkspaceAgentCreateOptions): Promise<PaseoAgentHandle>;
|
||||
};
|
||||
current(): PaseoWorkspace | null;
|
||||
refresh(options?: { requestId?: string }): Promise<PaseoWorkspace | null>;
|
||||
archive(requestId?: string): Promise<PaseoWorkspaceArchiveResult>;
|
||||
/**
|
||||
* Subscribes to already-emitted daemon workspace_update events for this id.
|
||||
@@ -143,11 +148,8 @@ export interface PaseoWorkspaceActions {
|
||||
open(
|
||||
input: string | PaseoWorkspaceOpenOptions,
|
||||
requestId?: string,
|
||||
): Promise<PaseoWorkspaceOpenResult>;
|
||||
create(
|
||||
input: string | PaseoWorkspaceOpenOptions,
|
||||
requestId?: string,
|
||||
): Promise<PaseoWorkspaceOpenResult>;
|
||||
): Promise<PaseoWorkspaceHandle>;
|
||||
create(options: PaseoWorkspaceCreateOptions): Promise<PaseoWorkspaceHandle>;
|
||||
archive(
|
||||
workspace: string | PaseoWorkspaceHandle,
|
||||
requestId?: string,
|
||||
@@ -160,25 +162,43 @@ export interface PaseoWorkspaceActions {
|
||||
}
|
||||
|
||||
type PaseoAgentSessionConfig = CreateAgentRequestMessage["config"];
|
||||
type PaseoAgentProvider = PaseoAgentSessionConfig["provider"];
|
||||
type PaseoAgentConfigOverrides = Partial<Omit<PaseoAgentSessionConfig, "provider" | "cwd">>;
|
||||
export type PaseoAgentProvider = PaseoAgentSessionConfig["provider"];
|
||||
|
||||
export interface PaseoAgentCreateOptions extends PaseoAgentConfigOverrides {
|
||||
config?: PaseoAgentSessionConfig;
|
||||
provider?: CreateAgentRequestMessage["config"]["provider"];
|
||||
cwd?: string;
|
||||
workspaceId?: string;
|
||||
callerAgentId?: string;
|
||||
initialPrompt?: string;
|
||||
export type PaseoProviderFeatureValues = Record<string, unknown>;
|
||||
|
||||
export interface PaseoAgentConfig {
|
||||
/** Provider and model in `provider/model` format. */
|
||||
provider: string;
|
||||
modeId?: PaseoAgentSessionConfig["modeId"];
|
||||
thinkingOptionId?: PaseoAgentSessionConfig["thinkingOptionId"];
|
||||
featureValues?: PaseoProviderFeatureValues;
|
||||
/** JSON-safe provider-native settings, validated by the selected provider. */
|
||||
options?: PaseoAgentSessionConfig["providerOptions"];
|
||||
systemPrompt?: PaseoAgentSessionConfig["systemPrompt"];
|
||||
toolPolicy?: PaseoAgentSessionConfig["toolPolicy"];
|
||||
mcpServers?: PaseoAgentSessionConfig["mcpServers"];
|
||||
}
|
||||
|
||||
export interface PaseoAgentCreateOptions {
|
||||
config: PaseoAgentConfig;
|
||||
cwd: string;
|
||||
parent?: string | PaseoAgentHandle;
|
||||
title?: PaseoAgentSessionConfig["title"];
|
||||
env?: CreateAgentRequestMessage["env"];
|
||||
prompt?: string;
|
||||
clientMessageId?: string;
|
||||
outputSchema?: Record<string, unknown>;
|
||||
images?: CreateAgentRequestMessage["images"];
|
||||
attachments?: CreateAgentRequestMessage["attachments"];
|
||||
git?: CreateAgentRequestMessage["git"];
|
||||
worktree?: CreateAgentRequestMessage["worktree"];
|
||||
autoArchive?: CreateAgentRequestMessage["autoArchive"];
|
||||
requestId?: string;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
export type PaseoWorkspaceAgentCreateOptions = Omit<PaseoAgentCreateOptions, "cwd">;
|
||||
|
||||
export interface PaseoAgentRefetchResult {
|
||||
agent: PaseoAgent;
|
||||
project: ProjectPlacementPayload | null;
|
||||
@@ -198,6 +218,12 @@ export interface PaseoAgentSendOptions {
|
||||
attachments?: SendAgentMessageRequest["attachments"];
|
||||
}
|
||||
|
||||
export interface PaseoAgentRunOptions extends PaseoAgentSendOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export type PaseoAgentRunResult = WaitForFinishResult;
|
||||
|
||||
export type PaseoAgentUpdate = Extract<SessionOutboundMessage, { type: "agent_update" }>["payload"];
|
||||
|
||||
export type PaseoAgentStream = Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"];
|
||||
@@ -207,8 +233,8 @@ export type PaseoAgentUpdateHandler = (update: PaseoAgentUpdate) => void;
|
||||
export interface PaseoAgentTimelineHandle {
|
||||
/**
|
||||
* Fetches a fresh timeline page through the existing daemon RPC. If the daemon
|
||||
* includes an agent snapshot in the response, the parent handle's `latest()`
|
||||
* is updated to that snapshot.
|
||||
* includes an agent snapshot in the response, the parent handle is updated to
|
||||
* that value.
|
||||
*/
|
||||
refetch(options?: PaseoAgentTimelineRefetchOptions): Promise<FetchAgentTimelinePayload>;
|
||||
/**
|
||||
@@ -218,24 +244,26 @@ export interface PaseoAgentTimelineHandle {
|
||||
subscribe(handler: (event: PaseoAgentStream) => void): () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent handles follow the same identity/snapshot rule as workspace handles:
|
||||
* `id` is stable, while `latest()` is only the newest snapshot observed by this
|
||||
* handle through construction, `refetch()`, timeline refetch, archive, or local
|
||||
* agent_update subscription.
|
||||
*/
|
||||
export interface PaseoAgentHandle {
|
||||
readonly id: string;
|
||||
readonly workspaceId: string | null;
|
||||
readonly cwd: string | null;
|
||||
readonly status: PaseoAgent["status"] | null;
|
||||
readonly timeline: PaseoAgentTimelineHandle;
|
||||
latest(): PaseoAgent | null;
|
||||
refetch(requestId?: string): Promise<PaseoAgentRefetchResult | null>;
|
||||
current(): PaseoAgent | null;
|
||||
refresh(requestId?: string): Promise<PaseoAgentRefetchResult | null>;
|
||||
send(text: string, options?: PaseoAgentSendOptions): Promise<void>;
|
||||
/** Sends a prompt and resolves when that turn finishes or needs attention. */
|
||||
run(text: string, options?: PaseoAgentRunOptions): Promise<PaseoAgentRunResult>;
|
||||
/** Waits for the current turn, including one started with `prompt`. */
|
||||
waitForFinish(timeoutMs?: number): Promise<PaseoAgentRunResult>;
|
||||
archive(): Promise<{ archivedAt: string }>;
|
||||
detach(): Promise<void>;
|
||||
subscribe(handler: (update: PaseoAgentUpdate) => void): () => void;
|
||||
}
|
||||
|
||||
export interface PaseoAgentActions {
|
||||
list(options?: PaseoAgentListOptions): Promise<PaseoAgentListResult>;
|
||||
ref(agent: string | PaseoAgent): PaseoAgentHandle;
|
||||
create(options: PaseoAgentCreateOptions): Promise<PaseoAgentHandle>;
|
||||
/**
|
||||
@@ -245,21 +273,16 @@ export interface PaseoAgentActions {
|
||||
subscribe(handler: PaseoAgentUpdateHandler): () => void;
|
||||
}
|
||||
|
||||
export interface PaseoProviderConfig extends PaseoProviderConfigInput {
|
||||
provider: PaseoAgentProvider;
|
||||
}
|
||||
export type PaseoProviderFeatureValues = Record<string, unknown>;
|
||||
|
||||
export interface PaseoProviderConfigInput {
|
||||
model?: string;
|
||||
modeId?: string;
|
||||
thinkingOptionId?: string;
|
||||
featureValues?: PaseoProviderFeatureValues;
|
||||
}
|
||||
|
||||
export type PaseoProviderModelsResult = ListProviderModelsResponseMessage["payload"];
|
||||
export type PaseoProviderModesResult = ListProviderModesResponseMessage["payload"];
|
||||
export type PaseoProviderFeaturesInput = ListProviderFeaturesRequestMessage["draftConfig"];
|
||||
type PaseoProviderFeaturesDraft = ListProviderFeaturesRequestMessage["draftConfig"];
|
||||
export interface PaseoProviderFeaturesInput extends Omit<
|
||||
PaseoProviderFeaturesDraft,
|
||||
"provider" | "model"
|
||||
> {
|
||||
/** Provider and model in `provider/model` format. */
|
||||
provider: string;
|
||||
}
|
||||
export type PaseoProviderFeaturesResult = ListProviderFeaturesResponseMessage["payload"];
|
||||
export type PaseoProviderAvailabilityResult = ListAvailableProvidersResponse["payload"];
|
||||
export type PaseoProviderSnapshotResult = GetProvidersSnapshotResponseMessage["payload"];
|
||||
@@ -281,12 +304,11 @@ export interface PaseoProviderRefreshOptions {
|
||||
requestId?: string;
|
||||
}
|
||||
|
||||
export interface PaseoProviderWaitOptions extends PaseoProviderListOptions {
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export interface PaseoProviderActions {
|
||||
codex(input?: PaseoProviderConfigInput): PaseoProviderConfig;
|
||||
claude(input?: PaseoProviderConfigInput): PaseoProviderConfig;
|
||||
opencode(input?: PaseoProviderConfigInput): PaseoProviderConfig;
|
||||
copilot(input?: PaseoProviderConfigInput): PaseoProviderConfig;
|
||||
config(provider: PaseoAgentProvider, input?: PaseoProviderConfigInput): PaseoProviderConfig;
|
||||
listModels(
|
||||
provider: PaseoAgentProvider,
|
||||
options?: PaseoProviderListOptions,
|
||||
@@ -301,6 +323,8 @@ export interface PaseoProviderActions {
|
||||
): Promise<PaseoProviderFeaturesResult>;
|
||||
listAvailable(options?: { requestId?: string }): Promise<PaseoProviderAvailabilityResult>;
|
||||
snapshot(options?: PaseoProviderListOptions): Promise<PaseoProviderSnapshotResult>;
|
||||
/** Resolves after the daemon's lazy provider discovery has finished. */
|
||||
waitForReady(options?: PaseoProviderWaitOptions): Promise<PaseoProviderSnapshotResult>;
|
||||
refresh(options?: PaseoProviderRefreshOptions): Promise<PaseoProviderRefreshResult>;
|
||||
diagnostic(
|
||||
provider: PaseoAgentProvider,
|
||||
@@ -346,8 +370,32 @@ export function createPaseoClient(config: PaseoClientConfig): PaseoClient {
|
||||
clientId: config.clientId ?? createGeneratedClientId(),
|
||||
clientType: "cli",
|
||||
});
|
||||
const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient);
|
||||
const createAgentHandle = createAgentHandleFactory(daemonClient);
|
||||
const createAgent = async (
|
||||
options: PaseoAgentCreateOptions,
|
||||
placement?: { workspaceId: string; cwd: string },
|
||||
) => {
|
||||
const { config: agentConfig, cwd, parent, title, prompt, ...requestOptions } = options;
|
||||
const { provider: providerModel, options: providerOptions, ...runtimeConfig } = agentConfig;
|
||||
const { provider, model } = parseProviderModel(providerModel);
|
||||
const effectiveCwd = placement?.cwd ?? cwd;
|
||||
const agent = await daemonClient.createAgent({
|
||||
...requestOptions,
|
||||
config: {
|
||||
...runtimeConfig,
|
||||
provider,
|
||||
model,
|
||||
cwd: effectiveCwd,
|
||||
...(title !== undefined ? { title } : {}),
|
||||
...(providerOptions !== undefined ? { providerOptions } : {}),
|
||||
},
|
||||
...(placement ? { workspaceId: placement.workspaceId } : {}),
|
||||
...(parent ? { callerAgentId: resolveAgentId(parent) } : {}),
|
||||
...(prompt !== undefined ? { initialPrompt: prompt } : {}),
|
||||
});
|
||||
return createAgentHandle(agent);
|
||||
};
|
||||
const createWorkspaceHandle = createWorkspaceHandleFactory(daemonClient, createAgent);
|
||||
|
||||
return {
|
||||
workspaces: {
|
||||
@@ -355,8 +403,13 @@ export function createPaseoClient(config: PaseoClientConfig): PaseoClient {
|
||||
ref: (workspace) => createWorkspaceHandle(workspace),
|
||||
open: (input, requestId) =>
|
||||
openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
|
||||
create: (input, requestId) =>
|
||||
openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
|
||||
create: async ({ requestId, ...options }) => {
|
||||
const result = await daemonClient.createWorkspace(options, requestId);
|
||||
if (result.error || !result.workspace) {
|
||||
throw new Error(result.error ?? "The daemon did not create a workspace");
|
||||
}
|
||||
return createWorkspaceHandle(result.workspace);
|
||||
},
|
||||
archive: (workspace, requestId) =>
|
||||
daemonClient.archiveWorkspace(resolveWorkspaceId(workspace), requestId),
|
||||
subscribe: (handler) =>
|
||||
@@ -365,28 +418,24 @@ export function createPaseoClient(config: PaseoClientConfig): PaseoClient {
|
||||
}),
|
||||
},
|
||||
agents: {
|
||||
list: (options) => daemonClient.fetchAgents(options),
|
||||
ref: (agent) => createAgentHandle(agent),
|
||||
create: async (options) => {
|
||||
const agent = await daemonClient.createAgent(options);
|
||||
return createAgentHandle(agent);
|
||||
},
|
||||
create: (options) => createAgent(options),
|
||||
subscribe: (handler) =>
|
||||
daemonClient.on("agent_update", (message) => {
|
||||
handler(message.payload);
|
||||
}),
|
||||
},
|
||||
providers: {
|
||||
codex: (input) => providerConfig("codex", input),
|
||||
claude: (input) => providerConfig("claude", input),
|
||||
opencode: (input) => providerConfig("opencode", input),
|
||||
copilot: (input) => providerConfig("copilot", input),
|
||||
config: (provider, input) => providerConfig(provider, input),
|
||||
listModels: (provider, options) => daemonClient.listProviderModels(provider, options),
|
||||
listModes: (provider, options) => daemonClient.listProviderModes(provider, options),
|
||||
listFeatures: (draftConfig, options) =>
|
||||
daemonClient.listProviderFeatures(draftConfig, options),
|
||||
listFeatures: ({ provider: providerModel, ...draftConfig }, options) => {
|
||||
const { provider, model } = parseProviderModel(providerModel);
|
||||
return daemonClient.listProviderFeatures({ ...draftConfig, provider, model }, options);
|
||||
},
|
||||
listAvailable: (options) => daemonClient.listAvailableProviders(options),
|
||||
snapshot: (options) => daemonClient.getProvidersSnapshot(options),
|
||||
waitForReady: (options) => waitForProvidersReady(daemonClient, options),
|
||||
refresh: (options) => daemonClient.refreshProvidersSnapshot(options),
|
||||
diagnostic: (provider, options) => daemonClient.getProviderDiagnostic(provider, options),
|
||||
subscribe: (handler) =>
|
||||
@@ -407,30 +456,71 @@ export function createPaseoClient(config: PaseoClientConfig): PaseoClient {
|
||||
|
||||
type WorkspaceHandleFactory = (workspace: string | PaseoWorkspace) => PaseoWorkspaceHandle;
|
||||
type AgentHandleFactory = (agent: string | PaseoAgent) => PaseoAgentHandle;
|
||||
type CreateAgent = (
|
||||
options: PaseoAgentCreateOptions,
|
||||
placement?: { workspaceId: string; cwd: string },
|
||||
) => Promise<PaseoAgentHandle>;
|
||||
|
||||
function createWorkspaceHandleFactory(daemonClient: DaemonClient): WorkspaceHandleFactory {
|
||||
function createWorkspaceHandleFactory(
|
||||
daemonClient: DaemonClient,
|
||||
createAgent: CreateAgent,
|
||||
): WorkspaceHandleFactory {
|
||||
return (workspace) => {
|
||||
const id = typeof workspace === "string" ? workspace : workspace.id;
|
||||
let latest = typeof workspace === "string" ? null : workspace;
|
||||
let current = typeof workspace === "string" ? null : workspace;
|
||||
|
||||
const refresh = async (options?: { requestId?: string }) => {
|
||||
let cursor: string | undefined;
|
||||
let requestId = options?.requestId;
|
||||
do {
|
||||
const result = await daemonClient.fetchWorkspaces({
|
||||
requestId,
|
||||
page: { limit: 200, ...(cursor ? { cursor } : {}) },
|
||||
});
|
||||
const match = result.entries.find((entry) => entry.id === id);
|
||||
if (match) {
|
||||
current = match;
|
||||
return current;
|
||||
}
|
||||
cursor = result.pageInfo.nextCursor ?? undefined;
|
||||
requestId = undefined;
|
||||
} while (cursor);
|
||||
current = null;
|
||||
return current;
|
||||
};
|
||||
|
||||
return {
|
||||
id,
|
||||
latest: () => latest,
|
||||
refetch: async (options) => {
|
||||
// Best-effort: fetches one page and matches by id client-side, so a workspace beyond
|
||||
// the first page won't be found. TODO: add a "get workspace by id" lookup and resolve
|
||||
// by exact id instead of paging.
|
||||
const result = await daemonClient.fetchWorkspaces({
|
||||
requestId: options?.requestId,
|
||||
page: { limit: 25 },
|
||||
});
|
||||
latest = result.entries.find((entry) => entry.id === id) ?? null;
|
||||
return latest;
|
||||
get projectId() {
|
||||
return current?.projectId ?? null;
|
||||
},
|
||||
get directory() {
|
||||
return current?.workspaceDirectory ?? null;
|
||||
},
|
||||
get name() {
|
||||
return current?.name ?? null;
|
||||
},
|
||||
get status() {
|
||||
return current?.status ?? null;
|
||||
},
|
||||
agents: {
|
||||
create: async (options) => {
|
||||
const snapshot = current ?? (await refresh());
|
||||
if (!snapshot?.workspaceDirectory) {
|
||||
throw new Error(`Workspace ${id} has no available directory`);
|
||||
}
|
||||
return createAgent(
|
||||
{ ...options, cwd: snapshot.workspaceDirectory },
|
||||
{ workspaceId: id, cwd: snapshot.workspaceDirectory },
|
||||
);
|
||||
},
|
||||
},
|
||||
current: () => current,
|
||||
refresh,
|
||||
archive: async (requestId) => {
|
||||
const result = await daemonClient.archiveWorkspace(id, requestId);
|
||||
if (latest) {
|
||||
latest = { ...latest, archivingAt: result.archivedAt };
|
||||
if (current) {
|
||||
current = { ...current, archivingAt: result.archivedAt };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -438,11 +528,11 @@ function createWorkspaceHandleFactory(daemonClient: DaemonClient): WorkspaceHand
|
||||
daemonClient.on("workspace_update", (message) => {
|
||||
const update = message.payload;
|
||||
if (update.kind === "upsert" && update.workspace.id === id) {
|
||||
latest = update.workspace;
|
||||
current = update.workspace;
|
||||
handler(update);
|
||||
}
|
||||
if (update.kind === "remove" && update.id === id) {
|
||||
latest = null;
|
||||
current = null;
|
||||
handler(update);
|
||||
}
|
||||
}),
|
||||
@@ -453,7 +543,7 @@ function createWorkspaceHandleFactory(daemonClient: DaemonClient): WorkspaceHand
|
||||
function createAgentHandleFactory(daemonClient: DaemonClient): AgentHandleFactory {
|
||||
return (agent) => {
|
||||
const id = typeof agent === "string" ? agent : agent.id;
|
||||
let latest = typeof agent === "string" ? null : agent;
|
||||
let current = typeof agent === "string" ? null : agent;
|
||||
|
||||
const handle: PaseoAgentHandle = {
|
||||
id,
|
||||
@@ -461,7 +551,7 @@ function createAgentHandleFactory(daemonClient: DaemonClient): AgentHandleFactor
|
||||
refetch: async (options) => {
|
||||
const result = await daemonClient.fetchAgentTimeline(id, options);
|
||||
if (result.agent) {
|
||||
latest = result.agent;
|
||||
current = result.agent;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -472,19 +562,50 @@ function createAgentHandleFactory(daemonClient: DaemonClient): AgentHandleFactor
|
||||
}
|
||||
}),
|
||||
},
|
||||
latest: () => latest,
|
||||
refetch: async (requestId) => {
|
||||
get workspaceId() {
|
||||
return current?.workspaceId ?? null;
|
||||
},
|
||||
get cwd() {
|
||||
return current?.cwd ?? null;
|
||||
},
|
||||
get status() {
|
||||
return current?.status ?? null;
|
||||
},
|
||||
current: () => current,
|
||||
refresh: async (requestId) => {
|
||||
const result = await daemonClient.fetchAgent({ agentId: id, requestId });
|
||||
latest = result?.agent ?? null;
|
||||
current = result?.agent ?? null;
|
||||
return result;
|
||||
},
|
||||
send: async (text, options) => {
|
||||
await daemonClient.sendAgentMessage(id, text, options);
|
||||
},
|
||||
run: async (text, options) => {
|
||||
const { timeoutMs, ...sendOptions } = options ?? {};
|
||||
await daemonClient.sendAgentMessage(id, text, sendOptions);
|
||||
const result = await daemonClient.waitForFinish(
|
||||
id,
|
||||
timeoutMs ?? DEFAULT_WAIT_FOR_FINISH_MS,
|
||||
);
|
||||
if (result.final) {
|
||||
current = result.final;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
waitForFinish: async (timeoutMs) => {
|
||||
const result = await daemonClient.waitForFinish(
|
||||
id,
|
||||
timeoutMs ?? DEFAULT_WAIT_FOR_FINISH_MS,
|
||||
);
|
||||
if (result.final) {
|
||||
current = result.final;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
archive: async () => {
|
||||
const result = await daemonClient.archiveAgent(id);
|
||||
if (latest) {
|
||||
latest = { ...latest, archivedAt: result.archivedAt };
|
||||
if (current) {
|
||||
current = { ...current, archivedAt: result.archivedAt };
|
||||
}
|
||||
return result;
|
||||
},
|
||||
@@ -495,11 +616,11 @@ function createAgentHandleFactory(daemonClient: DaemonClient): AgentHandleFactor
|
||||
daemonClient.on("agent_update", (message) => {
|
||||
const update = message.payload;
|
||||
if (update.kind === "upsert" && update.agent.id === id) {
|
||||
latest = update.agent;
|
||||
current = update.agent;
|
||||
handler(update);
|
||||
}
|
||||
if (update.kind === "remove" && update.agentId === id) {
|
||||
latest = null;
|
||||
current = null;
|
||||
handler(update);
|
||||
}
|
||||
}),
|
||||
@@ -514,32 +635,116 @@ async function openWorkspace(
|
||||
createWorkspaceHandle: WorkspaceHandleFactory,
|
||||
input: string | PaseoWorkspaceOpenOptions,
|
||||
requestId?: string,
|
||||
): Promise<PaseoWorkspaceOpenResult> {
|
||||
): Promise<PaseoWorkspaceHandle> {
|
||||
const options = typeof input === "string" ? { cwd: input, requestId } : input;
|
||||
const result = await daemonClient.openProject(options.cwd, options.requestId);
|
||||
return {
|
||||
...result,
|
||||
workspace: result.workspace ? createWorkspaceHandle(result.workspace) : null,
|
||||
};
|
||||
if (result.error || !result.workspace) {
|
||||
throw new Error(result.error ?? `The daemon did not open a workspace for ${options.cwd}`);
|
||||
}
|
||||
return createWorkspaceHandle(result.workspace);
|
||||
}
|
||||
|
||||
function resolveWorkspaceId(workspace: string | PaseoWorkspaceHandle): string {
|
||||
return typeof workspace === "string" ? workspace : workspace.id;
|
||||
}
|
||||
|
||||
function providerConfig(
|
||||
provider: PaseoAgentProvider,
|
||||
input: PaseoProviderConfigInput = {},
|
||||
): PaseoProviderConfig {
|
||||
function resolveAgentId(agent: string | PaseoAgentHandle): string {
|
||||
return typeof agent === "string" ? agent : agent.id;
|
||||
}
|
||||
|
||||
function parseProviderModel(selection: string): { provider: string; model: string } {
|
||||
const separator = selection.indexOf("/");
|
||||
if (separator <= 0 || separator === selection.length - 1) {
|
||||
throw new Error('Expected config.provider in "provider/model" format');
|
||||
}
|
||||
return {
|
||||
provider,
|
||||
...(input.model !== undefined ? { model: input.model } : {}),
|
||||
...(input.modeId !== undefined ? { modeId: input.modeId } : {}),
|
||||
...(input.thinkingOptionId !== undefined ? { thinkingOptionId: input.thinkingOptionId } : {}),
|
||||
...(input.featureValues !== undefined ? { featureValues: input.featureValues } : {}),
|
||||
provider: selection.slice(0, separator),
|
||||
model: selection.slice(separator + 1),
|
||||
};
|
||||
}
|
||||
|
||||
function waitForProvidersReady(
|
||||
daemonClient: DaemonClient,
|
||||
options: PaseoProviderWaitOptions = {},
|
||||
): Promise<PaseoProviderSnapshotResult> {
|
||||
// COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
|
||||
if (daemonClient.getLastServerInfoMessage()?.features?.providersSnapshotCwd !== true) {
|
||||
return Promise.reject(new Error("Update the host to wait for provider discovery."));
|
||||
}
|
||||
|
||||
const { timeoutMs = 60_000, ...snapshotOptions } = options;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let settled = false;
|
||||
let requestId: string | null = null;
|
||||
let snapshotCwd: string | undefined;
|
||||
const pendingUpdates = new Map<string | undefined, PaseoProviderSnapshotUpdate>();
|
||||
let latestEntries: PaseoProviderSnapshotResult["entries"] = [];
|
||||
|
||||
const cleanup = () => {
|
||||
clearTimeout(timeout);
|
||||
unsubscribe();
|
||||
};
|
||||
const finish = (snapshot: PaseoProviderSnapshotResult) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
resolve(snapshot);
|
||||
};
|
||||
const fail = (error: unknown) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
cleanup();
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
};
|
||||
const updateMatches = (update: PaseoProviderSnapshotUpdate) => update.cwd === snapshotCwd;
|
||||
|
||||
const unsubscribe = daemonClient.on("providers_snapshot_update", (message) => {
|
||||
const update = message.payload;
|
||||
if (!requestId) {
|
||||
pendingUpdates.set(update.cwd, update);
|
||||
return;
|
||||
}
|
||||
if (!updateMatches(update)) return;
|
||||
latestEntries = update.entries;
|
||||
if (update.entries.some((entry) => entry.status === "loading")) return;
|
||||
finish({ ...update, requestId });
|
||||
});
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const loading = latestEntries
|
||||
.filter((entry) => entry.status === "loading")
|
||||
.map((entry) => entry.provider)
|
||||
.join(", ");
|
||||
fail(
|
||||
new Error(
|
||||
loading
|
||||
? `Timed out waiting for providers: ${loading}`
|
||||
: "Timed out waiting for provider discovery",
|
||||
),
|
||||
);
|
||||
}, timeoutMs);
|
||||
|
||||
void daemonClient
|
||||
.getProvidersSnapshot(snapshotOptions)
|
||||
.then((snapshot) => {
|
||||
requestId = snapshot.requestId;
|
||||
snapshotCwd = snapshot.cwd;
|
||||
latestEntries = snapshot.entries;
|
||||
if (!snapshot.entries.some((entry) => entry.status === "loading")) {
|
||||
finish(snapshot);
|
||||
return;
|
||||
}
|
||||
const pendingUpdate = pendingUpdates.get(snapshotCwd);
|
||||
if (pendingUpdate && !pendingUpdate.entries.some((entry) => entry.status === "loading")) {
|
||||
finish({ ...pendingUpdate, requestId });
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.catch(fail);
|
||||
});
|
||||
}
|
||||
|
||||
function createGeneratedClientId(): string {
|
||||
const randomId =
|
||||
typeof globalThis.crypto?.randomUUID === "function"
|
||||
|
||||
@@ -2910,6 +2910,8 @@ export const ServerInfoStatusPayloadSchema = z
|
||||
features: z
|
||||
.object({
|
||||
providersSnapshot: z.boolean().optional(),
|
||||
// COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
|
||||
providersSnapshotCwd: z.boolean().optional(),
|
||||
// COMPAT(checkoutForgeSetAutoMerge): added in v0.1.106, remove old
|
||||
// checkoutGithubSetAutoMerge fallback after 2026-12-28.
|
||||
checkoutForgeSetAutoMerge: z.boolean().optional(),
|
||||
@@ -5064,6 +5066,7 @@ export const ListAvailableProvidersResponseSchema = z.object({
|
||||
export const GetProvidersSnapshotResponseMessageSchema = z.object({
|
||||
type: z.literal("get_providers_snapshot_response"),
|
||||
payload: z.object({
|
||||
cwd: z.string().optional(),
|
||||
entries: z.array(ProviderSnapshotEntrySchema),
|
||||
compactSnapshot: CompactProviderSnapshotSchema.optional(),
|
||||
snapshotHash: z.string().optional(),
|
||||
|
||||
@@ -116,6 +116,23 @@ describe("ProviderCatalogSession", () => {
|
||||
expect(pull?.payload.entries).toEqual(push?.payload.entries);
|
||||
});
|
||||
|
||||
it("returns the canonical cwd used by snapshot updates", async () => {
|
||||
const getSnapshot = vi.fn((_cwd?: string) => makeEntries());
|
||||
const { subsystem, emitted } = makeSubsystem({
|
||||
snapshot: { getSnapshot },
|
||||
});
|
||||
|
||||
await subsystem.handleGetProvidersSnapshotRequest({
|
||||
type: "get_providers_snapshot_request",
|
||||
requestId: "canonical-cwd",
|
||||
cwd: "/repo/./sdk",
|
||||
});
|
||||
|
||||
const canonicalCwd = getSnapshot.mock.calls[0]?.[0];
|
||||
expect(canonicalCwd).toEqual(expect.any(String));
|
||||
expect(findByType(emitted, "get_providers_snapshot_response")?.payload.cwd).toBe(canonicalCwd);
|
||||
});
|
||||
|
||||
it("pushes the compact encoding to capable clients", () => {
|
||||
const { subsystem, emitted, pushSnapshotChange } = makeSubsystem({
|
||||
supportsCustomModeIcons: true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import type { SessionInboundMessage, SessionOutboundMessage } from "../../messages.js";
|
||||
import {
|
||||
isGlobalProviderSnapshotKey,
|
||||
resolveSnapshotCwd,
|
||||
type ProviderSnapshotManager,
|
||||
} from "../../agent/provider-snapshot-manager.js";
|
||||
import {
|
||||
@@ -391,8 +392,9 @@ export class ProviderCatalogSession {
|
||||
msg: Extract<SessionInboundMessage, { type: "get_providers_snapshot_request" }>,
|
||||
): Promise<void> {
|
||||
// COMPAT(providersSnapshot): keep legacy provider-list RPCs alongside snapshot flow.
|
||||
const snapshotCwd = msg.cwd?.trim() ? resolveSnapshotCwd(expandTilde(msg.cwd)) : undefined;
|
||||
const entries = this.providerSnapshotManager
|
||||
.getSnapshot(msg.cwd ? expandTilde(msg.cwd) : undefined)
|
||||
.getSnapshot(snapshotCwd)
|
||||
.filter((entry) => this.host.isProviderVisibleToClient(entry.provider));
|
||||
const clientEntries = this.downgradeEntryModesForClient(entries);
|
||||
|
||||
@@ -402,6 +404,7 @@ export class ProviderCatalogSession {
|
||||
this.host.emit({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
...(snapshotCwd ? { cwd: snapshotCwd } : {}),
|
||||
entries: [],
|
||||
...(!notModified ? { compactSnapshot: encoded.compactSnapshot } : {}),
|
||||
snapshotHash: encoded.snapshotHash,
|
||||
@@ -416,6 +419,7 @@ export class ProviderCatalogSession {
|
||||
this.host.emit({
|
||||
type: "get_providers_snapshot_response",
|
||||
payload: {
|
||||
...(snapshotCwd ? { cwd: snapshotCwd } : {}),
|
||||
entries: clientEntries,
|
||||
generatedAt: new Date().toISOString(),
|
||||
requestId: msg.requestId,
|
||||
|
||||
@@ -933,6 +933,7 @@ describe("relay external socket reconnect behavior", () => {
|
||||
|
||||
expect(serverInfo.features?.stableProjectIdentity).toBe(true);
|
||||
expect(serverInfo.features?.canonicalSubmittedPrompts).toBe(true);
|
||||
expect(serverInfo.features?.providersSnapshotCwd).toBe(true);
|
||||
expect(serverInfo.features?.["terminal-input-mode-replay"]).toBe(true);
|
||||
expect(serverInfo.features?.["terminal-size-ownership"]).toBe(true);
|
||||
expect(serverInfo.features?.agentTurnIdentity).toBeUndefined();
|
||||
|
||||
@@ -1526,6 +1526,8 @@ export class VoiceAssistantWebSocketServer {
|
||||
features: {
|
||||
// COMPAT(providersSnapshot): keep optional until all clients rely on snapshot flow.
|
||||
providersSnapshot: true,
|
||||
// COMPAT(providersSnapshotCwd): added in v0.3.2, remove gate after 2027-02-10.
|
||||
providersSnapshotCwd: true,
|
||||
// COMPAT(checkoutForgeSetAutoMerge): added in v0.1.106, remove old
|
||||
// checkoutGithubSetAutoMerge fallback after 2026-12-28.
|
||||
checkoutForgeSetAutoMerge: true,
|
||||
|
||||
Reference in New Issue
Block a user