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:
@@ -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";
|
||||
|
||||
@@ -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-"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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,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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user