Merge branch 'main' of github.com:boudra/paseo
This commit is contained in:
@@ -163,6 +163,7 @@ export type CreateAgentRequestOptions = {
|
||||
git?: GitSetupOptions;
|
||||
worktreeName?: string;
|
||||
requestId?: string;
|
||||
labels?: Record<string, string>;
|
||||
} & AgentConfigOverrides;
|
||||
|
||||
type VoiceConversationLoadedPayload = VoiceConversationLoadedMessage["payload"];
|
||||
@@ -656,6 +657,29 @@ export class DaemonClientV2 {
|
||||
this.sendSessionMessage(message);
|
||||
}
|
||||
|
||||
async waitForSessionState(timeout = 5000, requestId?: string): Promise<void> {
|
||||
const resolvedRequestId = this.createRequestId(requestId);
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "request_session_state",
|
||||
requestId: resolvedRequestId,
|
||||
});
|
||||
|
||||
// First check the existing message queue in case session_state was already received
|
||||
for (const msg of this.messageQueue) {
|
||||
if (msg.type === "session_state") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If not in queue, wait for the session_state message
|
||||
await this.sendSessionMessageOrThrow(message);
|
||||
return this.waitFor(
|
||||
(msg) => msg.type === "session_state" ? undefined : null,
|
||||
timeout,
|
||||
{ skipQueue: false }
|
||||
);
|
||||
}
|
||||
|
||||
async loadVoiceConversation(
|
||||
voiceConversationId: string,
|
||||
requestId?: string
|
||||
@@ -751,6 +775,9 @@ export class DaemonClientV2 {
|
||||
: {}),
|
||||
...(options.git ? { git: options.git } : {}),
|
||||
...(options.worktreeName ? { worktreeName: options.worktreeName } : {}),
|
||||
...(options.labels && Object.keys(options.labels).length > 0
|
||||
? { labels: options.labels }
|
||||
: {}),
|
||||
});
|
||||
|
||||
const statusPromise = this.waitFor(
|
||||
@@ -1875,6 +1902,10 @@ export class DaemonClientV2 {
|
||||
sawRunningInQueue = true;
|
||||
queuedIdle = null; // Reset: any previous idle was before this run
|
||||
}
|
||||
// Return immediately if we have pending permissions (even if still running)
|
||||
if (sawRunningInQueue && hasPendingPermissions) {
|
||||
return msg.payload;
|
||||
}
|
||||
if (
|
||||
sawRunningInQueue &&
|
||||
(status === "idle" || status === "error") &&
|
||||
@@ -1907,6 +1938,11 @@ export class DaemonClientV2 {
|
||||
if (status === "running" || hasPendingPermissions) {
|
||||
sawRunning = true;
|
||||
}
|
||||
// Return if we have pending permissions (even if still running)
|
||||
// OR if agent is idle/error with no pending permissions (after having run)
|
||||
if (sawRunning && hasPendingPermissions) {
|
||||
return msg.payload;
|
||||
}
|
||||
if (
|
||||
sawRunning &&
|
||||
(status === "idle" || status === "error") &&
|
||||
@@ -2564,6 +2600,7 @@ function resolveAgentConfig(options: CreateAgentRequestOptions): AgentSessionCon
|
||||
git: _git,
|
||||
worktreeName: _worktreeName,
|
||||
requestId: _requestId,
|
||||
labels: _labels,
|
||||
...overrides
|
||||
} = options;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
AgentTimelineItem,
|
||||
AgentUsage,
|
||||
AgentRuntimeInfo,
|
||||
AgentControlMcpConfig,
|
||||
ListPersistedAgentsOptions,
|
||||
PersistedAgentDescriptor,
|
||||
} from "./agent-sdk-types.js";
|
||||
@@ -58,7 +57,6 @@ export type AgentManagerOptions = {
|
||||
maxTimelineItems?: number;
|
||||
idFactory?: () => string;
|
||||
registry?: AgentStorage;
|
||||
agentControlMcp?: AgentControlMcpConfig;
|
||||
onAgentAttention?: AgentAttentionCallback;
|
||||
logger: Logger;
|
||||
};
|
||||
@@ -101,11 +99,14 @@ type ManagedAgentBase = {
|
||||
lastUsage?: AgentUsage;
|
||||
lastError?: string;
|
||||
attention: AttentionState;
|
||||
parentAgentId?: string;
|
||||
/**
|
||||
* Internal agents are hidden from listings and don't trigger notifications.
|
||||
*/
|
||||
internal?: boolean;
|
||||
/**
|
||||
* User-defined labels for categorizing agents (e.g., { ui: "true" }).
|
||||
*/
|
||||
labels?: Record<string, string>;
|
||||
};
|
||||
|
||||
type ManagedAgentWithSession = ManagedAgentBase & {
|
||||
@@ -189,7 +190,6 @@ export class AgentManager {
|
||||
private readonly idFactory: () => string;
|
||||
private readonly registry?: AgentStorage;
|
||||
private readonly previousStatuses = new Map<string, AgentLifecycleStatus>();
|
||||
private readonly agentControlMcp?: AgentControlMcpConfig;
|
||||
private onAgentAttention?: AgentAttentionCallback;
|
||||
private logger: Logger;
|
||||
|
||||
@@ -198,7 +198,6 @@ export class AgentManager {
|
||||
options?.maxTimelineItems ?? DEFAULT_MAX_TIMELINE_ITEMS;
|
||||
this.idFactory = options?.idFactory ?? (() => randomUUID());
|
||||
this.registry = options?.registry;
|
||||
this.agentControlMcp = options?.agentControlMcp;
|
||||
this.onAgentAttention = options?.onAgentAttention;
|
||||
this.logger = options.logger.child({ module: "agent", component: "agent-manager" });
|
||||
if (options?.clients) {
|
||||
@@ -311,15 +310,17 @@ export class AgentManager {
|
||||
|
||||
async createAgent(
|
||||
config: AgentSessionConfig,
|
||||
agentId?: string
|
||||
agentId?: string,
|
||||
options?: { labels?: Record<string, string> }
|
||||
): Promise<ManagedAgent> {
|
||||
const normalizedConfig = await this.normalizeConfig(config);
|
||||
const normalizedConfig = await this.normalizeConfig(config, { labels: options?.labels });
|
||||
const client = this.requireClient(normalizedConfig.provider);
|
||||
const session = await client.createSession(normalizedConfig);
|
||||
return this.registerSession(
|
||||
session,
|
||||
normalizedConfig,
|
||||
agentId ?? this.idFactory()
|
||||
agentId ?? this.idFactory(),
|
||||
{ labels: options?.labels }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -327,7 +328,12 @@ export class AgentManager {
|
||||
handle: AgentPersistenceHandle,
|
||||
overrides?: Partial<AgentSessionConfig>,
|
||||
agentId?: string,
|
||||
timestamps?: { createdAt?: Date; updatedAt?: Date; lastUserMessageAt?: Date | null }
|
||||
options?: {
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
lastUserMessageAt?: Date | null;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
): Promise<ManagedAgent> {
|
||||
const metadata = (handle.metadata ?? {}) as Partial<AgentSessionConfig>;
|
||||
const mergedConfig = {
|
||||
@@ -346,7 +352,7 @@ export class AgentManager {
|
||||
session,
|
||||
normalizedConfig,
|
||||
agentId ?? this.idFactory(),
|
||||
timestamps
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
@@ -378,7 +384,8 @@ export class AgentManager {
|
||||
);
|
||||
}
|
||||
|
||||
return this.registerSession(session, overrides, agentId);
|
||||
// Preserve existing labels during refresh
|
||||
return this.registerSession(session, overrides, agentId, { labels: existing.labels });
|
||||
}
|
||||
|
||||
async closeAgent(agentId: string): Promise<void> {
|
||||
@@ -795,15 +802,17 @@ export class AgentManager {
|
||||
session: AgentSession,
|
||||
config: AgentSessionConfig,
|
||||
agentId: string,
|
||||
timestamps?: { createdAt?: Date; updatedAt?: Date; lastUserMessageAt?: Date | null }
|
||||
options?: {
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
lastUserMessageAt?: Date | null;
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
): Promise<ManagedAgent> {
|
||||
if (this.agents.has(agentId)) {
|
||||
throw new Error(`Agent with id ${agentId} already exists`);
|
||||
}
|
||||
|
||||
// Inform the session of its managed agent ID for MCP parent-child relationships
|
||||
session.setManagedAgentId?.(agentId);
|
||||
|
||||
const now = new Date();
|
||||
const managed = {
|
||||
id: agentId,
|
||||
@@ -814,8 +823,8 @@ export class AgentManager {
|
||||
config,
|
||||
runtimeInfo: undefined,
|
||||
lifecycle: "initializing",
|
||||
createdAt: timestamps?.createdAt ?? now,
|
||||
updatedAt: timestamps?.updatedAt ?? now,
|
||||
createdAt: options?.createdAt ?? now,
|
||||
updatedAt: options?.updatedAt ?? now,
|
||||
availableModes: [],
|
||||
currentModeId: null,
|
||||
pendingPermissions: new Map(),
|
||||
@@ -823,10 +832,10 @@ export class AgentManager {
|
||||
timeline: [],
|
||||
persistence: session.describePersistence(),
|
||||
historyPrimed: false,
|
||||
lastUserMessageAt: timestamps?.lastUserMessageAt ?? null,
|
||||
lastUserMessageAt: options?.lastUserMessageAt ?? null,
|
||||
attention: { requiresAttention: false },
|
||||
parentAgentId: config.parentAgentId,
|
||||
internal: config.internal ?? false,
|
||||
labels: options?.labels,
|
||||
} as ActiveManagedAgent;
|
||||
|
||||
this.agents.set(agentId, managed);
|
||||
@@ -1093,7 +1102,8 @@ export class AgentManager {
|
||||
|
||||
|
||||
private async normalizeConfig(
|
||||
config: AgentSessionConfig
|
||||
config: AgentSessionConfig,
|
||||
options?: { labels?: Record<string, string> }
|
||||
): Promise<AgentSessionConfig> {
|
||||
const normalized: AgentSessionConfig = { ...config };
|
||||
|
||||
@@ -1107,14 +1117,9 @@ export class AgentManager {
|
||||
normalized.model = trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
if (!normalized.agentControlMcp && this.agentControlMcp) {
|
||||
normalized.agentControlMcp = this.agentControlMcp;
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.paseoPromptInstructions === undefined &&
|
||||
normalized.agentControlMcp
|
||||
) {
|
||||
// Inject paseoPromptInstructions for UI agents (with ui=true label)
|
||||
const isUiAgent = options?.labels?.ui === "true";
|
||||
if (isUiAgent) {
|
||||
normalized.paseoPromptInstructions = getSelfIdentificationInstructions({
|
||||
cwd: normalized.cwd,
|
||||
});
|
||||
|
||||
@@ -102,9 +102,6 @@ describe("agent MCP end-to-end", () => {
|
||||
mcpDebug: false,
|
||||
agentClients: {},
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
agentControlMcp: {
|
||||
url: `http://127.0.0.1:${port}/mcp/agents`,
|
||||
},
|
||||
};
|
||||
|
||||
const previousCodexSessionDir = process.env.CODEX_SESSION_DIR;
|
||||
@@ -235,9 +232,6 @@ describe("agent MCP end-to-end", () => {
|
||||
mcpDebug: false,
|
||||
agentClients: {},
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
agentControlMcp: {
|
||||
url: `http://127.0.0.1:${port}/mcp/agents`,
|
||||
},
|
||||
};
|
||||
|
||||
const previousCodexSessionDir = process.env.CODEX_SESSION_DIR;
|
||||
|
||||
@@ -44,6 +44,7 @@ export function toStoredAgentRecord(
|
||||
? agent.lastUserMessageAt.toISOString()
|
||||
: null,
|
||||
title: options?.title ?? null,
|
||||
labels: agent.labels && Object.keys(agent.labels).length > 0 ? agent.labels : undefined,
|
||||
lastStatus: agent.lifecycle,
|
||||
lastModeId: agent.currentModeId ?? config?.modeId ?? null,
|
||||
config: config ?? null,
|
||||
@@ -56,7 +57,6 @@ export function toStoredAgentRecord(
|
||||
attentionTimestamp: agent.attention.requiresAttention
|
||||
? agent.attention.attentionTimestamp.toISOString()
|
||||
: null,
|
||||
parentAgentId: agent.parentAgentId ?? null,
|
||||
internal: options?.internal,
|
||||
} satisfies StoredAgentRecord;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export function toAgentPayload(
|
||||
pendingPermissions: sanitizePendingPermissions(agent.pendingPermissions),
|
||||
persistence: sanitizePersistenceHandle(agent.persistence),
|
||||
title: options?.title ?? null,
|
||||
parentAgentId: agent.parentAgentId ?? null,
|
||||
labels: agent.labels && Object.keys(agent.labels).length > 0 ? agent.labels : undefined,
|
||||
};
|
||||
|
||||
const usage = sanitizeUsage(agent.lastUsage);
|
||||
|
||||
@@ -154,7 +154,6 @@ describe("getStructuredAgentResponse (e2e)", () => {
|
||||
cwd = mkdtempSync(path.join(tmpdir(), "agent-response-loop-"));
|
||||
manager = new AgentManager({
|
||||
clients: createAllClients(logger),
|
||||
agentControlMcp: { url: agentMcpServer.url },
|
||||
logger,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -171,11 +171,6 @@ export type AgentCommandResult = {
|
||||
usage?: AgentUsage;
|
||||
};
|
||||
|
||||
export type AgentControlMcpConfig = {
|
||||
url: string;
|
||||
headers?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type ListPersistedAgentsOptions = {
|
||||
limit?: number;
|
||||
};
|
||||
@@ -201,7 +196,6 @@ export type AgentSessionConfig = {
|
||||
networkAccess?: boolean;
|
||||
webSearch?: boolean;
|
||||
reasoningEffort?: string;
|
||||
agentControlMcp?: AgentControlMcpConfig;
|
||||
/**
|
||||
* Paseo-owned instructions injected into the first user prompt via
|
||||
* <paseo-instructions>...</paseo-instructions>.
|
||||
@@ -215,7 +209,6 @@ export type AgentSessionConfig = {
|
||||
claude?: Partial<ClaudeAgentOptions>;
|
||||
};
|
||||
mcpServers?: AgentMetadata;
|
||||
parentAgentId?: string;
|
||||
/**
|
||||
* Internal agents are hidden from listings and don't trigger notifications.
|
||||
* They are used for ephemeral system tasks like commit/PR generation.
|
||||
@@ -239,12 +232,6 @@ export interface AgentSession {
|
||||
describePersistence(): AgentPersistenceHandle | null;
|
||||
interrupt(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
/**
|
||||
* Set the managed agent ID for this session. This is called by AgentManager
|
||||
* after registration to allow the session to include its ID in MCP requests
|
||||
* (for parent-child agent relationships).
|
||||
*/
|
||||
setManagedAgentId?(agentId: string): void;
|
||||
/**
|
||||
* List available slash commands for this session.
|
||||
* Commands are provider-specific - Claude supports skills and built-in commands.
|
||||
|
||||
@@ -37,6 +37,7 @@ const STORED_AGENT_SCHEMA = z.object({
|
||||
lastActivityAt: z.string().optional(),
|
||||
lastUserMessageAt: z.string().nullable().optional(),
|
||||
title: z.string().nullable().optional(),
|
||||
labels: z.record(z.string()).optional(),
|
||||
lastStatus: AgentStatusSchema.default("closed"),
|
||||
lastModeId: z.string().nullable().optional(),
|
||||
config: SERIALIZABLE_CONFIG_SCHEMA,
|
||||
@@ -53,7 +54,6 @@ const STORED_AGENT_SCHEMA = z.object({
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
attentionTimestamp: z.string().nullable().optional(),
|
||||
parentAgentId: z.string().nullable().optional(),
|
||||
internal: z.boolean().optional(),
|
||||
archivedAt: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface AgentMcpServerOptions {
|
||||
paseoHome?: string;
|
||||
/**
|
||||
* ID of the agent that is connecting to this MCP server.
|
||||
* When set, create_agent will auto-inject this as parentAgentId.
|
||||
* Used for cwd/mode inheritance when agents spawn child agents.
|
||||
*/
|
||||
callerAgentId?: string;
|
||||
logger: Logger;
|
||||
@@ -350,12 +350,6 @@ export async function createAgentMcpServer(
|
||||
.describe(
|
||||
"Run agent in background. If false (default), waits for completion or permission request. If true, returns immediately."
|
||||
),
|
||||
parentAgentId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Optional parent agent ID. When set, this agent is a child of the specified parent agent."
|
||||
),
|
||||
};
|
||||
|
||||
const createAgentInputSchema = callerAgentId
|
||||
@@ -400,12 +394,10 @@ export async function createAgentMcpServer(
|
||||
worktreeName?: string;
|
||||
background?: boolean;
|
||||
title: string;
|
||||
parentAgentId?: string;
|
||||
};
|
||||
|
||||
let resolvedCwd: string;
|
||||
let resolvedMode: string | undefined;
|
||||
let resolvedParentAgentId: string | undefined;
|
||||
|
||||
if (callerAgentId) {
|
||||
const parentAgent = agentManager.getAgent(callerAgentId);
|
||||
@@ -413,7 +405,6 @@ export async function createAgentMcpServer(
|
||||
throw new Error(`Parent agent ${callerAgentId} not found`);
|
||||
}
|
||||
resolvedCwd = parentAgent.cwd;
|
||||
resolvedParentAgentId = callerAgentId;
|
||||
|
||||
const provider: AgentProvider = agentType ?? "claude";
|
||||
const parentMode = parentAgent.currentModeId;
|
||||
@@ -430,14 +421,12 @@ export async function createAgentMcpServer(
|
||||
initialMode: string;
|
||||
worktreeName?: string;
|
||||
baseBranch?: string;
|
||||
parentAgentId?: string;
|
||||
};
|
||||
const {
|
||||
cwd,
|
||||
initialMode,
|
||||
worktreeName,
|
||||
baseBranch,
|
||||
parentAgentId,
|
||||
} = topLevelArgs;
|
||||
|
||||
resolvedCwd = expandPath(cwd);
|
||||
@@ -457,7 +446,6 @@ export async function createAgentMcpServer(
|
||||
}
|
||||
|
||||
resolvedMode = initialMode;
|
||||
resolvedParentAgentId = parentAgentId;
|
||||
}
|
||||
|
||||
const provider: AgentProvider = agentType ?? "claude";
|
||||
@@ -467,7 +455,6 @@ export async function createAgentMcpServer(
|
||||
cwd: resolvedCwd,
|
||||
modeId: resolvedMode,
|
||||
title: normalizedTitle ?? undefined,
|
||||
parentAgentId: resolvedParentAgentId,
|
||||
});
|
||||
|
||||
if (initialPrompt) {
|
||||
|
||||
@@ -14,7 +14,7 @@ Core rules:
|
||||
- In orchestrator mode, you accomplish tasks only by managing agents; do not perform the work yourself.
|
||||
- Always prefix agent titles (e.g., "🎭 Feature Implementation", "🎭 Design Discussion").
|
||||
- Set cwd to the repository root and choose the most permissive mode available.
|
||||
- If an agent control call fails, list agents before launching another; it may just be a wait timeout.
|
||||
- If an agent control tool call fails, list agents before launching another; it may just be a wait timeout.
|
||||
|
||||
Context management:
|
||||
- Reuse an existing agent when the next step needs the same context (same files/module/folder or immediate follow-up like investigate → fix in the same area).
|
||||
|
||||
@@ -26,9 +26,6 @@ const hasClaudeCredentials =
|
||||
provider: "claude",
|
||||
cwd: process.cwd(),
|
||||
modeId: "plan",
|
||||
agentControlMcp: {
|
||||
url: "http://localhost:6767/mcp", // Placeholder - not actually used in plan mode
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -240,7 +240,6 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
||||
provider: "claude",
|
||||
cwd,
|
||||
modeId: options?.modeId,
|
||||
agentControlMcp: { url: agentMcpServer.url },
|
||||
extra: {
|
||||
claude: {
|
||||
sandbox: { enabled: true, autoAllowBashIfSandboxed: false },
|
||||
|
||||
@@ -116,16 +116,6 @@ type ClaudeAgentSessionOptions = {
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
function appendCallerAgentId(url: string, agentId: string): string {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
parsed.searchParams.set("callerAgentId", agentId);
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
const separator = url.includes("?") ? "&" : "?";
|
||||
return `${url}${separator}callerAgentId=${encodeURIComponent(agentId)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractUserMessageText(content: unknown): string | null {
|
||||
if (typeof content === "string") {
|
||||
@@ -228,25 +218,6 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<Age
|
||||
if (typeof metadata.reasoningEffort === "string") {
|
||||
result.reasoningEffort = metadata.reasoningEffort;
|
||||
}
|
||||
if (isMetadata(metadata.agentControlMcp)) {
|
||||
const url = metadata.agentControlMcp.url;
|
||||
const headers = metadata.agentControlMcp.headers;
|
||||
if (typeof url === "string") {
|
||||
const agentControlMcp: AgentSessionConfig["agentControlMcp"] = { url };
|
||||
if (isMetadata(headers)) {
|
||||
const normalizedHeaders: { [key: string]: string } = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (typeof value === "string") {
|
||||
normalizedHeaders[key] = value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(normalizedHeaders).length > 0) {
|
||||
agentControlMcp.headers = normalizedHeaders;
|
||||
}
|
||||
}
|
||||
result.agentControlMcp = agentControlMcp;
|
||||
}
|
||||
}
|
||||
if (isMetadata(metadata.extra)) {
|
||||
const extra: AgentSessionConfig["extra"] = {};
|
||||
if (isMetadata(metadata.extra.codex)) {
|
||||
@@ -262,9 +233,6 @@ function coerceSessionMetadata(metadata: AgentMetadata | undefined): Partial<Age
|
||||
if (isMetadata(metadata.mcpServers)) {
|
||||
result.mcpServers = metadata.mcpServers;
|
||||
}
|
||||
if (typeof metadata.parentAgentId === "string") {
|
||||
result.parentAgentId = metadata.parentAgentId;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -432,7 +400,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
private activeTurnPromise: Promise<void> | null = null;
|
||||
private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
|
||||
private lastOptionsModel: string | null = null;
|
||||
private managedAgentId: string | null = null;
|
||||
|
||||
constructor(
|
||||
config: ClaudeAgentConfig,
|
||||
@@ -722,10 +689,6 @@ class ClaudeAgentSession implements AgentSession {
|
||||
this.input = null;
|
||||
}
|
||||
|
||||
setManagedAgentId(agentId: string): void {
|
||||
this.managedAgentId = agentId;
|
||||
}
|
||||
|
||||
async listCommands(): Promise<AgentSlashCommand[]> {
|
||||
const q = await this.ensureQuery();
|
||||
const commands = await q.supportedCommands();
|
||||
@@ -819,28 +782,11 @@ class ClaudeAgentSession implements AgentSession {
|
||||
...this.config.extra?.claude,
|
||||
};
|
||||
|
||||
// Always include the agent-control MCP server so agents can launch other agents
|
||||
if (!this.config.agentControlMcp) {
|
||||
throw new Error("agentControlMcp is required for ClaudeAgentSession");
|
||||
}
|
||||
const agentControlConfig = this.config.agentControlMcp;
|
||||
const agentControlUrl = this.managedAgentId
|
||||
? appendCallerAgentId(agentControlConfig.url, this.managedAgentId)
|
||||
: agentControlConfig.url;
|
||||
const defaultMcpServers: Record<string, ClaudeMcpServerConfig> = {
|
||||
"agent-control": {
|
||||
type: "http",
|
||||
url: agentControlUrl,
|
||||
...(agentControlConfig.headers ? { headers: agentControlConfig.headers } : {}),
|
||||
},
|
||||
};
|
||||
|
||||
if (this.config.mcpServers) {
|
||||
const normalizedUserServers = this.normalizeMcpServers(this.config.mcpServers);
|
||||
// Merge user-provided MCP servers with defaults, user servers take precedence
|
||||
base.mcpServers = { ...defaultMcpServers, ...normalizedUserServers };
|
||||
} else {
|
||||
base.mcpServers = defaultMcpServers;
|
||||
if (normalizedUserServers) {
|
||||
base.mcpServers = normalizedUserServers;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.model) {
|
||||
|
||||
@@ -1859,11 +1859,6 @@ const PermissionParamsSchema = z
|
||||
|
||||
type PermissionParams = z.infer<typeof PermissionParamsSchema>;
|
||||
|
||||
const AgentControlMcpConfigSchema = z.object({
|
||||
url: z.string(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
type AgentSessionExtra = NonNullable<AgentSessionConfig["extra"]>;
|
||||
|
||||
const AgentSessionExtraSchema = z.object({
|
||||
@@ -1883,10 +1878,8 @@ const AgentSessionConfigSchema = z
|
||||
networkAccess: z.boolean().optional(),
|
||||
webSearch: z.boolean().optional(),
|
||||
reasoningEffort: z.string().optional(),
|
||||
agentControlMcp: AgentControlMcpConfigSchema.optional(),
|
||||
extra: AgentSessionExtraSchema.optional(),
|
||||
mcpServers: z.record(z.unknown()).optional(),
|
||||
parentAgentId: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -2718,7 +2711,6 @@ function buildCodexMcpConfig(
|
||||
config: AgentSessionConfig,
|
||||
prompt: string,
|
||||
modeId: string,
|
||||
managedAgentId?: string,
|
||||
experimentalResume?: string | null
|
||||
): {
|
||||
prompt: string;
|
||||
@@ -2762,21 +2754,6 @@ function buildCodexMcpConfig(
|
||||
// Build MCP servers configuration
|
||||
const mcpServers: Record<string, CodexMcpServerConfig> = {};
|
||||
|
||||
// Add agent-control MCP server (HTTP-based) if configured
|
||||
if (config.agentControlMcp) {
|
||||
let agentControlUrl = config.agentControlMcp.url;
|
||||
// Append caller agent ID to URL if this is a managed agent
|
||||
if (managedAgentId) {
|
||||
const separator = agentControlUrl.includes("?") ? "&" : "?";
|
||||
agentControlUrl = `${agentControlUrl}${separator}callerAgentId=${encodeURIComponent(managedAgentId)}`;
|
||||
}
|
||||
mcpServers["agent-control"] = {
|
||||
url: agentControlUrl,
|
||||
tool_timeout_sec: 600, // 10 min timeout for child agents
|
||||
...(config.agentControlMcp.headers ? { http_headers: config.agentControlMcp.headers } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Merge MCP servers from extra.codex.mcp_servers (legacy location)
|
||||
const extraCodex = config.extra?.codex as Record<string, unknown> | undefined;
|
||||
if (extraCodex?.mcp_servers && typeof extraCodex.mcp_servers === "object") {
|
||||
@@ -3037,7 +3014,6 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
private turnState: TurnState | null = null;
|
||||
private pendingPatchChanges = new Map<string, PatchFileChange[]>();
|
||||
private patchChangesByCallId = new Map<string, PatchFileChange[]>();
|
||||
private managedAgentId: string | null = null;
|
||||
private resumeHandle: AgentPersistenceHandle | null = null;
|
||||
private pendingResumeFile: string | null = null;
|
||||
|
||||
@@ -3541,10 +3517,6 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
this.conversationId = null;
|
||||
}
|
||||
|
||||
setManagedAgentId(agentId: string): void {
|
||||
this.managedAgentId = agentId;
|
||||
}
|
||||
|
||||
async listCommands(): Promise<AgentSlashCommand[]> {
|
||||
const [skills, prompts] = await Promise.all([
|
||||
listCodexSkills(this.config.cwd),
|
||||
@@ -3591,7 +3563,6 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
this.config,
|
||||
prompt,
|
||||
this.currentMode,
|
||||
this.managedAgentId ?? undefined,
|
||||
resumeFile
|
||||
);
|
||||
const attempt = async (arguments_: CodexToolArguments) =>
|
||||
@@ -3631,7 +3602,7 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
);
|
||||
if (isMissingConversationIdResponse(response)) {
|
||||
const replayPrompt = this.buildResumePrompt(prompt);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode, this.managedAgentId ?? undefined);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode);
|
||||
const attempt = async (arguments_: CodexToolArguments) =>
|
||||
this.client.callTool(
|
||||
{ name: "codex", arguments: arguments_ },
|
||||
@@ -3654,7 +3625,7 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
} catch (error) {
|
||||
if (isMissingConversationIdError(error)) {
|
||||
const replayPrompt = this.buildResumePrompt(prompt);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode, this.managedAgentId ?? undefined);
|
||||
const config = buildCodexMcpConfig(this.config, replayPrompt, this.currentMode);
|
||||
const attempt = async (arguments_: CodexToolArguments) =>
|
||||
this.client.callTool(
|
||||
{ name: "codex", arguments: arguments_ },
|
||||
|
||||
@@ -56,7 +56,6 @@ import { printPairingQrIfEnabled } from "./pairing-qr.js";
|
||||
import { startRelayTransport, type RelayTransportController } from "./relay-transport.js";
|
||||
import type {
|
||||
AgentClient,
|
||||
AgentControlMcpConfig,
|
||||
AgentProvider,
|
||||
} from "./agent/agent-sdk-types.js";
|
||||
|
||||
@@ -78,7 +77,6 @@ export type PaseoDaemonConfig = {
|
||||
mcpDebug: boolean;
|
||||
agentClients: Partial<Record<AgentProvider, AgentClient>>;
|
||||
agentStoragePath: string;
|
||||
agentControlMcp: AgentControlMcpConfig;
|
||||
relayEnabled?: boolean;
|
||||
relayEndpoint?: string;
|
||||
appBaseUrl?: string;
|
||||
@@ -211,7 +209,6 @@ export async function createPaseoDaemon(
|
||||
...config.agentClients,
|
||||
},
|
||||
registry: agentStorage,
|
||||
agentControlMcp: config.agentControlMcp,
|
||||
logger,
|
||||
});
|
||||
|
||||
@@ -378,10 +375,7 @@ export async function createPaseoDaemon(
|
||||
agentStorage,
|
||||
downloadTokenStore,
|
||||
config.paseoHome,
|
||||
{
|
||||
agentMcpUrl: config.agentControlMcp.url,
|
||||
agentMcpHeaders: config.agentControlMcp.headers,
|
||||
},
|
||||
agentMcpRoute,
|
||||
{ allowedOrigins },
|
||||
{ stt: sttService, tts: ttsService },
|
||||
terminalManager
|
||||
|
||||
@@ -80,9 +80,6 @@ export function loadConfig(
|
||||
agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE,
|
||||
agentMcpAllowedHosts: [mcpListen, `localhost:${mcpListen.split(":")[1]}`],
|
||||
mcpDebug: env.MCP_DEBUG === "1",
|
||||
agentControlMcp: {
|
||||
url: `http://${mcpListen}${DEFAULT_AGENT_MCP_ROUTE}`,
|
||||
},
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
staticDir: "public",
|
||||
agentClients: {},
|
||||
|
||||
@@ -57,9 +57,6 @@ async function startDaemon(options: {
|
||||
mcpDebug: false,
|
||||
agentClients: {},
|
||||
agentStoragePath: path.join(options.paseoHome, "agents"),
|
||||
agentControlMcp: {
|
||||
url: `http://127.0.0.1:${port}/mcp/agents`,
|
||||
},
|
||||
openai: process.env.OPENAI_API_KEY ? { apiKey: process.env.OPENAI_API_KEY } : undefined,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync, readFileSync, readdirSync } from "fs";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import path from "path";
|
||||
import {
|
||||
createDaemonTestContext,
|
||||
type DaemonTestContext,
|
||||
} from "../test-utils/index.js";
|
||||
import type { AgentTimelineItem } from "../agent/agent-sdk-types.js";
|
||||
import type { AgentSnapshotPayload, SessionOutboundMessage } from "../messages.js";
|
||||
|
||||
function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
|
||||
}
|
||||
|
||||
// Use gpt-5.1-codex-mini with low reasoning effort for faster test execution
|
||||
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
|
||||
const CODEX_TEST_REASONING_EFFORT = "low";
|
||||
|
||||
describe("daemon E2E", () => {
|
||||
let ctx: DaemonTestContext;
|
||||
|
||||
@@ -29,140 +23,10 @@ describe("daemon E2E", () => {
|
||||
}, 60000);
|
||||
|
||||
describe("multi-agent orchestration", () => {
|
||||
test(
|
||||
"parent agent creates child agent via agent-control MCP",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const childCwd = tmpCwd();
|
||||
|
||||
// Create parent Codex agent
|
||||
const parent = await ctx.client.createAgent({
|
||||
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
|
||||
cwd,
|
||||
title: "Parent Agent",
|
||||
});
|
||||
|
||||
expect(parent.id).toBeTruthy();
|
||||
expect(parent.status).toBe("idle");
|
||||
|
||||
// Clear message queue before sending prompt
|
||||
ctx.client.clearMessageQueue();
|
||||
|
||||
// Prompt the parent to create a child agent using agent-control MCP
|
||||
const prompt = [
|
||||
`Use the create_agent tool from the agent-control MCP server to create a new codex agent.`,
|
||||
`Set the cwd to: ${childCwd}`,
|
||||
`Set the title to: Child Agent`,
|
||||
`Set agentType to: codex`,
|
||||
`Do NOT set an initialPrompt - just create the agent.`,
|
||||
`After creating the agent, reply with "CREATED" followed by the child's agentId.`,
|
||||
].join(" ");
|
||||
|
||||
await ctx.client.sendMessage(parent.id, prompt);
|
||||
|
||||
// Wait for parent to complete
|
||||
const afterCreate = await ctx.client.waitForAgentIdle(
|
||||
parent.id,
|
||||
120000
|
||||
);
|
||||
expect(afterCreate.status).toBe("idle");
|
||||
|
||||
// Verify timeline contains a tool call to create_agent
|
||||
const queue = ctx.client.getMessageQueue();
|
||||
const timelineItems: AgentTimelineItem[] = [];
|
||||
for (const m of queue) {
|
||||
if (
|
||||
m.type === "agent_stream" &&
|
||||
m.payload.agentId === parent.id &&
|
||||
m.payload.event.type === "timeline"
|
||||
) {
|
||||
timelineItems.push(m.payload.event.item);
|
||||
}
|
||||
}
|
||||
|
||||
// Should have a tool call to create_agent from agent-control
|
||||
const hasCreateAgentCall = timelineItems.some(
|
||||
(item) =>
|
||||
item.type === "tool_call" &&
|
||||
item.name === "agent-control.create_agent"
|
||||
);
|
||||
expect(hasCreateAgentCall).toBe(true);
|
||||
|
||||
// Now verify we can see both agents via session_state
|
||||
// Send a list_persisted_agents_request to trigger session_state refresh
|
||||
// Or we can check the queue for agent_state messages
|
||||
const agentStateMessages = queue.filter(
|
||||
(m) => m.type === "agent_state"
|
||||
);
|
||||
|
||||
// Extract unique agent IDs from state messages
|
||||
const agentIds = new Set<string>();
|
||||
for (const m of agentStateMessages) {
|
||||
if (m.type === "agent_state") {
|
||||
agentIds.add(m.payload.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Should have at least 2 agents (parent + child)
|
||||
expect(agentIds.size).toBeGreaterThanOrEqual(2);
|
||||
expect(agentIds.has(parent.id)).toBe(true);
|
||||
|
||||
// Get the child agent ID from the tool call output
|
||||
const createAgentCall = timelineItems.find(
|
||||
(item) =>
|
||||
item.type === "tool_call" &&
|
||||
item.name === "agent-control.create_agent"
|
||||
);
|
||||
|
||||
let childAgentId: string | null = null;
|
||||
if (
|
||||
createAgentCall &&
|
||||
createAgentCall.type === "tool_call" &&
|
||||
createAgentCall.output
|
||||
) {
|
||||
const output = createAgentCall.output as unknown;
|
||||
const tryExtract = (value: unknown): string | null => {
|
||||
if (!value) return null;
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return tryExtract(JSON.parse(value));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (typeof value !== "object") return null;
|
||||
const asObj = value as Record<string, unknown>;
|
||||
const direct = asObj.agentId;
|
||||
if (typeof direct === "string") return direct;
|
||||
const structured = asObj.structuredContent;
|
||||
if (structured && typeof structured === "object") {
|
||||
const nested = (structured as Record<string, unknown>).agentId;
|
||||
if (typeof nested === "string") return nested;
|
||||
}
|
||||
if (typeof structured === "string") {
|
||||
try {
|
||||
return tryExtract(JSON.parse(structured));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
childAgentId = tryExtract(output);
|
||||
}
|
||||
|
||||
// Verify we found the child agent ID
|
||||
expect(childAgentId).toBeTruthy();
|
||||
expect(agentIds.has(childAgentId!)).toBe(true);
|
||||
|
||||
// Cleanup
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
rmSync(childCwd, { recursive: true, force: true });
|
||||
},
|
||||
300000 // 5 minute timeout for multi-agent E2E
|
||||
);
|
||||
// TODO: Re-implement orchestration tests with new Paseo MCP
|
||||
// The old agent-control MCP has been removed
|
||||
test("placeholder for future orchestration tests", async () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import path from "path";
|
||||
import {
|
||||
createDaemonTestContext,
|
||||
type DaemonTestContext,
|
||||
} from "../test-utils/index.js";
|
||||
|
||||
function tmpCwd(): string {
|
||||
return mkdtempSync(path.join(tmpdir(), "wait-perm-e2e-"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests for wait returning on permission request.
|
||||
*
|
||||
* The `paseo wait` command should return when:
|
||||
* 1. Agent completes (goes idle)
|
||||
* 2. Agent requests permission
|
||||
*
|
||||
* This test verifies that waitForAgentIdle correctly returns when
|
||||
* an agent requests permission, not just when it goes idle.
|
||||
*/
|
||||
describe("wait returns on permission request", () => {
|
||||
let ctx: DaemonTestContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx = await createDaemonTestContext();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ctx.cleanup();
|
||||
}, 60000);
|
||||
|
||||
describe("Claude provider", () => {
|
||||
test(
|
||||
"waitForAgentIdle returns when permission is requested (not just when idle)",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const testFilePath = path.join(cwd, "test-wait-perm.txt");
|
||||
|
||||
// Create Claude agent with default mode (always ask for permissions)
|
||||
const agent = await ctx.client.createAgent({
|
||||
provider: "claude",
|
||||
model: "haiku",
|
||||
cwd,
|
||||
title: "Wait Permission Test",
|
||||
modeId: "default",
|
||||
});
|
||||
|
||||
expect(agent.id).toBeTruthy();
|
||||
expect(agent.status).toBe("idle");
|
||||
|
||||
// Clear message queue before sending prompt
|
||||
ctx.client.clearMessageQueue();
|
||||
|
||||
// Send a prompt that requires file write permission
|
||||
const prompt = [
|
||||
`You must use the Write tool to create a file at "${testFilePath}" with content "hello".`,
|
||||
"Do not respond before attempting to write the file.",
|
||||
].join(" ");
|
||||
|
||||
await ctx.client.sendMessage(agent.id, prompt);
|
||||
|
||||
// CRITICAL: This is the behavior we're testing
|
||||
// waitForAgentIdle should return when permission is requested,
|
||||
// NOT wait until the agent is fully idle (which would timeout or
|
||||
// require us to approve the permission first)
|
||||
const startTime = Date.now();
|
||||
const state = await ctx.client.waitForAgentIdle(agent.id, 60000);
|
||||
const waitDuration = Date.now() - startTime;
|
||||
|
||||
// If wait returns because of permission request, we should have:
|
||||
// 1. Agent status still "running" (not idle yet - waiting for permission)
|
||||
// 2. Pending permissions in the state
|
||||
// 3. Wait should return quickly (under 30 seconds, not timeout)
|
||||
|
||||
// Check that we have a pending permission
|
||||
const hasPendingPermission =
|
||||
(state.pendingPermissions && state.pendingPermissions.length > 0);
|
||||
|
||||
// Log for debugging
|
||||
console.log("Wait returned after", waitDuration, "ms");
|
||||
console.log("Agent status:", state.status);
|
||||
console.log("Pending permissions:", state.pendingPermissions?.length ?? 0);
|
||||
|
||||
// THE ASSERTION:
|
||||
// If waitForAgentIdle correctly yields on permission request,
|
||||
// we should either:
|
||||
// - Get a state with pending permissions (status might be "running")
|
||||
// - Or the state should be from right when permission was requested
|
||||
//
|
||||
// If waitForAgentIdle does NOT yield on permission request,
|
||||
// this test will either:
|
||||
// - Timeout (60s)
|
||||
// - Return only after we never approve permission and agent errors/gives up
|
||||
|
||||
// This test will FAIL if waitForAgentIdle waits for full idle
|
||||
// instead of returning on permission request
|
||||
expect(hasPendingPermission).toBe(true);
|
||||
|
||||
// Also verify we can get the permission via waitForPermission
|
||||
// (This should return immediately since permission is already pending)
|
||||
const permission = await ctx.client.waitForPermission(agent.id, 5000);
|
||||
expect(permission).toBeTruthy();
|
||||
expect(permission.kind).toBe("tool");
|
||||
|
||||
// Clean up: deny the permission so agent can finish
|
||||
await ctx.client.respondToPermission(agent.id, permission.id, {
|
||||
behavior: "deny",
|
||||
message: "Test complete",
|
||||
});
|
||||
|
||||
// Wait for agent to finish processing the denial
|
||||
await ctx.client.waitForAgentIdle(agent.id, 30000);
|
||||
|
||||
await ctx.client.deleteAgent(agent.id);
|
||||
rmSync(cwd, { recursive: true, force: true });
|
||||
},
|
||||
120000
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// CLI exports for @paseo/server
|
||||
export { createPaseoDaemon, type PaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js";
|
||||
export { loadConfig } from "./config.js";
|
||||
export { resolvePaseoHome } from "./paseo-home.js";
|
||||
export { createRootLogger, type LogLevel, type LogFormat } from "./logger.js";
|
||||
export { loadPersistedConfig, type PersistedConfig } from "./persisted-config.js";
|
||||
export { DaemonClientV2, type DaemonClientV2Config, type ConnectionState, type DaemonEvent } from "../client/daemon-client-v2.js";
|
||||
|
||||
// Agent SDK types for CLI commands
|
||||
export type {
|
||||
AgentMode,
|
||||
AgentUsage,
|
||||
AgentCapabilityFlags,
|
||||
AgentPermissionRequest,
|
||||
AgentTimelineItem,
|
||||
} from "./agent/agent-sdk-types.js";
|
||||
|
||||
// Agent activity curator for CLI logs
|
||||
export { curateAgentActivity } from "./agent/activity-curator.js";
|
||||
|
||||
// WebSocket message types for CLI streaming
|
||||
export type {
|
||||
AgentSnapshotPayload,
|
||||
AgentStreamEventPayload,
|
||||
AgentStreamMessage,
|
||||
AgentStreamSnapshotMessage,
|
||||
} from "../shared/messages.js";
|
||||
@@ -77,10 +77,16 @@ export function buildSessionConfig(
|
||||
|
||||
export function extractTimestamps(
|
||||
record: StoredAgentRecord
|
||||
): { createdAt: Date; updatedAt: Date; lastUserMessageAt: Date | null } {
|
||||
): {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
lastUserMessageAt: Date | null;
|
||||
labels?: Record<string, string>;
|
||||
} {
|
||||
return {
|
||||
createdAt: new Date(record.createdAt),
|
||||
updatedAt: new Date(record.lastActivityAt ?? record.updatedAt),
|
||||
lastUserMessageAt: record.lastUserMessageAt ? new Date(record.lastUserMessageAt) : null,
|
||||
labels: record.labels,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -104,11 +104,6 @@ import { getProjectIcon } from "../utils/project-icon.js";
|
||||
import { expandTilde } from "../utils/path.js";
|
||||
import type pino from "pino";
|
||||
|
||||
type AgentMcpClientConfig = {
|
||||
agentMcpUrl: string;
|
||||
agentMcpHeaders?: Record<string, string>;
|
||||
};
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const READ_ONLY_GIT_ENV: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
@@ -294,7 +289,7 @@ export class Session {
|
||||
private agentTools: ToolSet | null = null;
|
||||
private agentManager: AgentManager;
|
||||
private readonly agentStorage: AgentStorage;
|
||||
private readonly agentMcpConfig: AgentMcpClientConfig;
|
||||
private readonly agentMcpRoute: string;
|
||||
private readonly downloadTokenStore: DownloadTokenStore;
|
||||
private readonly pushTokenStore: PushTokenStore;
|
||||
private readonly providerRegistry: ReturnType<typeof buildProviderRegistry>;
|
||||
@@ -317,7 +312,7 @@ export class Session {
|
||||
paseoHome: string,
|
||||
agentManager: AgentManager,
|
||||
agentStorage: AgentStorage,
|
||||
agentMcpConfig: AgentMcpClientConfig,
|
||||
agentMcpRoute: string,
|
||||
stt: OpenAISTT | null,
|
||||
tts: OpenAITTS | null,
|
||||
terminalManager: TerminalManager | null,
|
||||
@@ -331,7 +326,7 @@ export class Session {
|
||||
this.paseoHome = paseoHome;
|
||||
this.agentManager = agentManager;
|
||||
this.agentStorage = agentStorage;
|
||||
this.agentMcpConfig = agentMcpConfig;
|
||||
this.agentMcpRoute = agentMcpRoute;
|
||||
this.terminalManager = terminalManager;
|
||||
this.voiceConversationStore = voiceConversationStore;
|
||||
this.abortController = new AbortController();
|
||||
@@ -505,15 +500,10 @@ export class Session {
|
||||
*/
|
||||
private async initializeAgentMcp(): Promise<void> {
|
||||
try {
|
||||
// Connect to the local MCP server using localhost
|
||||
const agentMcpUrl = `http://127.0.0.1:6767${this.agentMcpRoute}`;
|
||||
const transport = new StreamableHTTPClientTransport(
|
||||
new URL(this.agentMcpConfig.agentMcpUrl),
|
||||
this.agentMcpConfig.agentMcpHeaders
|
||||
? {
|
||||
requestInit: {
|
||||
headers: this.agentMcpConfig.agentMcpHeaders,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
new URL(agentMcpUrl)
|
||||
);
|
||||
|
||||
this.agentMcpClient = await experimental_createMCPClient({
|
||||
@@ -632,8 +622,8 @@ export class Session {
|
||||
requiresAttention: record.requiresAttention ?? false,
|
||||
attentionReason: record.attentionReason ?? null,
|
||||
attentionTimestamp: record.attentionTimestamp ?? null,
|
||||
parentAgentId: record.parentAgentId ?? null,
|
||||
archivedAt: record.archivedAt ?? null,
|
||||
labels: record.labels,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -665,7 +655,7 @@ export class Session {
|
||||
);
|
||||
} else {
|
||||
const config = buildSessionConfig(record);
|
||||
snapshot = await this.agentManager.createAgent(config, agentId);
|
||||
snapshot = await this.agentManager.createAgent(config, agentId, { labels: record.labels });
|
||||
}
|
||||
|
||||
await this.agentManager.primeAgentHistory(agentId);
|
||||
@@ -1326,7 +1316,7 @@ export class Session {
|
||||
private async handleCreateAgentRequest(
|
||||
msg: Extract<SessionInboundMessage, { type: "create_agent_request" }>
|
||||
): Promise<void> {
|
||||
const { config, worktreeName, requestId, initialPrompt, git, images } = msg;
|
||||
const { config, worktreeName, requestId, initialPrompt, git, images, labels } = msg;
|
||||
this.sessionLogger.info(
|
||||
{ cwd: config.cwd, provider: config.provider, worktreeName },
|
||||
`Creating agent in ${config.cwd} (${config.provider})${
|
||||
@@ -1356,9 +1346,14 @@ export class Session {
|
||||
const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(
|
||||
config,
|
||||
git,
|
||||
worktreeName
|
||||
worktreeName,
|
||||
labels
|
||||
);
|
||||
const snapshot = await this.agentManager.createAgent(
|
||||
sessionConfig,
|
||||
undefined,
|
||||
{ labels }
|
||||
);
|
||||
const snapshot = await this.agentManager.createAgent(sessionConfig);
|
||||
await this.forwardAgentState(snapshot);
|
||||
|
||||
const trimmedPrompt = initialPrompt?.trim();
|
||||
@@ -1584,7 +1579,8 @@ export class Session {
|
||||
private async buildAgentSessionConfig(
|
||||
config: AgentSessionConfig,
|
||||
gitOptions?: GitSetupOptions,
|
||||
legacyWorktreeName?: string
|
||||
legacyWorktreeName?: string,
|
||||
_labels?: Record<string, string>
|
||||
): Promise<{ sessionConfig: AgentSessionConfig; worktreeConfig?: WorktreeConfig }> {
|
||||
let cwd = expandTilde(config.cwd);
|
||||
const normalized = this.normalizeGitOptions(gitOptions, legacyWorktreeName);
|
||||
|
||||
@@ -64,9 +64,6 @@ export async function createTestPaseoDaemon(
|
||||
mcpDebug: false,
|
||||
agentClients: {},
|
||||
agentStoragePath: path.join(paseoHome, "agents"),
|
||||
agentControlMcp: {
|
||||
url: `http://127.0.0.1:${port}/mcp/agents`,
|
||||
},
|
||||
relayEnabled: options.relayEnabled ?? false,
|
||||
relayEndpoint: options.relayEndpoint ?? "relay.paseo.sh:443",
|
||||
appBaseUrl: "https://app.paseo.sh",
|
||||
|
||||
@@ -10,11 +10,6 @@ import type pino from "pino";
|
||||
import type { WSOutboundMessage } from "./messages.js";
|
||||
import { WebSocketSessionBridge } from "./websocket-session-bridge.js";
|
||||
|
||||
type AgentMcpClientConfig = {
|
||||
agentMcpUrl: string;
|
||||
agentMcpHeaders?: Record<string, string>;
|
||||
};
|
||||
|
||||
type WebSocketServerConfig = {
|
||||
allowedOrigins: Set<string>;
|
||||
};
|
||||
@@ -34,7 +29,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
agentStorage: AgentStorage,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
paseoHome: string,
|
||||
agentMcpConfig: AgentMcpClientConfig,
|
||||
agentMcpRoute: string,
|
||||
wsConfig: WebSocketServerConfig,
|
||||
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
|
||||
terminalManager?: TerminalManager | null
|
||||
@@ -46,7 +41,7 @@ export class VoiceAssistantWebSocketServer {
|
||||
agentStorage,
|
||||
downloadTokenStore,
|
||||
paseoHome,
|
||||
agentMcpConfig,
|
||||
agentMcpRoute,
|
||||
speech,
|
||||
terminalManager
|
||||
);
|
||||
|
||||
@@ -19,11 +19,6 @@ import type { OpenAITTS } from "./agent/tts-openai.js";
|
||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||
import type pino from "pino";
|
||||
|
||||
type AgentMcpClientConfig = {
|
||||
agentMcpUrl: string;
|
||||
agentMcpHeaders?: Record<string, string>;
|
||||
};
|
||||
|
||||
export class WebSocketSessionBridge {
|
||||
private readonly logger: pino.Logger;
|
||||
private readonly sessions: Map<WebSocket, Session> = new Map();
|
||||
@@ -34,7 +29,7 @@ export class WebSocketSessionBridge {
|
||||
private readonly paseoHome: string;
|
||||
private readonly pushTokenStore: PushTokenStore;
|
||||
private readonly pushService: PushService;
|
||||
private readonly agentMcpConfig: AgentMcpClientConfig;
|
||||
private readonly agentMcpRoute: string;
|
||||
private readonly stt: OpenAISTT | null;
|
||||
private readonly tts: OpenAITTS | null;
|
||||
private readonly terminalManager: TerminalManager | null;
|
||||
@@ -46,7 +41,7 @@ export class WebSocketSessionBridge {
|
||||
agentStorage: AgentStorage,
|
||||
downloadTokenStore: DownloadTokenStore,
|
||||
paseoHome: string,
|
||||
agentMcpConfig: AgentMcpClientConfig,
|
||||
agentMcpRoute: string,
|
||||
speech?: { stt: OpenAISTT | null; tts: OpenAITTS | null },
|
||||
terminalManager?: TerminalManager | null
|
||||
) {
|
||||
@@ -55,7 +50,7 @@ export class WebSocketSessionBridge {
|
||||
this.agentStorage = agentStorage;
|
||||
this.downloadTokenStore = downloadTokenStore;
|
||||
this.paseoHome = paseoHome;
|
||||
this.agentMcpConfig = agentMcpConfig;
|
||||
this.agentMcpRoute = agentMcpRoute;
|
||||
this.stt = speech?.stt ?? null;
|
||||
this.tts = speech?.tts ?? null;
|
||||
this.terminalManager = terminalManager ?? null;
|
||||
@@ -91,7 +86,7 @@ export class WebSocketSessionBridge {
|
||||
this.paseoHome,
|
||||
this.agentManager,
|
||||
this.agentStorage,
|
||||
this.agentMcpConfig,
|
||||
this.agentMcpRoute,
|
||||
this.stt,
|
||||
this.tts,
|
||||
this.terminalManager,
|
||||
|
||||
@@ -63,12 +63,6 @@ const AgentSessionConfigSchema = z.object({
|
||||
networkAccess: z.boolean().optional(),
|
||||
webSearch: z.boolean().optional(),
|
||||
reasoningEffort: z.string().optional(),
|
||||
agentControlMcp: z
|
||||
.object({
|
||||
url: z.string(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
extra: z
|
||||
.object({
|
||||
codex: z.record(z.unknown()).optional(),
|
||||
@@ -247,10 +241,10 @@ export const AgentSnapshotPayloadSchema = z.object({
|
||||
lastUsage: AgentUsageSchema.optional(),
|
||||
lastError: z.string().optional(),
|
||||
title: z.string().nullable(),
|
||||
labels: z.record(z.string()).optional(),
|
||||
requiresAttention: z.boolean().optional(),
|
||||
attentionReason: z.enum(["finished", "error", "permission"]).nullable().optional(),
|
||||
attentionTimestamp: z.string().nullable().optional(),
|
||||
parentAgentId: z.string().nullable().optional(),
|
||||
archivedAt: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
@@ -385,6 +379,7 @@ export const CreateAgentRequestMessageSchema = z.object({
|
||||
mimeType: z.string(), // e.g., "image/jpeg", "image/png"
|
||||
})).optional(),
|
||||
git: GitSetupOptionsSchema.optional(),
|
||||
labels: z.record(z.string()).optional(),
|
||||
requestId: z.string(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user