refactor(agent): add wait_for_agent tool and simplify prompt sending API
This commit is contained in:
@@ -1,48 +0,0 @@
|
|||||||
# Agent Lifecycle
|
|
||||||
|
|
||||||
This project uses client-driven lazy initialization for Claude Code agents. Sessions start without spinning up agent runtimes; the client opts in when needed.
|
|
||||||
|
|
||||||
## Connection Bootstrapping
|
|
||||||
- The session emits `session_state` after connection with an array of agents. Each entry matches the server `AgentInfo` structure (`id`, `status`, `createdAt`, `type`, `sessionId`, `error`, `currentModeId`, `availableModes`, `title`, `cwd`).
|
|
||||||
- No history or runtime is loaded at this point. Every agent starts in `uninitialized` and only streams live status updates once subscribed.
|
|
||||||
|
|
||||||
## Opting In To An Agent
|
|
||||||
1. When the UI needs an agent, send an inbound session message:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "initialize_agent_request",
|
|
||||||
"agentId": "<agent-id>",
|
|
||||||
"requestId": "<optional-correlation-id>"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. The session subscribes to the agent, calls `AgentManager.initializeAgentAndGetHistory`, and lazily starts the runtime if required.
|
|
||||||
3. The server responds with:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"type": "agent_initialized",
|
|
||||||
"payload": {
|
|
||||||
"agentId": "<agent-id>",
|
|
||||||
"info": { /* same shape as session_state agents */ },
|
|
||||||
"updates": [
|
|
||||||
{
|
|
||||||
"agentId": "<agent-id>",
|
|
||||||
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
||||||
"notification": { /* AgentNotification */ }
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestId": "<optional-correlation-id>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. After the response, all ongoing traffic uses the existing channels:
|
|
||||||
- `agent_status` continues to broadcast status changes.
|
|
||||||
- `agent_update` streams real-time session notifications, permission prompts, etc.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- `initialize_agent_request` is idempotent. The manager short-circuits if the agent is already ready and returns the cached history.
|
|
||||||
- Session state no longer replays history when clients reconnect; clients must explicitly reinitialize agents they care about.
|
|
||||||
- `requestId` lets the caller correlate UI state but remains optional throughout the flow.
|
|
||||||
Generated
+124
@@ -8302,6 +8302,119 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp/-/codex-acp-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-hX3N06QgGFM5mleB+L/+v8XTZ4iS4ScB5YXQXXbc20fYmkd5p6tEDG7m6p8sjtfDcdeKIZNtiiaEEHQqy2hVAA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"codex-acp": "bin/codex-acp.js"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@zed-industries/codex-acp-darwin-arm64": "0.3.12",
|
||||||
|
"@zed-industries/codex-acp-darwin-x64": "0.3.12",
|
||||||
|
"@zed-industries/codex-acp-linux-arm64": "0.3.12",
|
||||||
|
"@zed-industries/codex-acp-linux-x64": "0.3.12",
|
||||||
|
"@zed-industries/codex-acp-windows-arm64": "0.3.12",
|
||||||
|
"@zed-industries/codex-acp-windows-x64": "0.3.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp-darwin-arm64": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp-darwin-arm64/-/codex-acp-darwin-arm64-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-ajG2aU9I4Hg57Havk+elLR47tXPmxSsnSvZE0yxITiHr59IQacVJL0z/7hDdY3S6H10K+zb/fGQAvOXhcSMEtQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-acp-darwin-arm64": "bin/codex-acp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp-darwin-x64": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp-darwin-x64/-/codex-acp-darwin-x64-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-YQMe+G6hiVLY4R/lbC2UGkPJjnR8viIjaHmSETTfuOHAeQF9p8Wvr0k1DbIvacdyPst6X7ItoIb40fvtgsmVdw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-acp-darwin-x64": "bin/codex-acp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp-linux-arm64": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp-linux-arm64/-/codex-acp-linux-arm64-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-nrDFzs03Cm0/CB/sEvGPHJHl2Z0pMCSXEmpDVoUTKgZf2Iof23ZsV/VD+eHdLj98VEvmWiT2u7+kSBqBtpnMmA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-acp-linux-arm64": "bin/codex-acp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp-linux-x64": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp-linux-x64/-/codex-acp-linux-x64-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-BWGzV8dmCVAG64FfER6xtBkWedo9VX6u9ZjxOxr7JA9FhQ7KSPycH9r0BV/Nym/6LlvS4hLMcgXRvSfup8gLMA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-acp-linux-x64": "bin/codex-acp"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp-windows-arm64": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp-windows-arm64/-/codex-acp-windows-arm64-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-iQgXFk3OH8WlJgxMMSc+nDcrVH6m0HpItYca07fbrD4LdR+0ByW7EEy7VvONcuYqkmKLOHT1ufuQZEEraZp1WQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"windows"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-acp-windows-arm64": "bin/codex-acp.exe"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@zed-industries/codex-acp-windows-x64": {
|
||||||
|
"version": "0.3.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@zed-industries/codex-acp-windows-x64/-/codex-acp-windows-x64-0.3.12.tgz",
|
||||||
|
"integrity": "sha512-26fYm64qW87eRVC2yqAdeIVFdid+tpnfsNAn1Fc6zKcGpzgxfbYxfStjKUltoxI/X0ZHQI/4GvWpeNyshg5XGQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"windows"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"codex-acp-windows-x64": "bin/codex-acp.exe"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/abort-controller": {
|
"node_modules/abort-controller": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
@@ -12070,6 +12183,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-clipboard": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-RIKDsuHkYfaspifbFpVC8sBVFKR05L7Pj7mU2/XkbrW9m01OBNvdpGraXEMsTFCx97xMGsZpEw9pPquL4j4xVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
"version": "18.0.10",
|
"version": "18.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.10.tgz",
|
||||||
@@ -21185,6 +21307,7 @@
|
|||||||
"expo-audio": "~1.0.13",
|
"expo-audio": "~1.0.13",
|
||||||
"expo-av": "^16.0.7",
|
"expo-av": "^16.0.7",
|
||||||
"expo-build-properties": "^1.0.9",
|
"expo-build-properties": "^1.0.9",
|
||||||
|
"expo-clipboard": "~6.0.3",
|
||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
"expo-dev-client": "^6.0.15",
|
"expo-dev-client": "^6.0.15",
|
||||||
"expo-file-system": "~19.0.17",
|
"expo-file-system": "~19.0.17",
|
||||||
@@ -21237,6 +21360,7 @@
|
|||||||
"@deepgram/sdk": "^3.4.0",
|
"@deepgram/sdk": "^3.4.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.20.1",
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
||||||
"@openrouter/ai-sdk-provider": "^1.2.0",
|
"@openrouter/ai-sdk-provider": "^1.2.0",
|
||||||
|
"@zed-industries/codex-acp": "^0.3.9",
|
||||||
"ai": "^5.0.76",
|
"ai": "^5.0.76",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"expo-audio": "~1.0.13",
|
"expo-audio": "~1.0.13",
|
||||||
"expo-av": "^16.0.7",
|
"expo-av": "^16.0.7",
|
||||||
"expo-build-properties": "^1.0.9",
|
"expo-build-properties": "^1.0.9",
|
||||||
|
"expo-clipboard": "~6.0.3",
|
||||||
"expo-constants": "~18.0.9",
|
"expo-constants": "~18.0.9",
|
||||||
"expo-dev-client": "^6.0.15",
|
"expo-dev-client": "^6.0.15",
|
||||||
"expo-file-system": "~19.0.17",
|
"expo-file-system": "~19.0.17",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { View, Text, Pressable, Animated } from "react-native";
|
import { View, Text, Pressable, Animated } from "react-native";
|
||||||
import { useState, useEffect, useRef, memo, useMemo } from "react";
|
import { useState, useEffect, useRef, memo, useMemo, useCallback } from "react";
|
||||||
import Markdown from "react-native-markdown-display";
|
import Markdown from "react-native-markdown-display";
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { StyleSheet } from "react-native-unistyles";
|
import { StyleSheet } from "react-native-unistyles";
|
||||||
import { baseColors, theme } from "@/styles/theme";
|
import { baseColors, theme } from "@/styles/theme";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
import * as Clipboard from "expo-clipboard";
|
||||||
|
|
||||||
interface UserMessageProps {
|
interface UserMessageProps {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -47,17 +48,79 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.lg,
|
fontSize: theme.fontSize.lg,
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
},
|
},
|
||||||
|
bubblePressed: {
|
||||||
|
opacity: 0.85,
|
||||||
|
},
|
||||||
|
copiedTagContainer: {
|
||||||
|
marginTop: theme.spacing[1],
|
||||||
|
marginRight: theme.spacing[4],
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
backgroundColor: theme.colors.secondary,
|
||||||
|
borderRadius: theme.borderRadius.base,
|
||||||
|
paddingHorizontal: theme.spacing[2],
|
||||||
|
paddingVertical: 4,
|
||||||
|
},
|
||||||
|
copiedTagText: {
|
||||||
|
color: theme.colors.secondaryForeground,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const UserMessage = memo(function UserMessage({
|
export const UserMessage = memo(function UserMessage({
|
||||||
message,
|
message,
|
||||||
timestamp,
|
timestamp,
|
||||||
}: UserMessageProps) {
|
}: UserMessageProps) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
const handleLongPress = useCallback(async () => {
|
||||||
|
if (!message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Clipboard.setStringAsync(message);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
if (copyTimeoutRef.current) {
|
||||||
|
clearTimeout(copyTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
copyTimeoutRef.current = setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
copyTimeoutRef.current = null;
|
||||||
|
}, 1500);
|
||||||
|
}, [message]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (copyTimeoutRef.current) {
|
||||||
|
clearTimeout(copyTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={userMessageStylesheet.container}>
|
<View style={userMessageStylesheet.container}>
|
||||||
<View style={userMessageStylesheet.bubble}>
|
<Pressable
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Copy message"
|
||||||
|
accessibilityHint="Long press to copy this message"
|
||||||
|
delayLongPress={250}
|
||||||
|
onLongPress={handleLongPress}
|
||||||
|
style={({ pressed }) => [
|
||||||
|
userMessageStylesheet.bubble,
|
||||||
|
pressed ? userMessageStylesheet.bubblePressed : null,
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Text style={userMessageStylesheet.text}>{message}</Text>
|
<Text style={userMessageStylesheet.text}>{message}</Text>
|
||||||
</View>
|
</Pressable>
|
||||||
|
{copied && (
|
||||||
|
<View style={userMessageStylesheet.copiedTagContainer}>
|
||||||
|
<Text style={userMessageStylesheet.copiedTagText}>
|
||||||
|
Copied to clipboard
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
CLAUDE.md
|
||||||
@@ -335,7 +335,7 @@ present_artifact({
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Claude Code Integration
|
## 5. Agent Integrations
|
||||||
|
|
||||||
### Your Role: Orchestrator
|
### Your Role: Orchestrator
|
||||||
|
|
||||||
@@ -354,27 +354,43 @@ Load the agent list before any agent interaction. Always.
|
|||||||
- Quick commands → Execute directly
|
- Quick commands → Execute directly
|
||||||
- Active agent context → Send prompt to that agent
|
- Active agent context → Send prompt to that agent
|
||||||
|
|
||||||
### What is Claude Code?
|
### Available Agents (Source of Truth)
|
||||||
|
|
||||||
Claude Code is an AI coding agent that can handle complex coding tasks. Delegate work to it by creating agents with clear instructions.
|
We only have two coding agents. Do not call tools to discover them—treat this section as canonical. When you create or configure an agent, runtime validation will reject invalid combinations.
|
||||||
|
|
||||||
|
**Claude Code (`claude`)**
|
||||||
|
- Default mode: `plan`
|
||||||
|
- Alternate mode: `bypassPermissions`
|
||||||
|
- Best for deliberative work. Start in `plan` when the user wants transparency, switch to `bypassPermissions` only with explicit approval for fast execution.
|
||||||
|
|
||||||
|
**Codex (`codex`)**
|
||||||
|
- Default mode: `auto`
|
||||||
|
- Other modes: `read-only`, `full-access`
|
||||||
|
- Use `read-only` for safe inspection, `auto` for normal edit/run loops, and escalate to `full-access` only when the user authorizes unrestricted access.
|
||||||
|
|
||||||
### Creating Agents
|
### Creating Agents
|
||||||
|
|
||||||
**Creation requires confirmation. Always ask first.**
|
**Creation requires confirmation. Always ask first.**
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// After confirmation
|
// Claude Code with planning
|
||||||
create_coding_agent({
|
create_coding_agent({
|
||||||
cwd: "~/dev/voice-dev",
|
cwd: "~/dev/voice-dev",
|
||||||
|
agentType: "claude",
|
||||||
initialPrompt: "add dark mode toggle to settings page",
|
initialPrompt: "add dark mode toggle to settings page",
|
||||||
initialMode: "bypassPermissions"
|
initialMode: "plan"
|
||||||
|
})
|
||||||
|
|
||||||
|
// Codex for quick edits
|
||||||
|
create_coding_agent({
|
||||||
|
cwd: "~/dev/voice-dev",
|
||||||
|
agentType: "codex",
|
||||||
|
initialPrompt: "clean up the logging",
|
||||||
|
initialMode: "auto"
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Modes:**
|
If the user omits `initialMode`, the defaults above apply. Invalid agentType/mode pairs will throw—just surface the error.
|
||||||
- `"bypassPermissions"` - Auto-approve (fastest, default for most tasks)
|
|
||||||
- `"plan"` - Shows plan before executing
|
|
||||||
- `"default"` - Asks permission per action
|
|
||||||
|
|
||||||
### Working with Agents
|
### Working with Agents
|
||||||
|
|
||||||
@@ -395,11 +411,11 @@ send_agent_prompt({
|
|||||||
maxWait: 60000 // Wait up to 60 seconds
|
maxWait: 60000 // Wait up to 60 seconds
|
||||||
})
|
})
|
||||||
|
|
||||||
// Change mode and send prompt
|
// Change mode and send prompt (Claude -> bypassPermissions, Codex -> full-access)
|
||||||
send_agent_prompt({
|
send_agent_prompt({
|
||||||
agentId: "abc123",
|
agentId: "abc123",
|
||||||
prompt: "implement user registration",
|
prompt: "implement user registration",
|
||||||
sessionMode: "bypassPermissions" // Auto-approve all actions
|
sessionMode: "bypassPermissions"
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -447,11 +463,12 @@ list_agents()
|
|||||||
// You: "Create agent in ~/dev/project for authentication?"
|
// You: "Create agent in ~/dev/project for authentication?"
|
||||||
// User: "yes"
|
// User: "yes"
|
||||||
|
|
||||||
// 3. Create with initialPrompt + mode
|
// 3. Create with type + mode
|
||||||
create_coding_agent({
|
create_coding_agent({
|
||||||
cwd: "~/dev/project",
|
cwd: "~/dev/project",
|
||||||
|
agentType: "claude",
|
||||||
initialPrompt: "add authentication",
|
initialPrompt: "add authentication",
|
||||||
initialMode: "bypassPermissions"
|
initialMode: "plan"
|
||||||
})
|
})
|
||||||
|
|
||||||
// 4. Monitor or send follow-up tasks
|
// 4. Monitor or send follow-up tasks
|
||||||
|
|||||||
@@ -96,6 +96,73 @@ describe("AgentManager", () => {
|
|||||||
}
|
}
|
||||||
}, 120000);
|
}, 120000);
|
||||||
|
|
||||||
|
it("should wait for permission requests", async () => {
|
||||||
|
const manager = new AgentManager();
|
||||||
|
let capturedPermission: Awaited<
|
||||||
|
ReturnType<(typeof manager)["waitForPermissionRequest"]>
|
||||||
|
> | null = null;
|
||||||
|
|
||||||
|
const agentId = await manager.createAgent({
|
||||||
|
cwd: tmpDir,
|
||||||
|
type: "claude",
|
||||||
|
initialMode: "plan",
|
||||||
|
});
|
||||||
|
createdAgents.push({ manager, agentId });
|
||||||
|
|
||||||
|
const waitPromise = manager
|
||||||
|
.waitForPermissionRequest(agentId)
|
||||||
|
.then((permission) => {
|
||||||
|
capturedPermission = permission;
|
||||||
|
return permission;
|
||||||
|
});
|
||||||
|
|
||||||
|
await manager.sendPrompt(
|
||||||
|
agentId,
|
||||||
|
"Create a file called wait-for-permission.txt with the content 'hello world'"
|
||||||
|
);
|
||||||
|
|
||||||
|
const permission = await waitPromise;
|
||||||
|
expect(permission).not.toBeNull();
|
||||||
|
expect(permission!.agentId).toBe(agentId);
|
||||||
|
expect(permission!.requestId).toBeDefined();
|
||||||
|
expect(permission!.options.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const acceptOption = permission!.options.find(
|
||||||
|
(option) => option.kind === "allow_once" || option.kind === "allow_always"
|
||||||
|
);
|
||||||
|
expect(acceptOption).toBeDefined();
|
||||||
|
|
||||||
|
manager.respondToPermission(
|
||||||
|
permission!.agentId,
|
||||||
|
permission!.requestId,
|
||||||
|
acceptOption!.optionId
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure the captured permission matches the resolved value
|
||||||
|
expect(capturedPermission).toEqual(permission);
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
|
it("should support aborting waitForPermissionRequest", async () => {
|
||||||
|
const manager = new AgentManager();
|
||||||
|
|
||||||
|
const agentId = await manager.createAgent({
|
||||||
|
cwd: tmpDir,
|
||||||
|
type: "claude",
|
||||||
|
initialMode: "plan",
|
||||||
|
});
|
||||||
|
createdAgents.push({ manager, agentId });
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
const waitPromise = manager.waitForPermissionRequest(agentId, {
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
controller.abort();
|
||||||
|
|
||||||
|
await expect(waitPromise).rejects.toMatchObject({ name: "AbortError" });
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
it("should not fail when sending '.' after plan permission request", async () => {
|
it("should not fail when sending '.' after plan permission request", async () => {
|
||||||
const manager = new AgentManager();
|
const manager = new AgentManager();
|
||||||
let permissionRequest: RequestPermissionRequest | null = null;
|
let permissionRequest: RequestPermissionRequest | null = null;
|
||||||
|
|||||||
@@ -379,14 +379,13 @@ export class AgentManager {
|
|||||||
* Send a prompt to an agent
|
* Send a prompt to an agent
|
||||||
* @param agentId - Agent ID
|
* @param agentId - Agent ID
|
||||||
* @param prompt - The prompt text or ContentBlock array
|
* @param prompt - The prompt text or ContentBlock array
|
||||||
* @param options - Optional settings: maxWait (ms), sessionMode to set before sending, messageId for deduplication
|
* @param options - Optional settings: sessionMode to set before sending, messageId for deduplication
|
||||||
* @returns Object with didComplete boolean indicating if agent finished within maxWait time
|
|
||||||
*/
|
*/
|
||||||
async sendPrompt(
|
async sendPrompt(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
prompt: string | ContentBlock[],
|
prompt: string | ContentBlock[],
|
||||||
options?: { maxWait?: number; sessionMode?: string; messageId?: string }
|
options?: { sessionMode?: string; messageId?: string }
|
||||||
): Promise<{ didComplete: boolean; stopReason?: string }> {
|
): Promise<void> {
|
||||||
const agent = this.agents.get(agentId);
|
const agent = this.agents.get(agentId);
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
throw new Error(`Agent ${agentId} not found`);
|
throw new Error(`Agent ${agentId} not found`);
|
||||||
@@ -577,38 +576,9 @@ export class AgentManager {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const maxWait = options?.maxWait;
|
console.log(
|
||||||
|
`[Agent ${agentId}] Prompt sent (non-blocking, use get_agent_status to check completion)`
|
||||||
// If no maxWait specified, return immediately
|
);
|
||||||
if (maxWait === undefined) {
|
|
||||||
console.log(
|
|
||||||
`[Agent ${agentId}] Prompt sent (non-blocking, use get_agent_status to check completion)`
|
|
||||||
);
|
|
||||||
return { didComplete: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
// If maxWait specified, race between completion and timeout
|
|
||||||
try {
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error("timeout")), maxWait)
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await Promise.race([promptPromise, timeoutPromise]);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[Agent ${agentId}] Prompt completed within ${maxWait}ms with stopReason: ${response.stopReason}`
|
|
||||||
);
|
|
||||||
return { didComplete: true, stopReason: response.stopReason };
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === "timeout") {
|
|
||||||
console.log(
|
|
||||||
`[Agent ${agentId}] Prompt did not complete within ${maxWait}ms (still processing)`
|
|
||||||
);
|
|
||||||
return { didComplete: false };
|
|
||||||
}
|
|
||||||
// Real error - rethrow
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1701,6 +1671,158 @@ export class AgentManager {
|
|||||||
return allPermissions;
|
return allPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the next permission request from an agent, or until agent finishes
|
||||||
|
* Resolves immediately if a permission is already pending
|
||||||
|
* Returns permission data if agent requests permission, or null if agent finishes without requesting
|
||||||
|
*/
|
||||||
|
async waitForPermissionRequest(
|
||||||
|
agentId: string,
|
||||||
|
options?: { signal?: AbortSignal }
|
||||||
|
): Promise<{
|
||||||
|
agentId: string;
|
||||||
|
requestId: string;
|
||||||
|
sessionId: string;
|
||||||
|
toolCall: any;
|
||||||
|
options: Array<{
|
||||||
|
kind: string;
|
||||||
|
name: string;
|
||||||
|
optionId: string;
|
||||||
|
}>;
|
||||||
|
} | null> {
|
||||||
|
const agent = this.agents.get(agentId);
|
||||||
|
if (!agent) {
|
||||||
|
throw new Error(`Agent ${agentId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const abortSignal = options?.signal ?? null;
|
||||||
|
|
||||||
|
const createAbortError = () => {
|
||||||
|
const error = new Error(
|
||||||
|
`Wait for permission aborted for agent ${agentId}`
|
||||||
|
);
|
||||||
|
error.name = "AbortError";
|
||||||
|
return error;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatPending = (
|
||||||
|
requestId: string,
|
||||||
|
permission: PendingPermission
|
||||||
|
) => ({
|
||||||
|
agentId,
|
||||||
|
requestId,
|
||||||
|
sessionId: permission.sessionId,
|
||||||
|
toolCall: permission.params.toolCall,
|
||||||
|
options: permission.params.options,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return immediately if a permission is already pending
|
||||||
|
const existingPermission = agent.pendingPermissions.entries().next();
|
||||||
|
if (!existingPermission.done) {
|
||||||
|
const [requestId, permission] = existingPermission.value;
|
||||||
|
return formatPending(requestId, permission);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
throw createAbortError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialStatus = getAgentStatusFromState(agent.state);
|
||||||
|
|
||||||
|
// If agent is not processing or initializing, return null (no permission requested)
|
||||||
|
if (initialStatus !== "processing" && initialStatus !== "initializing") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
let unsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (unsubscribe) {
|
||||||
|
unsubscribe();
|
||||||
|
unsubscribe = null;
|
||||||
|
}
|
||||||
|
abortSignal?.removeEventListener("abort", onAbort);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvePending = (value: ReturnType<typeof formatPending>) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rejectWithError = (error: Error) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAbort = () => {
|
||||||
|
rejectWithError(createAbortError());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (abortSignal) {
|
||||||
|
abortSignal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
unsubscribe = this.subscribeToUpdates(agentId, (update) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const notification = update.notification;
|
||||||
|
|
||||||
|
if (notification.type === "permission") {
|
||||||
|
const { requestId } = notification;
|
||||||
|
const pendingPermission =
|
||||||
|
agent.pendingPermissions.get(requestId) ?? null;
|
||||||
|
|
||||||
|
if (!pendingPermission) {
|
||||||
|
rejectWithError(
|
||||||
|
new Error(
|
||||||
|
`Permission ${requestId} no longer pending for agent ${agentId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvePending(formatPending(requestId, pendingPermission));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notification.type === "status") {
|
||||||
|
const status = notification.status;
|
||||||
|
|
||||||
|
// If agent transitions out of processing without requesting permission, return null
|
||||||
|
if (status !== "processing") {
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
rejectWithError(
|
||||||
|
error instanceof Error ? error : new Error(String(error))
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
onAbort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gracefully shutdown all agents
|
* Gracefully shutdown all agents
|
||||||
* Waits for processing agents to finish, then terminates all processes
|
* Waits for processing agents to finish, then terminates all processes
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export interface AgentTypeDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CLAUDE_MODES: AgentModeDefinition[] = [
|
const CLAUDE_MODES: AgentModeDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "default",
|
||||||
|
name: "Default",
|
||||||
|
description: "Default Claude Code mode with standard permissions",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "plan",
|
id: "plan",
|
||||||
name: "Plan",
|
name: "Plan",
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { resolve } from "path";
|
|||||||
import { AgentManager } from "./agent-manager.js";
|
import { AgentManager } from "./agent-manager.js";
|
||||||
import type { AgentNotification } from "./types.js";
|
import type { AgentNotification } from "./types.js";
|
||||||
import { curateAgentActivity } from "./activity-curator.js";
|
import { curateAgentActivity } from "./activity-curator.js";
|
||||||
|
import {
|
||||||
|
listAgentTypeDefinitions,
|
||||||
|
type AgentType,
|
||||||
|
} from "./agent-types.js";
|
||||||
|
|
||||||
export interface AgentMcpServerOptions {
|
export interface AgentMcpServerOptions {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
@@ -50,6 +54,17 @@ export async function createAgentMcpServer(
|
|||||||
): Promise<McpServer> {
|
): Promise<McpServer> {
|
||||||
const { agentManager } = options;
|
const { agentManager } = options;
|
||||||
|
|
||||||
|
const agentTypeDefinitions = listAgentTypeDefinitions();
|
||||||
|
if (agentTypeDefinitions.length === 0) {
|
||||||
|
throw new Error("[Agent MCP] No agent types configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentTypeIds = agentTypeDefinitions.map(
|
||||||
|
(definition) => definition.id
|
||||||
|
) as [AgentType, ...AgentType[]];
|
||||||
|
|
||||||
|
const AgentTypeEnum = z.enum(agentTypeIds);
|
||||||
|
|
||||||
const server = new McpServer({
|
const server = new McpServer({
|
||||||
name: "agent-mcp",
|
name: "agent-mcp",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
@@ -61,25 +76,25 @@ export async function createAgentMcpServer(
|
|||||||
{
|
{
|
||||||
title: "Create Coding Agent",
|
title: "Create Coding Agent",
|
||||||
description:
|
description:
|
||||||
"Creates a new Claude Code agent via ACP. The agent runs as a separate process and can execute coding tasks autonomously in a specified directory. Returns immediately with agent ID and status. If an initial prompt is provided, the agent will start working on it automatically. Optionally create a git worktree for isolated development.",
|
"Creates a new Claude Code or Codex agent via ACP. The agent runs as a separate process and can execute coding tasks autonomously in a specified directory. Returns immediately with agent ID, type, and status. If an initial prompt is provided, the agent will start working on it automatically. Optionally create a git worktree for isolated development.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
cwd: z
|
cwd: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
"REQUIRED: Working directory for the agent. Can be absolute path, tilde-prefixed path, or relative path (e.g., '~/dev/project', '/path/to/repo', './subdir')."
|
"REQUIRED: Working directory for the agent. Can be absolute path, tilde-prefixed path, or relative path (e.g., '~/dev/project', '/path/to/repo', './subdir')."
|
||||||
),
|
),
|
||||||
|
agentType: AgentTypeEnum.optional().describe(
|
||||||
|
"Optional: Agent implementation to spawn. 'claude' provides full Claude Code capabilities with session persistence. 'codex' uses Zed Codex ACP with fast startup and explicit permission modes. Defaults to 'claude'."
|
||||||
|
),
|
||||||
initialPrompt: z
|
initialPrompt: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(
|
.describe(
|
||||||
"Optional initial task or prompt for the agent to start working on immediately after creation. If provided, agent will begin processing this task right away."
|
"Optional initial task or prompt for the agent to start working on immediately after creation. If provided, agent will begin processing this task right away."
|
||||||
),
|
),
|
||||||
initialMode: z
|
initialMode: z.string().optional().describe(
|
||||||
.string()
|
"Optional: Initial session mode for the agent. Mode is validated at runtime based on agent type. Claude Code supports 'default', 'plan', and 'bypassPermissions'. Codex supports 'read-only', 'auto', and 'full-access'. Defaults to the agent type's default mode."
|
||||||
.optional()
|
),
|
||||||
.describe(
|
|
||||||
"Initial session mode for the agent (e.g., 'ask', 'code', 'architect'). If not specified, the agent will use its default mode. The available modes depend on the specific agent implementation."
|
|
||||||
),
|
|
||||||
worktreeName: z
|
worktreeName: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -89,6 +104,7 @@ export async function createAgentMcpServer(
|
|||||||
},
|
},
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
agentId: z.string().describe("Unique identifier for the created agent"),
|
agentId: z.string().describe("Unique identifier for the created agent"),
|
||||||
|
type: AgentTypeEnum.describe("Agent implementation that was created"),
|
||||||
status: z
|
status: z
|
||||||
.string()
|
.string()
|
||||||
.describe(
|
.describe(
|
||||||
@@ -99,11 +115,11 @@ export async function createAgentMcpServer(
|
|||||||
availableModes: z.array(z.object({
|
availableModes: z.array(z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().nullable().optional(),
|
||||||
})).nullable().describe("Available session modes for this agent"),
|
})).nullable().describe("Available session modes for this agent"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ cwd, initialPrompt, initialMode, worktreeName }) => {
|
async ({ cwd, agentType, initialPrompt, initialMode, worktreeName }) => {
|
||||||
// Expand and resolve the working directory
|
// Expand and resolve the working directory
|
||||||
let resolvedCwd = expandPath(cwd);
|
let resolvedCwd = expandPath(cwd);
|
||||||
|
|
||||||
@@ -119,9 +135,11 @@ export async function createAgentMcpServer(
|
|||||||
resolvedCwd = worktreeConfig.worktreePath;
|
resolvedCwd = worktreeConfig.worktreePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedType: AgentType = agentType ?? "claude";
|
||||||
|
|
||||||
const agentId = await agentManager.createAgent({
|
const agentId = await agentManager.createAgent({
|
||||||
cwd: resolvedCwd,
|
cwd: resolvedCwd,
|
||||||
type: "claude",
|
type: resolvedType,
|
||||||
initialPrompt,
|
initialPrompt,
|
||||||
initialMode,
|
initialMode,
|
||||||
});
|
});
|
||||||
@@ -132,6 +150,7 @@ export async function createAgentMcpServer(
|
|||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
agentId,
|
agentId,
|
||||||
|
type: resolvedType,
|
||||||
status,
|
status,
|
||||||
cwd: resolvedCwd,
|
cwd: resolvedCwd,
|
||||||
currentModeId: currentModeId ?? null,
|
currentModeId: currentModeId ?? null,
|
||||||
@@ -145,13 +164,86 @@ export async function createAgentMcpServer(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Tool: wait_for_agent
|
||||||
|
server.registerTool(
|
||||||
|
"wait_for_agent",
|
||||||
|
{
|
||||||
|
title: "Wait For Agent",
|
||||||
|
description:
|
||||||
|
"Wait until the agent requests permission or finishes its task. Returns the agent's activity and permission request (if any). Use this to monitor agent progress without polling.",
|
||||||
|
inputSchema: {
|
||||||
|
agentId: z
|
||||||
|
.string()
|
||||||
|
.describe("Agent ID to wait on (typically the result of create_coding_agent)"),
|
||||||
|
},
|
||||||
|
outputSchema: {
|
||||||
|
agentId: z.string(),
|
||||||
|
status: z
|
||||||
|
.string()
|
||||||
|
.describe("Agent status after waiting completed"),
|
||||||
|
permission: z.object({
|
||||||
|
agentId: z.string(),
|
||||||
|
requestId: z.string(),
|
||||||
|
sessionId: z.string(),
|
||||||
|
toolCall: z.any(),
|
||||||
|
options: z.array(
|
||||||
|
z.object({
|
||||||
|
kind: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
optionId: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
}).nullable().describe("Permission request if agent requested one, null if agent finished without requesting"),
|
||||||
|
activity: z.object({
|
||||||
|
format: z
|
||||||
|
.literal("curated")
|
||||||
|
.describe("Activity format: curated text for easy reading"),
|
||||||
|
updateCount: z
|
||||||
|
.number()
|
||||||
|
.describe("Total number of updates recorded for the agent"),
|
||||||
|
currentModeId: z
|
||||||
|
.string()
|
||||||
|
.nullable()
|
||||||
|
.describe("Current session mode"),
|
||||||
|
content: z
|
||||||
|
.string()
|
||||||
|
.describe("Curated activity transcript including the latest updates"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async ({ agentId }, { signal }) => {
|
||||||
|
const permission = await agentManager.waitForPermissionRequest(agentId, {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
const updates = agentManager.getAgentUpdates(agentId);
|
||||||
|
const curatedText = curateAgentActivity(updates);
|
||||||
|
const currentModeId = agentManager.getCurrentMode(agentId);
|
||||||
|
const status = agentManager.getAgentStatus(agentId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [],
|
||||||
|
structuredContent: {
|
||||||
|
agentId,
|
||||||
|
status,
|
||||||
|
permission,
|
||||||
|
activity: {
|
||||||
|
format: "curated" as const,
|
||||||
|
updateCount: updates.length,
|
||||||
|
currentModeId,
|
||||||
|
content: curatedText,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Tool: send_agent_prompt
|
// Tool: send_agent_prompt
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
"send_agent_prompt",
|
"send_agent_prompt",
|
||||||
{
|
{
|
||||||
title: "Send Agent Prompt",
|
title: "Send Agent Prompt",
|
||||||
description:
|
description:
|
||||||
"Sends a task or prompt to an existing agent. By default, returns immediately without waiting (non-blocking). The agent will process the prompt in the background. Use get_agent_status or get_agent_activity to check progress. Optionally specify maxWait to wait for completion, or sessionMode to switch modes before sending.",
|
"Sends a task or prompt to an existing agent. Returns immediately without waiting (non-blocking). The agent will process the prompt in the background. Use wait_for_agent, get_agent_status, or get_agent_activity to monitor progress. Optionally switch session modes before sending.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
agentId: z.string().describe("Agent ID returned from create_coding_agent"),
|
agentId: z.string().describe("Agent ID returned from create_coding_agent"),
|
||||||
prompt: z
|
prompt: z
|
||||||
@@ -159,35 +251,25 @@ export async function createAgentMcpServer(
|
|||||||
.describe(
|
.describe(
|
||||||
"The task, instruction, or feedback to send to the agent. Be specific about what you want the agent to accomplish."
|
"The task, instruction, or feedback to send to the agent. Be specific about what you want the agent to accomplish."
|
||||||
),
|
),
|
||||||
sessionMode: z
|
sessionMode: z.string().optional().describe(
|
||||||
.string()
|
"Optional: Session mode to set before sending the prompt. Mode is validated at runtime based on agent type. Claude Code supports 'default', 'plan', and 'bypassPermissions'. Codex offers 'read-only', 'auto', and 'full-access' for progressively broader permissions."
|
||||||
.optional()
|
),
|
||||||
.describe(
|
|
||||||
"Optional: Session mode to set before sending the prompt (e.g., 'plan', 'code', 'default'). If specified, the agent's mode will be changed before processing the prompt."
|
|
||||||
),
|
|
||||||
maxWait: z
|
|
||||||
.number()
|
|
||||||
.optional()
|
|
||||||
.describe(
|
|
||||||
"Optional: Maximum milliseconds to wait for agent to complete. If not provided, returns immediately (non-blocking). If agent doesn't finish within this time, returns with didComplete=false and agent continues processing in background."
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
success: z.boolean().describe("Whether the prompt was sent successfully"),
|
success: z.boolean().describe("Whether the prompt was sent successfully"),
|
||||||
didComplete: z.boolean().describe("Whether the agent completed within maxWait time (always false if maxWait not specified)"),
|
status: z
|
||||||
stopReason: z.string().nullable().describe("Reason agent stopped if it completed: 'end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'"),
|
.string()
|
||||||
|
.describe("Agent status immediately after enqueuing the prompt (usually 'processing')"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async ({ agentId, prompt, sessionMode, maxWait }) => {
|
async ({ agentId, prompt, sessionMode }) => {
|
||||||
const response = await agentManager.sendPrompt(agentId, prompt, {
|
await agentManager.sendPrompt(agentId, prompt, {
|
||||||
maxWait,
|
|
||||||
sessionMode,
|
sessionMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
success: true,
|
success: true,
|
||||||
didComplete: response.didComplete,
|
status: agentManager.getAgentStatus(agentId),
|
||||||
stopReason: response.stopReason ?? null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -215,14 +297,14 @@ export async function createAgentMcpServer(
|
|||||||
status: z.string(),
|
status: z.string(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
lastActivityAt: z.string(),
|
lastActivityAt: z.string(),
|
||||||
type: z.enum(["claude", "codex"]),
|
type: AgentTypeEnum,
|
||||||
sessionId: z.string().nullable(),
|
sessionId: z.string().nullable(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
currentModeId: z.string().nullable(),
|
currentModeId: z.string().nullable(),
|
||||||
availableModes: z.array(z.object({
|
availableModes: z.array(z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().nullable().optional(),
|
||||||
})).nullable(),
|
})).nullable(),
|
||||||
})
|
})
|
||||||
.describe("Detailed agent information"),
|
.describe("Detailed agent information"),
|
||||||
@@ -264,14 +346,14 @@ export async function createAgentMcpServer(
|
|||||||
status: z.string(),
|
status: z.string(),
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
lastActivityAt: z.string(),
|
lastActivityAt: z.string(),
|
||||||
type: z.enum(["claude", "codex"]),
|
type: AgentTypeEnum,
|
||||||
sessionId: z.string().nullable(),
|
sessionId: z.string().nullable(),
|
||||||
error: z.string().nullable(),
|
error: z.string().nullable(),
|
||||||
currentModeId: z.string().nullable(),
|
currentModeId: z.string().nullable(),
|
||||||
availableModes: z.array(z.object({
|
availableModes: z.array(z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().nullable().optional(),
|
||||||
})).nullable(),
|
})).nullable(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@@ -435,14 +517,12 @@ export async function createAgentMcpServer(
|
|||||||
{
|
{
|
||||||
title: "Set Agent Session Mode",
|
title: "Set Agent Session Mode",
|
||||||
description:
|
description:
|
||||||
"Change the agent's session mode (e.g., from 'ask' to 'code', or 'architect' to 'code'). Each mode affects the agent's behavior - 'ask' mode requests permission before changes, 'code' mode writes code directly, 'architect' mode plans without implementation. The available modes depend on the specific agent. Use get_agent_status or list_agents to see available modes for each agent.",
|
"Change the agent's session mode. Claude Code supports 'plan' for step-by-step approvals and 'bypassPermissions' to auto-approve actions. Codex offers 'read-only', 'auto', and 'full-access' to control how freely it can modify the workspace or access the system. Use get_agent_status or list_agents to see which modes are currently available for each agent.",
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
agentId: z.string().describe("Agent ID to configure"),
|
agentId: z.string().describe("Agent ID to configure"),
|
||||||
modeId: z
|
modeId: z.string().describe(
|
||||||
.string()
|
"The session mode to set. Mode is validated at runtime based on the agent's available modes. Check agent's availableModes from get_agent_status or list_agents to see valid options for the specific agent."
|
||||||
.describe(
|
),
|
||||||
"The session mode to set (e.g., 'ask', 'code', 'architect'). Must be one of the agent's available modes."
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
outputSchema: {
|
outputSchema: {
|
||||||
success: z.boolean().describe("Whether the mode change succeeded"),
|
success: z.boolean().describe("Whether the mode change succeeded"),
|
||||||
|
|||||||
+10
-3
@@ -88,13 +88,20 @@ async function testWorktreeAgent() {
|
|||||||
|
|
||||||
// Step 6: Send a prompt to verify it's working in the worktree
|
// Step 6: Send a prompt to verify it's working in the worktree
|
||||||
console.log("Step 6: Sending test prompt to agent...");
|
console.log("Step 6: Sending test prompt to agent...");
|
||||||
const { didComplete } = await agentManager.sendPrompt(
|
await agentManager.sendPrompt(
|
||||||
agentId,
|
agentId,
|
||||||
"Run pwd and git status to verify you're in a worktree. Reply with the output.",
|
"Run pwd and git status to verify you're in a worktree. Reply with the output.",
|
||||||
{ maxWait: 30000 }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(` Prompt completed: ${didComplete}`);
|
let status = agentManager.getAgentStatus(agentId);
|
||||||
|
let attempts = 0;
|
||||||
|
while (status === "processing" && attempts < 60) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
status = agentManager.getAgentStatus(agentId);
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Agent status after prompt: ${status}`);
|
||||||
|
|
||||||
// Get agent updates to see the response
|
// Get agent updates to see the response
|
||||||
const updates = agentManager.getAgentUpdates(agentId);
|
const updates = agentManager.getAgentUpdates(agentId);
|
||||||
|
|||||||
+10
-3
@@ -116,13 +116,20 @@ async function testWorktreeE2E() {
|
|||||||
|
|
||||||
// Step 7: Test agent can run commands in worktree
|
// Step 7: Test agent can run commands in worktree
|
||||||
console.log("Step 7: Testing agent execution in worktree...");
|
console.log("Step 7: Testing agent execution in worktree...");
|
||||||
const { didComplete } = await agentManager.sendPrompt(
|
await agentManager.sendPrompt(
|
||||||
createdAgentId,
|
createdAgentId,
|
||||||
"Run 'pwd' and 'git branch --show-current'. Just show me the output.",
|
"Run 'pwd' and 'git branch --show-current'. Just show me the output.",
|
||||||
{ maxWait: 30000 }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(` Prompt completed: ${didComplete}`);
|
let status = agentManager.getAgentStatus(createdAgentId);
|
||||||
|
let attempts = 0;
|
||||||
|
while (status === "processing" && attempts < 60) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
status = agentManager.getAgentStatus(createdAgentId);
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` Agent status after prompt: ${status}`);
|
||||||
|
|
||||||
const updates = agentManager.getAgentUpdates(createdAgentId);
|
const updates = agentManager.getAgentUpdates(createdAgentId);
|
||||||
const messageChunks = updates
|
const messageChunks = updates
|
||||||
|
|||||||
Reference in New Issue
Block a user