@iinm/plain-agent 1.8.3 → 1.8.4
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/bin/plain +1 -1
- package/config/config.predefined.json +1 -1
- package/config/prompts.predefined/shortcuts/configure.md +1 -1
- package/dist/main.mjs +473 -0
- package/dist/main.mjs.map +7 -0
- package/package.json +5 -7
- package/src/agent.d.ts +0 -52
- package/src/agent.mjs +0 -204
- package/src/agentLoop.mjs +0 -419
- package/src/agentState.mjs +0 -41
- package/src/claudeCodePlugin.mjs +0 -164
- package/src/cliArgs.mjs +0 -175
- package/src/cliBatch.mjs +0 -147
- package/src/cliCommands.mjs +0 -283
- package/src/cliCompleter.mjs +0 -227
- package/src/cliCost.mjs +0 -309
- package/src/cliFormatter.mjs +0 -413
- package/src/cliInteractive.mjs +0 -529
- package/src/cliInterruptTransform.mjs +0 -51
- package/src/cliMuteTransform.mjs +0 -26
- package/src/cliPasteTransform.mjs +0 -183
- package/src/config.d.ts +0 -36
- package/src/config.mjs +0 -197
- package/src/context/loadAgentRoles.mjs +0 -283
- package/src/context/loadPrompts.mjs +0 -324
- package/src/context/loadUserMessageContext.mjs +0 -147
- package/src/costTracker.mjs +0 -210
- package/src/env.mjs +0 -44
- package/src/main.mjs +0 -279
- package/src/mcpClient.mjs +0 -351
- package/src/mcpIntegration.mjs +0 -160
- package/src/model.d.ts +0 -109
- package/src/modelCaller.mjs +0 -32
- package/src/modelDefinition.d.ts +0 -92
- package/src/prompt.mjs +0 -138
- package/src/providers/anthropic.d.ts +0 -248
- package/src/providers/anthropic.mjs +0 -587
- package/src/providers/bedrock.d.ts +0 -249
- package/src/providers/bedrock.mjs +0 -700
- package/src/providers/gemini.d.ts +0 -208
- package/src/providers/gemini.mjs +0 -754
- package/src/providers/openai.d.ts +0 -281
- package/src/providers/openai.mjs +0 -544
- package/src/providers/openaiCompatible.d.ts +0 -147
- package/src/providers/openaiCompatible.mjs +0 -652
- package/src/providers/platform/awsSigV4.mjs +0 -184
- package/src/providers/platform/azure.mjs +0 -42
- package/src/providers/platform/bedrock.mjs +0 -78
- package/src/providers/platform/googleCloud.mjs +0 -34
- package/src/subagent.mjs +0 -265
- package/src/tmpfile.mjs +0 -27
- package/src/tool.d.ts +0 -74
- package/src/toolExecutor.mjs +0 -236
- package/src/toolInputValidator.mjs +0 -183
- package/src/toolUseApprover.mjs +0 -99
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
- package/src/tools/compactContext.d.ts +0 -4
- package/src/tools/compactContext.mjs +0 -87
- package/src/tools/delegateToSubagent.d.ts +0 -4
- package/src/tools/delegateToSubagent.mjs +0 -48
- package/src/tools/execCommand.d.ts +0 -22
- package/src/tools/execCommand.mjs +0 -200
- package/src/tools/patchFile.d.ts +0 -4
- package/src/tools/patchFile.mjs +0 -133
- package/src/tools/reportAsSubagent.d.ts +0 -3
- package/src/tools/reportAsSubagent.mjs +0 -44
- package/src/tools/tmuxCommand.d.ts +0 -14
- package/src/tools/tmuxCommand.mjs +0 -194
- package/src/tools/writeFile.d.ts +0 -4
- package/src/tools/writeFile.mjs +0 -56
- package/src/usageStore.mjs +0 -167
- package/src/utils/evalJSONConfig.mjs +0 -72
- package/src/utils/matchValue.d.ts +0 -6
- package/src/utils/matchValue.mjs +0 -40
- package/src/utils/noThrow.mjs +0 -31
- package/src/utils/notify.mjs +0 -29
- package/src/utils/parseFileRange.mjs +0 -18
- package/src/utils/readFileRange.mjs +0 -33
- package/src/utils/retryOnError.mjs +0 -41
- package/src/voiceInput.mjs +0 -61
- package/src/voiceInputGemini.mjs +0 -105
- package/src/voiceInputOpenAI.mjs +0 -104
- package/src/voiceInputSession.mjs +0 -543
- package/src/voiceToggleKey.mjs +0 -62
|
@@ -1,184 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
}
|
package/src/subagent.mjs
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @import { Message, MessageContentToolResult, MessageContentToolUse } from "./model"
|
|
3
|
-
* @import { ReportAsSubagentInput } from "./tools/reportAsSubagent"
|
|
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 { reportAsSubagentToolName } from "./tools/reportAsSubagent.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; delegationMessageIndex: number}[]} */
|
|
27
|
-
const subagents = [];
|
|
28
|
-
let subagentCount = 0;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {DelegateSuccess | DelegateFailure} DelegateResult
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* @typedef {Object} DelegateSuccess
|
|
36
|
-
* @property {true} success
|
|
37
|
-
* @property {string} value
|
|
38
|
-
*/
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @typedef {Object} DelegateFailure
|
|
42
|
-
* @property {false} success
|
|
43
|
-
* @property {string} error
|
|
44
|
-
*/
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Delegate a task to a subagent.
|
|
48
|
-
* @param {string} name
|
|
49
|
-
* @param {string} goal
|
|
50
|
-
* @param {number} delegationMessageIndex
|
|
51
|
-
* @returns {DelegateResult}
|
|
52
|
-
*/
|
|
53
|
-
function delegateToSubagent(name, goal, delegationMessageIndex) {
|
|
54
|
-
if (subagents.length > 0) {
|
|
55
|
-
return {
|
|
56
|
-
success: false,
|
|
57
|
-
error:
|
|
58
|
-
"Cannot call delegate_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
|
-
delegationMessageIndex,
|
|
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 "report_as_subagent" with the memory file path. Start executing your goal now.`,
|
|
103
|
-
].join("\n\n"),
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* @typedef {ReportSuccess | ReportFailure} ReportResult
|
|
109
|
-
*/
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* @typedef {Object} ReportSuccess
|
|
113
|
-
* @property {true} success
|
|
114
|
-
* @property {string} memoryContent
|
|
115
|
-
*/
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @typedef {Object} ReportFailure
|
|
119
|
-
* @property {false} success
|
|
120
|
-
* @property {string} error
|
|
121
|
-
*/
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Report as a subagent and read the memory file.
|
|
125
|
-
* @param {string} memoryPath
|
|
126
|
-
* @returns {Promise<ReportResult>}
|
|
127
|
-
*/
|
|
128
|
-
async function reportAsSubagent(memoryPath) {
|
|
129
|
-
if (subagents.length === 0) {
|
|
130
|
-
return {
|
|
131
|
-
success: false,
|
|
132
|
-
error: "Cannot call report_as_subagent 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 === reportAsSubagentToolName,
|
|
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 delegation 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 delegation point
|
|
218
|
-
const truncatedMessages = messages.slice(
|
|
219
|
-
0,
|
|
220
|
-
currentSubagent.delegationMessageIndex,
|
|
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 {ReportAsSubagentInput} */ (
|
|
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
|
-
delegateToSubagent,
|
|
261
|
-
reportAsSubagent,
|
|
262
|
-
processToolResults,
|
|
263
|
-
isSubagentActive,
|
|
264
|
-
};
|
|
265
|
-
}
|
package/src/tmpfile.mjs
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
};
|