refactor: remove executeCommand in favor of standard run/stream path
This commit is contained in:
@@ -31,7 +31,6 @@ import type {
|
|||||||
PaseoWorktreeArchiveResponse,
|
PaseoWorktreeArchiveResponse,
|
||||||
ProjectIconResponse,
|
ProjectIconResponse,
|
||||||
ListCommandsResponse,
|
ListCommandsResponse,
|
||||||
ExecuteCommandResponse,
|
|
||||||
ListProviderModelsResponseMessage,
|
ListProviderModelsResponseMessage,
|
||||||
ListAvailableProvidersResponse,
|
ListAvailableProvidersResponse,
|
||||||
SpeechModelsListResponse,
|
SpeechModelsListResponse,
|
||||||
@@ -208,7 +207,6 @@ type ListAvailableProvidersPayload = ListAvailableProvidersResponse["payload"];
|
|||||||
type SpeechModelsListPayload = SpeechModelsListResponse["payload"];
|
type SpeechModelsListPayload = SpeechModelsListResponse["payload"];
|
||||||
type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"];
|
type SpeechModelsDownloadPayload = SpeechModelsDownloadResponse["payload"];
|
||||||
type ListCommandsPayload = ListCommandsResponse["payload"];
|
type ListCommandsPayload = ListCommandsResponse["payload"];
|
||||||
type ExecuteCommandPayload = ExecuteCommandResponse["payload"];
|
|
||||||
type ListCommandsDraftConfig = Pick<
|
type ListCommandsDraftConfig = Pick<
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId"
|
"provider" | "cwd" | "modeId" | "model" | "thinkingOptionId"
|
||||||
@@ -2209,25 +2207,6 @@ export class DaemonClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeCommand(
|
|
||||||
agentId: string,
|
|
||||||
commandName: string,
|
|
||||||
args?: string,
|
|
||||||
requestId?: string
|
|
||||||
): Promise<ExecuteCommandPayload> {
|
|
||||||
return this.sendCorrelatedSessionRequest({
|
|
||||||
requestId,
|
|
||||||
message: {
|
|
||||||
type: "execute_command_request",
|
|
||||||
agentId,
|
|
||||||
commandName,
|
|
||||||
args,
|
|
||||||
},
|
|
||||||
responseType: "execute_command_response",
|
|
||||||
timeout: 30000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Permissions
|
// Permissions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -281,15 +281,6 @@ export type AgentSlashCommand = {
|
|||||||
argumentHint: string;
|
argumentHint: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Result from executing a slash command.
|
|
||||||
*/
|
|
||||||
export type AgentCommandResult = {
|
|
||||||
text: string;
|
|
||||||
timeline: AgentTimelineItem[];
|
|
||||||
usage?: AgentUsage;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ListPersistedAgentsOptions = {
|
export type ListPersistedAgentsOptions = {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
};
|
};
|
||||||
@@ -353,13 +344,6 @@ export interface AgentSession {
|
|||||||
* Commands are provider-specific - Claude supports skills and built-in commands.
|
* Commands are provider-specific - Claude supports skills and built-in commands.
|
||||||
*/
|
*/
|
||||||
listCommands?(): Promise<AgentSlashCommand[]>;
|
listCommands?(): Promise<AgentSlashCommand[]>;
|
||||||
/**
|
|
||||||
* Execute a slash command by name.
|
|
||||||
* The command name should NOT include the leading "/" - it will be added automatically.
|
|
||||||
* @param commandName The command name (e.g., "help", "context")
|
|
||||||
* @param args Optional arguments to pass to the command
|
|
||||||
*/
|
|
||||||
executeCommand?(commandName: string, args?: string): Promise<AgentCommandResult>;
|
|
||||||
/**
|
/**
|
||||||
* Update the model used for subsequent turns (if supported by provider).
|
* Update the model used for subsequent turns (if supported by provider).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
*
|
*
|
||||||
* Tests the ability to:
|
* Tests the ability to:
|
||||||
* 1. List available slash commands from a ClaudeAgentSession
|
* 1. List available slash commands from a ClaudeAgentSession
|
||||||
* 2. Execute slash commands through ClaudeAgentSession
|
|
||||||
*
|
*
|
||||||
* These tests verify that the agent abstraction layer properly exposes
|
* These tests verify that the agent abstraction layer properly exposes
|
||||||
* the Claude Agent SDK's command capabilities.
|
* the Claude Agent SDK's command capabilities.
|
||||||
@@ -91,77 +90,4 @@ const hasClaudeCredentials =
|
|||||||
}, 30000);
|
}, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("executeCommand()", () => {
|
|
||||||
it("should execute a command and return a result", async () => {
|
|
||||||
session = await client.createSession(testConfig);
|
|
||||||
|
|
||||||
// The session should have an executeCommand method
|
|
||||||
expect(typeof session.executeCommand).toBe("function");
|
|
||||||
|
|
||||||
// First get available commands to use one that exists
|
|
||||||
const commands = await session.listCommands!();
|
|
||||||
expect(commands.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
// Execute one of the available commands
|
|
||||||
const firstCommand = commands[0];
|
|
||||||
const result = await session.executeCommand!(firstCommand.name);
|
|
||||||
|
|
||||||
// Should return a result object
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(typeof result.text).toBe("string");
|
|
||||||
// Note: Some commands may return empty text (e.g., plugin descriptions)
|
|
||||||
|
|
||||||
await session.close();
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
it("should return non-empty output for /context command", async () => {
|
|
||||||
session = await client.createSession(testConfig);
|
|
||||||
|
|
||||||
// Execute /context - a local command that returns token usage info
|
|
||||||
const result = await session.executeCommand!("context");
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
expect(typeof result.text).toBe("string");
|
|
||||||
|
|
||||||
// /context MUST return actual content - this is the key test
|
|
||||||
// If this fails, user messages from SDK are not being captured
|
|
||||||
expect(result.text.length).toBeGreaterThan(0);
|
|
||||||
expect(result.text).toContain("Context");
|
|
||||||
|
|
||||||
await session.close();
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
it("should include timeline items in result", async () => {
|
|
||||||
session = await client.createSession(testConfig);
|
|
||||||
|
|
||||||
// First get available commands to use one that exists
|
|
||||||
const commands = await session.listCommands!();
|
|
||||||
expect(commands.length).toBeGreaterThan(0);
|
|
||||||
|
|
||||||
const firstCommand = commands[0];
|
|
||||||
const result = await session.executeCommand!(firstCommand.name);
|
|
||||||
|
|
||||||
// Result should include timeline items from the command execution
|
|
||||||
expect(Array.isArray(result.timeline)).toBe(true);
|
|
||||||
|
|
||||||
// Timeline may or may not have items depending on command execution
|
|
||||||
// Just verify the structure is correct
|
|
||||||
|
|
||||||
await session.close();
|
|
||||||
}, 60000);
|
|
||||||
|
|
||||||
it("should throw for non-existent command", async () => {
|
|
||||||
session = await client.createSession(testConfig);
|
|
||||||
|
|
||||||
// Non-existent commands should still work (they get sent as prompts)
|
|
||||||
// but the response should indicate the command doesn't exist
|
|
||||||
// This is how the SDK works - invalid commands become regular prompts
|
|
||||||
const result = await session.executeCommand!("nonexistent_command_xyz");
|
|
||||||
|
|
||||||
// The result should exist (command is treated as a prompt)
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
|
|
||||||
await session.close();
|
|
||||||
}, 60000);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AgentCapabilityFlags,
|
AgentCapabilityFlags,
|
||||||
AgentClient,
|
AgentClient,
|
||||||
AgentCommandResult,
|
|
||||||
AgentMetadata,
|
AgentMetadata,
|
||||||
AgentMode,
|
AgentMode,
|
||||||
AgentModelDefinition,
|
AgentModelDefinition,
|
||||||
@@ -1015,45 +1014,6 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeCommand(commandName: string, args?: string): Promise<AgentCommandResult> {
|
|
||||||
const commandPrompt = args ? `/${commandName} ${args}` : `/${commandName}`;
|
|
||||||
|
|
||||||
// Commands return their output in a user message with <local-command-stdout> tags,
|
|
||||||
// NOT as an assistant message. We need to extract that specifically.
|
|
||||||
const events = this.stream(commandPrompt);
|
|
||||||
const timeline: AgentTimelineItem[] = [];
|
|
||||||
let commandOutput = "";
|
|
||||||
let usage: AgentUsage | undefined;
|
|
||||||
|
|
||||||
for await (const event of events) {
|
|
||||||
if (event.type === "timeline") {
|
|
||||||
timeline.push(event.item);
|
|
||||||
// Check for command output in user messages
|
|
||||||
if (event.item.type === "user_message") {
|
|
||||||
const text = event.item.text;
|
|
||||||
const match = text.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
||||||
if (match) {
|
|
||||||
commandOutput = match[1].trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Also capture assistant messages (some commands may produce both)
|
|
||||||
if (event.item.type === "assistant_message" && !commandOutput) {
|
|
||||||
commandOutput = event.item.text;
|
|
||||||
}
|
|
||||||
} else if (event.type === "turn_completed") {
|
|
||||||
usage = event.usage;
|
|
||||||
} else if (event.type === "turn_failed") {
|
|
||||||
throw new Error(event.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: commandOutput,
|
|
||||||
timeline,
|
|
||||||
usage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async primeSelectableModelIds(query: Query): Promise<void> {
|
private async primeSelectableModelIds(query: Query): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const models = await query.supportedModels();
|
const models = await query.supportedModels();
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
test.runIf(isCodexInstalled())(
|
test.runIf(isCodexInstalled())(
|
||||||
"listCommands includes custom prompts and executeCommand matches run('/prompts:*') expansion",
|
"listCommands includes custom prompts and run('/prompts:*') expands them",
|
||||||
async () => {
|
async () => {
|
||||||
const cleanup = useTempCodexSessionDir();
|
const cleanup = useTempCodexSessionDir();
|
||||||
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||||
@@ -553,9 +553,6 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
|
|
||||||
const executeArgs = "NAME=world extra_value";
|
const executeArgs = "NAME=world extra_value";
|
||||||
const expectedExpanded = `${token}::name=world::pos1=extra_value::dollar=$`;
|
const expectedExpanded = `${token}::name=world::pos1=extra_value::dollar=$`;
|
||||||
const executeResult = await session.executeCommand?.("prompts:test", executeArgs);
|
|
||||||
expect(executeResult?.text).toContain(expectedExpanded);
|
|
||||||
|
|
||||||
const rawSlashInput = "/prompts:test NAME=world extra_value";
|
const rawSlashInput = "/prompts:test NAME=world extra_value";
|
||||||
const runResult = await session.run(rawSlashInput);
|
const runResult = await session.run(rawSlashInput);
|
||||||
expect(runResult.finalText).toContain(expectedExpanded);
|
expect(runResult.finalText).toContain(expectedExpanded);
|
||||||
@@ -571,6 +568,63 @@ describe("Codex app-server provider (integration)", () => {
|
|||||||
120000
|
120000
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test.runIf(isCodexInstalled())(
|
||||||
|
"slash prompt run streams live turn events (turn_started/turn_completed)",
|
||||||
|
async () => {
|
||||||
|
const cleanup = useTempCodexSessionDir();
|
||||||
|
const codexHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||||
|
const promptsDir = path.join(codexHome, "prompts");
|
||||||
|
const promptPath = path.join(promptsDir, "stream-test.md");
|
||||||
|
const cwd = tmpCwd("codex-cmd-stream-");
|
||||||
|
const token = `PASEO_STREAM_TOKEN_${Date.now()}`;
|
||||||
|
|
||||||
|
mkdirSync(promptsDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
promptPath,
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"description: Stream Test Prompt",
|
||||||
|
"---",
|
||||||
|
`Reply with exactly: ${token}`,
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new CodexAppServerAgentClient(logger);
|
||||||
|
const session = await client.createSession({
|
||||||
|
provider: "codex",
|
||||||
|
cwd,
|
||||||
|
modeId: "auto",
|
||||||
|
model: CODEX_TEST_MODEL,
|
||||||
|
thinkingOptionId: CODEX_TEST_THINKING_OPTION_ID,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const events = session.stream("/prompts:stream-test");
|
||||||
|
const seenTypes = new Set<string>();
|
||||||
|
const assistantChunks: string[] = [];
|
||||||
|
for await (const event of events) {
|
||||||
|
seenTypes.add(event.type);
|
||||||
|
if (event.type === "timeline" && event.item.type === "assistant_message") {
|
||||||
|
assistantChunks.push(event.item.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(seenTypes.has("turn_started")).toBe(true);
|
||||||
|
expect(seenTypes.has("turn_completed")).toBe(true);
|
||||||
|
expect(assistantChunks.join("")).toContain(token);
|
||||||
|
} finally {
|
||||||
|
await session.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
rmSync(promptPath, { force: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
120000
|
||||||
|
);
|
||||||
|
|
||||||
test.runIf(isCodexInstalled())("command approval flow requests permission and runs command", async () => {
|
test.runIf(isCodexInstalled())("command approval flow requests permission and runs command", async () => {
|
||||||
const cleanup = useTempCodexSessionDir();
|
const cleanup = useTempCodexSessionDir();
|
||||||
const cwd = tmpCwd("codex-cmd-approval-");
|
const cwd = tmpCwd("codex-cmd-approval-");
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AgentCapabilityFlags,
|
AgentCapabilityFlags,
|
||||||
AgentClient,
|
AgentClient,
|
||||||
AgentCommandResult,
|
|
||||||
AgentMode,
|
AgentMode,
|
||||||
AgentModelDefinition,
|
AgentModelDefinition,
|
||||||
McpServerConfig,
|
McpServerConfig,
|
||||||
@@ -2034,17 +2033,48 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async buildCommandPromptInput(
|
||||||
|
commandName: string,
|
||||||
|
args?: string
|
||||||
|
): Promise<AgentPromptInput> {
|
||||||
|
if (commandName.startsWith("prompts:")) {
|
||||||
|
const promptName = commandName.slice("prompts:".length);
|
||||||
|
const codexHome = resolveCodexHomeDir();
|
||||||
|
const promptPath = path.join(codexHome, "prompts", `${promptName}.md`);
|
||||||
|
const raw = await fs.readFile(promptPath, "utf8");
|
||||||
|
const parsed = parseFrontMatter(raw);
|
||||||
|
return expandCodexCustomPrompt(parsed.body, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.connected) {
|
||||||
|
await this.connect();
|
||||||
|
} else {
|
||||||
|
await this.loadSkills();
|
||||||
|
}
|
||||||
|
const skill = this.cachedSkills.find((entry) => entry.name === commandName);
|
||||||
|
if (skill) {
|
||||||
|
const input = [
|
||||||
|
{ type: "skill", name: skill.name, path: skill.path },
|
||||||
|
] as unknown as AgentPromptContentBlock[];
|
||||||
|
if (args && args.trim().length > 0) {
|
||||||
|
input.push({ type: "text", text: args.trim() });
|
||||||
|
} else {
|
||||||
|
input.push({ type: "text", text: `$${skill.name}` });
|
||||||
|
}
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
return args ? `$${commandName} ${args}` : `$${commandName}`;
|
||||||
|
}
|
||||||
|
|
||||||
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
||||||
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
|
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
|
||||||
if (slashCommand) {
|
if (slashCommand) {
|
||||||
const result = await this.executeCommand(slashCommand.commandName, slashCommand.args);
|
const commandInput = await this.buildCommandPromptInput(
|
||||||
const info = await this.getRuntimeInfo();
|
slashCommand.commandName,
|
||||||
return {
|
slashCommand.args
|
||||||
sessionId: info.sessionId ?? "",
|
);
|
||||||
finalText: result.text,
|
return this.runInternal(commandInput, options);
|
||||||
usage: result.usage,
|
|
||||||
timeline: result.timeline,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return this.runInternal(prompt, options);
|
return this.runInternal(prompt, options);
|
||||||
}
|
}
|
||||||
@@ -2086,15 +2116,11 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
): AsyncGenerator<AgentStreamEvent> {
|
): AsyncGenerator<AgentStreamEvent> {
|
||||||
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
|
const slashCommand = await this.resolveSlashCommandInvocation(prompt);
|
||||||
if (slashCommand) {
|
if (slashCommand) {
|
||||||
const result = await this.executeCommand(slashCommand.commandName, slashCommand.args);
|
const commandInput = await this.buildCommandPromptInput(
|
||||||
for (const item of result.timeline) {
|
slashCommand.commandName,
|
||||||
yield { type: "timeline", provider: CODEX_PROVIDER, item };
|
slashCommand.args
|
||||||
}
|
);
|
||||||
yield {
|
yield* this.streamInternal(commandInput, options);
|
||||||
type: "turn_completed",
|
|
||||||
provider: CODEX_PROVIDER,
|
|
||||||
usage: result.usage,
|
|
||||||
};
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
yield* this.streamInternal(prompt, options);
|
yield* this.streamInternal(prompt, options);
|
||||||
@@ -2404,42 +2430,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeCommand(commandName: string, args?: string): Promise<AgentCommandResult> {
|
|
||||||
if (commandName.startsWith("prompts:")) {
|
|
||||||
const promptName = commandName.slice("prompts:".length);
|
|
||||||
const codexHome = resolveCodexHomeDir();
|
|
||||||
const promptPath = path.join(codexHome, "prompts", `${promptName}.md`);
|
|
||||||
const raw = await fs.readFile(promptPath, "utf8");
|
|
||||||
const parsed = parseFrontMatter(raw);
|
|
||||||
const expanded = expandCodexCustomPrompt(parsed.body, args);
|
|
||||||
const result = await this.runInternal(expanded);
|
|
||||||
return { text: result.finalText, timeline: result.timeline, usage: result.usage };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.connected) {
|
|
||||||
await this.connect();
|
|
||||||
} else {
|
|
||||||
await this.loadSkills();
|
|
||||||
}
|
|
||||||
const skill = this.cachedSkills.find((entry) => entry.name === commandName);
|
|
||||||
if (skill) {
|
|
||||||
const input = [
|
|
||||||
{ type: "skill", name: skill.name, path: skill.path },
|
|
||||||
] as unknown as AgentPromptContentBlock[];
|
|
||||||
if (args && args.trim().length > 0) {
|
|
||||||
input.push({ type: "text", text: args.trim() });
|
|
||||||
} else {
|
|
||||||
input.push({ type: "text", text: `$${skill.name}` });
|
|
||||||
}
|
|
||||||
const result = await this.runInternal(input);
|
|
||||||
return { text: result.finalText, timeline: result.timeline, usage: result.usage };
|
|
||||||
}
|
|
||||||
|
|
||||||
const skillPrompt = args ? `$${commandName} ${args}` : `$${commandName}`;
|
|
||||||
const result = await this.runInternal(skillPrompt);
|
|
||||||
return { text: result.finalText, timeline: result.timeline, usage: result.usage };
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ensureThread(): Promise<void> {
|
private async ensureThread(): Promise<void> {
|
||||||
if (!this.client) return;
|
if (!this.client) return;
|
||||||
if (this.currentThreadId) return;
|
if (this.currentThreadId) return;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ describe("codex agent commands E2E", () => {
|
|||||||
rmSync(codexHome, { recursive: true, force: true });
|
rmSync(codexHome, { recursive: true, force: true });
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
test("executes a custom prompt command (prompts:*)", async () => {
|
test("executes a custom prompt via normal sendMessage path (prompts:*)", async () => {
|
||||||
const codexHome = process.env.CODEX_HOME ?? path.join(process.env.HOME ?? "/tmp", ".codex");
|
const codexHome = process.env.CODEX_HOME ?? path.join(process.env.HOME ?? "/tmp", ".codex");
|
||||||
|
|
||||||
const promptsDir = path.join(codexHome, "prompts");
|
const promptsDir = path.join(codexHome, "prompts");
|
||||||
@@ -95,18 +95,19 @@ describe("codex agent commands E2E", () => {
|
|||||||
title: "Codex Prompt Execute Test Agent",
|
title: "Codex Prompt Execute Test Agent",
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await ctx.client.executeCommand(
|
await ctx.client.sendMessage(
|
||||||
agent.id,
|
agent.id,
|
||||||
"prompts:paseo-test-sayok",
|
"/prompts:paseo-test-sayok NAME=world"
|
||||||
"NAME=world"
|
|
||||||
);
|
);
|
||||||
expect(result.error).toBeNull();
|
const state = await ctx.client.waitForFinish(agent.id, 30_000);
|
||||||
expect(result.result?.text).toContain("PASEO_OK");
|
|
||||||
|
expect(state.status).toBe("idle");
|
||||||
|
expect(state.lastMessage).toContain("PASEO_OK");
|
||||||
|
|
||||||
rmSync(promptPath, { force: true });
|
rmSync(promptPath, { force: true });
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
|
|
||||||
test("sendMessage routes known slash commands through executeCommand", async () => {
|
test("sendMessage routes known slash commands through standard run/stream", async () => {
|
||||||
const prevCodexHome = process.env.CODEX_HOME;
|
const prevCodexHome = process.env.CODEX_HOME;
|
||||||
const codexHome = tmpDir("codex-home-send-command-");
|
const codexHome = tmpDir("codex-home-send-command-");
|
||||||
const promptsDir = path.join(codexHome, "prompts");
|
const promptsDir = path.join(codexHome, "prompts");
|
||||||
|
|||||||
@@ -1527,15 +1527,6 @@ export class Session {
|
|||||||
await this.handleListCommandsRequest(msg);
|
await this.handleListCommandsRequest(msg);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "execute_command_request":
|
|
||||||
await this.handleExecuteCommandRequest(
|
|
||||||
msg.agentId,
|
|
||||||
msg.commandName,
|
|
||||||
msg.args,
|
|
||||||
msg.requestId
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "register_push_token":
|
case "register_push_token":
|
||||||
this.handleRegisterPushToken(msg.token);
|
this.handleRegisterPushToken(msg.token);
|
||||||
break;
|
break;
|
||||||
@@ -3553,79 +3544,6 @@ export class Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle execute command request for an agent
|
|
||||||
*/
|
|
||||||
private async handleExecuteCommandRequest(
|
|
||||||
agentId: string,
|
|
||||||
commandName: string,
|
|
||||||
args: string | undefined,
|
|
||||||
requestId: string
|
|
||||||
): Promise<void> {
|
|
||||||
this.sessionLogger.debug(
|
|
||||||
{ agentId, commandName },
|
|
||||||
`Handling execute command request for agent ${agentId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const agents = this.agentManager.listAgents();
|
|
||||||
const agent = agents.find((a) => a.id === agentId);
|
|
||||||
|
|
||||||
if (!agent) {
|
|
||||||
this.emit({
|
|
||||||
type: "execute_command_response",
|
|
||||||
payload: {
|
|
||||||
agentId,
|
|
||||||
result: null,
|
|
||||||
error: `Agent not found: ${agentId}`,
|
|
||||||
requestId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = agent.session;
|
|
||||||
if (!session || !session.executeCommand) {
|
|
||||||
this.emit({
|
|
||||||
type: "execute_command_response",
|
|
||||||
payload: {
|
|
||||||
agentId,
|
|
||||||
result: null,
|
|
||||||
error: `Agent does not support executing commands`,
|
|
||||||
requestId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await session.executeCommand(commandName, args);
|
|
||||||
|
|
||||||
this.emit({
|
|
||||||
type: "execute_command_response",
|
|
||||||
payload: {
|
|
||||||
agentId,
|
|
||||||
result,
|
|
||||||
error: null,
|
|
||||||
requestId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
this.sessionLogger.error(
|
|
||||||
{ err: error, agentId, commandName },
|
|
||||||
"Failed to execute command"
|
|
||||||
);
|
|
||||||
this.emit({
|
|
||||||
type: "execute_command_response",
|
|
||||||
payload: {
|
|
||||||
agentId,
|
|
||||||
result: null,
|
|
||||||
error: error.message,
|
|
||||||
requestId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle agent permission response from user
|
* Handle agent permission response from user
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import type {
|
|||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
AgentSlashCommand,
|
AgentSlashCommand,
|
||||||
AgentCommandResult,
|
|
||||||
AgentUsage,
|
AgentUsage,
|
||||||
ListModelsOptions,
|
ListModelsOptions,
|
||||||
} from "../agent/agent-sdk-types.js";
|
} from "../agent/agent-sdk-types.js";
|
||||||
@@ -251,7 +250,7 @@ class FakeAgentSession implements AgentSession {
|
|||||||
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
async run(prompt: AgentPromptInput, options?: AgentRunOptions): Promise<AgentRunResult> {
|
||||||
const slashCommand = await this.resolveSlashCommandInput(prompt);
|
const slashCommand = await this.resolveSlashCommandInput(prompt);
|
||||||
if (slashCommand) {
|
if (slashCommand) {
|
||||||
const result = await this.executeCommand(
|
const result = await this.runSlashCommand(
|
||||||
slashCommand.commandName,
|
slashCommand.commandName,
|
||||||
slashCommand.args
|
slashCommand.args
|
||||||
);
|
);
|
||||||
@@ -290,7 +289,7 @@ class FakeAgentSession implements AgentSession {
|
|||||||
await this.appendHistoryEvent(turnStarted);
|
await this.appendHistoryEvent(turnStarted);
|
||||||
yield turnStarted;
|
yield turnStarted;
|
||||||
|
|
||||||
const result = await this.executeCommand(
|
const result = await this.runSlashCommand(
|
||||||
slashCommand.commandName,
|
slashCommand.commandName,
|
||||||
slashCommand.args
|
slashCommand.args
|
||||||
);
|
);
|
||||||
@@ -578,7 +577,14 @@ class FakeAgentSession implements AgentSession {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeCommand(commandName: string, args?: string): Promise<AgentCommandResult> {
|
private async runSlashCommand(
|
||||||
|
commandName: string,
|
||||||
|
args?: string
|
||||||
|
): Promise<{
|
||||||
|
text: string;
|
||||||
|
timeline: AgentRunResult["timeline"];
|
||||||
|
usage: AgentUsage;
|
||||||
|
}> {
|
||||||
const fullName = commandName.trim();
|
const fullName = commandName.trim();
|
||||||
if (this.providerName === "codex" && fullName.startsWith("prompts:")) {
|
if (this.providerName === "codex" && fullName.startsWith("prompts:")) {
|
||||||
const promptId = fullName.slice("prompts:".length);
|
const promptId = fullName.slice("prompts:".length);
|
||||||
|
|||||||
@@ -941,14 +941,6 @@ export const ListCommandsRequestSchema = z.object({
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ExecuteCommandRequestSchema = z.object({
|
|
||||||
type: z.literal("execute_command_request"),
|
|
||||||
agentId: z.string(),
|
|
||||||
commandName: z.string(),
|
|
||||||
args: z.string().optional(),
|
|
||||||
requestId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const RegisterPushTokenMessageSchema = z.object({
|
export const RegisterPushTokenMessageSchema = z.object({
|
||||||
type: z.literal("register_push_token"),
|
type: z.literal("register_push_token"),
|
||||||
token: z.string(),
|
token: z.string(),
|
||||||
@@ -1082,7 +1074,6 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
ClientHeartbeatMessageSchema,
|
ClientHeartbeatMessageSchema,
|
||||||
PingMessageSchema,
|
PingMessageSchema,
|
||||||
ListCommandsRequestSchema,
|
ListCommandsRequestSchema,
|
||||||
ExecuteCommandRequestSchema,
|
|
||||||
RegisterPushTokenMessageSchema,
|
RegisterPushTokenMessageSchema,
|
||||||
ListTerminalsRequestSchema,
|
ListTerminalsRequestSchema,
|
||||||
SubscribeTerminalsRequestSchema,
|
SubscribeTerminalsRequestSchema,
|
||||||
@@ -1868,22 +1859,6 @@ export const ListCommandsResponseSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const AgentCommandResultSchema = z.object({
|
|
||||||
text: z.string(),
|
|
||||||
timeline: z.array(AgentTimelineItemPayloadSchema),
|
|
||||||
usage: AgentUsageSchema.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ExecuteCommandResponseSchema = z.object({
|
|
||||||
type: z.literal("execute_command_response"),
|
|
||||||
payload: z.object({
|
|
||||||
agentId: z.string(),
|
|
||||||
result: AgentCommandResultSchema.nullable(),
|
|
||||||
error: z.string().nullable(),
|
|
||||||
requestId: z.string(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Terminal Outbound Messages
|
// Terminal Outbound Messages
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -2050,7 +2025,6 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [
|
|||||||
SpeechModelsListResponseSchema,
|
SpeechModelsListResponseSchema,
|
||||||
SpeechModelsDownloadResponseSchema,
|
SpeechModelsDownloadResponseSchema,
|
||||||
ListCommandsResponseSchema,
|
ListCommandsResponseSchema,
|
||||||
ExecuteCommandResponseSchema,
|
|
||||||
ListTerminalsResponseSchema,
|
ListTerminalsResponseSchema,
|
||||||
TerminalsChangedSchema,
|
TerminalsChangedSchema,
|
||||||
CreateTerminalResponseSchema,
|
CreateTerminalResponseSchema,
|
||||||
@@ -2191,8 +2165,6 @@ export type ClearAgentAttentionMessage = z.infer<typeof ClearAgentAttentionMessa
|
|||||||
export type ClientHeartbeatMessage = z.infer<typeof ClientHeartbeatMessageSchema>;
|
export type ClientHeartbeatMessage = z.infer<typeof ClientHeartbeatMessageSchema>;
|
||||||
export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>;
|
export type ListCommandsRequest = z.infer<typeof ListCommandsRequestSchema>;
|
||||||
export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>;
|
export type ListCommandsResponse = z.infer<typeof ListCommandsResponseSchema>;
|
||||||
export type ExecuteCommandRequest = z.infer<typeof ExecuteCommandRequestSchema>;
|
|
||||||
export type ExecuteCommandResponse = z.infer<typeof ExecuteCommandResponseSchema>;
|
|
||||||
export type RegisterPushTokenMessage = z.infer<typeof RegisterPushTokenMessageSchema>;
|
export type RegisterPushTokenMessage = z.infer<typeof RegisterPushTokenMessageSchema>;
|
||||||
|
|
||||||
// Terminal message types
|
// Terminal message types
|
||||||
|
|||||||
Reference in New Issue
Block a user