@iinm/plain-agent 1.8.4 → 1.8.6

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.
Files changed (85) hide show
  1. package/bin/plain +1 -1
  2. package/package.json +8 -9
  3. package/sandbox/bin/plain-sandbox +13 -0
  4. package/src/agent.d.ts +52 -0
  5. package/src/agent.mjs +204 -0
  6. package/src/agentLoop.mjs +419 -0
  7. package/src/agentState.mjs +41 -0
  8. package/src/claudeCodePlugin.mjs +164 -0
  9. package/src/cliArgs.mjs +175 -0
  10. package/src/cliBatch.mjs +147 -0
  11. package/src/cliCommands.mjs +283 -0
  12. package/src/cliCompleter.mjs +227 -0
  13. package/src/cliCost.mjs +309 -0
  14. package/src/cliFormatter.mjs +518 -0
  15. package/src/cliInteractive.mjs +533 -0
  16. package/src/cliInterruptTransform.mjs +51 -0
  17. package/src/cliMuteTransform.mjs +26 -0
  18. package/src/cliPasteTransform.mjs +183 -0
  19. package/src/config.d.ts +36 -0
  20. package/src/config.mjs +197 -0
  21. package/src/context/loadAgentRoles.mjs +267 -0
  22. package/src/context/loadPrompts.mjs +303 -0
  23. package/src/context/loadUserMessageContext.mjs +147 -0
  24. package/src/costTracker.mjs +210 -0
  25. package/src/env.mjs +44 -0
  26. package/src/main.mjs +281 -0
  27. package/src/mcpClient.mjs +351 -0
  28. package/src/mcpIntegration.mjs +160 -0
  29. package/src/model.d.ts +109 -0
  30. package/src/modelCaller.mjs +32 -0
  31. package/src/modelDefinition.d.ts +92 -0
  32. package/src/prompt.mjs +138 -0
  33. package/src/providers/anthropic.d.ts +248 -0
  34. package/src/providers/anthropic.mjs +587 -0
  35. package/src/providers/bedrock.d.ts +249 -0
  36. package/src/providers/bedrock.mjs +700 -0
  37. package/src/providers/gemini.d.ts +208 -0
  38. package/src/providers/gemini.mjs +754 -0
  39. package/src/providers/openai.d.ts +281 -0
  40. package/src/providers/openai.mjs +544 -0
  41. package/src/providers/openaiCompatible.d.ts +147 -0
  42. package/src/providers/openaiCompatible.mjs +652 -0
  43. package/src/providers/platform/awsSigV4.mjs +184 -0
  44. package/src/providers/platform/azure.mjs +42 -0
  45. package/src/providers/platform/bedrock.mjs +78 -0
  46. package/src/providers/platform/googleCloud.mjs +34 -0
  47. package/src/subagent.mjs +265 -0
  48. package/src/tmpfile.mjs +27 -0
  49. package/src/tool.d.ts +74 -0
  50. package/src/toolExecutor.mjs +236 -0
  51. package/src/toolInputValidator.mjs +183 -0
  52. package/src/toolUseApprover.mjs +99 -0
  53. package/src/tools/askURL.mjs +209 -0
  54. package/src/tools/askWeb.mjs +208 -0
  55. package/src/tools/compactContext.d.ts +4 -0
  56. package/src/tools/compactContext.mjs +87 -0
  57. package/src/tools/execCommand.d.ts +22 -0
  58. package/src/tools/execCommand.mjs +200 -0
  59. package/src/tools/patchFile.d.ts +4 -0
  60. package/src/tools/patchFile.mjs +133 -0
  61. package/src/tools/switchToMainAgent.d.ts +3 -0
  62. package/src/tools/switchToMainAgent.mjs +43 -0
  63. package/src/tools/switchToSubagent.d.ts +4 -0
  64. package/src/tools/switchToSubagent.mjs +59 -0
  65. package/src/tools/tmuxCommand.d.ts +14 -0
  66. package/src/tools/tmuxCommand.mjs +194 -0
  67. package/src/tools/writeFile.d.ts +4 -0
  68. package/src/tools/writeFile.mjs +56 -0
  69. package/src/usageStore.mjs +167 -0
  70. package/src/utils/evalJSONConfig.mjs +72 -0
  71. package/src/utils/matchValue.d.ts +6 -0
  72. package/src/utils/matchValue.mjs +40 -0
  73. package/src/utils/noThrow.mjs +31 -0
  74. package/src/utils/notify.mjs +29 -0
  75. package/src/utils/parseFileRange.mjs +18 -0
  76. package/src/utils/parseFrontmatter.mjs +19 -0
  77. package/src/utils/readFileRange.mjs +33 -0
  78. package/src/utils/retryOnError.mjs +41 -0
  79. package/src/voiceInput.mjs +61 -0
  80. package/src/voiceInputGemini.mjs +105 -0
  81. package/src/voiceInputOpenAI.mjs +104 -0
  82. package/src/voiceInputSession.mjs +543 -0
  83. package/src/voiceToggleKey.mjs +62 -0
  84. package/dist/main.mjs +0 -473
  85. package/dist/main.mjs.map +0 -7
