mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
* feat(protocol): define streamed diff revisions and resolve tag bases * feat(server): stream shared diff revisions with paced file bodies * feat(client): observe streamed diffs with atomic revision updates * feat(app): show the Changes tree while diff bodies stream * fix(server): retain the legacy structured diff payload budget * fix(diff): preserve revision ordering and readable files * fix(diff): select renamed patches and read dangling symlinks * fix(diff): preserve nested checkouts and ambiguous patch paths Only regular files and symlinks are readable untracked entries. Prefer patch metadata to ambiguous unquoted headers so paths containing a prefix delimiter keep their hunks. * fix(diff): preserve BOM text and complete file replacements Decode each string without consuming its leading BOM. File-type replacements emit multiple patch sections for one listed path, so retain all matching hunks. Assert the optional rename metadata in the existing commit-file journey. * fix(diff): highlight symlinks from their stored target text Working tree source context must match the symlink blob emitted by Git rather than the contents reached by following the link. Otherwise highlight runs do not cover the patch line and encoding fails the revision. * fix(diff): tolerate disappearing highlight sources Source context is optional after Git has produced the patch. Missing worktree link metadata must preserve the existing hunk-based highlighting fallback instead of failing the revision. * fix(diff): batch cold reads and refresh logical comparison inputs Acquire bounded pages of tracked patches lazily after Begin while retaining per-file highlighting and awaited frame delivery. Keep legacy preferred-base selection live across revisions and include submodule Git state in worktree cache identity. * fix(diff): distinguish gitlink directories from replacements A raw old gitlink mode also occurs when a submodule becomes a regular file. Query nested Git state only for a current directory, preserving normal file identity for replacements. * fix(diff): invalidate timestamp-preserving file replacements Size and modification time do not identify live file contents after a preserving copy. Include filesystem change time and file identity for tracked and untracked reuse, and verify live revisions agree with fresh reads. * fix(diff): align highlight tokens with normalized patch lines Git can normalize CRLF worktrees while highlighting reads their original line endings. Match tokens to the emitted patch text and trim only a normalization CR before encoding runs; mismatched source tokens are not attached. * fix(diff): refresh equivalent worktree paths across platforms Explicit refreshes must match filesystem-equivalent roots, including Windows path aliases. Use the existing realpath-aware matcher, budget real Git completion waits consistently, and assert optional status metadata in the filesystem commit-file journey. * fix(diff): validate Git inputs and resolve refresh roots Reuse now accounts for effective attributes, configuration, and canonical worktree blobs as well as filesystem state. Explicit refresh resolves repository identity like subscription creation, and callers await that resolution before acknowledging the command. * fix(diff): keep non-Git workspace refresh successful Diff invalidation has no target repository for an ordinary folder. Treat that expected absence as a no-op while preserving other root-resolution errors. * fix(diff): observe scoped Git interpretation settings Classify each metadata root with its worktree and common scopes, retaining both when the directories coincide. Shared attributes affect all checkouts, while config.worktree belongs to the owning checkout. Real subscriptions verify automatic binary and hunk-context refreshes in main and linked worktrees. * fix(diff): observe interpretation inputs for pinned comparisons Immutable commit objects still depend on mutable Git configuration and attributes. Declare those metadata inputs independently of ref, index, and worktree mutability, while keeping unrelated endpoint events out of pinned comparisons. * fix(diff): observe interpretation independently of comparison endpoints Commit comparisons still read worktree attributes. Watch those inputs independently, checking effective Git interpretation before invalidating fixed contents. Retain paths hidden by whitespace filtering so attribute changes can expose them. Forward binary frames through the branch-loading test interceptor.
1329 lines
48 KiB
TypeScript
1329 lines
48 KiB
TypeScript
import { workspaceLabelKey } from "@getpaseo/protocol/workspace-labels";
|
|
export type { TerminalReplayEvent, TerminalStreamEvent } from "./terminal-stream-router.js";
|
|
import type { AgentAddress } from "@getpaseo/protocol/addresses";
|
|
import { parentAddressLabels } from "@getpaseo/protocol/agent-labels";
|
|
export type { AgentAddress } from "@getpaseo/protocol/addresses";
|
|
import { createHostActions, type PaseoHostActions } from "./hosts/index.js";
|
|
export type { PaseoHostActions } from "./hosts/index.js";
|
|
export type { HostAdd, HostUpdate, HostSummary, HostDirectory } from "@getpaseo/protocol/hosts";
|
|
import type { OwnedSubscription } from "./connection/index.js";
|
|
export type { OwnedSubscription, SubscriptionObserver } from "./connection/index.js";
|
|
import type { DaemonClientConfig } from "./daemon-client.js";
|
|
import type { AgentPermissionResponse } from "@getpaseo/protocol/agent-types";
|
|
import type {
|
|
AgentSnapshotPayload,
|
|
CreationSnapshot,
|
|
CreateAgentRequestMessage,
|
|
FetchWorkspacesRequestMessage,
|
|
FetchWorkspacesResponseMessage,
|
|
GetProvidersSnapshotResponseMessage,
|
|
ListAvailableProvidersResponse,
|
|
ListCommandsResponse,
|
|
ListProviderFeaturesRequestMessage,
|
|
ListProviderFeaturesResponseMessage,
|
|
ListProviderModelsResponseMessage,
|
|
ProjectListRequestMessage,
|
|
ProjectListResponseMessage,
|
|
ListProviderModesResponseMessage,
|
|
MutableDaemonConfig,
|
|
MutableDaemonConfigPatch,
|
|
ProviderDiagnosticResponseMessage,
|
|
ProviderUsageListResponseMessage,
|
|
ProjectPlacementPayload,
|
|
WorkspaceProjectDescriptorPayload,
|
|
RefreshProvidersSnapshotResponseMessage,
|
|
SessionOutboundMessage,
|
|
WorkspaceDescriptorPayload,
|
|
WorkspaceCreateRequest,
|
|
} from "@getpaseo/protocol/messages";
|
|
import { DaemonClient, type CreateAgentRequestOptions } from "./daemon-client.js";
|
|
import {
|
|
createTerminalActions,
|
|
type PaseoTerminalActions,
|
|
type PaseoWorkspaceTerminalActions,
|
|
} from "./terminals/index.js";
|
|
export type {
|
|
PaseoTerminal,
|
|
PaseoTerminalActions,
|
|
PaseoTerminalHandle,
|
|
PaseoTerminalCreateOptions,
|
|
PaseoTerminalListOptions,
|
|
PaseoTerminalListResult,
|
|
PaseoTerminalCaptureOptions,
|
|
PaseoTerminalCaptureResult,
|
|
PaseoWorkspaceTerminalActions,
|
|
} from "./terminals/index.js";
|
|
import type { PluginTimelineItem } from "@getpaseo/protocol/agent-types";
|
|
import type {
|
|
AgentBackgroundWorkItem,
|
|
FetchAgentsEntry,
|
|
FetchAgentsOptions,
|
|
FetchAgentsPageInfo,
|
|
FetchAgentTimelineCursor,
|
|
FetchAgentTimelineDirection,
|
|
FetchAgentTimelinePayload,
|
|
FetchAgentTimelineProjection,
|
|
SendMessageOptions,
|
|
WaitForFinishResult,
|
|
} 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" }
|
|
| { status: "connecting"; attempt: number }
|
|
| { status: "connected" }
|
|
| { status: "disconnected"; reason?: string }
|
|
| { status: "disposed" };
|
|
|
|
export interface PaseoLogger {
|
|
debug(obj: object, msg?: string): void;
|
|
info(obj: object, msg?: string): void;
|
|
warn(obj: object, msg?: string): void;
|
|
error(obj: object, msg?: string): void;
|
|
}
|
|
|
|
export interface PaseoClientConfig {
|
|
capabilities?: DaemonClientConfig["capabilities"];
|
|
url: string;
|
|
clientId?: string;
|
|
appVersion?: string;
|
|
runtimeGeneration?: number | null;
|
|
password?: string;
|
|
authHeader?: string;
|
|
suppressSendErrors?: boolean;
|
|
logger?: PaseoLogger;
|
|
connectTimeoutMs?: number;
|
|
e2ee?: {
|
|
enabled?: boolean;
|
|
daemonPublicKeyB64?: string;
|
|
};
|
|
reconnect?: {
|
|
enabled?: boolean;
|
|
baseDelayMs?: number;
|
|
maxDelayMs?: number;
|
|
};
|
|
runtimeMetricsIntervalMs?: number;
|
|
runtimeMetricsWindowMs?: number;
|
|
}
|
|
|
|
export type PaseoWorkspace = WorkspaceDescriptorPayload;
|
|
export type PaseoAgent = AgentSnapshotPayload;
|
|
export type PaseoAgentListOptions = FetchAgentsOptions;
|
|
export type PaseoProject = WorkspaceProjectDescriptorPayload;
|
|
export type PaseoProjectListOptions = Omit<ProjectListRequestMessage, "type" | "requestId"> & {
|
|
requestId?: string;
|
|
};
|
|
export type PaseoProjectListResult = ProjectListResponseMessage["payload"];
|
|
export type PaseoProjectUpdate = Extract<
|
|
SessionOutboundMessage,
|
|
{ type: "project.update" }
|
|
>["payload"];
|
|
export type PaseoProjectUpdateHandler = (update: PaseoProjectUpdate) => void;
|
|
|
|
export interface PaseoAgentListResult {
|
|
subscription?: OwnedSubscription<PaseoAgentListResult>;
|
|
requestId: string;
|
|
subscriptionId?: string | null;
|
|
entries: FetchAgentsEntry[];
|
|
pageInfo: FetchAgentsPageInfo;
|
|
}
|
|
export type PaseoWorkspaceListOptions = Omit<
|
|
FetchWorkspacesRequestMessage,
|
|
"type" | "requestId"
|
|
> & {
|
|
requestId?: string;
|
|
};
|
|
|
|
export interface PaseoWorkspaceListResult {
|
|
subscription?: OwnedSubscription<PaseoWorkspaceListResult>;
|
|
requestId: string;
|
|
subscriptionId?: string | null;
|
|
entries: PaseoWorkspace[];
|
|
pageInfo: FetchWorkspacesResponseMessage["payload"]["pageInfo"];
|
|
}
|
|
|
|
export interface PaseoWorkspaceOpenOptions {
|
|
cwd: string;
|
|
requestId?: string;
|
|
}
|
|
|
|
export type PaseoWorkspaceCreateOptions = Omit<
|
|
WorkspaceCreateRequest,
|
|
"type" | "requestId" | "agent" | "subscribe"
|
|
> & {
|
|
requestId?: string;
|
|
agent?: Omit<
|
|
PaseoAgentCreateOptions,
|
|
"worktree" | "git" | "onEvent" | "idempotencyKey" | "requestId"
|
|
>;
|
|
onEvent?: (snapshot: CreationSnapshot) => void;
|
|
};
|
|
|
|
export interface PaseoWorkspaceArchiveResult {
|
|
requestId: string;
|
|
workspaceId: string;
|
|
archivedAt: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export type PaseoWorkspaceUpdate = Extract<
|
|
SessionOutboundMessage,
|
|
{ type: "workspace_update" }
|
|
>["payload"];
|
|
|
|
export type PaseoWorkspaceUpdateHandler = (update: PaseoWorkspaceUpdate) => void;
|
|
|
|
export interface PaseoWorkspaceHandle {
|
|
readonly id: string;
|
|
readonly projectId: string | null;
|
|
readonly directory: string | null;
|
|
readonly name: string | null;
|
|
readonly status: PaseoWorkspace["status"] | null;
|
|
readonly agents: {
|
|
create(options: PaseoWorkspaceAgentCreateOptions): Promise<PaseoAgentHandle>;
|
|
};
|
|
readonly terminals: PaseoWorkspaceTerminalActions;
|
|
current(): PaseoWorkspace | null;
|
|
refresh(options?: { requestId?: string }): Promise<PaseoWorkspace | null>;
|
|
setTitle(title: string | null, requestId?: string): Promise<{ title: string | null }>;
|
|
setPinned(pinned: boolean): Promise<{ pinnedAt: string | null }>;
|
|
setLabel(name: string, options: { assigned: boolean }): Promise<{ labels: string[] }>;
|
|
setBaseBranch(options: {
|
|
baseRef: string;
|
|
requestId?: string;
|
|
}): Promise<{ baseRef: string | null }>;
|
|
archive(requestId?: string): Promise<PaseoWorkspaceArchiveResult>;
|
|
/**
|
|
* Subscribes to already-emitted daemon workspace_update events for this id.
|
|
* This returns a local unsubscribe function; it does not own app cache state or
|
|
* send a daemon unsubscribe RPC. Call `workspaces.list({ subscribe: {} })` when
|
|
* the daemon should start streaming workspace directory updates.
|
|
*/
|
|
subscribe(handler: (update: PaseoWorkspaceUpdate) => void): () => void;
|
|
}
|
|
|
|
export interface PaseoProjectActions {
|
|
list(options?: PaseoProjectListOptions): Promise<PaseoProjectListResult>;
|
|
subscribe(handler: PaseoProjectUpdateHandler): () => void;
|
|
}
|
|
|
|
export interface PaseoWorkspaceActions {
|
|
list(options: PaseoWorkspaceListOptions & { subscribe: {} }): Promise<
|
|
PaseoWorkspaceListResult & {
|
|
subscriptionId: string;
|
|
subscription: OwnedSubscription<PaseoWorkspaceListResult>;
|
|
}
|
|
>;
|
|
list(options?: PaseoWorkspaceListOptions): Promise<PaseoWorkspaceListResult>;
|
|
ref(workspace: string | PaseoWorkspace): PaseoWorkspaceHandle;
|
|
open(
|
|
input: string | PaseoWorkspaceOpenOptions,
|
|
requestId?: string,
|
|
): Promise<PaseoWorkspaceHandle>;
|
|
create(options: PaseoWorkspaceCreateOptions): Promise<PaseoWorkspaceHandle>;
|
|
archive(
|
|
workspace: string | PaseoWorkspaceHandle,
|
|
requestId?: string,
|
|
): Promise<PaseoWorkspaceArchiveResult>;
|
|
/**
|
|
* Local event subscription over the low-level driver's workspace_update stream.
|
|
* The returned function only removes this SDK listener.
|
|
*/
|
|
subscribe(handler: PaseoWorkspaceUpdateHandler): () => void;
|
|
}
|
|
|
|
type PaseoAgentSessionConfig = CreateAgentRequestMessage["config"];
|
|
export type PaseoAgentProvider = PaseoAgentSessionConfig["provider"];
|
|
|
|
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 {
|
|
idempotencyKey?: string;
|
|
agentId?: string;
|
|
onEvent?: (snapshot: CreationSnapshot) => void;
|
|
config: PaseoAgentConfig;
|
|
cwd: string;
|
|
parent?: string | AgentAddress | 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"];
|
|
/**
|
|
* Creates a background agent: hidden from default lists, tabs, the parent's subagent track
|
|
* and notifications, and still addressable by id. A workspace this call creates is
|
|
* background too. With a parent and no value, the parent's own background applies.
|
|
*/
|
|
background?: CreateAgentRequestMessage["background"];
|
|
requestId?: string;
|
|
labels?: Record<string, string>;
|
|
}
|
|
|
|
export type PaseoWorkspaceAgentCreateOptions = Omit<PaseoAgentCreateOptions, "cwd">;
|
|
|
|
export interface PaseoAgentRefetchResult {
|
|
agent: PaseoAgent;
|
|
project: ProjectPlacementPayload | null;
|
|
}
|
|
|
|
export interface PaseoAgentTimelineRefetchOptions {
|
|
direction?: FetchAgentTimelineDirection;
|
|
cursor?: FetchAgentTimelineCursor;
|
|
limit?: number;
|
|
projection?: FetchAgentTimelineProjection;
|
|
requestId?: string;
|
|
}
|
|
|
|
export type PaseoAgentSendOptions = SendMessageOptions;
|
|
|
|
export interface PaseoAgentRunOptions extends PaseoAgentSendOptions {
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
export type PaseoAgentRunResult = WaitForFinishResult;
|
|
export type PaseoAgentPermissionResponse = AgentPermissionResponse;
|
|
|
|
export interface PaseoAgentRespondToPermissionOptions {
|
|
requestId: string;
|
|
response: PaseoAgentPermissionResponse;
|
|
}
|
|
|
|
export interface PaseoAgentCommandsOptions {
|
|
requestId?: string;
|
|
}
|
|
|
|
export type PaseoAgentCommandsResult = ListCommandsResponse["payload"];
|
|
|
|
export type PaseoAgentUpdate = Extract<SessionOutboundMessage, { type: "agent_update" }>["payload"];
|
|
|
|
export type PaseoAgentStream = Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"];
|
|
|
|
export type PaseoAgentUpdateHandler = (update: PaseoAgentUpdate) => void;
|
|
|
|
export type PaseoAgentTimelineEvent =
|
|
| PaseoAgentStream
|
|
| {
|
|
agentId: string;
|
|
event: { type: "replacement"; epoch: string };
|
|
}
|
|
| {
|
|
agentId: string;
|
|
subscriptionId: string;
|
|
event: { type: "subscription_restored" };
|
|
}
|
|
| { agentId: string; event: { type: "error"; error: string } };
|
|
|
|
export type PaseoAgentTimelineSubscription = ReturnType<DaemonClient["subscribeAgentTimeline"]>;
|
|
|
|
export interface PaseoAgentTimelineHandle {
|
|
append(item: Omit<PluginTimelineItem, "pluginId">): Promise<{ seq: number; epoch: string }>;
|
|
/**
|
|
* Fetches a fresh timeline page through the existing daemon RPC. If the daemon
|
|
* includes an agent snapshot in the response, the parent handle is updated to
|
|
* that value.
|
|
*/
|
|
refetch(options?: PaseoAgentTimelineRefetchOptions): Promise<FetchAgentTimelinePayload>;
|
|
/**
|
|
* Delivers live events only. After reconnect, subscription_restored precedes
|
|
* subsequent updates. History may have been missed; use refetch() to request
|
|
* the range you need. No history is fetched automatically. A replacement event
|
|
* invalidates the previous epoch. Subscription errors release this observation.
|
|
* Await the returned unsubscribe function's `ready` promise before starting
|
|
* work that must be observed. It rejects if establishment fails.
|
|
*/
|
|
subscribe(handler: (event: PaseoAgentTimelineEvent) => void): PaseoAgentTimelineSubscription;
|
|
}
|
|
|
|
export type PaseoAgentBackgroundWorkItem = AgentBackgroundWorkItem;
|
|
|
|
export interface PaseoAgentBackgroundWorkHandle {
|
|
/** Stop one item and wait for provider confirmation. */
|
|
stop(
|
|
item: PaseoAgentBackgroundWorkItem,
|
|
options?: { requestId?: string; timeout?: number },
|
|
): Promise<void>;
|
|
/** Reads the agent's current background work once. */
|
|
list(options?: { requestId?: string; timeout?: number }): Promise<PaseoAgentBackgroundWorkItem[]>;
|
|
/**
|
|
* Calls `listener` with the agent's current list once the subscription is established, again
|
|
* after each reconnect, then with the full list each time it changes. Returns the unsubscribe
|
|
* function.
|
|
*
|
|
* When reading the current list fails, `options.onError` receives the error and `listener` is
|
|
* not called; without `onError` the error is logged. The read is not retried: the next
|
|
* reconnect reads the list again.
|
|
*/
|
|
subscribe(
|
|
listener: (items: PaseoAgentBackgroundWorkItem[]) => void,
|
|
options?: PaseoAgentBackgroundWorkSubscribeOptions,
|
|
): () => void;
|
|
}
|
|
|
|
export interface PaseoAgentBackgroundWorkSubscribeOptions {
|
|
/** Receives the error when reading the agent's current list fails. */
|
|
onError?: (error: unknown) => void;
|
|
}
|
|
|
|
export interface PaseoAgentHandle {
|
|
readonly id: string;
|
|
readonly address: AgentAddress;
|
|
/**
|
|
* `workspaceId` through `archivedAt` mirror the last snapshot this handle
|
|
* observed. A handle from `ref()` reads `null` for all of them until
|
|
* `refresh()`, `run()`, `waitForFinish()`, a timeline refetch, or
|
|
* `subscribe()` delivers a snapshot. Optional snapshot values also read as
|
|
* `null`; use `current()` when you need to distinguish those states.
|
|
*/
|
|
readonly workspaceId: string | null;
|
|
readonly cwd: string | null;
|
|
readonly status: PaseoAgent["status"] | null;
|
|
readonly capabilities: PaseoAgent["capabilities"] | null;
|
|
readonly availableModes: PaseoAgent["availableModes"] | null;
|
|
readonly pendingPermissions: PaseoAgent["pendingPermissions"] | null;
|
|
readonly activeTurn: NonNullable<PaseoAgent["activeTurn"]> | null;
|
|
readonly lastUsage: NonNullable<PaseoAgent["lastUsage"]> | null;
|
|
readonly lastError: NonNullable<PaseoAgent["lastError"]> | null;
|
|
readonly features: NonNullable<PaseoAgent["features"]> | null;
|
|
readonly runtimeInfo: NonNullable<PaseoAgent["runtimeInfo"]> | null;
|
|
readonly archivedAt: NonNullable<PaseoAgent["archivedAt"]> | null;
|
|
readonly timeline: PaseoAgentTimelineHandle;
|
|
readonly backgroundWork: PaseoAgentBackgroundWorkHandle;
|
|
current(): PaseoAgent | null;
|
|
refresh(requestId?: string): Promise<PaseoAgentRefetchResult | null>;
|
|
/**
|
|
* Reopens the provider session with current configuration, rerunning session-open hooks.
|
|
* Interrupts a running turn and returns the refreshed snapshot.
|
|
*/
|
|
reload(options?: { requestId?: string }): Promise<PaseoAgentRefetchResult | null>;
|
|
send(text: string, options?: PaseoAgentSendOptions): Promise<void>;
|
|
respondToPermission(options: PaseoAgentRespondToPermissionOptions): 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>;
|
|
/**
|
|
* Asks the running session for the slash commands and skills it actually
|
|
* loaded. Providers answer from the live session, so this sees built-in and
|
|
* bundled entries that no directory scan can find. The payload carries its own
|
|
* `error` string; a provider that cannot answer reports it there rather than
|
|
* rejecting.
|
|
*/
|
|
commands(options?: PaseoAgentCommandsOptions): Promise<PaseoAgentCommandsResult>;
|
|
archive(): Promise<{ archivedAt: string }>;
|
|
detach(): Promise<void>;
|
|
subscribe(handler: (update: PaseoAgentUpdate) => void): () => void;
|
|
}
|
|
|
|
export interface PaseoAgentActions {
|
|
list(options: PaseoAgentListOptions & { subscribe: {} }): Promise<
|
|
PaseoAgentListResult & {
|
|
subscriptionId: string;
|
|
subscription: OwnedSubscription<PaseoAgentListResult>;
|
|
}
|
|
>;
|
|
list(options?: PaseoAgentListOptions): Promise<PaseoAgentListResult>;
|
|
ref(agent: string | PaseoAgent): PaseoAgentHandle;
|
|
create(options: PaseoAgentCreateOptions): Promise<PaseoAgentHandle>;
|
|
/**
|
|
* Local event subscription over the low-level driver's agent_update stream.
|
|
* The returned function only removes this SDK listener.
|
|
*/
|
|
subscribe(handler: PaseoAgentUpdateHandler): () => void;
|
|
}
|
|
|
|
export type PaseoProviderModelsResult = ListProviderModelsResponseMessage["payload"];
|
|
export type PaseoProviderModesResult = ListProviderModesResponseMessage["payload"];
|
|
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"];
|
|
export type PaseoProviderSnapshotUpdate = Extract<
|
|
SessionOutboundMessage,
|
|
{ type: "providers_snapshot_update" }
|
|
>["payload"];
|
|
export type PaseoProviderRefreshResult = RefreshProvidersSnapshotResponseMessage["payload"];
|
|
export type PaseoProviderDiagnosticResult = ProviderDiagnosticResponseMessage["payload"];
|
|
export type PaseoProviderUsageResult = ProviderUsageListResponseMessage["payload"];
|
|
export interface PaseoProviderUsageOptions {
|
|
requestId?: string;
|
|
}
|
|
|
|
export interface PaseoProviderListOptions {
|
|
cwd?: string;
|
|
requestId?: string;
|
|
}
|
|
|
|
export interface PaseoProviderRefreshOptions {
|
|
cwd?: string;
|
|
providers?: PaseoAgentProvider[];
|
|
requestId?: string;
|
|
}
|
|
|
|
export interface PaseoProviderWaitOptions extends PaseoProviderListOptions {
|
|
timeoutMs?: number;
|
|
}
|
|
|
|
export interface PaseoProviderActions {
|
|
listModels(
|
|
provider: PaseoAgentProvider,
|
|
options?: PaseoProviderListOptions,
|
|
): Promise<PaseoProviderModelsResult>;
|
|
listModes(
|
|
provider: PaseoAgentProvider,
|
|
options?: PaseoProviderListOptions,
|
|
): Promise<PaseoProviderModesResult>;
|
|
listFeatures(
|
|
draftConfig: PaseoProviderFeaturesInput,
|
|
options?: { requestId?: string },
|
|
): 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,
|
|
options?: { requestId?: string },
|
|
): Promise<PaseoProviderDiagnosticResult>;
|
|
listUsage(options?: PaseoProviderUsageOptions): Promise<PaseoProviderUsageResult>;
|
|
subscribe(handler: (update: PaseoProviderSnapshotUpdate) => void): () => void;
|
|
}
|
|
|
|
export interface PaseoConfigActions {
|
|
/**
|
|
* Reads daemon config through the existing config RPC. Provider profiles,
|
|
* custom provider entries, keys/env, custom binaries, and provider enablement
|
|
* are currently config-file-shaped daemon state, so the SDK exposes this raw
|
|
* typed surface instead of pretending there are higher-level provider-settings
|
|
* RPCs.
|
|
*/
|
|
get(requestId?: string): Promise<{ requestId: string; config: MutableDaemonConfig }>;
|
|
/**
|
|
* Patches daemon config through the existing config RPC. The daemon validates
|
|
* and persists supported fields; unsupported provider/settings workflows remain
|
|
* daemon gaps until first-class RPCs exist.
|
|
*/
|
|
patch(
|
|
config: MutableDaemonConfigPatch,
|
|
requestId?: string,
|
|
): Promise<{ requestId: string; config: MutableDaemonConfig }>;
|
|
}
|
|
|
|
export interface PaseoApi {
|
|
readonly hosts: PaseoHostActions;
|
|
dispose(): Promise<void>;
|
|
observeEvents: DaemonClient["observeEvents"];
|
|
readonly terminals: PaseoTerminalActions;
|
|
readonly workspaces: PaseoWorkspaceActions;
|
|
readonly projects: PaseoProjectActions;
|
|
readonly agents: PaseoAgentActions;
|
|
readonly providers: PaseoProviderActions;
|
|
readonly config: PaseoConfigActions;
|
|
}
|
|
|
|
export interface PaseoClient extends PaseoApi {
|
|
connect(): Promise<void>;
|
|
close(): Promise<void>;
|
|
ensureConnected(): void;
|
|
getConnectionState(): ConnectionState;
|
|
}
|
|
|
|
export function createPaseoClient(config: PaseoClientConfig): PaseoClient {
|
|
const daemonClient = new DaemonClient({
|
|
...config,
|
|
clientId: config.clientId ?? createGeneratedClientId(),
|
|
clientType: "cli",
|
|
});
|
|
const api = createPaseoApi(daemonClient);
|
|
return {
|
|
...api,
|
|
connect: () => daemonClient.connect(),
|
|
close: async () => {
|
|
try {
|
|
await api.dispose();
|
|
} finally {
|
|
await daemonClient.close();
|
|
}
|
|
},
|
|
ensureConnected: () => daemonClient.ensureConnected(),
|
|
getConnectionState: () => daemonClient.getConnectionState(),
|
|
};
|
|
}
|
|
|
|
function toDaemonAgentCreateOptions(
|
|
options: PaseoAgentCreateOptions,
|
|
placement?: { workspaceId: string; cwd: string },
|
|
serverId?: string,
|
|
): CreateAgentRequestOptions {
|
|
const { config: agentConfig, cwd, parent, title, prompt, ...requestOptions } = options;
|
|
const { provider: providerModel, options: providerOptions, ...runtimeConfig } = agentConfig;
|
|
const { provider, model } = parseProviderModel(providerModel);
|
|
const relationship = parent ? encodeParent(parent, serverId) : {};
|
|
return {
|
|
...requestOptions,
|
|
config: {
|
|
...runtimeConfig,
|
|
provider,
|
|
model,
|
|
cwd: placement?.cwd ?? cwd,
|
|
...(title !== undefined ? { title } : {}),
|
|
...(providerOptions !== undefined ? { providerOptions } : {}),
|
|
},
|
|
...(placement ? { workspaceId: placement.workspaceId } : {}),
|
|
...(parent
|
|
? {
|
|
...relationship,
|
|
labels: { ...requestOptions.labels, ...relationship.labels },
|
|
}
|
|
: {}),
|
|
...(prompt !== undefined ? { initialPrompt: prompt } : {}),
|
|
};
|
|
}
|
|
|
|
export function createPaseoApi(
|
|
daemonClient: DaemonClient,
|
|
scopeOptions?: { signal?: AbortSignal },
|
|
): PaseoApi {
|
|
const handles = new Set<{ release(): Promise<void> }>();
|
|
const agentListeners = new Set<PaseoAgentUpdateHandler>();
|
|
const workspaceListeners = new Set<PaseoWorkspaceUpdateHandler>();
|
|
const lifetime = new AbortController();
|
|
const own = <T extends { release(): Promise<void> }>(create: () => T): T => {
|
|
if (lifetime.signal.aborted) throw new Error("Paseo API is disposed");
|
|
const handle = create();
|
|
handles.add(handle);
|
|
const release = handle.release.bind(handle);
|
|
handle.release = async () => {
|
|
await release();
|
|
handles.delete(handle);
|
|
};
|
|
return handle;
|
|
};
|
|
const listenAgents = (handler: PaseoAgentUpdateHandler) => {
|
|
if (lifetime.signal.aborted) throw new Error("Paseo API is disposed");
|
|
agentListeners.add(handler);
|
|
return () => {
|
|
agentListeners.delete(handler);
|
|
};
|
|
};
|
|
const listenWorkspaces = (handler: PaseoWorkspaceUpdateHandler) => {
|
|
if (lifetime.signal.aborted) throw new Error("Paseo API is disposed");
|
|
workspaceListeners.add(handler);
|
|
return () => {
|
|
workspaceListeners.delete(handler);
|
|
};
|
|
};
|
|
const createAgentHandle = createAgentHandleFactory(
|
|
daemonClient,
|
|
listenAgents,
|
|
(agentId, handler) => own(() => daemonClient.subscribeAgentTimeline(agentId, handler)),
|
|
() => own(() => daemonClient.observeAgentBackgroundWork()),
|
|
);
|
|
const createAgent = async (
|
|
options: PaseoAgentCreateOptions,
|
|
placement?: { workspaceId: string; cwd: string },
|
|
) => {
|
|
const serverId = daemonClient.getLastServerInfoMessage()?.serverId;
|
|
const foreignParent =
|
|
options.background === undefined ? foreignParentHandle(options.parent, serverId) : null;
|
|
const resolved = foreignParent
|
|
? await inheritParentBackground(options, foreignParent)
|
|
: options;
|
|
const agent = await daemonClient.createAgent(
|
|
toDaemonAgentCreateOptions(resolved, placement, serverId),
|
|
);
|
|
return createAgentHandle(agent);
|
|
};
|
|
const terminals = createTerminalActions(daemonClient, async (workspaceId) => {
|
|
const workspace = await createWorkspaceHandle(workspaceId).refresh();
|
|
if (!workspace?.workspaceDirectory) {
|
|
throw new Error(`Workspace ${workspaceId} is not active or has no available directory`);
|
|
}
|
|
return workspace.workspaceDirectory;
|
|
});
|
|
const createWorkspaceHandle = createWorkspaceHandleFactory(
|
|
daemonClient,
|
|
createAgent,
|
|
terminals,
|
|
listenWorkspaces,
|
|
);
|
|
|
|
let disposal: Promise<void> | null = null;
|
|
const dispose = (): Promise<void> => {
|
|
if (disposal) return disposal;
|
|
lifetime.abort();
|
|
scopeOptions?.signal?.removeEventListener("abort", abort);
|
|
agentListeners.clear();
|
|
workspaceListeners.clear();
|
|
disposal = Promise.allSettled([...handles].map((handle) => handle.release())).then(
|
|
(results) => {
|
|
handles.clear();
|
|
const failures = results.flatMap((result) =>
|
|
result.status === "rejected" ? [result.reason] : [],
|
|
);
|
|
if (failures.length)
|
|
throw new AggregateError(failures, "Failed to release API subscriptions");
|
|
return undefined;
|
|
},
|
|
);
|
|
return disposal;
|
|
};
|
|
const abort = () => {
|
|
void dispose().catch((error) => console.error("API subscription cleanup failed", error));
|
|
};
|
|
if (scopeOptions?.signal?.aborted) abort();
|
|
else scopeOptions?.signal?.addEventListener("abort", abort, { once: true });
|
|
|
|
const observeEvents: DaemonClient["observeEvents"] = (events, options) =>
|
|
own(() => daemonClient.observeEvents(events, options));
|
|
|
|
const subscribeEvent = (
|
|
event: "project.update" | "providers_snapshot_update",
|
|
update: (message: SessionOutboundMessage) => void,
|
|
): (() => void) => {
|
|
const observation = observeEvents([event]);
|
|
observation.subscribe({ snapshot: () => {}, update });
|
|
return () => {
|
|
void observation
|
|
.release()
|
|
.catch((error) => console.error("Event subscription cleanup failed", error));
|
|
};
|
|
};
|
|
|
|
function listWorkspaces(options: PaseoWorkspaceListOptions & { subscribe: {} }): Promise<
|
|
PaseoWorkspaceListResult & {
|
|
subscriptionId: string;
|
|
subscription: OwnedSubscription<PaseoWorkspaceListResult>;
|
|
}
|
|
>;
|
|
function listWorkspaces(options?: PaseoWorkspaceListOptions): Promise<PaseoWorkspaceListResult>;
|
|
async function listWorkspaces(
|
|
options?: PaseoWorkspaceListOptions,
|
|
): Promise<PaseoWorkspaceListResult> {
|
|
if (!options?.subscribe) return daemonClient.fetchWorkspaces(options);
|
|
if (options.subscribe.subscriptionId !== undefined)
|
|
throw new Error("Subscription IDs are assigned by the host");
|
|
const subscription = own(() => daemonClient.observeWorkspaces(options));
|
|
subscription.subscribe({
|
|
snapshot: () => {},
|
|
update: (message) => {
|
|
if (message.type === "workspace_update")
|
|
for (const listener of workspaceListeners) listener(message.payload);
|
|
},
|
|
});
|
|
return { ...(await subscription.ready), subscription };
|
|
}
|
|
|
|
function listAgents(options: PaseoAgentListOptions & { subscribe: {} }): Promise<
|
|
PaseoAgentListResult & {
|
|
subscriptionId: string;
|
|
subscription: OwnedSubscription<PaseoAgentListResult>;
|
|
}
|
|
>;
|
|
function listAgents(options?: PaseoAgentListOptions): Promise<PaseoAgentListResult>;
|
|
async function listAgents(options?: PaseoAgentListOptions): Promise<PaseoAgentListResult> {
|
|
if (!options?.subscribe) return daemonClient.fetchAgents(options);
|
|
if (options.subscribe.subscriptionId !== undefined)
|
|
throw new Error("Subscription IDs are assigned by the host");
|
|
const subscription = own(() => daemonClient.observeAgents(options));
|
|
subscription.subscribe({
|
|
snapshot: () => {},
|
|
update: (message) => {
|
|
if (message.type === "agent_update")
|
|
for (const listener of agentListeners) listener(message.payload);
|
|
},
|
|
});
|
|
return { ...(await subscription.ready), subscription };
|
|
}
|
|
|
|
return {
|
|
dispose,
|
|
hosts: createHostActions(daemonClient, { own, signal: lifetime.signal, api: createPaseoApi }),
|
|
observeEvents,
|
|
terminals,
|
|
projects: {
|
|
list: (options) => daemonClient.listProjects(options),
|
|
subscribe: (handler) => {
|
|
return subscribeEvent("project.update", (message) => {
|
|
if (message.type === "project.update") handler(message.payload);
|
|
});
|
|
},
|
|
},
|
|
workspaces: {
|
|
list: listWorkspaces,
|
|
ref: (workspace) => createWorkspaceHandle(workspace),
|
|
open: (input, requestId) =>
|
|
openWorkspace(daemonClient, createWorkspaceHandle, input, requestId),
|
|
create: async ({ requestId, agent, ...options }) => {
|
|
const result = await daemonClient.createWorkspace(
|
|
{
|
|
...options,
|
|
...(agent
|
|
? {
|
|
agent: toDaemonAgentCreateOptions(
|
|
agent,
|
|
undefined,
|
|
daemonClient.getLastServerInfoMessage()?.serverId,
|
|
),
|
|
}
|
|
: {}),
|
|
},
|
|
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: listenWorkspaces,
|
|
},
|
|
agents: {
|
|
list: listAgents,
|
|
ref: (agent) => createAgentHandle(agent),
|
|
create: (options) => createAgent(options),
|
|
subscribe: listenAgents,
|
|
},
|
|
providers: {
|
|
listModels: (provider, options) => daemonClient.listProviderModels(provider, options),
|
|
listModes: (provider, options) => daemonClient.listProviderModes(provider, 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,
|
|
observeEvents(["providers_snapshot_update"]),
|
|
lifetime.signal,
|
|
options,
|
|
),
|
|
refresh: (options) => daemonClient.refreshProvidersSnapshot(options),
|
|
diagnostic: (provider, options) => daemonClient.getProviderDiagnostic(provider, options),
|
|
listUsage: (options) => listProviderUsage(daemonClient, options),
|
|
subscribe: (handler) => {
|
|
return subscribeEvent("providers_snapshot_update", (message) => {
|
|
if (message.type === "providers_snapshot_update") handler(message.payload);
|
|
});
|
|
},
|
|
},
|
|
config: {
|
|
get: (requestId) => daemonClient.getDaemonConfig(requestId),
|
|
patch: (patch, requestId) => daemonClient.patchDaemonConfig(patch, requestId),
|
|
},
|
|
};
|
|
}
|
|
|
|
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,
|
|
createAgent: CreateAgent,
|
|
terminals: PaseoTerminalActions,
|
|
listen: (handler: PaseoWorkspaceUpdateHandler) => () => void,
|
|
): WorkspaceHandleFactory {
|
|
return (workspace) => {
|
|
const id = typeof workspace === "string" ? workspace : workspace.id;
|
|
let current = typeof workspace === "string" ? null : workspace;
|
|
|
|
const refresh = async (options?: { requestId?: string }) => {
|
|
current = await daemonClient.findWorkspace(id, options);
|
|
return current;
|
|
};
|
|
|
|
return {
|
|
id,
|
|
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 },
|
|
);
|
|
},
|
|
},
|
|
terminals: {
|
|
create: (options) => terminals.create({ ...options, workspaceId: id }),
|
|
list: (options) => terminals.list({ ...options, workspaceId: id }),
|
|
},
|
|
current: () => current,
|
|
refresh,
|
|
setTitle: (title, requestId) => daemonClient.setWorkspaceTitle(id, title, requestId),
|
|
setPinned: (pinned) => daemonClient.setWorkspacePinned(id, pinned),
|
|
setLabel: (name, options) =>
|
|
assignWorkspaceLabel({
|
|
client: daemonClient,
|
|
workspaceId: id,
|
|
name,
|
|
assigned: options.assigned,
|
|
}),
|
|
setBaseBranch: (options) =>
|
|
daemonClient.setWorkspaceBaseBranch({ ...options, workspaceId: id }),
|
|
archive: async (requestId) => {
|
|
const result = await daemonClient.archiveWorkspace(id, requestId);
|
|
if (current) {
|
|
current = { ...current, archivingAt: result.archivedAt };
|
|
}
|
|
return result;
|
|
},
|
|
subscribe: (handler) =>
|
|
listen((update) => {
|
|
if (update.kind === "upsert" && update.workspace.id === id) {
|
|
current = update.workspace;
|
|
handler(update);
|
|
}
|
|
if (update.kind === "remove" && update.id === id) {
|
|
handler(update);
|
|
}
|
|
}),
|
|
};
|
|
};
|
|
}
|
|
|
|
function createAgentHandleFactory(
|
|
daemonClient: DaemonClient,
|
|
listen: (handler: PaseoAgentUpdateHandler) => () => void,
|
|
subscribeTimeline: DaemonClient["subscribeAgentTimeline"],
|
|
observeBackgroundWork: () => ReturnType<DaemonClient["observeAgentBackgroundWork"]>,
|
|
): AgentHandleFactory {
|
|
return (agent) => {
|
|
const id = typeof agent === "string" ? agent : agent.id;
|
|
let current = typeof agent === "string" ? null : agent;
|
|
|
|
let serverId = daemonClient.getLastServerInfoMessage()?.serverId;
|
|
const handle: PaseoAgentHandle = {
|
|
id,
|
|
get address() {
|
|
serverId ??= daemonClient.getLastServerInfoMessage()?.serverId;
|
|
if (!serverId) throw new Error("Connect to a host before obtaining an agent address");
|
|
return { serverId, agentId: id };
|
|
},
|
|
timeline: {
|
|
append: (item) => daemonClient.appendAgentTimelineItem(id, item),
|
|
refetch: async (options) => {
|
|
const result = await daemonClient.fetchAgentTimeline(id, options);
|
|
if (result.agent) {
|
|
current = result.agent;
|
|
}
|
|
return result;
|
|
},
|
|
subscribe: (handler) =>
|
|
subscribeTimeline(id, (message) => {
|
|
switch (message.type) {
|
|
case "agent_stream":
|
|
return handler(message.payload);
|
|
case "agent.timeline.subscription_restored":
|
|
return handler({
|
|
agentId: id,
|
|
subscriptionId: message.payload.subscriptionId,
|
|
event: { type: "subscription_restored" },
|
|
});
|
|
case "agent.timeline.error":
|
|
return handler({
|
|
agentId: id,
|
|
event: { type: "error", error: message.payload.error },
|
|
});
|
|
case "agent.timeline.replacement":
|
|
return handler({
|
|
agentId: id,
|
|
event: { type: "replacement", epoch: message.payload.epoch },
|
|
});
|
|
}
|
|
}),
|
|
},
|
|
backgroundWork: {
|
|
stop: (item, options) => daemonClient.stopAgentBackgroundWork(id, item, options),
|
|
list: (options) => daemonClient.listAgentBackgroundWork(id, options),
|
|
subscribe: (listener, options) => {
|
|
const observation = observeBackgroundWork();
|
|
// Bumped by every delivery so a list answered after a newer update is dropped.
|
|
let sequence = 0;
|
|
let released = false;
|
|
const reportError = (error: unknown) => {
|
|
if (released) return;
|
|
if (options?.onError) options.onError(error);
|
|
else console.error("Background work subscription failed", error);
|
|
};
|
|
observation.subscribe({
|
|
error: reportError,
|
|
snapshot: () => {
|
|
const listed = ++sequence;
|
|
void daemonClient
|
|
.listAgentBackgroundWork(id)
|
|
.then((items) => {
|
|
if (!released && listed === sequence) listener(items);
|
|
return undefined;
|
|
})
|
|
.catch(reportError);
|
|
},
|
|
update: (message) => {
|
|
if (message.type !== "agent.background_work.update") return;
|
|
if (message.payload.agentId !== id) return;
|
|
sequence++;
|
|
listener(message.payload.items);
|
|
},
|
|
});
|
|
return () => {
|
|
released = true;
|
|
void observation
|
|
.release()
|
|
.catch((error) =>
|
|
console.error("Background work subscription cleanup failed", error),
|
|
);
|
|
};
|
|
},
|
|
},
|
|
get workspaceId() {
|
|
return current?.workspaceId ?? null;
|
|
},
|
|
get cwd() {
|
|
return current?.cwd ?? null;
|
|
},
|
|
get status() {
|
|
return current?.status ?? null;
|
|
},
|
|
get capabilities() {
|
|
return current?.capabilities ?? null;
|
|
},
|
|
get availableModes() {
|
|
return current?.availableModes ?? null;
|
|
},
|
|
get pendingPermissions() {
|
|
return current?.pendingPermissions ?? null;
|
|
},
|
|
get activeTurn() {
|
|
return current?.activeTurn ?? null;
|
|
},
|
|
get lastUsage() {
|
|
return current?.lastUsage ?? null;
|
|
},
|
|
get lastError() {
|
|
return current?.lastError ?? null;
|
|
},
|
|
get features() {
|
|
return current?.features ?? null;
|
|
},
|
|
get runtimeInfo() {
|
|
return current?.runtimeInfo ?? null;
|
|
},
|
|
get archivedAt() {
|
|
return current?.archivedAt ?? null;
|
|
},
|
|
current: () => current,
|
|
refresh: async (requestId) => {
|
|
const result = await daemonClient.fetchAgent({ agentId: id, requestId });
|
|
current = result?.agent ?? null;
|
|
return result;
|
|
},
|
|
reload: async (options) => {
|
|
await daemonClient.refreshAgent(id, options?.requestId);
|
|
const result = await daemonClient.fetchAgent({ agentId: id });
|
|
current = result?.agent ?? null;
|
|
return result;
|
|
},
|
|
send: async (text, options) => {
|
|
await daemonClient.sendAgentMessage(id, text, options);
|
|
},
|
|
respondToPermission: async ({ requestId, response }) => {
|
|
await daemonClient.respondToPermission(id, requestId, response);
|
|
},
|
|
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;
|
|
},
|
|
commands: (options) => daemonClient.listCommands({ agentId: id, ...options }),
|
|
archive: async () => {
|
|
const result = await daemonClient.archiveAgent(id);
|
|
if (current) {
|
|
current = { ...current, archivedAt: result.archivedAt };
|
|
}
|
|
return result;
|
|
},
|
|
detach: async () => {
|
|
await daemonClient.detachAgent(id);
|
|
},
|
|
subscribe: (handler) =>
|
|
listen((update) => {
|
|
if (update.kind === "upsert" && update.agent.id === id) {
|
|
current = update.agent;
|
|
handler(update);
|
|
}
|
|
if (update.kind === "remove" && update.agentId === id) {
|
|
handler(update);
|
|
}
|
|
}),
|
|
};
|
|
|
|
return handle;
|
|
};
|
|
}
|
|
|
|
async function openWorkspace(
|
|
daemonClient: DaemonClient,
|
|
createWorkspaceHandle: WorkspaceHandleFactory,
|
|
input: string | PaseoWorkspaceOpenOptions,
|
|
requestId?: string,
|
|
): Promise<PaseoWorkspaceHandle> {
|
|
const options = typeof input === "string" ? { cwd: input, requestId } : input;
|
|
const result = await daemonClient.openProject(options.cwd, options.requestId);
|
|
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 foreignParentHandle(
|
|
parent: PaseoAgentCreateOptions["parent"],
|
|
serverId: string | undefined,
|
|
): PaseoAgentHandle | null {
|
|
if (!parent || typeof parent === "string" || !("address" in parent)) return null;
|
|
return parent.address.serverId === serverId ? null : parent;
|
|
}
|
|
|
|
// A host inherits background from a caller it can see. A parent on another host is invisible to
|
|
// it, so a handle passes its own value on. A bare foreign address can't; pass background then.
|
|
async function inheritParentBackground(
|
|
options: PaseoAgentCreateOptions,
|
|
parent: PaseoAgentHandle,
|
|
): Promise<PaseoAgentCreateOptions> {
|
|
const snapshot = parent.current() ?? (await parent.refresh())?.agent;
|
|
return snapshot?.background ? { ...options, background: true } : options;
|
|
}
|
|
|
|
function encodeParent(
|
|
parent: string | AgentAddress | PaseoAgentHandle,
|
|
serverId?: string,
|
|
): Pick<CreateAgentRequestOptions, "callerAgentId" | "labels"> {
|
|
if (typeof parent === "string") return { callerAgentId: parent };
|
|
const address = "address" in parent ? parent.address : parent;
|
|
if (!serverId) throw new Error("Connect to a host before creating a child");
|
|
return address.serverId === serverId
|
|
? { callerAgentId: address.agentId, labels: parentAddressLabels(address) }
|
|
: { labels: parentAddressLabels(address) };
|
|
}
|
|
|
|
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: selection.slice(0, separator),
|
|
model: selection.slice(separator + 1),
|
|
};
|
|
}
|
|
|
|
function listProviderUsage(
|
|
daemonClient: DaemonClient,
|
|
options?: PaseoProviderUsageOptions,
|
|
): Promise<PaseoProviderUsageResult> {
|
|
daemonClient.ensureConnected();
|
|
// COMPAT(providerUsageList): added in v0.1.98, remove after 2027-02-28 once daemon floor >= v0.1.98.
|
|
if (daemonClient.getLastServerInfoMessage()?.features?.providerUsageList !== true) {
|
|
return Promise.reject(new Error("Update the host to list provider usage."));
|
|
}
|
|
return daemonClient.listProviderUsage(options);
|
|
}
|
|
|
|
async function waitForProvidersReady(
|
|
daemonClient: DaemonClient,
|
|
observation: ReturnType<DaemonClient["observeEvents"]>,
|
|
signal: AbortSignal,
|
|
options: PaseoProviderWaitOptions = {},
|
|
): Promise<PaseoProviderSnapshotResult> {
|
|
const { timeoutMs = 60_000, ...snapshotOptions } = options;
|
|
|
|
try {
|
|
await observation.ready;
|
|
if (signal.aborted) throw new Error("Paseo API is disposed");
|
|
return await new Promise<PaseoProviderSnapshotResult>((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();
|
|
signal.removeEventListener("abort", abort);
|
|
};
|
|
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 = observation.subscribe({
|
|
snapshot: () => {},
|
|
update: (message) => {
|
|
if (message.type !== "providers_snapshot_update") return;
|
|
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 abort = () => fail(new Error("Paseo API is disposed"));
|
|
signal.addEventListener("abort", abort, { once: true });
|
|
|
|
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);
|
|
});
|
|
} finally {
|
|
await observation.release();
|
|
}
|
|
}
|
|
|
|
function createGeneratedClientId(): string {
|
|
const randomId =
|
|
typeof globalThis.crypto?.randomUUID === "function"
|
|
? globalThis.crypto.randomUUID()
|
|
: Math.random().toString(36).slice(2);
|
|
return `paseo-sdk-${randomId}`;
|
|
}
|
|
|
|
export type { DiffCompare, DiffState, DiffEntry, ResolvedShas } from "./diff/index.js";
|
|
|
|
interface WorkspaceLabelAssignment {
|
|
client: DaemonClient;
|
|
workspaceId: string;
|
|
name: string;
|
|
assigned: boolean;
|
|
}
|
|
async function assignWorkspaceLabel(
|
|
input: WorkspaceLabelAssignment,
|
|
): Promise<{ labels: string[] }> {
|
|
input.client.ensureConnected();
|
|
// COMPAT(workspaceLabels): added in v0.5.0, remove gate after 2027-08-14.
|
|
if (input.client.getLastServerInfoMessage()?.features?.workspaceLabels !== true)
|
|
throw new Error("Update the host to assign workspace labels.");
|
|
const catalog = await input.client.listWorkspaceLabels();
|
|
const key = workspaceLabelKey(input.name);
|
|
const existing = catalog.labels.find((label) => workspaceLabelKey(label.name) === key);
|
|
const label = existing ?? { name: input.name, color: "violet" as const };
|
|
const result = await input.client.setWorkspaceLabel({
|
|
workspaceId: input.workspaceId,
|
|
label,
|
|
assigned: input.assigned,
|
|
});
|
|
return { labels: result.workspaceLabels };
|
|
}
|