Extract client SDK package (#1052)

* Extract client SDK package

* Polish SDK client identity defaults

* Build client before dependent CI jobs

* Restore daemon client server export

* Extract protocol and client SDK packages

* Fix provider override schema validation

* Fix app test daemon client imports

* Simplify workspace build targets

* Fix CLI test server build bootstrap

* Run SDK package tests in CI

* Fix rebase package split drift

* Restore lockfile registry metadata

* Update SDK config test for prompt default

* Move terminal stream router test to client package

* Fix rebase drift for protocol imports

* Fix SDK agent capability fixture

* Restore legacy server client exports

* Fix server export compatibility test

* Advertise custom mode icon client capability

* Remove server daemon-client exports

* Format rebased mode control import

* Fix rebase drift for protocol imports

Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split
shared/ paths that this branch moves into @getpaseo/protocol. Redirect
those imports to the protocol package so typecheck stays green after the
rebase.
This commit is contained in:
Mohamed Boudra
2026-05-28 01:58:18 +08:00
committed by GitHub
parent 0ea41378a4
commit 53c14d9855
403 changed files with 3193 additions and 1417 deletions
@@ -1,199 +0,0 @@
import {
createClientChannel,
type EncryptedChannel,
type Transport as RelayTransport,
} from "@getpaseo/relay/e2ee";
import type {
DaemonTransport,
DaemonTransportFactory,
TransportLogger,
} from "./daemon-client-transport-types.js";
import {
extractRelayMessageData,
normalizeTransportPayload,
} from "./daemon-client-transport-utils.js";
type OpenHandler = () => void;
type CloseHandler = (event?: unknown) => void;
type ErrorHandler = (event?: unknown) => void;
type MessageHandler = (data: unknown) => void;
export function createRelayE2eeTransportFactory(args: {
baseFactory: DaemonTransportFactory;
daemonPublicKeyB64: string;
logger: TransportLogger;
}): DaemonTransportFactory {
return ({ url, headers }) => {
const base = args.baseFactory({ url, headers });
return createEncryptedTransport(base, args.daemonPublicKeyB64, args.logger);
};
}
export function createEncryptedTransport(
base: DaemonTransport,
daemonPublicKeyB64: string,
logger: TransportLogger,
): DaemonTransport {
let channel: EncryptedChannel | null = null;
let opened = false;
let closed = false;
const openHandlers = new Set<OpenHandler>();
const closeHandlers = new Set<CloseHandler>();
const errorHandlers = new Set<ErrorHandler>();
const messageHandlers = new Set<MessageHandler>();
const emitOpen = () => {
if (opened || closed) {
return;
}
opened = true;
emitHandlers(openHandlers);
};
const emitClose = (event?: unknown) => {
if (closed) {
return;
}
closed = true;
emitHandlers(closeHandlers, event);
};
const emitError = (event?: unknown) => {
if (closed) {
return;
}
emitHandlers(errorHandlers, event);
};
const emitMessage = (data: unknown) => {
if (closed) {
return;
}
emitHandlers(messageHandlers, data);
};
const relayTransport: RelayTransport = {
send: (data) => {
if (typeof data === "string") {
base.send(data);
return;
}
if (ArrayBuffer.isView(data)) {
base.send(normalizeTransportPayload(data));
return;
}
if (data instanceof ArrayBuffer) {
base.send(data);
return;
}
base.send(String(data));
},
close: (code?: number, reason?: string) => base.close(code, reason),
onmessage: null,
onclose: null,
onerror: null,
};
const startHandshake = async () => {
try {
channel = await createClientChannel(relayTransport, daemonPublicKeyB64, {
onopen: emitOpen,
onmessage: (data) => emitMessage(data),
onclose: (code, reason) => emitClose({ code, reason }),
onerror: (error) => emitError(error),
});
} catch (error) {
logger.warn({ err: normalizeTransportError(error) }, "relay_e2ee_handshake_failed");
emitError(error);
// Browser WebSocket.close only accepts 1000 or 3000-4999.
// Use an app-defined code so this path works in browser and Node runtimes.
base.close(4001, "E2EE handshake failed");
}
};
base.onOpen(() => {
void startHandshake();
});
base.onMessage((event) => {
relayTransport.onmessage?.(extractRelayMessageData(event));
});
base.onClose((event) => {
const record = event as { code?: number; reason?: string } | undefined;
relayTransport.onclose?.(record?.code ?? 0, record?.reason ?? "");
emitClose(event);
});
base.onError((event) => {
relayTransport.onerror?.(event instanceof Error ? event : new Error(String(event)));
emitError(event);
});
return {
send: (data) => {
if (!channel) {
throw new Error("Encrypted channel not ready");
}
void channel.send(normalizeTransportPayload(data)).catch((error) => {
emitError(error);
});
},
close: (code?: number, reason?: string) => {
if (channel) {
channel.close(code, reason);
} else {
base.close(code, reason);
}
emitClose({ code, reason });
},
onMessage: (handler) => {
messageHandlers.add(handler);
return () => messageHandlers.delete(handler);
},
onOpen: (handler) => {
openHandlers.add(handler);
if (opened) {
invokeHandler(handler);
}
return () => openHandlers.delete(handler);
},
onClose: (handler) => {
closeHandlers.add(handler);
if (closed) {
invokeHandler(handler);
}
return () => closeHandlers.delete(handler);
},
onError: (handler) => {
errorHandlers.add(handler);
return () => errorHandlers.delete(handler);
},
};
}
function emitHandlers<TArgs extends unknown[]>(
handlers: Set<(...args: TArgs) => void>,
...args: TArgs
) {
for (const handler of handlers) {
invokeHandler(handler, ...args);
}
}
function invokeHandler<TArgs extends unknown[]>(handler: (...args: TArgs) => void, ...args: TArgs) {
try {
handler(...args);
} catch {
// no-op
}
}
function normalizeTransportError(error: unknown): Record<string, string> {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
...(typeof error.stack === "string" ? { stack: error.stack } : {}),
};
}
return { message: String(error) };
}
@@ -1,258 +0,0 @@
import type { SessionOutboundMessage } from "../shared/messages.js";
interface RuntimeMetricsLogger {
info(obj: object, msg?: string): void;
}
interface RuntimeMetricsHandlerTiming {
count: number;
totalMs: number;
maxMs: number;
}
interface RuntimeMetricsBucket {
inboundMessageCounts: Map<string, number>;
inboundMessageBytes: Map<string, number>;
inboundMessageHandlerMs: Map<string, RuntimeMetricsHandlerTiming>;
inboundAgentStreamCounts: Map<string, number>;
inboundAgentStreamByAgentCounts: Map<string, number>;
inboundBinaryFrameCounts: Map<string, number>;
endedAt: number;
}
interface RuntimeMetricsContext {
connectionPath: "direct" | "relay";
serverId: string | null;
getConnectionStatus: () => string;
}
interface RuntimeMetricsOptions {
windowMs?: number;
}
const DEFAULT_ROLLING_WINDOW_MS = 60_000;
export class DaemonClientRuntimeMetrics {
private readonly startedAt = Date.now();
private readonly windowMs: number;
private readonly buckets: RuntimeMetricsBucket[] = [];
private readonly inboundMessageCounts = new Map<string, number>();
private readonly inboundMessageBytes = new Map<string, number>();
private readonly inboundMessageHandlerMs = new Map<string, RuntimeMetricsHandlerTiming>();
private readonly inboundAgentStreamCounts = new Map<string, number>();
private readonly inboundAgentStreamByAgentCounts = new Map<string, number>();
private readonly inboundBinaryFrameCounts = new Map<string, number>();
constructor(
private readonly logger: RuntimeMetricsLogger,
private readonly context: RuntimeMetricsContext,
options?: RuntimeMetricsOptions,
) {
this.windowMs =
typeof options?.windowMs === "number" && options.windowMs > 0
? options.windowMs
: DEFAULT_ROLLING_WINDOW_MS;
}
recordMessage(type: string, bytes: number, handlerMs: number): void {
incrementCount(this.inboundMessageCounts, type, 1);
incrementCount(this.inboundMessageBytes, type, bytes);
incrementHandlerTiming(this.inboundMessageHandlerMs, type, handlerMs);
}
recordAgentStream(
payload: Extract<SessionOutboundMessage, { type: "agent_stream" }>["payload"],
): void {
const { agentId, event } = payload;
const eventType = event.type === "timeline" ? `timeline:${event.item.type}` : event.type;
incrementCount(this.inboundAgentStreamCounts, eventType, 1);
incrementCount(this.inboundAgentStreamByAgentCounts, agentId, 1);
}
recordBinaryFrame(kind: string, bytes: number, handlerMs: number): void {
incrementCount(this.inboundBinaryFrameCounts, kind, 1);
incrementCount(this.inboundMessageBytes, `binary:${kind}`, bytes);
incrementHandlerTiming(this.inboundMessageHandlerMs, `binary:${kind}`, handlerMs);
}
flush(options?: { final?: boolean }): void {
const now = Date.now();
const bucket = this.consumeCurrentBucket(now);
if (bucket) {
this.buckets.push(bucket);
}
this.pruneBuckets(now);
const aggregate = this.aggregateBuckets();
const hasActivity =
aggregate.inboundMessageCounts.size > 0 || aggregate.inboundBinaryFrameCounts.size > 0;
if (!hasActivity && !options?.final) {
return;
}
this.logger.info(
{
windowMs: Math.min(this.windowMs, Math.max(0, now - this.startedAt)),
rollingWindowMs: this.windowMs,
bucketCount: this.buckets.length,
final: Boolean(options?.final),
connectionPath: this.context.connectionPath,
serverId: this.context.serverId,
connectionStatus: this.context.getConnectionStatus(),
inboundMessageTypesTop: getTopCounts(aggregate.inboundMessageCounts, 20),
inboundMessageBytesTop: getTopCounts(aggregate.inboundMessageBytes, 20),
inboundAgentStreamTypesTop: getTopCounts(aggregate.inboundAgentStreamCounts, 20),
inboundAgentStreamAgentsTop: getTopCounts(aggregate.inboundAgentStreamByAgentCounts, 20),
inboundBinaryFrameTypesTop: getTopCounts(aggregate.inboundBinaryFrameCounts, 12),
handlerTimingTop: getTopHandlerTimings(aggregate.inboundMessageHandlerMs, 20),
},
"ws_runtime_metrics_client",
);
}
private consumeCurrentBucket(now: number): RuntimeMetricsBucket | null {
const hasActivity =
this.inboundMessageCounts.size > 0 || this.inboundBinaryFrameCounts.size > 0;
if (!hasActivity) {
return null;
}
const bucket = {
inboundMessageCounts: new Map(this.inboundMessageCounts),
inboundMessageBytes: new Map(this.inboundMessageBytes),
inboundMessageHandlerMs: cloneHandlerTimingMap(this.inboundMessageHandlerMs),
inboundAgentStreamCounts: new Map(this.inboundAgentStreamCounts),
inboundAgentStreamByAgentCounts: new Map(this.inboundAgentStreamByAgentCounts),
inboundBinaryFrameCounts: new Map(this.inboundBinaryFrameCounts),
endedAt: now,
};
this.inboundMessageCounts.clear();
this.inboundMessageBytes.clear();
this.inboundMessageHandlerMs.clear();
this.inboundAgentStreamCounts.clear();
this.inboundAgentStreamByAgentCounts.clear();
this.inboundBinaryFrameCounts.clear();
return bucket;
}
private pruneBuckets(now: number): void {
const cutoff = now - this.windowMs;
while (this.buckets.length > 0 && this.buckets[0].endedAt < cutoff) {
this.buckets.shift();
}
}
private aggregateBuckets(): RuntimeMetricsBucket {
const aggregate = createEmptyBucket(Date.now());
for (const bucket of this.buckets) {
mergeCountMap(aggregate.inboundMessageCounts, bucket.inboundMessageCounts);
mergeCountMap(aggregate.inboundMessageBytes, bucket.inboundMessageBytes);
mergeHandlerTimingMap(aggregate.inboundMessageHandlerMs, bucket.inboundMessageHandlerMs);
mergeCountMap(aggregate.inboundAgentStreamCounts, bucket.inboundAgentStreamCounts);
mergeCountMap(
aggregate.inboundAgentStreamByAgentCounts,
bucket.inboundAgentStreamByAgentCounts,
);
mergeCountMap(aggregate.inboundBinaryFrameCounts, bucket.inboundBinaryFrameCounts);
}
return aggregate;
}
}
function createEmptyBucket(endedAt: number): RuntimeMetricsBucket {
return {
inboundMessageCounts: new Map(),
inboundMessageBytes: new Map(),
inboundMessageHandlerMs: new Map(),
inboundAgentStreamCounts: new Map(),
inboundAgentStreamByAgentCounts: new Map(),
inboundBinaryFrameCounts: new Map(),
endedAt,
};
}
function incrementCount(map: Map<string, number>, key: string, amount: number): void {
map.set(key, (map.get(key) ?? 0) + amount);
}
function incrementHandlerTiming(
map: Map<string, RuntimeMetricsHandlerTiming>,
key: string,
handlerMs: number,
): void {
const existing = map.get(key);
if (existing) {
existing.count += 1;
existing.totalMs += handlerMs;
existing.maxMs = Math.max(existing.maxMs, handlerMs);
return;
}
map.set(key, {
count: 1,
totalMs: handlerMs,
maxMs: handlerMs,
});
}
function cloneHandlerTimingMap(
map: Map<string, RuntimeMetricsHandlerTiming>,
): Map<string, RuntimeMetricsHandlerTiming> {
return new Map(
[...map.entries()].map(([key, value]) => [
key,
{ count: value.count, totalMs: value.totalMs, maxMs: value.maxMs },
]),
);
}
function mergeCountMap(target: Map<string, number>, source: Map<string, number>): void {
for (const [key, value] of source) {
incrementCount(target, key, value);
}
}
function mergeHandlerTimingMap(
target: Map<string, RuntimeMetricsHandlerTiming>,
source: Map<string, RuntimeMetricsHandlerTiming>,
): void {
for (const [key, value] of source) {
const existing = target.get(key);
if (existing) {
existing.count += value.count;
existing.totalMs += value.totalMs;
existing.maxMs = Math.max(existing.maxMs, value.maxMs);
continue;
}
target.set(key, {
count: value.count,
totalMs: value.totalMs,
maxMs: value.maxMs,
});
}
}
function getTopCounts(map: Map<string, number>, limit: number): Array<[string, number]> {
return [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
}
function getTopHandlerTimings(
map: Map<string, RuntimeMetricsHandlerTiming>,
limit: number,
): Array<{
type: string;
count: number;
totalMs: number;
avgMs: number;
maxMs: number;
}> {
const rows = [...map.entries()].map(([type, value]) => ({
type,
count: value.count,
totalMs: Math.round(value.totalMs),
avgMs: Math.round((value.totalMs / value.count) * 100) / 100,
maxMs: Math.round(value.maxMs * 100) / 100,
}));
rows.sort((a, b) => b.totalMs - a.totalMs);
return rows.slice(0, limit);
}
@@ -1,39 +0,0 @@
export interface DaemonTransport {
send: (data: string | Uint8Array | ArrayBuffer) => void;
close: (code?: number, reason?: string) => void;
onMessage: (handler: (data: unknown) => void) => () => void;
onOpen: (handler: () => void) => () => void;
onClose: (handler: (event?: unknown) => void) => () => void;
onError: (handler: (event?: unknown) => void) => () => void;
}
export type DaemonTransportFactory = (options: {
url: string;
headers?: Record<string, string>;
protocols?: string[];
}) => DaemonTransport;
export type WebSocketFactory = (
url: string,
options?: { headers?: Record<string, string>; protocols?: string[] },
) => WebSocketLike;
export interface WebSocketLike {
readyState: number;
send: (data: string | Uint8Array | ArrayBuffer) => void;
close: (code?: number, reason?: string) => void;
binaryType?: string;
on?: (event: string, listener: (...args: unknown[]) => void) => void;
off?: (event: string, listener: (...args: unknown[]) => void) => void;
removeListener?: (event: string, listener: (...args: unknown[]) => void) => void;
addEventListener?: (event: string, listener: (event: unknown) => void) => void;
removeEventListener?: (event: string, listener: (event: unknown) => void) => void;
onopen?: ((event: unknown) => void) | null;
onclose?: ((event: unknown) => void) | null;
onerror?: ((event: unknown) => void) | null;
onmessage?: ((event: unknown) => void) | null;
}
export interface TransportLogger {
warn(obj: object, msg?: string): void;
}
@@ -1,127 +0,0 @@
export function copyArrayBufferViewToBuffer(data: ArrayBufferView): ArrayBuffer {
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
const out = new Uint8Array(view.byteLength);
out.set(view);
return out.buffer;
}
export function normalizeTransportPayload(
data: string | Uint8Array | ArrayBuffer,
): string | ArrayBuffer {
if (typeof data === "string" || data instanceof ArrayBuffer) {
return data;
}
return copyArrayBufferViewToBuffer(data);
}
export function extractRelayMessageData(event: unknown): string | ArrayBuffer {
const raw =
event && typeof event === "object" && "data" in event
? (event as { data: unknown }).data
: event;
if (typeof raw === "string") return raw;
if (raw instanceof ArrayBuffer) return raw;
if (ArrayBuffer.isView(raw)) {
return copyArrayBufferViewToBuffer(raw);
}
return String(raw ?? "");
}
export function describeTransportClose(event?: unknown): string {
if (!event) {
return "Transport closed";
}
if (event instanceof Error) {
return event.message;
}
if (typeof event === "string") {
return event;
}
if (typeof event === "object") {
const record = event as { reason?: unknown; message?: unknown; code?: unknown };
if (typeof record.reason === "string" && record.reason.trim().length > 0) {
return record.reason.trim();
}
if (typeof record.message === "string" && record.message.trim().length > 0) {
return record.message.trim();
}
if (typeof record.code === "number") {
return `Transport closed (code ${record.code})`;
}
}
return "Transport closed";
}
export function describeTransportError(event?: unknown): string {
if (!event) {
return "Transport error";
}
if (event instanceof Error) {
return event.message;
}
if (typeof event === "string") {
return event;
}
if (typeof event === "object") {
const record = event as { message?: unknown };
if (typeof record.message === "string" && record.message.trim().length > 0) {
return record.message.trim();
}
}
return "Transport error";
}
export function safeRandomId(): string {
try {
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
} catch {
// ignore
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
}
export function decodeMessageData(data: unknown): string | null {
if (data === null || data === undefined) {
return null;
}
if (typeof data === "string") {
return data;
}
if (typeof ArrayBuffer !== "undefined" && data instanceof ArrayBuffer) {
if (typeof Buffer !== "undefined") {
return Buffer.from(data).toString("utf8");
}
if (typeof TextDecoder !== "undefined") {
return new TextDecoder().decode(data);
}
}
if (ArrayBuffer.isView(data)) {
const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
if (typeof Buffer !== "undefined") {
return Buffer.from(view).toString("utf8");
}
if (typeof TextDecoder !== "undefined") {
return new TextDecoder().decode(view);
}
}
if (typeof (data as { toString?: () => string }).toString === "function") {
return (data as { toString: () => string }).toString();
}
return null;
}
export function encodeUtf8String(value: string): Uint8Array {
if (typeof TextEncoder !== "undefined") {
return new TextEncoder().encode(value);
}
if (typeof Buffer !== "undefined") {
return new Uint8Array(Buffer.from(value, "utf8"));
}
const out = new Uint8Array(value.length);
for (let i = 0; i < value.length; i++) {
out[i] = value.charCodeAt(i) & 0xff;
}
return out;
}
@@ -1,183 +0,0 @@
import { EventEmitter } from "node:events";
import { describe, expect, test, vi } from "vitest";
import {
createEncryptedTransport,
createWebSocketTransportFactory,
decodeMessageData,
describeTransportClose,
describeTransportError,
encodeUtf8String,
extractRelayMessageData,
} from "./daemon-client-transport.js";
const createClientChannelMock = vi.hoisted(() => vi.fn());
vi.mock("@getpaseo/relay/e2ee", () => ({
createClientChannel: createClientChannelMock,
}));
describe("daemon-client transport helpers", () => {
test("createEncryptedTransport closes handshake failures with browser-safe code", async () => {
createClientChannelMock.mockReset();
createClientChannelMock.mockRejectedValueOnce(new Error("handshake failed"));
let openHandler: (() => void) | null = null;
const close = vi.fn();
createEncryptedTransport(
{
send: vi.fn(),
close,
onOpen: (handler) => {
openHandler = handler;
return () => {
if (openHandler === handler) {
openHandler = null;
}
};
},
onClose: () => () => {},
onError: () => () => {},
onMessage: () => () => {},
},
"daemon-public-key",
{ warn: vi.fn() },
);
expect(openHandler).not.toBeNull();
openHandler?.();
await vi.waitFor(() => {
expect(close).toHaveBeenCalledWith(4001, "E2EE handshake failed");
});
});
test("createWebSocketTransportFactory forwards sends when socket is open", () => {
const addEventListener = vi.fn();
const removeEventListener = vi.fn();
const send = vi.fn();
const close = vi.fn();
const factory = createWebSocketTransportFactory(() => ({
readyState: 1,
send,
close,
binaryType: "blob",
addEventListener,
removeEventListener,
}));
const transport = factory({ url: "ws://example.test" });
transport.send("hello");
transport.close(1000, "bye");
expect(send).toHaveBeenCalledWith("hello");
expect(close).toHaveBeenCalledWith(1000, "bye");
});
test("createWebSocketTransportFactory passes WebSocket protocols to the socket factory", () => {
const socketFactory = vi.fn(() => ({
readyState: 1,
send: vi.fn(),
close: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));
createWebSocketTransportFactory(socketFactory)({
url: "ws://example.test",
headers: { Authorization: "Bearer shared-secret" },
protocols: ["paseo.bearer.shared-secret"],
});
expect(socketFactory).toHaveBeenCalledWith("ws://example.test", {
headers: { Authorization: "Bearer shared-secret" },
protocols: ["paseo.bearer.shared-secret"],
});
});
test("createWebSocketTransportFactory rejects sends when socket is not open", () => {
const factory = createWebSocketTransportFactory(() => ({
readyState: 3,
send: vi.fn(),
close: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}));
const transport = factory({ url: "ws://example.test" });
expect(() => transport.send("hello")).toThrow("WebSocket not open (readyState=3)");
});
test("createWebSocketTransportFactory binds and unbinds event listeners", () => {
const listeners = new Map<string, (...args: unknown[]) => void>();
const ws = {
readyState: 1,
send: vi.fn(),
close: vi.fn(),
addEventListener: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
listeners.set(event, handler);
}),
removeEventListener: vi.fn((event: string) => {
listeners.delete(event);
}),
};
const transport = createWebSocketTransportFactory(() => ws)({ url: "ws://example.test" });
const onMessage = vi.fn();
const unsubscribe = transport.onMessage(onMessage);
const message = { data: "payload" };
listeners.get("message")?.(message);
expect(onMessage).toHaveBeenCalledWith(message);
unsubscribe();
expect(ws.removeEventListener).toHaveBeenCalledWith("message", expect.any(Function));
});
test("createWebSocketTransportFactory suppresses close-before-open ws errors", () => {
class MockNodeWebSocket extends EventEmitter {
readyState = 0;
send = vi.fn();
close = vi.fn((code?: number, reason?: string) => {
this.emit("error", new Error("WebSocket was closed before the connection was established"));
this.emit("close", { code, reason });
});
}
const ws = new MockNodeWebSocket();
const transport = createWebSocketTransportFactory(() => ws)({ url: "ws://example.test" });
expect(() => transport.close(1001, "Connection timed out")).not.toThrow();
});
test("describeTransportClose prefers reason, then message, then code", () => {
expect(describeTransportClose({ reason: "peer closed" })).toBe("peer closed");
expect(describeTransportClose({ message: "closed" })).toBe("closed");
expect(describeTransportClose({ code: 1001 })).toBe("Transport closed (code 1001)");
expect(describeTransportClose()).toBe("Transport closed");
});
test("describeTransportError returns normalized messages", () => {
expect(describeTransportError(new Error("boom"))).toBe("boom");
expect(describeTransportError({ message: "bad frame" })).toBe("bad frame");
expect(describeTransportError()).toBe("Transport error");
});
test("extractRelayMessageData returns strings and array buffers", () => {
expect(extractRelayMessageData({ data: "hello" })).toBe("hello");
const view = new Uint8Array([1, 2, 3]);
const extracted = extractRelayMessageData({ data: view });
expect(extracted).toBeInstanceOf(ArrayBuffer);
expect(Array.from(new Uint8Array(extracted as ArrayBuffer))).toEqual([1, 2, 3]);
});
test("decodeMessageData decodes strings, array buffers, and typed arrays", () => {
const bytes = encodeUtf8String("hello");
expect(decodeMessageData("hello")).toBe("hello");
expect(decodeMessageData(bytes.buffer)).toBe("hello");
expect(decodeMessageData(bytes)).toBe("hello");
});
});
@@ -1,25 +0,0 @@
export type {
DaemonTransport,
DaemonTransportFactory,
TransportLogger,
WebSocketFactory,
WebSocketLike,
} from "./daemon-client-transport-types.js";
export {
decodeMessageData,
describeTransportClose,
describeTransportError,
encodeUtf8String,
extractRelayMessageData,
normalizeTransportPayload,
safeRandomId,
} from "./daemon-client-transport-utils.js";
export {
createEncryptedTransport,
createRelayE2eeTransportFactory,
} from "./daemon-client-relay-e2ee-transport.js";
export {
bindWsHandler,
createWebSocketTransportFactory,
defaultWebSocketFactory,
} from "./daemon-client-websocket-transport.js";
@@ -1,140 +0,0 @@
import type {
DaemonTransportFactory,
WebSocketFactory,
WebSocketLike,
} from "./daemon-client-transport-types.js";
export function defaultWebSocketFactory(
url: string,
options?: { headers?: Record<string, string>; protocols?: string[] },
): WebSocketLike {
const globalWs = (
globalThis as {
WebSocket?: new (url: string, protocols?: string | string[]) => WebSocketLike;
}
).WebSocket;
if (!globalWs) {
throw new Error("WebSocket is not available in this runtime");
}
return new globalWs(url, options?.protocols);
}
export function createWebSocketTransportFactory(factory: WebSocketFactory): DaemonTransportFactory {
return ({ url, headers, protocols }) => {
const ws = factory(url, { headers, protocols });
if ("binaryType" in ws) {
try {
ws.binaryType = "arraybuffer";
} catch {
// no-op
}
}
return {
send: (data) => {
if (typeof ws.readyState === "number" && ws.readyState !== 1) {
throw new Error(`WebSocket not open (readyState=${ws.readyState})`);
}
ws.send(data);
},
close: (code?: number, reason?: string) => {
// Node's `ws` may emit an `error` when a connecting socket is closed before the
// handshake completes. Keep a temporary no-op handler attached so cleanup during
// connect timeouts does not crash the CLI with an unhandled error event.
const suppressEarlyCloseError = bindTemporaryEarlyCloseErrorHandler(ws);
try {
ws.close(code, reason);
} finally {
if (typeof ws.on !== "function" && typeof ws.addEventListener !== "function") {
suppressEarlyCloseError();
}
}
},
onOpen: (handler) => bindWsHandler(ws, "open", handler),
onClose: (handler) => bindWsHandler(ws, "close", handler),
onError: (handler) => bindWsHandler(ws, "error", handler),
onMessage: (handler) => bindWsHandler(ws, "message", handler),
};
};
}
function bindTemporaryEarlyCloseErrorHandler(ws: WebSocketLike): () => void {
const noop = () => {};
if (typeof ws.addEventListener === "function") {
ws.addEventListener("error", noop);
const removeOnClose = bindWsHandler(ws, "close", () => {
removeOnClose();
if (typeof ws.removeEventListener === "function") {
ws.removeEventListener("error", noop);
}
});
return () => {
removeOnClose();
if (typeof ws.removeEventListener === "function") {
ws.removeEventListener("error", noop);
}
};
}
if (typeof ws.on === "function") {
ws.on("error", noop);
const removeOnClose = bindWsHandler(ws, "close", () => {
removeOnClose();
if (typeof ws.off === "function") {
ws.off("error", noop);
return;
}
if (typeof ws.removeListener === "function") {
ws.removeListener("error", noop);
}
});
return () => {
removeOnClose();
if (typeof ws.off === "function") {
ws.off("error", noop);
return;
}
if (typeof ws.removeListener === "function") {
ws.removeListener("error", noop);
}
};
}
return () => {};
}
export function bindWsHandler(
ws: WebSocketLike,
event: "open" | "close" | "error" | "message",
handler: (...args: unknown[]) => void,
): () => void {
if (typeof ws.addEventListener === "function") {
ws.addEventListener(event, handler);
return () => {
if (typeof ws.removeEventListener === "function") {
ws.removeEventListener(event, handler);
}
};
}
if (typeof ws.on === "function") {
ws.on(event, handler);
return () => {
if (typeof ws.off === "function") {
ws.off(event, handler);
return;
}
if (typeof ws.removeListener === "function") {
ws.removeListener(event, handler);
}
};
}
const prop = `on${event}`;
const wsRecord = ws as unknown as Record<string, unknown>;
const previous = wsRecord[prop];
wsRecord[prop] = handler;
return () => {
if (wsRecord[prop] === handler) {
wsRecord[prop] = previous ?? null;
}
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,28 +0,0 @@
import { describe, expect, test } from "vitest";
import { TerminalStreamOpcode } from "../shared/binary-frames/index.js";
import { TerminalStreamRouter, type TerminalStreamEvent } from "./terminal-stream-router.js";
describe("terminal-stream-router", () => {
test("routes restore frames as restore events", () => {
const router = new TerminalStreamRouter();
const events: TerminalStreamEvent[] = [];
const payload = new TextEncoder().encode("restored screen");
router.setSlot("term-1", 7);
router.onEvent((event) => events.push(event));
router.handleFrame({
opcode: TerminalStreamOpcode.Restore,
slot: 7,
payload,
});
expect(events).toEqual([
{
terminalId: "term-1",
type: "restore",
data: payload,
},
]);
});
});
@@ -1,132 +0,0 @@
import {
decodeTerminalSnapshotPayload,
encodeTerminalResizePayload,
encodeTerminalStreamFrame,
TerminalStreamOpcode,
type TerminalStreamFrame,
} from "../shared/binary-frames/index.js";
import type { TerminalInput, TerminalState } from "../shared/messages.js";
export type TerminalStreamEvent =
| { terminalId: string; type: "output"; data: Uint8Array }
| { terminalId: string; type: "snapshot"; state: TerminalState }
| { terminalId: string; type: "restore"; data: Uint8Array };
export class TerminalStreamRouter {
private readonly terminalSlots = new Map<string, number>();
private readonly slotTerminals = new Map<number, string>();
private readonly listeners = new Set<(event: TerminalStreamEvent) => void>();
onEvent(handler: (event: TerminalStreamEvent) => void): () => void {
this.listeners.add(handler);
return () => {
this.listeners.delete(handler);
};
}
setSlot(terminalId: string, slot: number): void {
const existingTerminalId = this.slotTerminals.get(slot);
if (existingTerminalId && existingTerminalId !== terminalId) {
this.terminalSlots.delete(existingTerminalId);
}
const existingSlot = this.terminalSlots.get(terminalId);
if (typeof existingSlot === "number" && existingSlot !== slot) {
this.slotTerminals.delete(existingSlot);
}
this.terminalSlots.set(terminalId, slot);
this.slotTerminals.set(slot, terminalId);
}
removeTerminal(terminalId: string): void {
const slot = this.terminalSlots.get(terminalId);
if (typeof slot !== "number") {
return;
}
this.terminalSlots.delete(terminalId);
if (this.slotTerminals.get(slot) === terminalId) {
this.slotTerminals.delete(slot);
}
}
clearSlots(): void {
this.terminalSlots.clear();
this.slotTerminals.clear();
}
encodeInput(terminalId: string, message: TerminalInput["message"]): Uint8Array | null {
const slot = this.terminalSlots.get(terminalId);
if (typeof slot !== "number") {
return null;
}
if (message.type === "input") {
return encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Input,
slot,
payload: message.data,
});
}
if (message.type === "resize") {
return encodeTerminalStreamFrame({
opcode: TerminalStreamOpcode.Resize,
slot,
payload: encodeTerminalResizePayload({
rows: message.rows,
cols: message.cols,
}),
});
}
return null;
}
handleFrame(frame: TerminalStreamFrame): void {
const terminalId = this.slotTerminals.get(frame.slot);
if (!terminalId) {
return;
}
if (frame.opcode === TerminalStreamOpcode.Output) {
this.emit({
terminalId,
type: "output",
data: frame.payload,
});
return;
}
if (frame.opcode === TerminalStreamOpcode.Restore) {
this.emit({
terminalId,
type: "restore",
data: frame.payload,
});
return;
}
if (frame.opcode === TerminalStreamOpcode.Snapshot) {
const state = decodeTerminalSnapshotPayload(frame.payload);
if (!state) {
return;
}
this.emit({
terminalId,
type: "snapshot",
state,
});
}
}
private emit(event: TerminalStreamEvent): void {
for (const listener of this.listeners) {
try {
listener(event);
} catch {
// no-op
}
}
}
}
@@ -1,4 +1,4 @@
import type { AgentAttentionReason } from "../shared/agent-attention-notification.js";
import type { AgentAttentionReason } from "@getpaseo/protocol/agent-attention-notification";
export const PRESENCE_THRESHOLD_MS = 180_000;
@@ -1,6 +1,6 @@
import type { AgentTimelineItem } from "./agent-sdk-types.js";
import { isLikelyExternalToolName } from "./tool-name-normalization.js";
import { buildToolCallDisplayModel } from "../../shared/tool-call-display.js";
import { isLikelyExternalToolName } from "@getpaseo/protocol/tool-name-normalization";
import { buildToolCallDisplayModel } from "@getpaseo/protocol/tool-call-display";
import { projectTimelineRows } from "./timeline-projection.js";
const DEFAULT_MAX_ITEMS = 0;
@@ -8,7 +8,7 @@ import { randomUUID } from "node:crypto";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { AgentManager, type ManagedAgent } from "./agent-manager.js";
import { AgentStorage } from "./agent-storage.js";
import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.js";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import { formatSystemNotificationPrompt } from "./agent-prompt.js";
import type { StoredAgentRecord } from "./agent-storage.js";
import type {
@@ -4,8 +4,8 @@ import { stat } from "node:fs/promises";
import {
AGENT_LIFECYCLE_STATUSES,
type AgentLifecycleStatus,
} from "../../shared/agent-lifecycle.js";
import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.js";
} from "@getpaseo/protocol/agent-lifecycle";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import type { Logger } from "pino";
import { z } from "zod";
import type { TerminalManager } from "../../terminal/terminal-manager.js";
@@ -53,7 +53,7 @@ import {
AgentStreamCoalescer,
} from "./agent-stream-coalescer.js";
import { ForegroundRunState, type ForegroundTurnWaiter } from "./foreground-run-state.js";
import { getAgentProviderDefinition } from "./provider-manifest.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import { IMPORTABLE_PROVIDERS } from "./provider-registry.js";
import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js";
import { isSystemInjectedEnvelope } from "./agent-prompt.js";
@@ -8,7 +8,7 @@ import {
StructuredAgentResponseError,
generateStructuredAgentResponseWithFallback,
} from "./agent-response-loop.js";
import { MAX_AUTO_AGENT_TITLE_CHARS } from "./agent-title-limits.js";
import { MAX_AUTO_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits";
import { buildMetadataPrompt } from "../../utils/build-metadata-prompt.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
@@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { createNoopWorkspaceGitService } from "../test-utils/workspace-git-service-stub.js";
import { MAX_AUTO_AGENT_TITLE_CHARS } from "./agent-title-limits.js";
import { MAX_AUTO_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits";
import {
generateAndApplyAgentMetadata,
type AgentMetadataGeneratorDeps,
@@ -3,7 +3,7 @@ import { zodToJsonSchema } from "zod-to-json-schema";
import Ajv, { type ErrorObject, type Options as AjvOptions } from "ajv";
import type { AgentProvider, AgentSessionConfig } from "./agent-sdk-types.js";
import type { AgentManager } from "./agent-manager.js";
import { getAgentProviderDefinition } from "./provider-manifest.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
export interface StructuredGenerationLogger {
info: (obj: object, msg?: string) => void;
@@ -1,5 +1,5 @@
import type { Options as ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";
import type { AgentAttachment } from "../../shared/messages.js";
import type { AgentAttachment } from "@getpaseo/protocol/messages";
export type AgentProvider = string;
@@ -1,2 +0,0 @@
export const MAX_EXPLICIT_AGENT_TITLE_CHARS = 200;
export const MAX_AUTO_AGENT_TITLE_CHARS = 40;
@@ -1,4 +1,4 @@
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "./agent-title-limits.js";
import { MAX_EXPLICIT_AGENT_TITLE_CHARS } from "@getpaseo/protocol/agent-title-limits";
const MAX_INITIAL_AGENT_TITLE_CHARS = Math.min(60, MAX_EXPLICIT_AGENT_TITLE_CHARS);
@@ -1,6 +1,6 @@
import type { Logger } from "pino";
import { PARENT_AGENT_ID_LABEL } from "../../../shared/agent-labels.js";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
import type { TerminalManager } from "../../../terminal/terminal-manager.js";
import type { CreatePaseoWorktreeInput } from "../../paseo-worktree-service.js";
import { expandUserPath, resolvePathFromBase } from "../../path-utils.js";
@@ -23,7 +23,7 @@ import type {
AgentSessionConfig,
} from "../agent-sdk-types.js";
import type { AgentStorage } from "../agent-storage.js";
import { getAgentProviderDefinition } from "../provider-manifest.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
import type { ProviderDefinition } from "../provider-registry.js";
import { setupFinishNotification, startCreatedAgentInitialPrompt } from "../agent-prompt.js";
import { resolveAndValidateCreateAgentMode } from "../create-agent-mode.js";
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { FetchRecentProviderSessionsRequestMessage } from "../../shared/messages.js";
import type { FetchRecentProviderSessionsRequestMessage } from "@getpaseo/protocol/messages";
import type { AgentTimelineItem, PersistedAgentDescriptor } from "./agent-sdk-types.js";
import {
ImportSessionsRequestError,
@@ -18,7 +18,7 @@ import type {
FetchRecentProviderSessionsRequestMessage,
ImportAgentRequestMessageSchema,
RecentProviderSessionDescriptorPayload,
} from "../../shared/messages.js";
} from "@getpaseo/protocol/messages";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import { createRealpathAwarePathMatcher } from "../../utils/path.js";
@@ -9,7 +9,7 @@ import { z } from "zod";
import { AGENT_WAIT_TIMEOUT_MS } from "./mcp-shared.js";
import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.js";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
interface StructuredContent {
[key: string]: unknown;
@@ -13,13 +13,16 @@ import { createAgentMcpServer } from "./mcp-server.js";
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
import type { ProviderDefinition } from "./provider-registry.js";
import { AgentListItemPayloadSchema, AgentSnapshotPayloadSchema } from "../../shared/messages.js";
import {
AgentListItemPayloadSchema,
AgentSnapshotPayloadSchema,
} from "@getpaseo/protocol/messages";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "../workspace-registry.js";
import type {
CreateScheduleInput,
StoredSchedule,
UpdateScheduleInput,
} from "../schedule/types.js";
} from "@getpaseo/protocol/schedule/types";
import type { ScheduleService } from "../schedule/service.js";
import type { WorkspaceGitService } from "../workspace-git-service.js";
import {
@@ -30,7 +33,7 @@ import type { CreatePaseoWorktreeWorkflowFn } from "../worktree-session.js";
import { WorkspaceGitServiceImpl } from "../workspace-git-service.js";
import type { GitHubService } from "../../services/github-service.js";
import type { TerminalManager } from "../../terminal/terminal-manager.js";
import { PARENT_AGENT_ID_LABEL } from "../../shared/agent-labels.js";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
const REPO_CWD = resolvePath("/tmp/repo");
const TARGET_CWD = resolvePath("/tmp/target");
@@ -44,8 +44,9 @@ import {
ScheduleRunSchema,
ScheduleSummarySchema,
StoredScheduleSchema,
} from "../schedule/types.js";
import type { ScheduleCadence, UpdateScheduleInput } from "../schedule/types.js";
type ScheduleCadence,
type UpdateScheduleInput,
} from "@getpaseo/protocol/schedule/types";
import type { ProviderDefinition } from "./provider-registry.js";
import { resolveSnapshotCwd } from "./provider-snapshot-manager.js";
import {
@@ -7,7 +7,7 @@ import { curateAgentActivity } from "./activity-curator.js";
import { selectItemsByProjectedLimit } from "./timeline-projection.js";
import type { AgentStorage } from "./agent-storage.js";
import { serializeAgentSnapshot } from "../messages.js";
import { StoredScheduleSchema } from "../schedule/types.js";
import { StoredScheduleSchema } from "@getpaseo/protocol/schedule/types";
import type { AgentProvider } from "./agent-sdk-types.js";
export const AgentProviderEnum = z.string();
@@ -1,4 +1,4 @@
import type { AgentAttachment } from "../../shared/messages.js";
import type { AgentAttachment } from "@getpaseo/protocol/messages";
const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
@@ -1,91 +1,29 @@
import { z } from "zod";
import { isAbsolute } from "node:path";
import { executableExists, findExecutable } from "../../utils/executable.js";
import { createExternalProcessEnv, type ProcessEnvRecord } from "../paseo-env.js";
import type { AgentProvider } from "./agent-sdk-types.js";
import { AgentProviderSchema } from "./provider-manifest.js";
const ProviderCommandDefaultSchema = z
.object({
mode: z.literal("default"),
})
.strict();
const ProviderCommandAppendSchema = z
.object({
mode: z.literal("append"),
args: z.array(z.string()).optional(),
})
.strict();
const ProviderCommandReplaceSchema = z
.object({
mode: z.literal("replace"),
argv: z.array(z.string().min(1)).min(1),
})
.strict();
export const ProviderCommandSchema = z.discriminatedUnion("mode", [
ProviderCommandDefaultSchema,
ProviderCommandAppendSchema,
ProviderCommandReplaceSchema,
]);
export const ProviderRuntimeSettingsSchema = z
.object({
command: ProviderCommandSchema.optional(),
env: z.record(z.string()).optional(),
disallowedTools: z.array(z.string()).optional(),
})
.strict();
const ProviderProfileThinkingOptionSchema = z
.object({
id: z.string(),
label: z.string(),
description: z.string().optional(),
isDefault: z.boolean().optional(),
})
.strict();
export const ProviderProfileModelSchema = z
.object({
id: z.string().min(1),
label: z.string().min(1),
description: z.string().optional(),
isDefault: z.boolean().optional(),
thinkingOptions: z.array(ProviderProfileThinkingOptionSchema).optional(),
})
.strict();
export const ProviderOverrideSchema = z
.object({
extends: z.string().optional(),
label: z.string().optional(),
description: z.string().optional(),
command: z.array(z.string().min(1)).min(1).optional(),
env: z.record(z.string()).optional(),
models: z.array(ProviderProfileModelSchema).optional(),
additionalModels: z.array(ProviderProfileModelSchema).optional(),
disallowedTools: z.array(z.string()).optional(),
enabled: z.boolean().optional(),
order: z.number().optional(),
})
.strict();
export const AgentProviderRuntimeSettingsMapSchema = z.record(
AgentProviderSchema,
export {
AgentProviderRuntimeSettingsMapSchema,
ProviderCommandSchema,
ProviderOverrideSchema,
ProviderOverridesSchema,
ProviderProfileModelSchema,
ProviderRuntimeSettingsSchema,
);
export type ProviderCommand = z.infer<typeof ProviderCommandSchema>;
export type ProviderRuntimeSettings = z.infer<typeof ProviderRuntimeSettingsSchema>;
export type ProviderProfileModel = z.infer<typeof ProviderProfileModelSchema>;
export type ProviderOverride = z.infer<typeof ProviderOverrideSchema>;
export type AgentProviderRuntimeSettingsMap = Partial<
Record<AgentProvider, ProviderRuntimeSettings>
>;
type AgentProviderRuntimeSettingsMap,
type ProviderCommand,
type ProviderOverride,
type ProviderOverrides,
type ProviderProfileModel,
type ProviderRuntimeSettings,
} from "@getpaseo/protocol/provider-config";
import {
ProviderOverrideSchema,
ProviderOverridesSchema,
ProviderRuntimeSettingsSchema,
type ProviderCommand,
type ProviderOverride,
type ProviderOverrides,
type ProviderRuntimeSettings,
} from "@getpaseo/protocol/provider-config";
export interface ProviderCommandPrefix {
command: string;
@@ -223,7 +161,7 @@ export function resolveShellEnv(): Record<string, string> {
export function migrateProviderSettings(
raw: Record<string, unknown>,
builtinProviderIds: string[],
): Record<string, ProviderOverride> {
): ProviderOverrides {
const migrated: Record<string, ProviderOverride> = {};
const builtinProviderIdSet = new Set(builtinProviderIds);
@@ -256,7 +194,7 @@ export function migrateProviderSettings(
migrated[providerId] = nextEntry;
}
return migrated;
return ProviderOverridesSchema.parse(migrated);
}
// Env vars that indicate a running Claude Code session. If the daemon itself is
@@ -1,280 +0,0 @@
import { z } from "zod";
import type { AgentMode } from "./agent-sdk-types.js";
export type AgentModeColorTier = "safe" | "moderate" | "dangerous" | "planning" | `#${string}`;
// Open string by design: the client looks icons up in a registry and falls back
// to a default for unknown values. Daemon downgrades unknown icons for clients
// that pre-date the open-string contract (see CLIENT_CAPS.customModeIcons).
export type AgentModeIcon = string;
export interface AgentModeVisuals {
icon: AgentModeIcon;
colorTier: AgentModeColorTier;
}
export type AgentProviderModeDefinition = Omit<AgentMode, "icon" | "colorTier"> &
AgentModeVisuals & {
// Marks the provider's most-permissioned no-prompt mode. Selecting it means tools run without approval; the runtime mechanism is provider-specific.
isUnattended?: boolean;
};
// TODO: `modes` should not be static. Providers (especially ACP) report their
// own modes at runtime via session/new. We should fetch modes from the provider
// as source of truth and enrich with UI metadata (icons, colorTier) on top.
export interface AgentProviderDefinition {
id: string;
label: string;
description: string;
defaultModeId: string | null;
modes: AgentProviderModeDefinition[];
voice?: {
enabled: boolean;
defaultModeId: string;
defaultModel?: string;
};
}
const CLAUDE_MODES: AgentProviderModeDefinition[] = [
{
id: "default",
label: "Always Ask",
description: "Prompts for permission the first time a tool is used",
icon: "ShieldCheck",
colorTier: "safe",
},
{
id: "auto",
label: "Auto mode",
description: "Uses a model classifier to review permission prompts automatically",
icon: "ShieldQuestionMark",
colorTier: "moderate",
},
{
id: "acceptEdits",
label: "Accept File Edits",
description: "Automatically approves edit-focused tools without prompting",
icon: "ShieldAlert",
colorTier: "moderate",
},
{
id: "plan",
label: "Plan Mode",
description: "Analyze the codebase without executing tools or edits",
icon: "ShieldCheck",
colorTier: "planning",
},
{
id: "bypassPermissions",
label: "Bypass",
description: "Skip all permission prompts (use with caution)",
icon: "ShieldAlert",
colorTier: "dangerous",
isUnattended: true,
},
];
const CODEX_MODES: AgentProviderModeDefinition[] = [
{
id: "auto",
label: "Default Permissions",
description: "Edit files and run commands with Codex's default approval flow.",
icon: "ShieldAlert",
colorTier: "moderate",
},
{
id: "auto-review",
label: "Auto-review",
description:
"Same workspace-write permissions as Default, but eligible `on-request` approvals are routed through the auto-reviewer subagent.",
icon: "ShieldQuestionMark",
colorTier: "moderate",
},
{
id: "full-access",
label: "Full Access",
description: "Edit files, run commands, and access the network without additional prompts.",
icon: "ShieldAlert",
colorTier: "dangerous",
isUnattended: true,
},
];
const COPILOT_MODES: AgentProviderModeDefinition[] = [
{
id: "https://agentclientprotocol.com/protocol/session-modes#agent",
label: "Agent",
description: "Default agent mode for conversational interactions",
icon: "ShieldAlert",
colorTier: "moderate",
},
{
id: "https://agentclientprotocol.com/protocol/session-modes#plan",
label: "Plan",
description: "Plan mode for creating and executing multi-step plans",
icon: "ShieldCheck",
colorTier: "planning",
},
{
id: "allow-all",
label: "Allow All",
description: "Automatically approves all Copilot tool, path, and URL requests.",
icon: "ShieldOff",
colorTier: "dangerous",
isUnattended: true,
},
];
const OPENCODE_MODES: AgentProviderModeDefinition[] = [
{
id: "build",
label: "Build",
description: "Allows edits and tool execution for implementation work",
icon: "Bot",
colorTier: "moderate",
},
{
id: "plan",
label: "Plan",
description: "Read-only planning mode that avoids file edits",
icon: "Bot",
colorTier: "planning",
},
];
const MOCK_LOAD_TEST_MODES: AgentProviderModeDefinition[] = [
{
id: "load-test",
label: "Load Test",
description: "Streams repeated markdown, reasoning, and tool calls for app stress testing",
icon: "ShieldOff",
colorTier: "dangerous",
},
];
const MOCK_SLOW_MODES: AgentProviderModeDefinition[] = [
{
id: "default",
label: "Default",
description: "Dev-only mode for the mock slow provider",
icon: "ShieldOff",
colorTier: "dangerous",
},
];
export const AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
{
id: "claude",
label: "Claude",
description: "Anthropic's multi-tool assistant with MCP support, streaming, and deep reasoning",
defaultModeId: "default",
modes: CLAUDE_MODES,
voice: {
enabled: true,
defaultModeId: "default",
defaultModel: "haiku",
},
},
{
id: "codex",
label: "Codex",
description: "OpenAI's Codex workspace agent with sandbox controls and optional network access",
defaultModeId: "auto",
modes: CODEX_MODES,
voice: {
enabled: true,
defaultModeId: "auto",
defaultModel: "gpt-5.4-mini",
},
},
{
id: "copilot",
label: "Copilot",
description: "GitHub Copilot via Agent Client Protocol with dynamic modes and session support",
defaultModeId: "https://agentclientprotocol.com/protocol/session-modes#agent",
modes: COPILOT_MODES,
},
{
id: "opencode",
label: "OpenCode",
description: "Open-source coding assistant with multi-provider model support",
defaultModeId: "build",
modes: OPENCODE_MODES,
voice: {
enabled: true,
defaultModeId: "build",
},
},
{
id: "pi",
label: "Pi",
description: "Minimal terminal-based coding agent with multi-provider LLM support",
defaultModeId: null,
modes: [],
},
];
export const DEV_AGENT_PROVIDER_DEFINITIONS: AgentProviderDefinition[] = [
{
id: "mock",
label: "Mock Load Test",
description:
"Development-only provider that emits synthetic agent traffic for performance tests",
defaultModeId: "load-test",
modes: MOCK_LOAD_TEST_MODES,
},
{
id: "mock-slow",
label: "Mock Slow Provider",
description: "Dev-only: hangs during model discovery to test loading and timeout UI",
defaultModeId: "default",
modes: MOCK_SLOW_MODES,
},
];
export function getAgentProviderDefinition(
provider: string,
definitions: AgentProviderDefinition[] = [
...AGENT_PROVIDER_DEFINITIONS,
...DEV_AGENT_PROVIDER_DEFINITIONS,
],
): AgentProviderDefinition {
const definition = definitions.find((entry) => entry.id === provider);
if (!definition) {
throw new Error(`Unknown agent provider: ${provider}`);
}
return definition;
}
export const BUILTIN_PROVIDER_IDS = AGENT_PROVIDER_DEFINITIONS.map((d) => d.id);
export const AGENT_PROVIDER_IDS = BUILTIN_PROVIDER_IDS;
export const AgentProviderSchema = z.string();
export function isValidAgentProvider(
value: string,
validIds: Iterable<string> = BUILTIN_PROVIDER_IDS,
): boolean {
return Array.isArray(validIds) ? validIds.includes(value) : new Set(validIds).has(value);
}
export function getUnattendedModeId(
provider: string,
definitions: AgentProviderDefinition[] = [
...AGENT_PROVIDER_DEFINITIONS,
...DEV_AGENT_PROVIDER_DEFINITIONS,
],
): string | undefined {
const definition = definitions.find((entry) => entry.id === provider);
return definition?.modes.find((mode) => mode.isUnattended)?.id;
}
export function getModeVisuals(
provider: string,
modeId: string,
definitions: AgentProviderDefinition[],
): AgentModeVisuals | undefined {
const definition = definitions.find((entry) => entry.id === provider);
const mode = definition?.modes.find((m) => m.id === modeId);
if (!mode) return undefined;
return { icon: mode.icon, colorTier: mode.colorTier };
}
@@ -38,7 +38,7 @@ import {
DEV_AGENT_PROVIDER_DEFINITIONS,
getAgentProviderDefinition,
type AgentProviderDefinition,
} from "./provider-manifest.js";
} from "@getpaseo/protocol/provider-manifest";
function isNonEmptyStringArray(value: string[]): value is [string, ...string[]] {
return value.length > 0;
@@ -61,7 +61,7 @@ export interface ProviderDefinition extends AgentProviderDefinition {
fetchModes: (options: ListModesOptions) => Promise<AgentMode[]>;
}
export { IMPORTABLE_PROVIDERS } from "../../shared/importable-providers.js";
export { IMPORTABLE_PROVIDERS } from "@getpaseo/protocol/importable-providers";
export interface BuildProviderRegistryOptions {
runtimeSettings?: AgentProviderRuntimeSettingsMap;
@@ -1,7 +1,7 @@
import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
import { mapClaudeRunningToolCall } from "./tool-call-mapper.js";
import { buildToolCallDisplayModel } from "../../../../shared/tool-call-display.js";
import { buildToolCallDisplayModel } from "@getpaseo/protocol/tool-call-display";
import type { AgentMetadata, AgentStreamEvent, AgentTimelineItem } from "../../agent-sdk-types.js";
@@ -1,7 +1,7 @@
import { z } from "zod";
import type { ToolCallTimelineItem } from "../../agent-sdk-types.js";
import { isSpeakToolName } from "../../tool-name-normalization.js";
import { isSpeakToolName } from "@getpaseo/protocol/tool-name-normalization";
import { deriveClaudeToolDetail } from "./tool-call-detail-parser.js";
interface MapperParams {
@@ -1,7 +1,7 @@
import { z } from "zod";
import type { ToolCallDetail } from "../../agent-sdk-types.js";
import { stripCwdPrefix } from "../../../../shared/path-utils.js";
import { stripCwdPrefix } from "@getpaseo/protocol/path-utils";
import {
ToolEditInputSchema,
ToolEditOutputSchema,
@@ -7,7 +7,7 @@ import {
truncateDiffText,
} from "../tool-call-mapper-utils.js";
import { deriveCodexToolDetail, normalizeCodexFilePath } from "./tool-call-detail-parser.js";
import { isSpeakToolName } from "../../tool-name-normalization.js";
import { isSpeakToolName } from "@getpaseo/protocol/tool-name-normalization";
interface CodexMapperOptions {
cwd?: string | null;
@@ -27,7 +27,7 @@ import type {
ToolCallDetail,
ToolCallTimelineItem,
} from "../agent-sdk-types.js";
import { getAgentProviderDefinition } from "../provider-manifest.js";
import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest";
export const MOCK_LOAD_TEST_PROVIDER_ID = "mock";
export const MOCK_LOAD_TEST_DEFAULT_MODEL_ID = "five-minute-stream";
@@ -53,7 +53,7 @@ import {
} from "../provider-launch-config.js";
import { withTimeout } from "../../../utils/promise-timeout.js";
import { execCommand } from "../../../utils/spawn.js";
import { buildToolCallDisplayModel } from "../../../shared/tool-call-display.js";
import { buildToolCallDisplayModel } from "@getpaseo/protocol/tool-call-display";
import { mapOpencodeToolCall } from "./opencode/tool-call-mapper.js";
import { OpenCodeServerManager } from "./opencode/server-manager.js";
import {
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { getPaseoToolLeafName, isPaseoToolName } from "./tool-name-normalization.js";
import { getPaseoToolLeafName, isPaseoToolName } from "@getpaseo/protocol/tool-name-normalization";
describe("isPaseoToolName", () => {
it("detects Claude Code format", () => {
@@ -1,94 +0,0 @@
const TOOL_TOKEN_REGEX = /[a-z0-9]+/g;
const STANDARD_NAMESPACE_SEPARATOR_REGEX = /[.:/]/;
export function normalizeToolName(name: string): string {
return name.trim().toLowerCase();
}
export function tokenizeToolName(name: string): string[] {
const normalized = normalizeToolName(name);
return normalized.match(TOOL_TOKEN_REGEX) ?? [];
}
export function getToolLeafName(name: string): string | null {
const tokens = tokenizeToolName(name);
return tokens.length > 0 ? tokens[tokens.length - 1] : null;
}
export function isSpeakToolName(name: string): boolean {
return getToolLeafName(name) === "speak";
}
export function isLikelyNamespacedToolName(name: string): boolean {
const normalized = normalizeToolName(name);
if (STANDARD_NAMESPACE_SEPARATOR_REGEX.test(normalized)) {
return true;
}
if (!normalized.includes("__")) {
return false;
}
// Keep `__` handling strict to avoid false positives on arbitrary custom names.
const segments = normalized.split("__").filter((segment) => segment.length > 0);
if (segments.length >= 3) {
return true;
}
if (segments.length === 2 && segments[1].includes("_")) {
return true;
}
return false;
}
export function isPaseoToolName(name: string): boolean {
const normalized = normalizeToolName(name);
if (isSpeakToolName(normalized)) {
return false;
}
if (normalized.includes("__")) {
const segments = normalized.split("__").filter((s) => s.length > 0);
return (
segments.length >= 3 &&
segments[0] === "mcp" &&
(segments[1] === "paseo" || segments[1].startsWith("paseo_"))
);
}
if (normalized.includes(".")) {
const firstSegment = normalized.split(".")[0];
return firstSegment === "paseo" || firstSegment.startsWith("paseo_");
}
return false;
}
export function getPaseoToolLeafName(name: string): string | null {
const normalized = normalizeToolName(name);
if (normalized.includes("__")) {
const segments = normalized.split("__").filter((s) => s.length > 0);
if (
segments.length >= 3 &&
segments[0] === "mcp" &&
(segments[1] === "paseo" || segments[1].startsWith("paseo_"))
) {
return segments.slice(2).join("__");
}
return null;
}
if (normalized.includes(".")) {
const firstSegment = normalized.split(".")[0];
if (firstSegment === "paseo" || firstSegment.startsWith("paseo_")) {
return normalized.split(".").slice(1).join(".");
}
return null;
}
return null;
}
export function isLikelyExternalToolName(name: string): boolean {
const normalized = normalizeToolName(name);
if (!normalized) {
return false;
}
if (isSpeakToolName(normalized)) {
return true;
}
return isLikelyNamespacedToolName(normalized);
}
@@ -1,116 +0,0 @@
import { z } from "zod";
import { ChatMessageSchema, ChatRoomDetailSchema } from "./chat-types.js";
export const ChatCreateRequestSchema = z.object({
type: z.literal("chat/create"),
requestId: z.string(),
name: z.string(),
purpose: z.string().optional(),
});
export const ChatListRequestSchema = z.object({
type: z.literal("chat/list"),
requestId: z.string(),
});
export const ChatInspectRequestSchema = z.object({
type: z.literal("chat/inspect"),
requestId: z.string(),
room: z.string(),
});
export const ChatDeleteRequestSchema = z.object({
type: z.literal("chat/delete"),
requestId: z.string(),
room: z.string(),
});
export const ChatPostRequestSchema = z.object({
type: z.literal("chat/post"),
requestId: z.string(),
room: z.string(),
body: z.string(),
authorAgentId: z.string().optional(),
replyToMessageId: z.string().optional(),
});
export const ChatReadRequestSchema = z.object({
type: z.literal("chat/read"),
requestId: z.string(),
room: z.string(),
limit: z.number().int().nonnegative().optional(),
since: z.string().optional(),
authorAgentId: z.string().optional(),
});
export const ChatWaitRequestSchema = z.object({
type: z.literal("chat/wait"),
requestId: z.string(),
room: z.string(),
afterMessageId: z.string().optional(),
timeoutMs: z.number().int().nonnegative().optional(),
});
export const ChatCreateResponseSchema = z.object({
type: z.literal("chat/create/response"),
payload: z.object({
requestId: z.string(),
room: ChatRoomDetailSchema.nullable(),
error: z.string().nullable(),
}),
});
export const ChatListResponseSchema = z.object({
type: z.literal("chat/list/response"),
payload: z.object({
requestId: z.string(),
rooms: z.array(ChatRoomDetailSchema),
error: z.string().nullable(),
}),
});
export const ChatInspectResponseSchema = z.object({
type: z.literal("chat/inspect/response"),
payload: z.object({
requestId: z.string(),
room: ChatRoomDetailSchema.nullable(),
error: z.string().nullable(),
}),
});
export const ChatDeleteResponseSchema = z.object({
type: z.literal("chat/delete/response"),
payload: z.object({
requestId: z.string(),
room: ChatRoomDetailSchema.nullable(),
error: z.string().nullable(),
}),
});
export const ChatPostResponseSchema = z.object({
type: z.literal("chat/post/response"),
payload: z.object({
requestId: z.string(),
message: ChatMessageSchema.nullable(),
error: z.string().nullable(),
}),
});
export const ChatReadResponseSchema = z.object({
type: z.literal("chat/read/response"),
payload: z.object({
requestId: z.string(),
messages: z.array(ChatMessageSchema),
error: z.string().nullable(),
}),
});
export const ChatWaitResponseSchema = z.object({
type: z.literal("chat/wait/response"),
payload: z.object({
requestId: z.string(),
messages: z.array(ChatMessageSchema),
timedOut: z.boolean(),
error: z.string().nullable(),
}),
});
@@ -10,7 +10,7 @@ import {
type ChatMessage,
type ChatRoom,
type ChatRoomDetail,
} from "./chat-types.js";
} from "@getpaseo/protocol/chat/types";
const ChatStorePayloadSchema = z.object({
rooms: z.array(ChatRoomSchema),
@@ -1,30 +0,0 @@
import { z } from "zod";
export const ChatRoomSchema = z.object({
id: z.string(),
name: z.string(),
purpose: z.string().nullable(),
createdAt: z.string(),
updatedAt: z.string(),
});
export type ChatRoom = z.infer<typeof ChatRoomSchema>;
export const ChatMessageSchema = z.object({
id: z.string(),
roomId: z.string(),
authorAgentId: z.string(),
body: z.string(),
replyToMessageId: z.string().nullable(),
mentionAgentIds: z.array(z.string()),
createdAt: z.string(),
});
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
export const ChatRoomDetailSchema = ChatRoomSchema.extend({
messageCount: z.number().int().nonnegative(),
lastMessageAt: z.string().nullable(),
});
export type ChatRoomDetail = z.infer<typeof ChatRoomDetailSchema>;
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { CheckoutPrStatusSchema } from "../../shared/messages.js";
import { CheckoutPrStatusSchema } from "@getpaseo/protocol/messages";
import { normalizeCheckoutPrStatusPayload } from "./status-projection.js";
describe("checkout status projection", () => {
@@ -2,7 +2,7 @@ import type {
CheckoutPrStatusResponse,
CheckoutStatusResponse,
SessionOutboundMessage,
} from "../../shared/messages.js";
} from "@getpaseo/protocol/messages";
import type { WorkspaceGitRuntimeSnapshot } from "../workspace-git-service.js";
type CheckoutPrStatusPayload = Extract<
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from "vitest";
import { createTestPaseoDaemon, type TestPaseoDaemon } from "./test-utils/paseo-daemon.js";
import { DaemonClient } from "./test-utils/daemon-client.js";
import type { AgentStreamEventPayload } from "../shared/messages.js";
import type { AgentStreamEventPayload } from "@getpaseo/protocol/messages";
import type { AgentSnapshotPayload } from "./messages.js";
import type { PushNotificationSender, PushPayload } from "./push/notifications.js";
import { PRESENCE_THRESHOLD_MS } from "./agent-attention-policy.js";
+1 -1
View File
@@ -15,7 +15,7 @@ import type {
ProviderOverride,
} from "./agent/provider-launch-config.js";
import { ProviderOverrideSchema } from "./agent/provider-launch-config.js";
import { AgentProviderSchema } from "./agent/provider-manifest.js";
import { AgentProviderSchema } from "@getpaseo/protocol/provider-manifest";
import { hashDaemonPassword } from "./auth.js";
import { resolveSpeechConfig } from "./speech/speech-config-resolver.js";
import { mergeHostnames, parseHostnamesEnv, type HostnamesConfig } from "./hostnames.js";
@@ -1,6 +1,6 @@
import os from "node:os";
import { ConnectionOfferV2Schema, type ConnectionOffer } from "../shared/connection-offer.js";
import { ConnectionOfferV2Schema, type ConnectionOffer } from "@getpaseo/protocol/connection-offer";
interface BuildOfferEndpointsArgs {
listenHost: string;
@@ -4,12 +4,15 @@ import {
type PersistedConfig,
} from "./persisted-config.js";
import { ProviderOverrideSchema } from "./agent/provider-launch-config.js";
import { MutableDaemonConfigSchema, MutableDaemonConfigPatchSchema } from "../shared/messages.js";
import {
MutableDaemonConfigSchema,
MutableDaemonConfigPatchSchema,
} from "@getpaseo/protocol/messages";
export type { MutableDaemonConfig, MutableDaemonConfigPatch } from "../shared/messages.js";
export type { MutableDaemonConfig, MutableDaemonConfigPatch } from "@getpaseo/protocol/messages";
type MutableDaemonConfig = import("../shared/messages.js").MutableDaemonConfig;
type MutableDaemonConfigPatch = import("../shared/messages.js").MutableDaemonConfigPatch;
type MutableDaemonConfig = import("@getpaseo/protocol/messages").MutableDaemonConfig;
type MutableDaemonConfigPatch = import("@getpaseo/protocol/messages").MutableDaemonConfigPatch;
type ProviderOverride = import("./agent/provider-launch-config.js").ProviderOverride;
interface LoggerLike {
@@ -11,7 +11,7 @@ import {
type DaemonTestContext,
} from "../test-utils/index.js";
import { createWorktree as createWorktreePrimitive } from "../../utils/worktree.js";
import type { PullRequestMergeable } from "../../shared/messages.js";
import type { PullRequestMergeable } from "@getpaseo/protocol/messages";
const GhPrViewSchema = z.object({
state: z.string(),
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
import path from "node:path";
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
import type { PersistenceHandle } from "../../shared/messages.js";
import type { PersistenceHandle } from "@getpaseo/protocol/messages";
function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "daemon-restart-resume-"));
@@ -6,7 +6,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js";
import type { AgentTimelineItem } from "../agent/agent-sdk-types.js";
import type { AgentLifecycleStatus } from "../../shared/agent-lifecycle.js";
import type { AgentLifecycleStatus } from "@getpaseo/protocol/agent-lifecycle";
import { DaemonClient } from "../test-utils/daemon-client.js";
import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { getFullAccessConfig } from "./agent-configs.js";
@@ -18,9 +18,9 @@ import {
generateKeyPair,
importPublicKey,
} from "@getpaseo/relay";
import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js";
import { ConnectionOfferSchema } from "../../shared/connection-offer.js";
import { WSOutboundMessageSchema } from "../../shared/messages.js";
import { buildRelayWebSocketUrl } from "@getpaseo/protocol/daemon-endpoints";
import { ConnectionOfferSchema } from "@getpaseo/protocol/connection-offer";
import { WSOutboundMessageSchema } from "@getpaseo/protocol/messages";
const nodeMajor = Number((process.versions.node ?? "0").split(".")[0] ?? "0");
const shouldRunRelayE2e = process.env.FORCE_RELAY_E2E === "1" || nodeMajor < 25;
@@ -3,19 +3,19 @@ import { mkdtempSync, rmSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import { WebSocket } from "ws";
import { DaemonClient } from "../../client/daemon-client.js";
import { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import {
WSOutboundMessageSchema,
type TerminalState,
type WSOutboundMessage,
} from "../../shared/messages.js";
} from "@getpaseo/protocol/messages";
import {
decodeTerminalSnapshotPayload,
decodeTerminalStreamFrame,
encodeTerminalStreamFrame,
TerminalStreamOpcode,
type TerminalStreamFrame,
} from "../../shared/terminal-stream-protocol.js";
} from "@getpaseo/protocol/terminal-stream-protocol";
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
type RawSessionEnvelope = Extract<WSOutboundMessage, { type: "session" }>;
@@ -5,7 +5,7 @@ import path from "node:path";
import { createDaemonTestContext, type DaemonTestContext } from "../test-utils/index.js";
import { createMessageCollector, type MessageCollector } from "../test-utils/message-collector.js";
import type { PersistenceHandle } from "../../shared/messages.js";
import type { PersistenceHandle } from "@getpaseo/protocol/messages";
function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "two-cycle-resume-"));
+1 -1
View File
@@ -5,7 +5,7 @@ import type {
EditorTargetDescriptorPayload,
EditorTargetId,
KnownEditorTargetId,
} from "../shared/messages.js";
} from "@getpaseo/protocol/messages";
import { createExternalProcessEnv } from "./paseo-env.js";
import { findExecutable } from "../utils/executable.js";
import { spawnProcess } from "../utils/spawn.js";
@@ -0,0 +1,19 @@
import { expect, test } from "vitest";
test("keeps daemon-client APIs out of the server public entry", async () => {
const serverExports = await import("./exports.js");
expect(serverExports.createPaseoDaemon).toBeTypeOf("function");
expect(serverExports.resolvePaseoHome).toBeTypeOf("function");
for (const name of [
"DaemonClient",
"DaemonClientConfig",
"ConnectionState",
"DaemonEvent",
"WebSocketFactory",
"WebSocketLike",
]) {
expect(serverExports).not.toHaveProperty(name);
}
});
+7 -14
View File
@@ -16,28 +16,21 @@ export {
decodeOfferFragmentPayload,
parseConnectionOfferFromUrl,
type ConnectionOffer,
} from "../shared/connection-offer.js";
export { buildRelayWebSocketUrl } from "../shared/daemon-endpoints.js";
export {
DaemonClient,
type DaemonClientConfig,
type ConnectionState,
type DaemonEvent,
} from "../client/daemon-client.js";
export type { WebSocketFactory, WebSocketLike } from "../client/daemon-client-transport-types.js";
} from "@getpaseo/protocol/connection-offer";
export { buildRelayWebSocketUrl } from "@getpaseo/protocol/daemon-endpoints";
export {
buildDaemonWebSocketUrl,
deriveLabelFromEndpoint,
normalizeHostPort,
parseConnectionUri,
shouldUseTlsForDefaultHostedRelay,
} from "../shared/daemon-endpoints.js";
export { PARENT_AGENT_ID_LABEL } from "../shared/agent-labels.js";
} from "@getpaseo/protocol/daemon-endpoints";
export { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
export {
DirectTcpHostConnectionSchema,
type DirectTcpHostConnection,
type NormalizedDirectTcpHostConnection,
} from "../shared/host-connection-schema.js";
} from "@getpaseo/protocol/host-connection-schema";
export {
ensureLocalSpeechModels,
listLocalSpeechModels,
@@ -68,7 +61,7 @@ export {
AGENT_PROVIDER_DEFINITIONS,
BUILTIN_PROVIDER_IDS,
type AgentProviderDefinition,
} from "./agent/provider-manifest.js";
} from "@getpaseo/protocol/provider-manifest";
// Agent SDK types for CLI commands
export type {
@@ -102,4 +95,4 @@ export type {
AgentSnapshotPayload,
AgentStreamEventPayload,
AgentStreamMessage,
} from "../shared/messages.js";
} from "@getpaseo/protocol/messages";
+1 -1
View File
@@ -14,7 +14,7 @@ import type {
AgentProvider,
} from "./agent/agent-sdk-types.js";
import { execCommand, platformShell } from "../utils/spawn.js";
import { getUnattendedModeId } from "./agent/provider-manifest.js";
import { getUnattendedModeId } from "@getpaseo/protocol/provider-manifest";
const LOOP_ID_LENGTH = 8;
const DEFAULT_LOOP_PROVIDER: AgentProvider = "claude";
@@ -1,195 +0,0 @@
import { z } from "zod";
import { AgentProviderSchema } from "../agent/provider-manifest.js";
export const LoopLogEntrySchema = z.object({
seq: z.number().int().positive(),
timestamp: z.string(),
iteration: z.number().int().positive().nullable(),
source: z.enum(["loop", "worker", "verifier", "verify-check"]),
level: z.enum(["info", "error"]),
text: z.string(),
});
export const LoopVerifyCheckResultSchema = z.object({
command: z.string(),
exitCode: z.number().int(),
passed: z.boolean(),
stdout: z.string(),
stderr: z.string(),
startedAt: z.string(),
completedAt: z.string(),
});
export const LoopVerifyPromptResultSchema = z.object({
passed: z.boolean(),
reason: z.string(),
verifierAgentId: z.string().nullable(),
startedAt: z.string(),
completedAt: z.string(),
});
export const LoopIterationRecordSchema = z.object({
index: z.number().int().positive(),
workerAgentId: z.string().nullable(),
workerStartedAt: z.string(),
workerCompletedAt: z.string().nullable(),
verifierAgentId: z.string().nullable(),
status: z.enum(["running", "succeeded", "failed", "stopped"]),
workerOutcome: z.enum(["completed", "failed", "canceled"]).nullable(),
failureReason: z.string().nullable(),
verifyChecks: z.array(LoopVerifyCheckResultSchema),
verifyPrompt: LoopVerifyPromptResultSchema.nullable(),
});
export const LoopRecordSchema = z.object({
id: z.string(),
name: z.string().nullable(),
prompt: z.string(),
cwd: z.string(),
provider: AgentProviderSchema,
model: z.string().nullable(),
modeId: z.string().nullable().default(null),
workerProvider: AgentProviderSchema.nullable(),
workerModel: z.string().nullable(),
verifierProvider: AgentProviderSchema.nullable(),
verifierModel: z.string().nullable(),
verifierModeId: z.string().nullable().default(null),
verifyPrompt: z.string().nullable(),
verifyChecks: z.array(z.string()),
archive: z.boolean(),
sleepMs: z.number().int().nonnegative(),
maxIterations: z.number().int().positive().nullable(),
maxTimeMs: z.number().int().positive().nullable(),
status: z.enum(["running", "succeeded", "failed", "stopped"]),
createdAt: z.string(),
updatedAt: z.string(),
startedAt: z.string(),
completedAt: z.string().nullable(),
stopRequestedAt: z.string().nullable(),
iterations: z.array(LoopIterationRecordSchema),
logs: z.array(LoopLogEntrySchema),
nextLogSeq: z.number().int().positive(),
activeIteration: z.number().int().positive().nullable(),
activeWorkerAgentId: z.string().nullable(),
activeVerifierAgentId: z.string().nullable(),
});
export const LoopListItemSchema = z.object({
id: z.string(),
name: z.string().nullable(),
status: z.enum(["running", "succeeded", "failed", "stopped"]),
cwd: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
activeIteration: z.number().int().positive().nullable(),
});
export const LoopRunRequestSchema = z.object({
type: z.literal("loop/run"),
requestId: z.string(),
prompt: z.string().trim().min(1),
cwd: z.string(),
provider: AgentProviderSchema.optional(),
model: z.string().trim().min(1).optional(),
modeId: z.string().trim().min(1).optional(),
workerProvider: AgentProviderSchema.optional(),
workerModel: z.string().trim().min(1).optional(),
verifierProvider: AgentProviderSchema.optional(),
verifierModel: z.string().trim().min(1).optional(),
verifierModeId: z.string().trim().min(1).optional(),
verifyPrompt: z.string().trim().min(1).optional(),
verifyChecks: z.array(z.string().trim().min(1)).optional(),
archive: z.boolean().optional(),
name: z.string().trim().min(1).optional(),
sleepMs: z.number().int().nonnegative().optional(),
maxIterations: z.number().int().positive().optional(),
maxTimeMs: z.number().int().positive().optional(),
});
export const LoopListRequestSchema = z.object({
type: z.literal("loop/list"),
requestId: z.string(),
});
export const LoopInspectRequestSchema = z.object({
type: z.literal("loop/inspect"),
requestId: z.string(),
id: z.string().trim().min(1),
});
export const LoopLogsRequestSchema = z.object({
type: z.literal("loop/logs"),
requestId: z.string(),
id: z.string().trim().min(1),
afterSeq: z.number().int().nonnegative().optional(),
});
export const LoopStopRequestSchema = z.object({
type: z.literal("loop/stop"),
requestId: z.string(),
id: z.string().trim().min(1),
});
export const LoopRunResponseSchema = z.object({
type: z.literal("loop/run/response"),
payload: z.object({
requestId: z.string(),
loop: LoopRecordSchema.nullable(),
error: z.string().nullable(),
}),
});
export const LoopListResponseSchema = z.object({
type: z.literal("loop/list/response"),
payload: z.object({
requestId: z.string(),
loops: z.array(LoopListItemSchema),
error: z.string().nullable(),
}),
});
export const LoopInspectResponseSchema = z.object({
type: z.literal("loop/inspect/response"),
payload: z.object({
requestId: z.string(),
loop: LoopRecordSchema.nullable(),
error: z.string().nullable(),
}),
});
export const LoopLogsResponseSchema = z.object({
type: z.literal("loop/logs/response"),
payload: z.object({
requestId: z.string(),
loop: LoopRecordSchema.nullable(),
entries: z.array(LoopLogEntrySchema),
nextCursor: z.number().int().nonnegative(),
error: z.string().nullable(),
}),
});
export const LoopStopResponseSchema = z.object({
type: z.literal("loop/stop/response"),
payload: z.object({
requestId: z.string(),
loop: LoopRecordSchema.nullable(),
error: z.string().nullable(),
}),
});
export type LoopLogEntry = z.infer<typeof LoopLogEntrySchema>;
export type LoopVerifyCheckResult = z.infer<typeof LoopVerifyCheckResultSchema>;
export type LoopVerifyPromptResult = z.infer<typeof LoopVerifyPromptResultSchema>;
export type LoopIterationRecord = z.infer<typeof LoopIterationRecordSchema>;
export type LoopRecord = z.infer<typeof LoopRecordSchema>;
export type LoopListItem = z.infer<typeof LoopListItemSchema>;
export type LoopRunRequest = z.infer<typeof LoopRunRequestSchema>;
export type LoopListRequest = z.infer<typeof LoopListRequestSchema>;
export type LoopInspectRequest = z.infer<typeof LoopInspectRequestSchema>;
export type LoopLogsRequest = z.infer<typeof LoopLogsRequestSchema>;
export type LoopStopRequest = z.infer<typeof LoopStopRequestSchema>;
export type LoopRunResponse = z.infer<typeof LoopRunResponseSchema>;
export type LoopListResponse = z.infer<typeof LoopListResponseSchema>;
export type LoopInspectResponse = z.infer<typeof LoopInspectResponseSchema>;
export type LoopLogsResponse = z.infer<typeof LoopLogsResponseSchema>;
export type LoopStopResponse = z.infer<typeof LoopStopResponseSchema>;
+3 -3
View File
@@ -1,10 +1,10 @@
import type { ManagedAgent } from "./agent/agent-manager.js";
import { toAgentPayload } from "./agent/agent-projections.js";
import type { AgentStreamEvent } from "./agent/agent-sdk-types.js";
import type { AgentSnapshotPayload, AgentStreamEventPayload } from "../shared/messages.js";
import { AgentStreamEventPayloadSchema as AgentStreamEventPayloadRuntimeSchema } from "../shared/messages.js";
import type { AgentSnapshotPayload, AgentStreamEventPayload } from "@getpaseo/protocol/messages";
import { AgentStreamEventPayloadSchema as AgentStreamEventPayloadRuntimeSchema } from "@getpaseo/protocol/messages";
export * from "../shared/messages.js";
export * from "@getpaseo/protocol/messages";
function validateStreamEventPayload(payload: unknown): AgentStreamEventPayload | null {
const parsed = AgentStreamEventPayloadRuntimeSchema.safeParse(payload);
@@ -21,7 +21,7 @@ import {
} from "../utils/worktree-metadata.js";
import type { WorktreeCreationIntent } from "./resolve-worktree-creation-intent.js";
import { buildAgentBranchNameSeed } from "./agent/prompt-attachments.js";
import type { FirstAgentContext } from "../shared/messages.js";
import type { FirstAgentContext } from "@getpaseo/protocol/messages";
export interface CreatePaseoWorktreeInput extends CreateWorktreeCoreInput {
projectId?: string;
+1 -52
View File
@@ -5,7 +5,7 @@ import { z } from "zod";
import {
AgentProviderRuntimeSettingsMapSchema,
migrateProviderSettings,
ProviderOverrideSchema,
ProviderOverridesSchema,
} from "./agent/provider-launch-config.js";
import type { AgentProviderRuntimeSettingsMap } from "./agent/provider-launch-config.js";
import { ensurePrivateFile, writePrivateFileSync } from "./private-files.js";
@@ -132,57 +132,6 @@ const FeatureVoiceModeSchema = z
.strict();
const BUILTIN_PROVIDER_IDS = ["claude", "codex", "copilot", "opencode", "pi"] as const;
const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
const ProviderOverridesSchema = z
.record(z.string(), ProviderOverrideSchema)
.superRefine((providers, ctx) => {
const builtinProviderIdSet = new Set<string>(BUILTIN_PROVIDER_IDS);
const validExtendsValues = new Set<string>([...BUILTIN_PROVIDER_IDS, "acp"]);
for (const [providerId, provider] of Object.entries(providers)) {
if (!PROVIDER_ID_PATTERN.test(providerId)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [providerId],
message: `Provider ID "${providerId}" must match ${PROVIDER_ID_PATTERN}.`,
});
}
const isBuiltinProvider = builtinProviderIdSet.has(providerId);
if (!isBuiltinProvider && !provider.extends) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [providerId, "extends"],
message: `Custom provider "${providerId}" must declare extends.`,
});
}
if (!isBuiltinProvider && !provider.label) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [providerId, "label"],
message: `Custom provider "${providerId}" must declare label.`,
});
}
if (provider.extends && !validExtendsValues.has(provider.extends)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [providerId, "extends"],
message: `Provider "${providerId}" extends unknown provider "${provider.extends}".`,
});
}
if (provider.extends === "acp" && !provider.command) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [providerId, "command"],
message: `Provider "${providerId}" extending "acp" must declare command.`,
});
}
}
});
function isLegacyProviderEntry(value: unknown): boolean {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -8,7 +8,7 @@ import {
type Transport as RelayTransport,
type KeyPair,
} from "@getpaseo/relay/e2ee";
import { buildRelayWebSocketUrl } from "../shared/daemon-endpoints.js";
import { buildRelayWebSocketUrl } from "@getpaseo/protocol/daemon-endpoints";
import type { ExternalSocketMetadata } from "./websocket-server.js";
interface RelayTransportOptions {
@@ -583,5 +583,3 @@ function bufferFromWsData(data: unknown): Buffer | null {
}
return null;
}
// buildRelayWebSocketUrl + parseHostPort live in ../shared/daemon-endpoints.ts
+1 -1
View File
@@ -1,4 +1,4 @@
import type { ScheduleCadence } from "./types.js";
import type { ScheduleCadence } from "@getpaseo/protocol/schedule/types";
interface CronFieldMatcher {
matches(value: number): boolean;
@@ -1,176 +0,0 @@
import { z } from "zod";
import {
ScheduleCadenceSchema,
ScheduleRunSchema,
ScheduleSummarySchema,
StoredScheduleSchema,
ScheduleTargetSchema,
} from "./types.js";
const ScheduleCreateTargetSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("self"),
agentId: z.string().uuid(),
}),
z.object({
type: z.literal("agent"),
agentId: z.string().uuid(),
}),
z.object({
type: z.literal("new-agent"),
config: ScheduleTargetSchema.options[1].shape.config,
}),
]);
export const ScheduleCreateRequestSchema = z.object({
type: z.literal("schedule/create"),
requestId: z.string(),
prompt: z.string().min(1),
name: z.string().optional(),
cadence: ScheduleCadenceSchema,
target: ScheduleCreateTargetSchema,
maxRuns: z.number().int().positive().optional(),
expiresAt: z.string().optional(),
runOnCreate: z.boolean().optional(),
});
export const ScheduleListRequestSchema = z.object({
type: z.literal("schedule/list"),
requestId: z.string(),
});
export const ScheduleInspectRequestSchema = z.object({
type: z.literal("schedule/inspect"),
requestId: z.string(),
scheduleId: z.string(),
});
export const ScheduleLogsRequestSchema = z.object({
type: z.literal("schedule/logs"),
requestId: z.string(),
scheduleId: z.string(),
});
export const SchedulePauseRequestSchema = z.object({
type: z.literal("schedule/pause"),
requestId: z.string(),
scheduleId: z.string(),
});
export const ScheduleResumeRequestSchema = z.object({
type: z.literal("schedule/resume"),
requestId: z.string(),
scheduleId: z.string(),
});
export const ScheduleDeleteRequestSchema = z.object({
type: z.literal("schedule/delete"),
requestId: z.string(),
scheduleId: z.string(),
});
export const ScheduleRunOnceRequestSchema = z.object({
type: z.literal("schedule/run-once"),
requestId: z.string(),
scheduleId: z.string(),
});
const ScheduleUpdateNewAgentConfigSchema = z.object({
provider: z.string().trim().min(1).optional(),
model: z.string().trim().min(1).nullable().optional(),
modeId: z.string().trim().min(1).nullable().optional(),
cwd: z.string().trim().min(1).optional(),
});
export const ScheduleUpdateRequestSchema = z.object({
type: z.literal("schedule/update"),
requestId: z.string(),
scheduleId: z.string(),
name: z.string().nullable().optional(),
prompt: z.string().min(1).optional(),
cadence: ScheduleCadenceSchema.optional(),
newAgentConfig: ScheduleUpdateNewAgentConfigSchema.optional(),
maxRuns: z.number().int().positive().nullable().optional(),
expiresAt: z.string().nullable().optional(),
});
export const ScheduleCreateResponseSchema = z.object({
type: z.literal("schedule/create/response"),
payload: z.object({
requestId: z.string(),
schedule: ScheduleSummarySchema.nullable(),
error: z.string().nullable(),
}),
});
export const ScheduleListResponseSchema = z.object({
type: z.literal("schedule/list/response"),
payload: z.object({
requestId: z.string(),
schedules: z.array(ScheduleSummarySchema),
error: z.string().nullable(),
}),
});
export const ScheduleInspectResponseSchema = z.object({
type: z.literal("schedule/inspect/response"),
payload: z.object({
requestId: z.string(),
schedule: StoredScheduleSchema.nullable(),
error: z.string().nullable(),
}),
});
export const ScheduleLogsResponseSchema = z.object({
type: z.literal("schedule/logs/response"),
payload: z.object({
requestId: z.string(),
runs: z.array(ScheduleRunSchema),
error: z.string().nullable(),
}),
});
export const SchedulePauseResponseSchema = z.object({
type: z.literal("schedule/pause/response"),
payload: z.object({
requestId: z.string(),
schedule: ScheduleSummarySchema.nullable(),
error: z.string().nullable(),
}),
});
export const ScheduleResumeResponseSchema = z.object({
type: z.literal("schedule/resume/response"),
payload: z.object({
requestId: z.string(),
schedule: ScheduleSummarySchema.nullable(),
error: z.string().nullable(),
}),
});
export const ScheduleDeleteResponseSchema = z.object({
type: z.literal("schedule/delete/response"),
payload: z.object({
requestId: z.string(),
scheduleId: z.string(),
error: z.string().nullable(),
}),
});
export const ScheduleRunOnceResponseSchema = z.object({
type: z.literal("schedule/run-once/response"),
payload: z.object({
requestId: z.string(),
schedule: StoredScheduleSchema.nullable(),
error: z.string().nullable(),
}),
});
export const ScheduleUpdateResponseSchema = z.object({
type: z.literal("schedule/update/response"),
payload: z.object({
requestId: z.string(),
schedule: StoredScheduleSchema.nullable(),
error: z.string().nullable(),
}),
});
@@ -23,7 +23,7 @@ import type {
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
import { createTestLogger } from "../../test-utils/test-logger.js";
import { ScheduleService } from "./service.js";
import type { StoredSchedule, ScheduleExecutionResult } from "./types.js";
import type { ScheduleExecutionResult, StoredSchedule } from "@getpaseo/protocol/schedule/types";
interface ScheduleServiceInternals {
executeSchedule(schedule: StoredSchedule): Promise<ScheduleExecutionResult>;
@@ -7,7 +7,7 @@ import type { AgentSessionConfig } from "../agent/agent-sdk-types.js";
import { curateAgentActivity } from "../agent/activity-curator.js";
import { ensureAgentLoaded } from "../agent/agent-loading.js";
import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js";
import { getUnattendedModeId } from "../agent/provider-manifest.js";
import { getUnattendedModeId } from "@getpaseo/protocol/provider-manifest";
import { ScheduleStore } from "./store.js";
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
import type {
@@ -18,7 +18,7 @@ import type {
StoredSchedule,
UpdateScheduleInput,
UpdateScheduleNewAgentConfig,
} from "./types.js";
} from "@getpaseo/protocol/schedule/types";
const SCHEDULE_TICK_INTERVAL_MS = 1000;
+1 -1
View File
@@ -1,7 +1,7 @@
import { randomBytes } from "node:crypto";
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { StoredScheduleSchema, type StoredSchedule } from "./types.js";
import { StoredScheduleSchema, type StoredSchedule } from "@getpaseo/protocol/schedule/types";
function generateScheduleId(): string {
return randomBytes(4).toString("hex");
@@ -1,117 +0,0 @@
import { z } from "zod";
import { AgentProviderSchema } from "../agent/provider-manifest.js";
export const ScheduleStatusSchema = z.enum(["active", "paused", "completed"]);
export type ScheduleStatus = z.infer<typeof ScheduleStatusSchema>;
export const ScheduleCadenceSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("every"),
everyMs: z.number().int().positive(),
}),
z.object({
type: z.literal("cron"),
expression: z.string().trim().min(1),
}),
]);
export type ScheduleCadence = z.infer<typeof ScheduleCadenceSchema>;
export const ScheduleTargetSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("agent"),
agentId: z.string().uuid(),
}),
z.object({
type: z.literal("new-agent"),
config: z.object({
provider: AgentProviderSchema,
cwd: z.string().trim().min(1),
modeId: z.string().trim().min(1).optional(),
model: z.string().trim().min(1).optional(),
thinkingOptionId: z.string().trim().min(1).optional(),
title: z.string().trim().min(1).nullable().optional(),
approvalPolicy: z.string().trim().min(1).optional(),
sandboxMode: z.string().trim().min(1).optional(),
networkAccess: z.boolean().optional(),
webSearch: z.boolean().optional(),
featureValues: z.record(z.unknown()).optional(),
extra: z
.object({
codex: z.record(z.unknown()).optional(),
claude: z.record(z.unknown()).optional(),
})
.partial()
.optional(),
systemPrompt: z.string().optional(),
mcpServers: z.record(z.unknown()).optional(),
}),
}),
]);
export type ScheduleTarget = z.infer<typeof ScheduleTargetSchema>;
export const ScheduleRunSchema = z.object({
id: z.string(),
scheduledFor: z.string(),
startedAt: z.string(),
endedAt: z.string().nullable(),
status: z.enum(["running", "succeeded", "failed"]),
agentId: z.string().uuid().nullable(),
output: z.string().nullable(),
error: z.string().nullable(),
});
export type ScheduleRun = z.infer<typeof ScheduleRunSchema>;
export const StoredScheduleSchema = z.object({
id: z.string(),
name: z.string().nullable(),
prompt: z.string().min(1),
cadence: ScheduleCadenceSchema,
target: ScheduleTargetSchema,
status: ScheduleStatusSchema,
createdAt: z.string(),
updatedAt: z.string(),
nextRunAt: z.string().nullable(),
lastRunAt: z.string().nullable(),
pausedAt: z.string().nullable(),
expiresAt: z.string().nullable(),
maxRuns: z.number().int().positive().nullable(),
runs: z.array(ScheduleRunSchema),
});
export type StoredSchedule = z.infer<typeof StoredScheduleSchema>;
export const ScheduleSummarySchema = StoredScheduleSchema.omit({
runs: true,
});
export type ScheduleSummary = z.infer<typeof ScheduleSummarySchema>;
export interface CreateScheduleInput {
name?: string | null;
prompt: string;
cadence: ScheduleCadence;
target: ScheduleTarget;
maxRuns?: number | null;
expiresAt?: string | null;
runOnCreate?: boolean | null;
}
export interface UpdateScheduleNewAgentConfig {
provider?: string;
model?: string | null;
modeId?: string | null;
cwd?: string;
}
export interface UpdateScheduleInput {
id: string;
name?: string | null;
prompt?: string;
cadence?: ScheduleCadence;
newAgentConfig?: UpdateScheduleNewAgentConfig;
maxRuns?: number | null;
expiresAt?: string | null;
}
export interface ScheduleExecutionResult {
agentId: string | null;
output: string | null;
}
@@ -8,11 +8,11 @@ import {
buildWorkspaceScriptPayloads,
createScriptStatusEmitter,
} from "./script-status-projection.js";
import { WorkspaceScriptPayloadSchema } from "../shared/messages.js";
import { WorkspaceScriptPayloadSchema } from "@getpaseo/protocol/messages";
import type { ScriptHealthState } from "./script-health-monitor.js";
import { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import { readPaseoConfig } from "../utils/worktree.js";
import type { PaseoConfig } from "../utils/paseo-config-schema.js";
import type { PaseoConfig } from "@getpaseo/protocol/paseo-config-schema";
import { createTestLogger } from "../test-utils/test-logger.js";
function createWorkspaceRepo(options?: {
@@ -3,8 +3,8 @@ import type {
ScriptStatusUpdateMessage,
SessionOutboundMessage,
WorkspaceScriptPayload,
} from "../shared/messages.js";
import type { PaseoConfig } from "../utils/paseo-config-schema.js";
} from "@getpaseo/protocol/messages";
import type { PaseoConfig } from "@getpaseo/protocol/paseo-config-schema";
import { buildScriptHostname } from "../utils/script-hostname.js";
import { getScriptConfigs, isServiceScript, readPaseoConfig } from "../utils/worktree.js";
import { deriveProjectSlug } from "./workspace-git-metadata.js";
+5 -2
View File
@@ -6,8 +6,11 @@ import { join, resolve as resolvePath } from "path";
import pino from "pino";
import { afterEach, describe, expect, test, vi } from "vitest";
import type { WorkspaceDescriptorPayload } from "../shared/messages.js";
import { decodeFileTransferFrame, FileTransferOpcode } from "../shared/binary-frames/index.js";
import type { WorkspaceDescriptorPayload } from "@getpaseo/protocol/messages";
import {
decodeFileTransferFrame,
FileTransferOpcode,
} from "@getpaseo/protocol/binary-frames/index";
import { Session } from "./session.js";
import { StructuredAgentFallbackError } from "./agent/agent-response-loop.js";
import type {
+4 -4
View File
@@ -8,7 +8,7 @@ import { basename, resolve, sep } from "path";
import { homedir } from "node:os";
import { z } from "zod";
import type { ToolSet } from "ai";
import { CLIENT_CAPS, type ClientCapability } from "../shared/client-capabilities.js";
import { CLIENT_CAPS, type ClientCapability } from "@getpaseo/protocol/client-capabilities";
import {
isLegacyEditorTargetId,
serializeAgentStreamEvent,
@@ -38,7 +38,7 @@ import {
encodeFileTransferFrame,
FileTransferOpcode,
type TerminalStreamFrame,
} from "../shared/binary-frames/index.js";
} from "@getpaseo/protocol/binary-frames/index";
import { CursorError } from "./pagination/cursor.js";
import { SortablePager, type SortSpec } from "./pagination/sortable-pager.js";
import { TTSManager } from "./agent/tts-manager.js";
@@ -86,8 +86,8 @@ import { spawnWorkspaceScript } from "./worktree-bootstrap.js";
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
import type { DaemonConfigStore } from "./daemon-config-store.js";
import { applyMutableProviderConfigToOverrides } from "./daemon-config-store.js";
import { getErrorMessage, getErrorMessageOr } from "../shared/error-utils.js";
import { getAgentStatusPriority } from "../shared/agent-state-bucket.js";
import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils";
import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket";
import type {
WorkspaceGitRuntimeSnapshot,
WorkspaceGitService,
@@ -9,7 +9,7 @@ import path from "node:path";
import { expect, test, vi } from "vitest";
import { Session, type SessionOptions } from "./session.js";
import type { SessionOutboundMessage } from "../shared/messages.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
import { createNoopWorkspaceGitService } from "./test-utils/workspace-git-service-stub.js";
import { asInternals, createStub } from "./test-utils/class-mocks.js";
import {
@@ -9,7 +9,7 @@ import type {
AgentSnapshotPayload,
EditorTargetDescriptorPayload,
SessionOutboundMessage,
} from "../shared/messages.js";
} from "@getpaseo/protocol/messages";
import { AgentManager } from "./agent/agent-manager.js";
import { AgentStorage } from "./agent/agent-storage.js";
import type {
@@ -7,7 +7,7 @@ import {
type DaemonEventHandler,
type SendMessageOptions,
type WebSocketLike,
} from "../../client/daemon-client.js";
} from "@getpaseo/client/internal/daemon-client";
export type DaemonClientConfig = Omit<
SharedDaemonClientConfig,
@@ -22,7 +22,7 @@ import type {
ListModelsOptions,
} from "../agent/agent-sdk-types.js";
import type { AgentPermissionRequest, AgentPermissionResponse } from "../agent/agent-sdk-types.js";
import { isLikelyExternalToolName } from "../agent/tool-name-normalization.js";
import { isLikelyExternalToolName } from "@getpaseo/protocol/tool-name-normalization";
const TEST_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
@@ -1,5 +1,5 @@
import type { DaemonClient } from "../../client/daemon-client.js";
import type { SessionOutboundMessage } from "../../shared/messages.js";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
export interface MessageCollector {
messages: SessionOutboundMessage[];
@@ -3,7 +3,7 @@ import { vi } from "vitest";
import type { ProviderSnapshotEntry } from "../agent/agent-sdk-types.js";
import { ProviderSnapshotManager } from "../agent/provider-snapshot-manager.js";
import type { SessionOptions } from "../session.js";
import type { SessionOutboundMessage } from "../../shared/messages.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
import { asInternals, createStub } from "./class-mocks.js";
// ---------------------------------------------------------------------------
@@ -7,7 +7,7 @@ import pino from "pino";
import { createDaemonTestContext, type DaemonTestContext } from "./test-utils/index.js";
import { getFullAccessConfig } from "./daemon-e2e/agent-configs.js";
import { OpenAITTS } from "./speech/providers/openai/tts.js";
import type { SessionOutboundMessage } from "../shared/messages.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
type SessionMessage<T extends SessionOutboundMessage["type"]> = Extract<
SessionOutboundMessage,
@@ -1,5 +1,5 @@
import type { AgentPermissionRequest } from "./agent/agent-sdk-types.js";
import { isSpeakToolName } from "./agent/tool-name-normalization.js";
import { isSpeakToolName } from "@getpaseo/protocol/tool-name-normalization";
/** Voice assistant policy: only allow the speak tool. */
export function isVoicePermissionAllowed(request: AgentPermissionRequest): boolean {
@@ -10,7 +10,7 @@ import { OpenAITTS } from "./speech/providers/openai/tts.js";
import { OpenAISTT } from "./speech/providers/openai/stt.js";
import { STTManager } from "./agent/stt-manager.js";
import { withTimeout } from "../utils/promise-timeout.js";
import type { SessionOutboundMessage } from "../shared/messages.js";
import type { SessionOutboundMessage } from "@getpaseo/protocol/messages";
type SessionMessage<T extends SessionOutboundMessage["type"]> = Extract<
SessionOutboundMessage,
@@ -15,8 +15,8 @@ import {
decodeTerminalStreamFrame,
encodeTerminalStreamFrame,
TerminalStreamOpcode,
} from "../shared/terminal-stream-protocol.js";
import { CLIENT_CAPS } from "../shared/client-capabilities.js";
} from "@getpaseo/protocol/terminal-stream-protocol";
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
type SocketListener = (...args: unknown[]) => void;
@@ -25,7 +25,7 @@ import {
type WSOutboundMessage,
wrapSessionMessage,
} from "./messages.js";
import { asUint8Array, decodeTerminalStreamFrame } from "../shared/binary-frames/index.js";
import { asUint8Array, decodeTerminalStreamFrame } from "@getpaseo/protocol/binary-frames/index";
import type { HostnamesConfig } from "./hostnames.js";
import { isHostnameAllowed } from "./hostnames.js";
import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js";
@@ -49,7 +49,7 @@ import { computeNotificationPlan, type ClientPresenceState } from "./agent-atten
import {
buildAgentAttentionNotificationPayload,
findLatestPermissionRequest,
} from "../shared/agent-attention-notification.js";
} from "@getpaseo/protocol/agent-attention-notification";
import { createGitHubService, type GitHubService } from "../services/github-service.js";
import {
extractWsBearerProtocol,
@@ -2,17 +2,17 @@ import pino from "pino";
import { z } from "zod";
import { describe, expect, test } from "vitest";
import { CLIENT_CAPS } from "./client-capabilities.js";
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
import {
AgentSnapshotPayloadSchema,
AgentTimelineItemPayloadSchema,
FetchAgentTimelineResponseMessageSchema,
SessionInboundMessageSchema,
type SessionOutboundMessage,
} from "./messages.js";
import { Session, type SessionOptions } from "../server/session.js";
import type { AgentTimelineRow } from "../server/agent/agent-manager.js";
import { handleCreatePaseoWorktreeRequest } from "../server/worktree-session.js";
} from "@getpaseo/protocol/messages";
import { Session, type SessionOptions } from "./session.js";
import type { AgentTimelineRow } from "./agent/agent-manager.js";
import { handleCreatePaseoWorktreeRequest } from "./worktree-session.js";
const LegacyTimelineEntryPayloadSchema = z.object({
provider: z.enum(["claude", "codex", "opencode"]),
@@ -10,7 +10,7 @@ import type {
import {
deriveAgentStateBucket,
getWorkspaceStateBucketPriority,
} from "../shared/agent-state-bucket.js";
} from "@getpaseo/protocol/agent-state-bucket";
import { SortablePager } from "./pagination/sortable-pager.js";
import type { PersistedProjectRecord, PersistedWorkspaceRecord } from "./workspace-registry.js";
import { normalizeWorkspaceId } from "./workspace-registry-model.js";
@@ -3,7 +3,7 @@ import { readFile, readdir } from "node:fs/promises";
import { join, resolve } from "node:path";
import pLimit from "p-limit";
import type pino from "pino";
import type { ProjectCheckoutLitePayload } from "../shared/messages.js";
import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages";
import type { CheckoutContext } from "../utils/checkout-git.js";
import {
type BranchCheckoutResolution,
@@ -1,6 +1,9 @@
import { resolve } from "node:path";
import type { ProjectCheckoutLitePayload, ProjectPlacementPayload } from "../shared/messages.js";
import type {
ProjectCheckoutLitePayload,
ProjectPlacementPayload,
} from "@getpaseo/protocol/messages";
import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js";
import type { PersistedWorkspaceRecord } from "./workspace-registry.js";
@@ -1,5 +1,5 @@
import { z } from "zod";
import type { FirstAgentContext } from "../shared/messages.js";
import type { FirstAgentContext } from "@getpaseo/protocol/messages";
import type { AgentManager } from "./agent/agent-manager.js";
import {
DEFAULT_STRUCTURED_GENERATION_PROVIDERS,
+1 -1
View File
@@ -13,7 +13,7 @@ import {
type ResolveWorktreeCreationIntentInput,
type WorktreeCreationIntent,
} from "./resolve-worktree-creation-intent.js";
import type { FirstAgentContext } from "../shared/messages.js";
import type { FirstAgentContext } from "@getpaseo/protocol/messages";
import type { WorkspaceGitService } from "./workspace-git-service.js";
export interface CreateWorktreeCoreInput {
@@ -15,7 +15,7 @@ import {
type GitHubCurrentPullRequestStatus,
type GitHubReadOptions,
} from "./github-service.js";
import { CheckoutPrStatusResponseSchema } from "../shared/messages.js";
import { CheckoutPrStatusResponseSchema } from "@getpaseo/protocol/messages";
const EXPECTED_GITHUB_FAST_POLL_MS = 20_000;
const EXPECTED_GITHUB_SLOW_POLL_MS = 120_000;
@@ -1,5 +1,5 @@
import { z } from "zod";
import type { GitHubSearchKind } from "../shared/messages.js";
import type { GitHubSearchKind } from "@getpaseo/protocol/messages";
import { findExecutable } from "../utils/executable.js";
import { resolveGitHubRemote } from "../utils/github-remote.js";
import { runGitCommand } from "../utils/run-git-command.js";
@@ -1,97 +0,0 @@
import { describe, expect, it } from "vitest";
import {
buildAgentAttentionNotificationPayload,
findLatestAssistantMessageFromTimeline,
findLatestPermissionRequest,
} from "./agent-attention-notification.js";
describe("buildAgentAttentionNotificationPayload", () => {
it("builds finished notifications from markdown assistant text", () => {
const payload = buildAgentAttentionNotificationPayload({
reason: "finished",
serverId: "srv-1",
agentId: "agent-1",
assistantMessage: "**Done**. Updated `README.md` and [link](https://example.com).",
});
expect(payload).toEqual({
title: "Agent finished",
body: "Done. Updated README.md and link.",
data: {
serverId: "srv-1",
agentId: "agent-1",
reason: "finished",
},
});
});
it("builds permission notifications from request details", () => {
const payload = buildAgentAttentionNotificationPayload({
reason: "permission",
serverId: "srv-2",
agentId: "agent-2",
permissionRequest: {
id: "perm-1",
provider: "claude",
name: "exec",
kind: "tool",
title: "**Approve command**",
description: "Run `git push`",
},
});
expect(payload).toEqual({
title: "Agent needs permission",
body: "Approve command - Run git push",
data: {
serverId: "srv-2",
agentId: "agent-2",
reason: "permission",
},
});
});
it("uses error-specific defaults when reason is error", () => {
const payload = buildAgentAttentionNotificationPayload({
reason: "error",
serverId: "srv-3",
agentId: "agent-3",
});
expect(payload).toEqual({
title: "Agent needs attention",
body: "Encountered an error.",
data: {
serverId: "srv-3",
agentId: "agent-3",
reason: "error",
},
});
});
});
describe("findLatestAssistantMessageFromTimeline", () => {
it("joins the latest contiguous assistant chunks", () => {
expect(
findLatestAssistantMessageFromTimeline([
{ type: "user_message", text: "start" },
{ type: "assistant_message", text: "Part " },
{ type: "assistant_message", text: "one" },
{ type: "reasoning", text: "thinking..." },
{ type: "assistant_message", text: "Done " },
{ type: "assistant_message", text: "now" },
]),
).toBe("Done now");
});
});
describe("findLatestPermissionRequest", () => {
it("returns the most recently inserted request", () => {
const pending = new Map([
["first", { id: "first", provider: "claude", name: "a", kind: "tool" } as const],
["second", { id: "second", provider: "claude", name: "b", kind: "tool" } as const],
]);
expect(findLatestPermissionRequest(pending)?.id).toBe("second");
});
});
@@ -1,210 +0,0 @@
const NOTIFICATION_PREVIEW_LIMIT = 220;
export type AgentAttentionReason = "finished" | "error" | "permission";
export interface AgentAttentionNotificationData {
[key: string]: unknown;
serverId: string;
agentId: string;
reason: AgentAttentionReason;
}
export interface AgentAttentionNotificationPayload {
title: string;
body: string;
data: AgentAttentionNotificationData;
}
interface BuildAgentAttentionNotificationPayloadInput {
reason: AgentAttentionReason;
serverId: string;
agentId: string;
assistantMessage?: string | null;
permissionRequest?: NotificationPermissionRequest | null;
}
export interface NotificationPermissionRequest {
id: string;
provider: string;
name: string;
kind: "tool" | "plan" | "question" | "mode" | "other";
title?: string;
description?: string;
input?: Record<string, unknown>;
metadata?: Record<string, unknown>;
}
export type AssistantTimelineItem =
| { type: "assistant_message"; text: string }
| { type: string; text?: string };
const normalizeNotificationText = (text: string): string => text.replace(/\s+/g, " ").trim();
const truncateNotificationText = (text: string, limit: number): string => {
if (text.length <= limit) {
return text;
}
const trimmed = text.slice(0, Math.max(0, limit - 3)).trimEnd();
return trimmed.length > 0 ? `${trimmed}...` : text.slice(0, limit);
};
const stripMarkdownToText = (markdown: string): string => {
let text = markdown.replace(/\r\n/g, "\n");
// Strip fenced code markers but keep the code content itself.
text = text.replace(/^\s*```[^\n]*$/gm, "");
text = text.replace(/^\s*~~~[^\n]*$/gm, "");
// Markdown links/images.
text = text.replace(/!\[([^\]]*)\]\((?:[^()\\]|\\.)*\)/g, "$1");
text = text.replace(/\[([^\]]+)\]\((?:[^()\\]|\\.)*\)/g, "$1");
// Structural prefixes.
text = text.replace(/^\s{0,3}#{1,6}\s+/gm, "");
text = text.replace(/^\s{0,3}>+\s?/gm, "");
text = text.replace(/^\s{0,3}(?:[*+-]|\d+\.)\s+/gm, "");
text = text.replace(/^\s{0,3}(?:[-*_]\s*){3,}$/gm, "");
// Inline markdown markers.
text = text.replace(/`([^`]+)`/g, "$1");
text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
text = text.replace(/__([^_]+)__/g, "$1");
text = text.replace(/\*([^*\n]+)\*/g, "$1");
text = text.replace(/_([^_\n]+)_/g, "$1");
text = text.replace(/~~([^~]+)~~/g, "$1");
// Angle-bracketed URL autolinks.
text = text.replace(/<([^>\n]+)>/g, "$1");
return text;
};
const buildNotificationPreview = (text: string | null | undefined): string | null => {
if (!text) {
return null;
}
const normalized = normalizeNotificationText(stripMarkdownToText(text));
if (!normalized) {
return null;
}
return truncateNotificationText(normalized, NOTIFICATION_PREVIEW_LIMIT);
};
const safeStringify = (value: unknown): string | null => {
try {
return JSON.stringify(value);
} catch {
return null;
}
};
const buildPermissionDetails = (
request: NotificationPermissionRequest | null | undefined,
): string | null => {
if (!request) {
return null;
}
const title = request.title?.trim();
const description = request.description?.trim();
const details: string[] = [];
if (title) {
details.push(title);
}
if (description && description !== title) {
details.push(description);
}
if (details.length > 0) {
return details.join(" - ");
}
const inputPreview = request.input ? safeStringify(request.input) : null;
if (inputPreview) {
return inputPreview;
}
const metadataPreview = request.metadata ? safeStringify(request.metadata) : null;
if (metadataPreview) {
return metadataPreview;
}
return request.name?.trim() || request.kind;
};
export function findLatestAssistantMessageFromTimeline(
timeline: readonly AssistantTimelineItem[],
): string | null {
// Providers may stream assistant content in consecutive chunks.
const chunks: string[] = [];
for (let i = timeline.length - 1; i >= 0; i -= 1) {
const item = timeline[i];
if (item.type !== "assistant_message" || typeof item.text !== "string") {
if (chunks.length > 0) {
break;
}
continue;
}
chunks.push(item.text);
}
if (chunks.length === 0) {
return null;
}
return chunks.toReversed().join("");
}
export function findLatestPermissionRequest(
pendingPermissions: ReadonlyMap<string, NotificationPermissionRequest>,
): NotificationPermissionRequest | null {
let latest: NotificationPermissionRequest | null = null;
for (const request of pendingPermissions.values()) {
latest = request;
}
return latest;
}
function resolveAgentAttentionTitle(reason: AgentAttentionReason): string {
if (reason === "permission") return "Agent needs permission";
if (reason === "error") return "Agent needs attention";
return "Agent finished";
}
function resolveAgentAttentionPreview(
input: BuildAgentAttentionNotificationPayloadInput,
): string | null {
if (input.reason === "finished") {
return buildNotificationPreview(input.assistantMessage);
}
if (input.reason === "permission") {
return buildNotificationPreview(buildPermissionDetails(input.permissionRequest));
}
return null;
}
function resolveAgentAttentionFallbackBody(reason: AgentAttentionReason): string {
if (reason === "permission") return "Permission requested.";
if (reason === "error") return "Encountered an error.";
return "Finished working.";
}
export function buildAgentAttentionNotificationPayload(
input: BuildAgentAttentionNotificationPayloadInput,
): AgentAttentionNotificationPayload {
const title = resolveAgentAttentionTitle(input.reason);
const preview = resolveAgentAttentionPreview(input);
const body = preview ?? resolveAgentAttentionFallbackBody(input.reason);
return {
title,
body,
data: {
serverId: input.serverId,
agentId: input.agentId,
reason: input.reason,
},
};
}
@@ -1,179 +0,0 @@
import { describe, expect, it } from "vitest";
import {
AgentFeatureSchema,
AgentSnapshotPayloadSchema,
SetAgentFeatureRequestMessageSchema,
SetAgentFeatureResponseMessageSchema,
} from "./messages.js";
describe("agent feature schemas", () => {
it("parses valid toggle features", () => {
const parsed = AgentFeatureSchema.parse({
type: "toggle",
id: "fast_mode",
label: "Fast mode",
description: "Uses lower latency service tier",
tooltip: "Toggle fast mode",
icon: "bolt",
value: true,
});
expect(parsed.type).toBe("toggle");
if (parsed.type !== "toggle") {
throw new Error("Expected toggle feature");
}
expect(parsed.value).toBe(true);
});
it("parses valid select features", () => {
const parsed = AgentFeatureSchema.parse({
type: "select",
id: "service_tier",
label: "Service tier",
description: "Choose a processing tier",
tooltip: "Select service tier",
icon: "gauge",
value: "flex",
options: [
{ id: "default", label: "Default", isDefault: true },
{ id: "flex", label: "Flex" },
],
});
expect(parsed.type).toBe("select");
if (parsed.type !== "select") {
throw new Error("Expected select feature");
}
expect(parsed.options).toHaveLength(2);
expect(parsed.value).toBe("flex");
});
it("rejects invalid features", () => {
const invalidDiscriminator = AgentFeatureSchema.safeParse({
type: "slider",
id: "fast_mode",
label: "Fast mode",
value: true,
});
const missingToggleValue = AgentFeatureSchema.safeParse({
type: "toggle",
id: "fast_mode",
label: "Fast mode",
});
const missingSelectOptions = AgentFeatureSchema.safeParse({
type: "select",
id: "service_tier",
label: "Service tier",
value: null,
});
expect(invalidDiscriminator.success).toBe(false);
expect(missingToggleValue.success).toBe(false);
expect(missingSelectOptions.success).toBe(false);
});
it("parses valid requests", () => {
const parsed = SetAgentFeatureRequestMessageSchema.parse({
type: "set_agent_feature_request",
agentId: "agent-123",
featureId: "fast_mode",
value: true,
requestId: "req-123",
});
expect(parsed.featureId).toBe("fast_mode");
expect(parsed.value).toBe(true);
});
it("parses valid responses", () => {
const parsed = SetAgentFeatureResponseMessageSchema.parse({
type: "set_agent_feature_response",
payload: {
requestId: "req-123",
agentId: "agent-123",
accepted: true,
error: null,
},
});
expect(parsed.payload.accepted).toBe(true);
expect(parsed.payload.error).toBeNull();
});
it("accepts features on agent snapshot payloads", () => {
const parsed = AgentSnapshotPayloadSchema.parse({
id: "agent-123",
provider: "codex",
cwd: "/tmp/project",
model: "gpt-5",
features: [
{
type: "toggle",
id: "fast_mode",
label: "Fast mode",
tooltip: "Toggle fast mode",
value: false,
},
],
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-04-03T12:00:00.000Z",
updatedAt: "2026-04-03T12:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
labels: {},
});
expect(parsed.features).toHaveLength(1);
expect(parsed.features?.[0]?.id).toBe("fast_mode");
});
it("defaults missing rewind capabilities to false", () => {
const parsed = AgentSnapshotPayloadSchema.parse({
id: "agent-123",
provider: "codex",
cwd: "/tmp/project",
model: "gpt-5",
thinkingOptionId: null,
effectiveThinkingOptionId: null,
createdAt: "2026-04-03T12:00:00.000Z",
updatedAt: "2026-04-03T12:00:00.000Z",
lastUserMessageAt: null,
status: "idle",
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: null,
labels: {},
});
expect(parsed.capabilities.supportsRewindConversation).toBe(false);
expect(parsed.capabilities.supportsRewindFiles).toBe(false);
expect(parsed.capabilities.supportsRewindBoth).toBe(false);
});
});
@@ -1 +0,0 @@
export const PARENT_AGENT_ID_LABEL = "paseo.parent-agent-id";
@@ -1,9 +0,0 @@
export const AGENT_LIFECYCLE_STATUSES = [
"initializing",
"idle",
"running",
"error",
"closed",
] as const;
export type AgentLifecycleStatus = (typeof AGENT_LIFECYCLE_STATUSES)[number];
@@ -1,92 +0,0 @@
import { describe, expect, it } from "vitest";
import {
deriveAgentStateBucket,
getAgentStatusPriority,
getWorkspaceStateBucketPriority,
} from "./agent-state-bucket.js";
describe("deriveAgentStateBucket", () => {
it("prioritizes pending permissions as needs_input", () => {
expect(
deriveAgentStateBucket({
status: "idle",
pendingPermissionCount: 1,
requiresAttention: false,
attentionReason: null,
}),
).toBe("needs_input");
});
it("keeps legacy permission attention in needs_input", () => {
expect(
deriveAgentStateBucket({
status: "idle",
pendingPermissionCount: 0,
requiresAttention: true,
attentionReason: "permission",
}),
).toBe("needs_input");
});
it("prioritizes error attention before running status", () => {
expect(
deriveAgentStateBucket({
status: "running",
pendingPermissionCount: 0,
requiresAttention: true,
attentionReason: "error",
}),
).toBe("failed");
});
it("treats unread finished agents as attention", () => {
expect(
deriveAgentStateBucket({
status: "idle",
pendingPermissionCount: 0,
requiresAttention: true,
attentionReason: "finished",
}),
).toBe("attention");
});
it("treats initializing agents as done for workspace buckets", () => {
expect(
deriveAgentStateBucket({
status: "initializing",
pendingPermissionCount: 0,
requiresAttention: false,
attentionReason: null,
}),
).toBe("done");
});
});
describe("getWorkspaceStateBucketPriority", () => {
it("orders active buckets before done", () => {
expect(
["done", "attention", "running", "failed", "needs_input"].sort(
(left, right) =>
getWorkspaceStateBucketPriority(left) - getWorkspaceStateBucketPriority(right),
),
).toEqual(["needs_input", "failed", "running", "attention", "done"]);
});
});
describe("getAgentStatusPriority", () => {
it("keeps initializing agents ahead of completed agents in agent lists", () => {
expect(getAgentStatusPriority({ status: "initializing" })).toBeLessThan(
getAgentStatusPriority({ status: "idle" }),
);
});
it("prioritizes pending permissions before errors and running agents", () => {
const permission = getAgentStatusPriority({ status: "running", pendingPermissionCount: 1 });
expect(permission).toBeLessThan(
getAgentStatusPriority({ status: "error", pendingPermissionCount: 0 }),
);
expect(permission).toBeLessThan(
getAgentStatusPriority({ status: "running", pendingPermissionCount: 0 }),
);
});
});
@@ -1,55 +0,0 @@
import type { AgentLifecycleStatus } from "./agent-lifecycle.js";
import type { WorkspaceStateBucket } from "./messages.js";
export type AgentAttentionReason = "finished" | "error" | "permission" | null | undefined;
export interface AgentStateBucketInput {
status: AgentLifecycleStatus;
pendingPermissionCount?: number;
requiresAttention?: boolean;
attentionReason?: AgentAttentionReason;
}
const WORKSPACE_STATE_BUCKET_PRIORITY = {
needs_input: 0,
failed: 1,
running: 2,
attention: 3,
done: 4,
} as const satisfies Record<WorkspaceStateBucket, number>;
export function deriveAgentStateBucket(input: AgentStateBucketInput): WorkspaceStateBucket {
if ((input.pendingPermissionCount ?? 0) > 0 || input.attentionReason === "permission") {
return "needs_input";
}
if (input.status === "error" || input.attentionReason === "error") {
return "failed";
}
if (input.status === "running") {
return "running";
}
if (input.requiresAttention) {
return "attention";
}
return "done";
}
export function getWorkspaceStateBucketPriority(bucket: WorkspaceStateBucket): number {
return WORKSPACE_STATE_BUCKET_PRIORITY[bucket];
}
export function getAgentStatusPriority(input: AgentStateBucketInput): number {
if ((input.pendingPermissionCount ?? 0) > 0 || input.attentionReason === "permission") {
return 0;
}
if (input.status === "error" || input.attentionReason === "error") {
return 1;
}
if (input.status === "running") {
return 2;
}
if (input.status === "initializing") {
return 3;
}
return 4;
}

Some files were not shown because too many files have changed in this diff Show More