@@ -0,0 +1,184 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash, createHmac } from "node:crypto";
3
+
4
+ /**
5
+ * @typedef {{ accessKeyId: string, secretAccessKey: string, sessionToken?: string }} AwsCredentials
6
+ */
7
+
8
+ /** @type {Map<string, { credentials: AwsCredentials, expiration: Date }>} */
9
+ const credentialCache = new Map();
10
+
11
+ const EXPIRATION_MARGIN_MS = 60 * 1000;
12
+
13
+ /**
14
+ * Load AWS credentials for the given profile using the AWS CLI.
15
+ * Results are cached and reused until the credentials expire.
16
+ * @param {string} profile
17
+ * @returns {Promise<AwsCredentials>}
18
+ */
19
+ export async function loadAwsCredentials(profile) {
20
+ const cached = credentialCache.get(profile);
21
+ if (
22
+ cached &&
23
+ Date.now() < cached.expiration.getTime() - EXPIRATION_MARGIN_MS
24
+ ) {
25
+ return cached.credentials;
26
+ }
27
+
28
+ /** @type {string} */
29
+ const stdout = await new Promise((resolve, reject) => {
30
+ execFile(
31
+ "aws",
32
+ ["configure", "export-credentials", "--profile", profile],
33
+ {
34
+ shell: false,
35
+ timeout: 30 * 1000,
36
+ },
37
+ (error, stdout, _stderr) => {
38
+ if (error) {
39
+ reject(error);
40
+ return;
41
+ }
42
+ resolve(stdout.trim());
43
+ },
44
+ );
45
+ });
46
+ const parsed = JSON.parse(stdout);
47
+ for (const key of ["AccessKeyId", "SecretAccessKey"]) {
48
+ if (!parsed[key] || typeof parsed[key] !== "string") {
49
+ throw new Error(
50
+ `AWS credentials output missing ${key}. Raw output: ${stdout.slice(0, 200)}`,
51
+ );
52
+ }
53
+ }
54
+ const credentials = {
55
+ accessKeyId: parsed.AccessKeyId,
56
+ secretAccessKey: parsed.SecretAccessKey,
57
+ ...(parsed.SessionToken && { sessionToken: parsed.SessionToken }),
58
+ };
59
+
60
+ if (parsed.Expiration) {
61
+ const expiration = new Date(parsed.Expiration);
62
+ if (!Number.isNaN(expiration.getTime())) {
63
+ credentialCache.set(profile, { credentials, expiration });
64
+ }
65
+ }
66
+
67
+ return credentials;
68
+ }
69
+
70
+ /**
71
+ * Sign an HTTP request with AWS Signature V4.
72
+ *
73
+ * Known limitation: if duplicate header names with different casing exist
74
+ * (e.g. `Content-Type` and `content-type`), only the first match is used.
75
+ * Per AWS SigV4 spec, values should be combined with commas. Callers should
76
+ * ensure header names are unique (case-insensitive) before signing.
77
+ *
78
+ * @param {{
79
+ * method: string,
80
+ * hostname: string,
81
+ * path: string,
82
+ * headers: Record<string, string>,
83
+ * body: string,
84
+ * }} request
85
+ * @param {{
86
+ * region: string,
87
+ * service: string,
88
+ * credentials: AwsCredentials,
89
+ * }} options
90
+ * @returns {{ method: string, headers: Record<string, string>, body: string }}
91
+ */
92
+ export function signAwsRequest(request, options) {
93
+ const { method, hostname, path, headers, body } = request;
94
+ const { region, service, credentials } = options;
95
+
96
+ const now = new Date();
97
+ const amzDate = now
98
+ .toISOString()
99
+ .replace(/[-:]/g, "")
100
+ .replace(/\.\d{3}/, "");
101
+ const dateStamp = amzDate.slice(0, 8);
102
+
103
+ /** @type {Record<string, string>} */
104
+ const signedHeaders = { ...headers, host: hostname, "x-amz-date": amzDate };
105
+ if (credentials.sessionToken) {
106
+ signedHeaders["x-amz-security-token"] = credentials.sessionToken;
107
+ }
108
+
109
+ // Canonical headers: sorted, lowercased, trimmed
110
+ const sortedKeys = Object.keys(signedHeaders)
111
+ .map((k) => k.toLowerCase())
112
+ .sort();
113
+ const canonicalHeaders = sortedKeys
114
+ .map((k) => {
115
+ const original = Object.keys(signedHeaders).find(
116
+ (h) => h.toLowerCase() === k,
117
+ );
118
+ return `${k}:${signedHeaders[/** @type {string} */ (original)].trim()}`;
119
+ })
120
+ .join("\n");
121
+ const signedHeadersList = sortedKeys.join(";");
122
+
123
+ const payloadHash = sha256Hex(body || "");
124
+
125
+ // AWS SigV4 Canonical URI requires each path segment to be URI-encoded.
126
+ // URL.pathname returns a decoded string, so we need to re-encode it.
127
+ const canonicalUri = path
128
+ .split("/")
129
+ .map((segment) => encodeURIComponent(segment))
130
+ .join("/");
131
+
132
+ const canonicalRequest = [
133
+ method,
134
+ canonicalUri,
135
+ "", // query string (empty for POST)
136
+ `${canonicalHeaders}\n`,
137
+ signedHeadersList,
138
+ payloadHash,
139
+ ].join("\n");
140
+
141
+ // String to sign
142
+ const scope = `${dateStamp}/${region}/${service}/aws4_request`;
143
+ const stringToSign = [
144
+ "AWS4-HMAC-SHA256",
145
+ amzDate,
146
+ scope,
147
+ sha256Hex(canonicalRequest),
148
+ ].join("\n");
149
+
150
+ // Signing key
151
+ const kDate = hmacSha256(`AWS4${credentials.secretAccessKey}`, dateStamp);
152
+ const kRegion = hmacSha256(kDate, region);
153
+ const kService = hmacSha256(kRegion, service);
154
+ const kSigning = hmacSha256(kService, "aws4_request");
155
+
156
+ const signature = createHmac("sha256", kSigning)
157
+ .update(stringToSign, "utf-8")
158
+ .digest("hex");
159
+
160
+ const authorization = `AWS4-HMAC-SHA256 Credential=${credentials.accessKeyId}/${scope}, SignedHeaders=${signedHeadersList}, Signature=${signature}`;
161
+
162
+ return {
163
+ method,
164
+ headers: { ...signedHeaders, Authorization: authorization },
165
+ body,
166
+ };
167
+ }
168
+
169
+ /**
170
+ * @param {string} data
171
+ * @returns {string}
172
+ */
173
+ function sha256Hex(data) {
174
+ return createHash("sha256").update(data, "utf-8").digest("hex");
175
+ }
176
+
177
+ /**
178
+ * @param {string | Buffer} key
179
+ * @param {string} data
180
+ * @returns {Buffer}
181
+ */
182
+ function hmacSha256(key, data) {
183
+ return createHmac("sha256", key).update(data, "utf-8").digest();
184
+ }
@@ -0,0 +1,42 @@
1
+ import { execFile } from "node:child_process";
2
+
3
+ /**
4
+ * @param {{azureConfigDir: string}=} config
5
+ * @returns {Promise<string>}
6
+ */
7
+ export async function getAzureAccessToken(config) {
8
+ /** @type {string} */
9
+ const stdout = await new Promise((resolve, reject) => {
10
+ execFile(
11
+ "az",
12
+ [
13
+ "account",
14
+ "get-access-token",
15
+ "--resource",
16
+ "https://cognitiveservices.azure.com",
17
+ "--query",
18
+ "accessToken",
19
+ "--output",
20
+ "tsv",
21
+ ],
22
+ {
23
+ shell: false,
24
+ timeout: 10 * 1000,
25
+ env: config
26
+ ? {
27
+ AZURE_CONFIG_DIR: config.azureConfigDir,
28
+ }
29
+ : undefined,
30
+ },
31
+ (error, stdout, _stderr) => {
32
+ if (error) {
33
+ reject(error);
34
+ return;
35
+ }
36
+ resolve(stdout.trim());
37
+ },
38
+ );
39
+ });
40
+
41
+ return stdout;
42
+ }
@@ -0,0 +1,78 @@
1
+ import { styleText } from "node:util";
2
+
3
+ /**
4
+ * @param {ReadableStreamDefaultReader<Uint8Array>} reader
5
+ */
6
+ export async function* readBedrockStreamEvents(reader) {
7
+ let buffer = new Uint8Array();
8
+
9
+ while (true) {
10
+ const { done, value } = await reader.read();
11
+ if (done) {
12
+ break;
13
+ }
14
+
15
+ const nextBuffer = new Uint8Array(buffer.length + value.length);
16
+ nextBuffer.set(buffer);
17
+ nextBuffer.set(value, buffer.length);
18
+ buffer = nextBuffer;
19
+
20
+ // AWS event stream format
21
+ // https://github.com/awslabs/aws-c-event-stream/blob/main/docs/images/encoding.png
22
+ while (buffer.length >= 12) {
23
+ const view = new DataView(
24
+ buffer.buffer,
25
+ buffer.byteOffset,
26
+ buffer.byteLength,
27
+ );
28
+ const totalLength = view.getUint32(0);
29
+ const headersLength = view.getUint32(4);
30
+
31
+ if (buffer.length < totalLength) {
32
+ break;
33
+ }
34
+
35
+ const payloadOffset = 12 + headersLength;
36
+ // prelude 12 bytes + CRC 4 bytes = 16
37
+ const payloadLength = totalLength - headersLength - 16;
38
+ const payloadRaw = buffer.slice(
39
+ payloadOffset,
40
+ payloadOffset + payloadLength,
41
+ );
42
+
43
+ const payloadDecoded = new TextDecoder().decode(payloadRaw);
44
+ try {
45
+ const payloadParsed = JSON.parse(payloadDecoded);
46
+ if (payloadParsed.bytes) {
47
+ // Invoke API format (base64 encoded event)
48
+ const event = Buffer.from(payloadParsed.bytes, "base64").toString(
49
+ "utf-8",
50
+ );
51
+ const eventParsed = JSON.parse(event);
52
+ yield eventParsed;
53
+ } else if (payloadParsed.message) {
54
+ console.error(
55
+ styleText(
56
+ "yellow",
57
+ `Bedrock message received: ${JSON.stringify(payloadParsed.message)}`,
58
+ ),
59
+ );
60
+ } else {
61
+ // Converse API format (direct event data)
62
+ yield payloadParsed;
63
+ }
64
+ } catch (err) {
65
+ if (err instanceof Error) {
66
+ console.error(
67
+ styleText(
68
+ "red",
69
+ `Error decoding payload: ${err.message}\nPayload: ${payloadDecoded}`,
70
+ ),
71
+ );
72
+ }
73
+ }
74
+
75
+ buffer = buffer.slice(totalLength);
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,34 @@
1
+ import { execFile } from "node:child_process";
2
+
3
+ /**
4
+ * @param {string=} account
5
+ * @returns {Promise<string>}
6
+ */
7
+ export async function getGoogleCloudAccessToken(account) {
8
+ const accountOption = account?.endsWith(".iam.gserviceaccount.com")
9
+ ? ["--impersonate-service-account", account]
10
+ : account
11
+ ? [account]
12
+ : [];
13
+
14
+ /** @type {string} */
15
+ const stdout = await new Promise((resolve, reject) => {
16
+ execFile(
17
+ "gcloud",
18
+ ["auth", "print-access-token", ...accountOption],
19
+ {
20
+ shell: false,
21
+ timeout: 10 * 1000,
22
+ },
23
+ (error, stdout, _stderr) => {
24
+ if (error) {
25
+ reject(error);
26
+ return;
27
+ }
28
+ resolve(stdout.trim());
29
+ },
30
+ );
31
+ });
32
+
33
+ return stdout;
34
+ }
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @import { Message, MessageContentToolResult, MessageContentToolUse } from "./model"
3
+ * @import { SwitchToMainAgentInput } from "./tools/switchToMainAgent"
4
+ * @import { AgentRole } from "./context/loadAgentRoles.mjs"
5
+ */
6
+
7
+ import fs from "node:fs/promises";
8
+ import path from "node:path";
9
+ import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
10
+ import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "./prompt.mjs";
11
+ import { switchToMainAgentToolName } from "./tools/switchToMainAgent.mjs";
12
+
13
+ /** @typedef {ReturnType<typeof createSubagentManager>} SubagentManager */
14
+
15
+ /**
16
+ * @typedef {Object} SubagentStateEventHandlers
17
+ * @property {(subagent: {name:string} | null) => void} onSubagentSwitched
18
+ */
19
+
20
+ /**
21
+ * Creates a manager for subagent lifecycle and state.
22
+ * @param {Map<string, AgentRole>} agentRoles
23
+ * @param {SubagentStateEventHandlers} handlers
24
+ */
25
+ export function createSubagentManager(agentRoles, handlers) {
26
+ /** @type {{name: string; goal: string; switchMessageIndex: number}[]} */
27
+ const subagents = [];
28
+ let subagentCount = 0;
29
+
30
+ /**
31
+ * @typedef {SwitchToSubagentSuccess | SwitchToSubagentFailure} SwitchToSubagentResult
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} SwitchToSubagentSuccess
36
+ * @property {true} success
37
+ * @property {string} value
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} SwitchToSubagentFailure
42
+ * @property {false} success
43
+ * @property {string} error
44
+ */
45
+
46
+ /**
47
+ * Switch to a subagent role.
48
+ * @param {string} name
49
+ * @param {string} goal
50
+ * @param {number} switchMessageIndex
51
+ * @returns {SwitchToSubagentResult}
52
+ */
53
+ function switchToSubagent(name, goal, switchMessageIndex) {
54
+ if (subagents.length > 0) {
55
+ return {
56
+ success: false,
57
+ error:
58
+ "Cannot call switch_to_subagent while already acting as a subagent.",
59
+ };
60
+ }
61
+
62
+ const isCustomRole = name.startsWith("custom:");
63
+ const actualName = isCustomRole ? name.substring(7) : name;
64
+
65
+ let roleContent = "";
66
+ if (!isCustomRole) {
67
+ const role = agentRoles.get(name);
68
+ if (!role) {
69
+ const availableRoles = Array.from(agentRoles.keys())
70
+ .sort()
71
+ .map((id) => ` - ${id}`)
72
+ .join("\n");
73
+ return {
74
+ success: false,
75
+ error: `Agent role "${name}" not found. Available agent roles:\n${availableRoles}\n\nTo use an ad-hoc role, prefix the name with "custom:" (e.g., "custom:researcher").`,
76
+ };
77
+ }
78
+ roleContent = role.claudeOriginated
79
+ ? `${role.content}\n\n---\n\n${CLAUDE_CODE_COMPATIBILITY_NOTES}`
80
+ : role.content;
81
+ }
82
+
83
+ subagentCount++;
84
+ const sequenceNumber = String(subagentCount).padStart(2, "0");
85
+
86
+ subagents.push({
87
+ name: actualName,
88
+ goal,
89
+ switchMessageIndex,
90
+ });
91
+ handlers.onSubagentSwitched({ name: actualName });
92
+
93
+ return {
94
+ success: true,
95
+ value: [
96
+ `[SUBAGENT MODE ACTIVATED] You are now operating as the subagent "${actualName}".`,
97
+ roleContent
98
+ ? `Role: ${actualName}\n---\n${roleContent}\n---`
99
+ : `Role: ${actualName}`,
100
+ `Your goal: ${goal}`,
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 "switch_to_main_agent" with the memory file path. Start executing your goal now.`,
103
+ ].join("\n\n"),
104
+ };
105
+ }
106
+
107
+ /**
108
+ * @typedef {SwitchToMainAgentSuccess | SwitchToMainAgentFailure} SwitchToMainAgentResult
109
+ */
110
+
111
+ /**
112
+ * @typedef {Object} SwitchToMainAgentSuccess
113
+ * @property {true} success
114
+ * @property {string} memoryContent
115
+ */
116
+
117
+ /**
118
+ * @typedef {Object} SwitchToMainAgentFailure
119
+ * @property {false} success
120
+ * @property {string} error
121
+ */
122
+
123
+ /**
124
+ * Switch back to the main agent role and read the memory file.
125
+ * @param {string} memoryPath
126
+ * @returns {Promise<SwitchToMainAgentResult>}
127
+ */
128
+ async function switchToMainAgent(memoryPath) {
129
+ if (subagents.length === 0) {
130
+ return {
131
+ success: false,
132
+ error: "Cannot call switch_to_main_agent from the main agent.",
133
+ };
134
+ }
135
+
136
+ const absolutePath = path.resolve(memoryPath);
137
+ const memoryDir = path.resolve(AGENT_PROJECT_METADATA_DIR, "memory");
138
+ const relativePath = path.relative(memoryDir, absolutePath);
139
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
140
+ return {
141
+ success: false,
142
+ error: `Access denied: memoryPath must be within ${AGENT_PROJECT_METADATA_DIR}/memory`,
143
+ };
144
+ }
145
+
146
+ try {
147
+ const memoryContent = await fs.readFile(absolutePath, {
148
+ encoding: "utf-8",
149
+ });
150
+ return {
151
+ success: true,
152
+ memoryContent: memoryContent,
153
+ };
154
+ } catch (error) {
155
+ return {
156
+ success: false,
157
+ error: `Failed to read memory file: ${error instanceof Error ? error.message : String(error)}`,
158
+ };
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Process tool results and update state based on special tools.
164
+ * Returns the truncated message history and a new message to add.
165
+ * @param {MessageContentToolUse[]} toolUseParts
166
+ * @param {MessageContentToolResult[]} toolResults
167
+ * @param {Message[]} messages
168
+ * @returns {{ messages: Message[], newMessage: Message | null }}
169
+ * - messages: The potentially truncated message history (new array)
170
+ * - newMessage: The user message to add, or null if tool results should be added directly
171
+ */
172
+ function processToolResults(toolUseParts, toolResults, messages) {
173
+ const reportSubagentToolUse = toolUseParts.find(
174
+ (toolUse) => toolUse.toolName === switchToMainAgentToolName,
175
+ );
176
+
177
+ if (reportSubagentToolUse) {
178
+ const reportResult = toolResults.find(
179
+ (res) => res.toolUseId === reportSubagentToolUse.toolUseId,
180
+ );
181
+ if (!reportResult) {
182
+ return { messages, newMessage: null };
183
+ }
184
+ return handleSubagentReport(
185
+ reportSubagentToolUse,
186
+ reportResult,
187
+ messages,
188
+ );
189
+ }
190
+
191
+ return { messages, newMessage: null };
192
+ }
193
+
194
+ /**
195
+ * Handle the result of a subagent reporting back.
196
+ * On success, truncates conversation history back to the switch point
197
+ * and converts the report into a standard user message.
198
+ * @param {MessageContentToolUse} reportToolUse
199
+ * @param {MessageContentToolResult} reportResult
200
+ * @param {Message[]} messages
201
+ * @returns {{ messages: Message[], newMessage: Message | null }}
202
+ * - messages: The truncated message history (new array)
203
+ * - newMessage: The user message to add, or null if not handled
204
+ */
205
+ function handleSubagentReport(reportToolUse, reportResult, messages) {
206
+ if (reportResult.isError) {
207
+ return { messages, newMessage: null };
208
+ }
209
+
210
+ const currentSubagent = subagents.pop();
211
+ if (!currentSubagent) {
212
+ return { messages, newMessage: null };
213
+ }
214
+
215
+ handlers.onSubagentSwitched(subagents.at(-1) ?? null);
216
+
217
+ // Truncate history back to the switch point
218
+ const truncatedMessages = messages.slice(
219
+ 0,
220
+ currentSubagent.switchMessageIndex,
221
+ );
222
+
223
+ // Convert the tool result into a standard user message
224
+ const resultText = reportResult.content
225
+ .map((c) => (c.type === "text" ? c.text : ""))
226
+ .join("\n\n");
227
+
228
+ const reportInput = /** @type {SwitchToMainAgentInput} */ (
229
+ reportToolUse.input
230
+ );
231
+
232
+ /** @type {import('./model').UserMessage} */
233
+ const newMessage = {
234
+ role: "user",
235
+ content: [
236
+ {
237
+ type: "text",
238
+ text: [
239
+ `The subagent "${currentSubagent.name}" has completed the task.`,
240
+ `Goal: ${currentSubagent.goal}`,
241
+ `Memory file: ${reportInput.memoryPath}`,
242
+ `Result:\n${resultText}`,
243
+ ].join("\n\n"),
244
+ },
245
+ ],
246
+ };
247
+
248
+ return { messages: truncatedMessages, newMessage };
249
+ }
250
+
251
+ /**
252
+ * Whether the main agent is currently running as a subagent.
253
+ * @returns {boolean}
254
+ */
255
+ function isSubagentActive() {
256
+ return subagents.length > 0;
257
+ }
258
+
259
+ return {
260
+ switchToSubagent,
261
+ switchToMainAgent,
262
+ processToolResults,
263
+ isSubagentActive,
264
+ };
265
+ }
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { AGENT_TMP_DIR } from "./env.mjs";
4
+
5
+ /**
6
+ * Write content to a temporary file and return the file path
7
+ * @param {string} content - Content to write
8
+ * @param {string} name - File name (e.g., "read_web_page")
9
+ * @param {string} extension - File extension (e.g., "md", "txt")
10
+ * @returns {Promise<string>} Path to the created temporary file
11
+ */
12
+ export async function writeTmpFile(content, name, extension = "txt") {
13
+ const timestamp = new Date()
14
+ .toISOString()
15
+ .slice(0, 19)
16
+ .replace("T", "-")
17
+ .replace(/:/g, "");
18
+ const randomSuffix = Math.random().toString(36).substring(2, 8);
19
+
20
+ const fileName = `${timestamp}-${randomSuffix}--${name}.${extension}`;
21
+ const filePath = path.join(AGENT_TMP_DIR, fileName);
22
+
23
+ await fs.mkdir(AGENT_TMP_DIR, { recursive: true });
24
+ await fs.writeFile(filePath, content, "utf8");
25
+
26
+ return filePath;
27
+ }
package/src/tool.d.ts ADDED
@@ -0,0 +1,74 @@
1
+ import type { MessageContentToolUse } from "./model";
2
+
3
+ export type Tool = {
4
+ def: ToolDefinition;
5
+ impl: ToolImplementation;
6
+ validateInput?: (input: Record<string, unknown>) => Error | undefined;
7
+ maskApprovalInput?: (
8
+ input: Record<string, unknown>,
9
+ ) => Record<string, unknown>;
10
+ injectImpl?: (impl: ToolImplementation) => void;
11
+ };
12
+
13
+ export type ToolDefinition = {
14
+ name: string;
15
+ description: string;
16
+ inputSchema: Record<string, unknown>;
17
+ };
18
+
19
+ export type ToolImplementation = (
20
+ input: Record,
21
+ ) => Promise<string | StructuredToolResultContent[] | Error>;
22
+
23
+ export type StructuredToolResultContent =
24
+ | {
25
+ type: "text";
26
+ text: string;
27
+ }
28
+ | {
29
+ type: "image";
30
+ // base64 encoded
31
+ data: string;
32
+ // e.g., image/jpeg
33
+ mimeType: string;
34
+ };
35
+
36
+ export type ToolUseApproverConfig = {
37
+ patterns: ToolUsePattern[];
38
+ maxApprovals: number;
39
+ defaultAction: "deny" | "ask";
40
+
41
+ /**
42
+ * Mask the input before auto-approval checks and recording.
43
+ * Return a redacted object (e.g., keep only necessary fields) that will be used for:
44
+ * - safety validation via isSafeToolInput
45
+ * - storing per-session allowed tool-use patterns
46
+ */
47
+ maskApprovalInput: (
48
+ toolName: string,
49
+ input: Record<string, unknown>,
50
+ ) => Record<string, unknown>;
51
+ };
52
+
53
+ export type ToolUseDecision = {
54
+ action: "allow" | "deny" | "ask";
55
+ reason?: string;
56
+ };
57
+
58
+ export type ToolUseApprover = {
59
+ isAllowedToolUse: (toolUse: MessageContentToolUse) => ToolUseDecision;
60
+ allowToolUse: (toolUse: MessageContentToolUse) => void;
61
+ resetApprovalCount: () => void;
62
+ };
63
+
64
+ export type ToolUsePattern = {
65
+ toolName: ValuePattern;
66
+ input?: ObjectPattern;
67
+ action?: "allow" | "deny" | "ask";
68
+ reason?: string;
69
+ };
70
+
71
+ export type ToolUse = {
72
+ toolName: string;
73
+ input: Record<string, unknown>;
74
+ };