mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Unify cwd validation and improve MCP tool log visibility
This commit is contained in:
@@ -328,5 +328,37 @@ describe("curateAgentActivity", () => {
|
||||
|
||||
expect(result).toBe("[Grep] TODO");
|
||||
});
|
||||
|
||||
test("shows speak tool text input", () => {
|
||||
const timeline: AgentTimelineItem[] = [
|
||||
{
|
||||
type: "tool_call",
|
||||
callId: "s1",
|
||||
name: "speak",
|
||||
input: { text: "hello from voice" },
|
||||
status: "completed",
|
||||
},
|
||||
];
|
||||
|
||||
const result = curateAgentActivity(timeline);
|
||||
expect(result).toBe('[speak] {"text":"hello from voice"}');
|
||||
});
|
||||
|
||||
test("shows MCP tool input JSON", () => {
|
||||
const timeline: AgentTimelineItem[] = [
|
||||
{
|
||||
type: "tool_call",
|
||||
callId: "m1",
|
||||
name: "paseo__create_agent",
|
||||
input: { cwd: "/tmp/repo", initialPrompt: "do the thing" },
|
||||
status: "completed",
|
||||
},
|
||||
];
|
||||
|
||||
const result = curateAgentActivity(timeline);
|
||||
expect(result).toBe(
|
||||
'[paseo__create_agent] {"cwd":"/tmp/repo","initialPrompt":"do the thing"}'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
||||
import { extractPrincipalParam } from "../../utils/tool-call-parsers.js";
|
||||
|
||||
const DEFAULT_MAX_ITEMS = 40;
|
||||
const MAX_TOOL_INPUT_CHARS = 400;
|
||||
|
||||
function appendText(buffer: string, text: string): string {
|
||||
const normalized = text.trim();
|
||||
@@ -25,6 +26,34 @@ function flushBuffers(lines: string[], buffers: { message: string; thought: stri
|
||||
buffers.thought = "";
|
||||
}
|
||||
|
||||
function isLikelyMcpToolCall(name: string): boolean {
|
||||
const normalized = name.toLowerCase();
|
||||
return (
|
||||
normalized.includes("mcp") ||
|
||||
normalized.includes("paseo") ||
|
||||
normalized.includes("__") ||
|
||||
normalized === "speak"
|
||||
);
|
||||
}
|
||||
|
||||
function formatToolInputJson(input: unknown): string | null {
|
||||
if (input === undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const encoded = JSON.stringify(input);
|
||||
if (!encoded) {
|
||||
return null;
|
||||
}
|
||||
if (encoded.length <= MAX_TOOL_INPUT_CHARS) {
|
||||
return encoded;
|
||||
}
|
||||
return `${encoded.slice(0, MAX_TOOL_INPUT_CHARS)}...`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse timeline items:
|
||||
* - Dedupe tool calls by callId (pending/completed -> single)
|
||||
@@ -127,6 +156,11 @@ export function curateAgentActivity(
|
||||
break;
|
||||
case "tool_call": {
|
||||
flushBuffers(lines, buffers);
|
||||
const inputJson = formatToolInputJson(item.input);
|
||||
if (isLikelyMcpToolCall(item.name) && inputJson) {
|
||||
lines.push(`[${item.name}] ${inputJson}`);
|
||||
break;
|
||||
}
|
||||
const principal = extractPrincipalParam(item.input);
|
||||
if (principal) {
|
||||
lines.push(`[${item.name}] ${principal}`);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||
import { createAgentMcpServer } from "./mcp-server.js";
|
||||
@@ -48,6 +51,7 @@ function createTestDeps(): TestDeps {
|
||||
|
||||
describe("create_agent MCP tool", () => {
|
||||
const logger = createTestLogger();
|
||||
const existingCwd = process.cwd();
|
||||
|
||||
it("requires a concise title no longer than 60 characters", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
@@ -56,7 +60,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const missingTitle = await tool.inputSchema.safeParseAsync({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
initialMode: "default",
|
||||
initialPrompt: "test",
|
||||
});
|
||||
@@ -64,7 +68,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(missingTitle.error.issues[0].path).toEqual(["title"]);
|
||||
|
||||
const tooLong = await tool.inputSchema.safeParseAsync({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
initialMode: "default",
|
||||
title: "x".repeat(61),
|
||||
initialPrompt: "test",
|
||||
@@ -73,7 +77,7 @@ describe("create_agent MCP tool", () => {
|
||||
expect(tooLong.error.issues[0].path).toEqual(["title"]);
|
||||
|
||||
const ok = await tool.inputSchema.safeParseAsync({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
initialMode: "default",
|
||||
title: "Short title",
|
||||
initialPrompt: "test",
|
||||
@@ -86,7 +90,7 @@ describe("create_agent MCP tool", () => {
|
||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||
const tool = (server as any)._registeredTools["create_agent"];
|
||||
const parsed = await tool.inputSchema.safeParseAsync({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
initialMode: "default",
|
||||
title: "Short title",
|
||||
});
|
||||
@@ -94,6 +98,20 @@ describe("create_agent MCP tool", () => {
|
||||
expect(parsed.error.issues.some((issue: { path: string[] }) => issue.path[0] === "initialPrompt")).toBe(true);
|
||||
});
|
||||
|
||||
it("fails immediately when cwd does not exist", async () => {
|
||||
const { agentManager, agentStorage } = createTestDeps();
|
||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||
const tool = (server as any)._registeredTools["create_agent"];
|
||||
|
||||
await expect(
|
||||
tool.callback({
|
||||
cwd: "/path/that/does/not/exist",
|
||||
title: "Short title",
|
||||
initialPrompt: "Do work",
|
||||
})
|
||||
).rejects.toThrow("Working directory does not exist");
|
||||
});
|
||||
|
||||
it("passes caller-provided titles directly into createAgent", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
spies.agentManager.createAgent.mockResolvedValue({
|
||||
@@ -108,14 +126,14 @@ describe("create_agent MCP tool", () => {
|
||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||
const tool = (server as any)._registeredTools["create_agent"];
|
||||
await tool.callback({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
title: " Fix auth bug ",
|
||||
initialPrompt: "Do work",
|
||||
});
|
||||
|
||||
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
title: "Fix auth bug",
|
||||
}),
|
||||
undefined,
|
||||
@@ -137,7 +155,7 @@ describe("create_agent MCP tool", () => {
|
||||
const server = await createAgentMcpServer({ agentManager, agentStorage, logger });
|
||||
const tool = (server as any)._registeredTools["create_agent"];
|
||||
await tool.callback({
|
||||
cwd: "/tmp/repo",
|
||||
cwd: existingCwd,
|
||||
title: " Fix auth ",
|
||||
initialPrompt: "Do work",
|
||||
});
|
||||
@@ -153,15 +171,18 @@ describe("create_agent MCP tool", () => {
|
||||
|
||||
it("allows caller agents to override cwd and applies caller context labels", async () => {
|
||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||
const baseDir = await mkdtemp(join(tmpdir(), "paseo-mcp-test-"));
|
||||
const subdir = join(baseDir, "subdir");
|
||||
await mkdir(subdir, { recursive: true });
|
||||
spies.agentManager.getAgent.mockReturnValue({
|
||||
id: "voice-agent",
|
||||
cwd: "/tmp/voice",
|
||||
cwd: baseDir,
|
||||
provider: "codex",
|
||||
currentModeId: "full-access",
|
||||
} as ManagedAgent);
|
||||
spies.agentManager.createAgent.mockResolvedValue({
|
||||
id: "child-agent",
|
||||
cwd: "/tmp/voice/subdir",
|
||||
cwd: subdir,
|
||||
lifecycle: "idle",
|
||||
currentModeId: null,
|
||||
availableModes: [],
|
||||
@@ -189,11 +210,12 @@ describe("create_agent MCP tool", () => {
|
||||
|
||||
expect(spies.agentManager.createAgent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
cwd: "/tmp/voice/subdir",
|
||||
cwd: subdir,
|
||||
}),
|
||||
undefined,
|
||||
{ labels: { ui: "true" } }
|
||||
);
|
||||
await rm(baseDir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { AgentStorage } from "./agent-storage.js";
|
||||
import { createWorktree } from "../../utils/worktree.js";
|
||||
import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
|
||||
import { scheduleAgentMetadataGeneration } from "./agent-metadata-generator.js";
|
||||
import { validateWorkingDirectoryExists } from "./working-directory-validation.js";
|
||||
|
||||
export interface AgentMcpServerOptions {
|
||||
agentManager: AgentManager;
|
||||
@@ -496,6 +497,7 @@ export async function createAgentMcpServer(
|
||||
} = topLevelArgs;
|
||||
|
||||
resolvedCwd = expandPath(cwd);
|
||||
await validateWorkingDirectoryExists(resolvedCwd);
|
||||
|
||||
if (worktreeName) {
|
||||
if (!baseBranch) {
|
||||
@@ -513,6 +515,7 @@ export async function createAgentMcpServer(
|
||||
|
||||
resolvedMode = initialMode;
|
||||
}
|
||||
await validateWorkingDirectoryExists(resolvedCwd);
|
||||
|
||||
const provider: AgentProvider = agentType ?? "claude";
|
||||
const normalizedTitle = title?.trim() ?? null;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
|
||||
export async function validateWorkingDirectoryExists(
|
||||
cwd: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const cwdStats = await stat(cwd);
|
||||
if (!cwdStats.isDirectory()) {
|
||||
throw new Error(`Working directory is not a directory: ${cwd}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
throw new Error(`Working directory does not exist: ${cwd}`);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to access working directory: ${cwd}`);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ import { AgentManager } from "./agent/agent-manager.js";
|
||||
import type { ManagedAgent } from "./agent/agent-manager.js";
|
||||
import { scheduleAgentMetadataGeneration } from "./agent/agent-metadata-generator.js";
|
||||
import { toAgentPayload } from "./agent/agent-projections.js";
|
||||
import { validateWorkingDirectoryExists } from "./agent/working-directory-validation.js";
|
||||
import {
|
||||
StructuredAgentResponseError,
|
||||
generateStructuredAgentResponse,
|
||||
@@ -298,6 +299,7 @@ export class Session {
|
||||
// Voice mode state
|
||||
private isVoiceMode = false;
|
||||
private speechInProgress = false;
|
||||
// OpenRouter voice-only conversation storage identifier.
|
||||
private voiceConversationId: string | null = null;
|
||||
|
||||
private readonly dictationStreamManager: DictationStreamManager;
|
||||
@@ -1400,6 +1402,16 @@ export class Session {
|
||||
}
|
||||
|
||||
this.isVoiceMode = true;
|
||||
if (this.voiceLlmProvider !== "openrouter") {
|
||||
this.voiceConversationId = null;
|
||||
this.voiceAssistantAgentId = voiceConversationId;
|
||||
this.sessionLogger.info(
|
||||
{ voiceAssistantAgentId: this.voiceAssistantAgentId },
|
||||
"Voice conversation enabled (agent-backed)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.voiceConversationId = voiceConversationId;
|
||||
|
||||
const loaded = await this.voiceConversationStore.load(
|
||||
@@ -1416,6 +1428,14 @@ export class Session {
|
||||
}
|
||||
|
||||
this.isVoiceMode = false;
|
||||
if (this.voiceLlmProvider !== "openrouter") {
|
||||
this.sessionLogger.info(
|
||||
{ voiceAssistantAgentId: this.voiceAssistantAgentId },
|
||||
"Voice conversation disabled (agent-backed)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const idToPersist = this.voiceConversationId;
|
||||
if (idToPersist) {
|
||||
try {
|
||||
@@ -1548,21 +1568,7 @@ export class Session {
|
||||
try {
|
||||
// Validate that the working directory exists
|
||||
const resolvedCwd = expandTilde(config.cwd);
|
||||
try {
|
||||
const stats = await stat(resolvedCwd);
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(
|
||||
`Working directory is not a directory: ${config.cwd}`
|
||||
);
|
||||
}
|
||||
} catch (statError: any) {
|
||||
if (statError.code === "ENOENT") {
|
||||
throw new Error(
|
||||
`Working directory does not exist: ${config.cwd}`
|
||||
);
|
||||
}
|
||||
throw statError;
|
||||
}
|
||||
await validateWorkingDirectoryExists(resolvedCwd);
|
||||
|
||||
const { sessionConfig, worktreeConfig } = await this.buildAgentSessionConfig(
|
||||
config,
|
||||
@@ -4653,11 +4659,20 @@ export class Session {
|
||||
if (existing) {
|
||||
return existing.id;
|
||||
}
|
||||
this.voiceAssistantAgentId = null;
|
||||
try {
|
||||
const hydrated = await this.ensureAgentLoaded(this.voiceAssistantAgentId);
|
||||
this.voiceAssistantAgentId = hydrated.id;
|
||||
return hydrated.id;
|
||||
} catch (error) {
|
||||
this.sessionLogger.debug(
|
||||
{ err: error, voiceAssistantAgentId: this.voiceAssistantAgentId },
|
||||
"Voice assistant agent not found in active/persisted state; creating new session"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const provider = this.resolveVoiceAgentProvider();
|
||||
const voiceAgentId = `voice-${uuidv4()}`;
|
||||
const voiceAgentId = this.voiceAssistantAgentId ?? uuidv4();
|
||||
const cwd = join(this.paseoHome, "voice-agent-workspace");
|
||||
await mkdir(cwd, { recursive: true });
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ const PrincipalParamSchema = z.union([
|
||||
z.object({ pattern: z.string() }).transform((d) => ({ type: "text" as const, value: d.pattern })),
|
||||
z.object({ query: z.string() }).transform((d) => ({ type: "text" as const, value: d.query })),
|
||||
z.object({ url: z.string() }).transform((d) => ({ type: "text" as const, value: d.url })),
|
||||
z.object({ text: z.string() }).transform((d) => ({ type: "text" as const, value: d.text })),
|
||||
// Files array (Codex apply_patch)
|
||||
z.object({ files: z.array(FileEntrySchema).nonempty() }).transform((d) => ({ type: "path" as const, value: d.files[0].path })),
|
||||
// TodoWrite - show in_progress item or count
|
||||
|
||||
Reference in New Issue
Block a user