@iinm/plain-agent 1.8.3 → 1.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/config/config.predefined.json +1 -1
- package/config/prompts.predefined/shortcuts/configure.md +1 -1
- package/package.json +1 -1
- package/sandbox/bin/plain-sandbox +13 -0
- package/src/agent.mjs +16 -16
- package/src/cliCommands.mjs +1 -1
- package/src/cliFormatter.mjs +10 -10
- package/src/context/loadAgentRoles.mjs +45 -34
- package/src/context/loadPrompts.mjs +55 -42
- package/src/main.mjs +8 -6
- package/src/prompt.mjs +1 -1
- package/src/subagent.mjs +27 -27
- package/src/tools/switchToMainAgent.d.ts +3 -0
- package/src/tools/{reportAsSubagent.mjs → switchToMainAgent.mjs} +4 -5
- package/src/tools/switchToSubagent.d.ts +4 -0
- package/src/tools/{delegateToSubagent.mjs → switchToSubagent.mjs} +15 -4
- package/src/tools/delegateToSubagent.d.ts +0 -4
- package/src/tools/reportAsSubagent.d.ts +0 -3
package/README.md
CHANGED
|
@@ -504,8 +504,8 @@ The agent can use the following tools to assist with tasks:
|
|
|
504
504
|
- **tmux_command**: Run a tmux command.
|
|
505
505
|
- **ask_web**: Use the web search to answer questions that need up-to-date information or supporting sources. (requires Google API key or Vertex AI configuration).
|
|
506
506
|
- **ask_url**: Use one or more provided URLs to answer a question. Include the URLs in your question. (requires Google API key or Vertex AI configuration).
|
|
507
|
-
- **
|
|
508
|
-
- **
|
|
507
|
+
- **switch_to_subagent**: Switch to a subagent role within the same conversation, focusing on the specified goal.
|
|
508
|
+
- **switch_to_main_agent**: Switch back to the main agent role and report the result. After reporting, the subagent's conversation history is removed from the context.
|
|
509
509
|
- **compact_context**: Compact the conversation context by discarding prior messages and reloading task state from a memory file. Use when the context has grown large but the task is not yet complete. Can also be invoked via the `/compact` slash command.
|
|
510
510
|
|
|
511
511
|
## Prompts
|
|
@@ -43,7 +43,7 @@ Ask what the user wants to configure. Common topics:
|
|
|
43
43
|
|
|
44
44
|
If the request is vague, ask a focused clarifying question before proceeding.
|
|
45
45
|
|
|
46
|
-
**If the user wants to configure Sandbox**: immediately
|
|
46
|
+
**If the user wants to configure Sandbox**: immediately switch to the `sandbox-configurator` subagent and do not proceed further yourself.
|
|
47
47
|
|
|
48
48
|
## Step 4: Apply Changes
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -731,6 +731,14 @@ setup_container_firewall() {
|
|
|
731
731
|
# allow established connections
|
|
732
732
|
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
733
733
|
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
734
|
+
|
|
735
|
+
# allow localhost on IPv6
|
|
736
|
+
ip6tables -A INPUT -i lo -j ACCEPT
|
|
737
|
+
ip6tables -A OUTPUT -o lo -j ACCEPT
|
|
738
|
+
|
|
739
|
+
# allow established connections on IPv6
|
|
740
|
+
ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
741
|
+
ip6tables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
|
734
742
|
|
|
735
743
|
# allow given addresses
|
|
736
744
|
for address in "${addresses[@]}"; do
|
|
@@ -750,6 +758,11 @@ setup_container_firewall() {
|
|
|
750
758
|
fi
|
|
751
759
|
done
|
|
752
760
|
iptables -A OUTPUT -p tcp -m set --match-set allow_list dst,dst -j ACCEPT
|
|
761
|
+
|
|
762
|
+
# Reject unauthorized TCP connections immediately with RST
|
|
763
|
+
# instead of silently dropping and causing timeout
|
|
764
|
+
iptables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset
|
|
765
|
+
ip6tables -A OUTPUT -p tcp -j REJECT --reject-with tcp-reset
|
|
753
766
|
}
|
|
754
767
|
|
|
755
768
|
setup_container_user() {
|
package/src/agent.mjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @import { Agent, AgentConfig, AgentEventEmitter, UserEventEmitter } from "./agent"
|
|
3
3
|
* @import { Tool, ToolDefinition } from "./tool"
|
|
4
4
|
* @import { CompactContextInput } from "./tools/compactContext"
|
|
5
|
-
* @import {
|
|
6
|
-
* @import {
|
|
5
|
+
* @import { SwitchToSubagentInput } from "./tools/switchToSubagent"
|
|
6
|
+
* @import { SwitchToMainAgentInput } from "./tools/switchToMainAgent"
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
compactContextToolName,
|
|
19
19
|
readMemoryForCompaction,
|
|
20
20
|
} from "./tools/compactContext.mjs";
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import { switchToMainAgentToolName } from "./tools/switchToMainAgent.mjs";
|
|
22
|
+
import { switchToSubagentToolName } from "./tools/switchToSubagent.mjs";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* @param {AgentConfig} config
|
|
@@ -69,10 +69,10 @@ export function createAgent({
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* @param {
|
|
72
|
+
* @param {SwitchToSubagentInput} input
|
|
73
73
|
*/
|
|
74
|
-
const
|
|
75
|
-
const result = subagentManager.
|
|
74
|
+
const switchToSubagentImpl = async (input) => {
|
|
75
|
+
const result = subagentManager.switchToSubagent(
|
|
76
76
|
input.name,
|
|
77
77
|
input.goal,
|
|
78
78
|
stateManager.getMessages().length - 1,
|
|
@@ -84,10 +84,10 @@ export function createAgent({
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* @param {
|
|
87
|
+
* @param {SwitchToMainAgentInput} input
|
|
88
88
|
*/
|
|
89
|
-
const
|
|
90
|
-
const result = await subagentManager.
|
|
89
|
+
const switchToMainAgentImpl = async (input) => {
|
|
90
|
+
const result = await subagentManager.switchToMainAgent(input.memoryPath);
|
|
91
91
|
if (!result.success) {
|
|
92
92
|
return new Error(result.error);
|
|
93
93
|
}
|
|
@@ -101,7 +101,7 @@ export function createAgent({
|
|
|
101
101
|
if (subagentManager.isSubagentActive()) {
|
|
102
102
|
return new Error(
|
|
103
103
|
"compact_context cannot be used while running as a subagent. " +
|
|
104
|
-
"Call
|
|
104
|
+
"Call switch_to_main_agent to return to the main agent first.",
|
|
105
105
|
);
|
|
106
106
|
}
|
|
107
107
|
const input = /** @type {CompactContextInput} */ (rawInput);
|
|
@@ -111,11 +111,11 @@ export function createAgent({
|
|
|
111
111
|
/** @type {Map<string, Tool>} */
|
|
112
112
|
const toolByName = new Map();
|
|
113
113
|
for (const tool of tools) {
|
|
114
|
-
if (tool.def.name ===
|
|
115
|
-
tool.injectImpl(
|
|
114
|
+
if (tool.def.name === switchToSubagentToolName && tool.injectImpl) {
|
|
115
|
+
tool.injectImpl(switchToSubagentImpl);
|
|
116
116
|
}
|
|
117
|
-
if (tool.def.name ===
|
|
118
|
-
tool.injectImpl(
|
|
117
|
+
if (tool.def.name === switchToMainAgentToolName && tool.injectImpl) {
|
|
118
|
+
tool.injectImpl(switchToMainAgentImpl);
|
|
119
119
|
}
|
|
120
120
|
if (tool.def.name === compactContextToolName && tool.injectImpl) {
|
|
121
121
|
tool.injectImpl(compactContextImpl);
|
|
@@ -127,7 +127,7 @@ export function createAgent({
|
|
|
127
127
|
const toolDefs = tools.map(({ def }) => def);
|
|
128
128
|
|
|
129
129
|
const toolExecutor = createToolExecutor(toolByName, {
|
|
130
|
-
exclusiveToolNames: [
|
|
130
|
+
exclusiveToolNames: [switchToSubagentToolName, switchToMainAgentToolName],
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
async function dumpMessages() {
|
package/src/cliCommands.mjs
CHANGED
|
@@ -54,7 +54,7 @@ export function createCommandHandler({
|
|
|
54
54
|
const goalText =
|
|
55
55
|
goalTextContent?.type === "text" ? goalTextContent.text : goal;
|
|
56
56
|
|
|
57
|
-
const messageText = `
|
|
57
|
+
const messageText = `Switch to "${name}" subagent with goal: ${goalText}`;
|
|
58
58
|
userEventEmitter.emit("userInput", [
|
|
59
59
|
{ type: "text", text: messageText },
|
|
60
60
|
...goalImages,
|
package/src/cliFormatter.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @import { PatchFileInput } from "./tools/patchFile"
|
|
6
6
|
* @import { WriteFileInput } from "./tools/writeFile"
|
|
7
7
|
* @import { TmuxCommandInput } from "./tools/tmuxCommand"
|
|
8
|
-
* @import {
|
|
8
|
+
* @import { SwitchToSubagentInput } from "./tools/switchToSubagent"
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { styleText } from "node:util";
|
|
@@ -129,13 +129,13 @@ export function formatToolUse(toolUse) {
|
|
|
129
129
|
].join("\n");
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
if (toolName === "
|
|
133
|
-
/** @type {Partial<
|
|
134
|
-
const
|
|
132
|
+
if (toolName === "switch_to_subagent") {
|
|
133
|
+
/** @type {Partial<SwitchToSubagentInput>} */
|
|
134
|
+
const switchToSubagentInput = input;
|
|
135
135
|
return [
|
|
136
136
|
`tool: ${toolName}`,
|
|
137
|
-
`name: ${
|
|
138
|
-
`goal: ${
|
|
137
|
+
`name: ${switchToSubagentInput.name}`,
|
|
138
|
+
`goal: ${switchToSubagentInput.goal}`,
|
|
139
139
|
].join("\n");
|
|
140
140
|
}
|
|
141
141
|
|
|
@@ -149,12 +149,12 @@ export function formatToolUse(toolUse) {
|
|
|
149
149
|
].join("\n");
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
if (toolName === "
|
|
153
|
-
/** @type {Partial<import("./tools/
|
|
154
|
-
const
|
|
152
|
+
if (toolName === "switch_to_main_agent") {
|
|
153
|
+
/** @type {Partial<import("./tools/switchToMainAgent").SwitchToMainAgentInput>} */
|
|
154
|
+
const switchToMainAgentInput = input;
|
|
155
155
|
return [
|
|
156
156
|
`tool: ${toolName}`,
|
|
157
|
-
`memoryPath: ${
|
|
157
|
+
`memoryPath: ${switchToMainAgentInput.memoryPath}`,
|
|
158
158
|
].join("\n");
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -52,41 +52,52 @@ export async function loadAgentRoles(claudeCodePlugins) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
55
|
+
const files = (
|
|
56
|
+
await Promise.all(
|
|
57
|
+
agentDirs.map(async ({ dir, idPrefix, only }) => {
|
|
58
|
+
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
59
|
+
if (err.code !== "ENOENT") {
|
|
60
|
+
console.warn(`Failed to list agent roles in ${dir}:`, err);
|
|
61
|
+
}
|
|
62
|
+
return /** @type {string[]} */ ([]);
|
|
63
|
+
});
|
|
64
|
+
return files.map((file) => ({ dir, file, idPrefix, only }));
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
.flat()
|
|
69
|
+
// Filter by only pattern if specified
|
|
70
|
+
.filter(({ file, only }) => !(only && !only.test(file)));
|
|
71
|
+
|
|
72
|
+
const roles = /** @type {AgentRole[]} */ (
|
|
73
|
+
(
|
|
74
|
+
await Promise.all(
|
|
75
|
+
files.map(async ({ dir, file, idPrefix }) => {
|
|
76
|
+
const fullPath = path.join(dir, file);
|
|
77
|
+
const content = await fs.readFile(fullPath, "utf-8").catch((err) => {
|
|
78
|
+
console.warn(`Failed to read agent role file ${fullPath}:`, err);
|
|
79
|
+
return null;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (content === null) return null;
|
|
83
|
+
|
|
84
|
+
let role = parseAgentRole(file, content, fullPath, idPrefix);
|
|
85
|
+
if (role.import) {
|
|
86
|
+
try {
|
|
87
|
+
role = await mergeRemoteRole(role, file, fullPath);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.warn(`Failed to import remote role ${role.id}:`, err);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return role;
|
|
95
|
+
}),
|
|
96
|
+
)
|
|
97
|
+
).filter((role) => role)
|
|
98
|
+
);
|
|
88
99
|
|
|
89
|
-
return roles;
|
|
100
|
+
return new Map(roles.map((role) => [role.id, role]));
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/**
|
|
@@ -68,50 +68,63 @@ export async function loadPrompts(claudeCodePlugins) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Ignore all files in the skills/ directory except for SKILL.md.
|
|
97
|
-
if (fullPath.match(/\/skills\//) && !file.endsWith("/SKILL.md")) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let prompt = parsePrompt(file, content, fullPath, idPrefix);
|
|
102
|
-
if (prompt.import) {
|
|
103
|
-
prompt = await mergeRemotePrompt(prompt, file, fullPath);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (prompt.userInvocable === false) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
71
|
+
const files = (
|
|
72
|
+
await Promise.all(
|
|
73
|
+
promptDirs.map(async ({ dir, idPrefix, only }) => {
|
|
74
|
+
const files = await getMarkdownFiles(dir).catch((err) => {
|
|
75
|
+
if (err.code !== "ENOENT") {
|
|
76
|
+
console.warn(`Failed to list prompts in ${dir}:`, err);
|
|
77
|
+
}
|
|
78
|
+
return /** @type {string[]} */ ([]);
|
|
79
|
+
});
|
|
80
|
+
return files.map((file) => ({ dir, file, idPrefix, only }));
|
|
81
|
+
}),
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
.flat()
|
|
85
|
+
// Filter by only pattern if specified
|
|
86
|
+
.filter(({ file, only }) => !(only && !only.test(file)))
|
|
87
|
+
// Ignore all files in the skills/ directory except for SKILL.md.
|
|
88
|
+
.filter(
|
|
89
|
+
({ file, dir }) =>
|
|
90
|
+
!(
|
|
91
|
+
path.join(dir, file).includes("/skills/") &&
|
|
92
|
+
!file.endsWith("/SKILL.md")
|
|
93
|
+
),
|
|
94
|
+
);
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
96
|
+
const prompts = /** @type {Prompt[]} */ (
|
|
97
|
+
(
|
|
98
|
+
await Promise.all(
|
|
99
|
+
files.map(async ({ dir, file, idPrefix }) => {
|
|
100
|
+
const fullPath = path.join(dir, file);
|
|
101
|
+
const content = await fs.readFile(fullPath, "utf-8").catch((err) => {
|
|
102
|
+
console.warn(`Failed to read prompt file ${fullPath}:`, err);
|
|
103
|
+
return null;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (content === null) return null;
|
|
107
|
+
|
|
108
|
+
let prompt = parsePrompt(file, content, fullPath, idPrefix);
|
|
109
|
+
if (prompt.import) {
|
|
110
|
+
try {
|
|
111
|
+
prompt = await mergeRemotePrompt(prompt, file, fullPath);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.warn(`Failed to import remote prompt ${prompt.id}:`, err);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (prompt.userInvocable === false) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return prompt;
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
).filter((prompt) => prompt)
|
|
125
|
+
);
|
|
113
126
|
|
|
114
|
-
return prompts;
|
|
127
|
+
return new Map(prompts.map((prompt) => [prompt.id, prompt]));
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
/**
|
package/src/main.mjs
CHANGED
|
@@ -22,10 +22,10 @@ import { createPrompt } from "./prompt.mjs";
|
|
|
22
22
|
import { createAskURLTool } from "./tools/askURL.mjs";
|
|
23
23
|
import { createAskWebTool } from "./tools/askWeb.mjs";
|
|
24
24
|
import { createCompactContextTool } from "./tools/compactContext.mjs";
|
|
25
|
-
import { createDelegateToSubagentTool } from "./tools/delegateToSubagent.mjs";
|
|
26
25
|
import { createExecCommandTool } from "./tools/execCommand.mjs";
|
|
27
26
|
import { createPatchFileTool } from "./tools/patchFile.mjs";
|
|
28
|
-
import {
|
|
27
|
+
import { createSwitchToMainAgentTool } from "./tools/switchToMainAgent.mjs";
|
|
28
|
+
import { createSwitchToSubagentTool } from "./tools/switchToSubagent.mjs";
|
|
29
29
|
import { createTmuxCommandTool } from "./tools/tmuxCommand.mjs";
|
|
30
30
|
import { writeFileTool } from "./tools/writeFile.mjs";
|
|
31
31
|
import { createToolUseApprover } from "./toolUseApprover.mjs";
|
|
@@ -158,8 +158,10 @@ if (cliArgs.subcommand.type === "cost") {
|
|
|
158
158
|
const modelNameWithVariant = modelFromArgs || modelFromConfig;
|
|
159
159
|
|
|
160
160
|
const pluginPaths = resolvePluginPaths(appConfig.claudeCodePlugins ?? []);
|
|
161
|
-
const agentRoles = await
|
|
162
|
-
|
|
161
|
+
const [prompts, agentRoles] = await Promise.all([
|
|
162
|
+
loadPrompts(pluginPaths),
|
|
163
|
+
loadAgentRoles(pluginPaths),
|
|
164
|
+
]);
|
|
163
165
|
|
|
164
166
|
const prompt = createPrompt({
|
|
165
167
|
username: USER_NAME,
|
|
@@ -179,8 +181,8 @@ if (cliArgs.subcommand.type === "cost") {
|
|
|
179
181
|
createPatchFileTool(),
|
|
180
182
|
createTmuxCommandTool({ sandbox: appConfig.sandbox }),
|
|
181
183
|
createCompactContextTool(),
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
createSwitchToSubagentTool(),
|
|
185
|
+
createSwitchToMainAgentTool(),
|
|
184
186
|
];
|
|
185
187
|
|
|
186
188
|
if (appConfig.tools?.askWeb) {
|
package/src/prompt.mjs
CHANGED
|
@@ -133,6 +133,6 @@ ${agentRoleDescriptions}
|
|
|
133
133
|
export const CLAUDE_CODE_COMPATIBILITY_NOTES = `# Environment Constraints
|
|
134
134
|
|
|
135
135
|
- Use memory file to manage todo list.
|
|
136
|
-
- Subagents cannot run in parallel.
|
|
136
|
+
- Subagents cannot run in parallel. Switch to them one at a time.
|
|
137
137
|
- Use AGENTS.md instead when CLAUDE.md is absent.
|
|
138
138
|
- If instructed to use "haiku agent", "sonnet agent", or "opus agent", use "worker" instead.`;
|
package/src/subagent.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { Message, MessageContentToolResult, MessageContentToolUse } from "./model"
|
|
3
|
-
* @import {
|
|
3
|
+
* @import { SwitchToMainAgentInput } from "./tools/switchToMainAgent"
|
|
4
4
|
* @import { AgentRole } from "./context/loadAgentRoles.mjs"
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ import fs from "node:fs/promises";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
|
|
10
10
|
import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "./prompt.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { switchToMainAgentToolName } from "./tools/switchToMainAgent.mjs";
|
|
12
12
|
|
|
13
13
|
/** @typedef {ReturnType<typeof createSubagentManager>} SubagentManager */
|
|
14
14
|
|
|
@@ -23,39 +23,39 @@ import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
|
|
|
23
23
|
* @param {SubagentStateEventHandlers} handlers
|
|
24
24
|
*/
|
|
25
25
|
export function createSubagentManager(agentRoles, handlers) {
|
|
26
|
-
/** @type {{name: string; goal: string;
|
|
26
|
+
/** @type {{name: string; goal: string; switchMessageIndex: number}[]} */
|
|
27
27
|
const subagents = [];
|
|
28
28
|
let subagentCount = 0;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* @typedef {
|
|
31
|
+
* @typedef {SwitchToSubagentSuccess | SwitchToSubagentFailure} SwitchToSubagentResult
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* @typedef {Object}
|
|
35
|
+
* @typedef {Object} SwitchToSubagentSuccess
|
|
36
36
|
* @property {true} success
|
|
37
37
|
* @property {string} value
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* @typedef {Object}
|
|
41
|
+
* @typedef {Object} SwitchToSubagentFailure
|
|
42
42
|
* @property {false} success
|
|
43
43
|
* @property {string} error
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Switch to a subagent role.
|
|
48
48
|
* @param {string} name
|
|
49
49
|
* @param {string} goal
|
|
50
|
-
* @param {number}
|
|
51
|
-
* @returns {
|
|
50
|
+
* @param {number} switchMessageIndex
|
|
51
|
+
* @returns {SwitchToSubagentResult}
|
|
52
52
|
*/
|
|
53
|
-
function
|
|
53
|
+
function switchToSubagent(name, goal, switchMessageIndex) {
|
|
54
54
|
if (subagents.length > 0) {
|
|
55
55
|
return {
|
|
56
56
|
success: false,
|
|
57
57
|
error:
|
|
58
|
-
"Cannot call
|
|
58
|
+
"Cannot call switch_to_subagent while already acting as a subagent.",
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -86,7 +86,7 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
86
86
|
subagents.push({
|
|
87
87
|
name: actualName,
|
|
88
88
|
goal,
|
|
89
|
-
|
|
89
|
+
switchMessageIndex,
|
|
90
90
|
});
|
|
91
91
|
handlers.onSubagentSwitched({ name: actualName });
|
|
92
92
|
|
|
@@ -99,37 +99,37 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
99
99
|
: `Role: ${actualName}`,
|
|
100
100
|
`Your goal: ${goal}`,
|
|
101
101
|
`Memory file path format: ${AGENT_PROJECT_METADATA_DIR}/memory/<session-id>--${sequenceNumber}--${actualName.replace("/", "-")}--<kebab-case-title>.md (Replace <kebab-case-title> with a short title describing your own goal)`,
|
|
102
|
-
`When finished, call "
|
|
102
|
+
`When finished, call "switch_to_main_agent" with the memory file path. Start executing your goal now.`,
|
|
103
103
|
].join("\n\n"),
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* @typedef {
|
|
108
|
+
* @typedef {SwitchToMainAgentSuccess | SwitchToMainAgentFailure} SwitchToMainAgentResult
|
|
109
109
|
*/
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* @typedef {Object}
|
|
112
|
+
* @typedef {Object} SwitchToMainAgentSuccess
|
|
113
113
|
* @property {true} success
|
|
114
114
|
* @property {string} memoryContent
|
|
115
115
|
*/
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
* @typedef {Object}
|
|
118
|
+
* @typedef {Object} SwitchToMainAgentFailure
|
|
119
119
|
* @property {false} success
|
|
120
120
|
* @property {string} error
|
|
121
121
|
*/
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
|
-
*
|
|
124
|
+
* Switch back to the main agent role and read the memory file.
|
|
125
125
|
* @param {string} memoryPath
|
|
126
|
-
* @returns {Promise<
|
|
126
|
+
* @returns {Promise<SwitchToMainAgentResult>}
|
|
127
127
|
*/
|
|
128
|
-
async function
|
|
128
|
+
async function switchToMainAgent(memoryPath) {
|
|
129
129
|
if (subagents.length === 0) {
|
|
130
130
|
return {
|
|
131
131
|
success: false,
|
|
132
|
-
error: "Cannot call
|
|
132
|
+
error: "Cannot call switch_to_main_agent from the main agent.",
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -171,7 +171,7 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
171
171
|
*/
|
|
172
172
|
function processToolResults(toolUseParts, toolResults, messages) {
|
|
173
173
|
const reportSubagentToolUse = toolUseParts.find(
|
|
174
|
-
(toolUse) => toolUse.toolName ===
|
|
174
|
+
(toolUse) => toolUse.toolName === switchToMainAgentToolName,
|
|
175
175
|
);
|
|
176
176
|
|
|
177
177
|
if (reportSubagentToolUse) {
|
|
@@ -193,7 +193,7 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
195
|
* Handle the result of a subagent reporting back.
|
|
196
|
-
* On success, truncates conversation history back to the
|
|
196
|
+
* On success, truncates conversation history back to the switch point
|
|
197
197
|
* and converts the report into a standard user message.
|
|
198
198
|
* @param {MessageContentToolUse} reportToolUse
|
|
199
199
|
* @param {MessageContentToolResult} reportResult
|
|
@@ -214,10 +214,10 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
214
214
|
|
|
215
215
|
handlers.onSubagentSwitched(subagents.at(-1) ?? null);
|
|
216
216
|
|
|
217
|
-
// Truncate history back to the
|
|
217
|
+
// Truncate history back to the switch point
|
|
218
218
|
const truncatedMessages = messages.slice(
|
|
219
219
|
0,
|
|
220
|
-
currentSubagent.
|
|
220
|
+
currentSubagent.switchMessageIndex,
|
|
221
221
|
);
|
|
222
222
|
|
|
223
223
|
// Convert the tool result into a standard user message
|
|
@@ -225,7 +225,7 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
225
225
|
.map((c) => (c.type === "text" ? c.text : ""))
|
|
226
226
|
.join("\n\n");
|
|
227
227
|
|
|
228
|
-
const reportInput = /** @type {
|
|
228
|
+
const reportInput = /** @type {SwitchToMainAgentInput} */ (
|
|
229
229
|
reportToolUse.input
|
|
230
230
|
);
|
|
231
231
|
|
|
@@ -257,8 +257,8 @@ export function createSubagentManager(agentRoles, handlers) {
|
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
return {
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
switchToSubagent,
|
|
261
|
+
switchToMainAgent,
|
|
262
262
|
processToolResults,
|
|
263
263
|
isSubagentActive,
|
|
264
264
|
};
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* @import { Tool, ToolImplementation } from '../tool'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const switchToMainAgentToolName = "switch_to_main_agent";
|
|
6
6
|
|
|
7
7
|
/** @returns {Tool} */
|
|
8
|
-
export function
|
|
8
|
+
export function createSwitchToMainAgentTool() {
|
|
9
9
|
/** @type {ToolImplementation} */
|
|
10
10
|
let impl = async () => {
|
|
11
11
|
throw new Error("Not implemented");
|
|
@@ -14,9 +14,8 @@ export function createReportAsSubagentTool() {
|
|
|
14
14
|
/** @type {Tool} */
|
|
15
15
|
const tool = {
|
|
16
16
|
def: {
|
|
17
|
-
name:
|
|
18
|
-
description:
|
|
19
|
-
"End the subagent role and report the result to the main agent.",
|
|
17
|
+
name: switchToMainAgentToolName,
|
|
18
|
+
description: "Switch back to the main agent role and report the result.",
|
|
20
19
|
inputSchema: {
|
|
21
20
|
type: "object",
|
|
22
21
|
properties: {
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* @import { Tool, ToolImplementation } from '../tool'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const switchToSubagentToolName = "switch_to_subagent";
|
|
6
6
|
|
|
7
7
|
/** @returns {Tool} */
|
|
8
|
-
export function
|
|
8
|
+
export function createSwitchToSubagentTool() {
|
|
9
9
|
/** @type {ToolImplementation} */
|
|
10
10
|
let impl = async () => {
|
|
11
11
|
throw new Error("Not implemented");
|
|
@@ -14,9 +14,9 @@ export function createDelegateToSubagentTool() {
|
|
|
14
14
|
/** @type {Tool} */
|
|
15
15
|
const tool = {
|
|
16
16
|
def: {
|
|
17
|
-
name:
|
|
17
|
+
name: switchToSubagentToolName,
|
|
18
18
|
description:
|
|
19
|
-
"
|
|
19
|
+
"Switch to a subagent role within the same conversation, focusing on the specified goal. You inherit the current context.",
|
|
20
20
|
inputSchema: {
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
@@ -42,6 +42,17 @@ export function createDelegateToSubagentTool() {
|
|
|
42
42
|
injectImpl(fn) {
|
|
43
43
|
impl = fn;
|
|
44
44
|
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {Record<string, unknown>} input
|
|
48
|
+
* @returns {Record<string, unknown>}
|
|
49
|
+
*/
|
|
50
|
+
maskApprovalInput: (input) => {
|
|
51
|
+
const { name } = /** @type {{name: string}} */ (input);
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
};
|
|
55
|
+
},
|
|
45
56
|
};
|
|
46
57
|
|
|
47
58
|
return tool;
|