hoomanjs 1.0.0
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/.github/screenshot.png +0 -0
- package/.github/workflows/build-publish.yml +49 -0
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/docker-compose.yml +13 -0
- package/package.json +78 -0
- package/src/acp/acp-agent.ts +803 -0
- package/src/acp/approvals.ts +147 -0
- package/src/acp/index.ts +1 -0
- package/src/acp/meta/system-prompt.ts +44 -0
- package/src/acp/meta/user-id.ts +44 -0
- package/src/acp/prompt-invoke.ts +149 -0
- package/src/acp/sessions/config-options.ts +56 -0
- package/src/acp/sessions/replay.ts +131 -0
- package/src/acp/sessions/store.ts +158 -0
- package/src/acp/sessions/title.ts +22 -0
- package/src/acp/utils/paths.ts +5 -0
- package/src/acp/utils/tool-kind.ts +38 -0
- package/src/acp/utils/tool-locations.ts +46 -0
- package/src/acp/utils/tool-result-content.ts +27 -0
- package/src/chat/app.tsx +428 -0
- package/src/chat/approvals.ts +96 -0
- package/src/chat/components/ApprovalPrompt.tsx +25 -0
- package/src/chat/components/ChatMessage.tsx +47 -0
- package/src/chat/components/Composer.tsx +39 -0
- package/src/chat/components/EmptyChatBanner.tsx +26 -0
- package/src/chat/components/ReasoningStrip.tsx +30 -0
- package/src/chat/components/Spinner.tsx +34 -0
- package/src/chat/components/StatusBar.tsx +65 -0
- package/src/chat/components/ThinkingStatus.tsx +128 -0
- package/src/chat/components/ToolEvent.tsx +34 -0
- package/src/chat/components/Transcript.tsx +34 -0
- package/src/chat/components/ascii-logo.ts +11 -0
- package/src/chat/components/shared.ts +70 -0
- package/src/chat/index.tsx +42 -0
- package/src/chat/types.ts +21 -0
- package/src/cli.ts +146 -0
- package/src/configure/app.tsx +911 -0
- package/src/configure/components/BusyScreen.tsx +22 -0
- package/src/configure/components/HomeScreen.tsx +43 -0
- package/src/configure/components/MenuScreen.tsx +44 -0
- package/src/configure/components/PromptForm.tsx +40 -0
- package/src/configure/components/SelectMenuItem.tsx +30 -0
- package/src/configure/index.tsx +43 -0
- package/src/configure/open-in-editor.ts +133 -0
- package/src/configure/types.ts +45 -0
- package/src/configure/utils.ts +113 -0
- package/src/core/agent/index.ts +76 -0
- package/src/core/config.ts +157 -0
- package/src/core/index.ts +54 -0
- package/src/core/mcp/config.ts +80 -0
- package/src/core/mcp/index.ts +13 -0
- package/src/core/mcp/manager.ts +109 -0
- package/src/core/mcp/prefixed-mcp-tool.ts +45 -0
- package/src/core/mcp/tools.ts +92 -0
- package/src/core/mcp/types.ts +37 -0
- package/src/core/memory/index.ts +17 -0
- package/src/core/memory/ltm/embed.ts +67 -0
- package/src/core/memory/ltm/index.ts +18 -0
- package/src/core/memory/ltm/store.ts +376 -0
- package/src/core/memory/ltm/tools.ts +146 -0
- package/src/core/memory/ltm/types.ts +111 -0
- package/src/core/memory/ltm/utils.ts +218 -0
- package/src/core/memory/stm/index.ts +17 -0
- package/src/core/models/anthropic.ts +53 -0
- package/src/core/models/bedrock.ts +54 -0
- package/src/core/models/google.ts +51 -0
- package/src/core/models/index.ts +16 -0
- package/src/core/models/ollama/index.ts +13 -0
- package/src/core/models/ollama/strands-ollama.ts +439 -0
- package/src/core/models/openai.ts +12 -0
- package/src/core/prompts/index.ts +23 -0
- package/src/core/prompts/skills.ts +66 -0
- package/src/core/prompts/static/fetch.md +33 -0
- package/src/core/prompts/static/filesystem.md +38 -0
- package/src/core/prompts/static/identity.md +22 -0
- package/src/core/prompts/static/ltm.md +39 -0
- package/src/core/prompts/static/memory.md +39 -0
- package/src/core/prompts/static/shell.md +34 -0
- package/src/core/prompts/static/skills.md +19 -0
- package/src/core/prompts/static/thinking.md +27 -0
- package/src/core/prompts/system.ts +109 -0
- package/src/core/skills/index.ts +2 -0
- package/src/core/skills/registry.ts +239 -0
- package/src/core/skills/tools.ts +80 -0
- package/src/core/toolkit.ts +13 -0
- package/src/core/tools/fetch.ts +288 -0
- package/src/core/tools/filesystem.ts +747 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/shell.ts +426 -0
- package/src/core/tools/thinking.ts +184 -0
- package/src/core/tools/time.ts +121 -0
- package/src/core/utils/cwd-context.ts +11 -0
- package/src/core/utils/paths.ts +28 -0
- package/src/exec/approvals.ts +85 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import { tool } from "@strands-agents/sdk";
|
|
6
|
+
import type { JSONValue, ToolContext } from "@strands-agents/sdk";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { getCwd } from "../utils/cwd-context.ts";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT_SECONDS = Number.parseInt(
|
|
11
|
+
process.env.SHELL_DEFAULT_TIMEOUT ?? "900",
|
|
12
|
+
10,
|
|
13
|
+
);
|
|
14
|
+
const SIGKILL_TIMEOUT_MS = 200;
|
|
15
|
+
const MAX_OUTPUT_CHARS = 12_000;
|
|
16
|
+
|
|
17
|
+
const CommandObjectSchema = z.object({
|
|
18
|
+
command: z.string().min(1).describe("Shell command to execute."),
|
|
19
|
+
timeout: z
|
|
20
|
+
.number()
|
|
21
|
+
.positive()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Per-command timeout in seconds."),
|
|
24
|
+
work_dir: z.string().optional().describe("Per-command working directory."),
|
|
25
|
+
stdin: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Optional stdin content to send to the command."),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const CommandSchema = z.union([z.string(), CommandObjectSchema]);
|
|
32
|
+
|
|
33
|
+
type CommandInput = z.infer<typeof CommandSchema>;
|
|
34
|
+
|
|
35
|
+
type ShellConfig = {
|
|
36
|
+
file: string;
|
|
37
|
+
args: string[];
|
|
38
|
+
windowsHide?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type NormalizedCommand = {
|
|
42
|
+
command: string;
|
|
43
|
+
timeoutSeconds: number;
|
|
44
|
+
workDir?: string;
|
|
45
|
+
stdin?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type CommandResult = {
|
|
49
|
+
command: string;
|
|
50
|
+
cwd: string;
|
|
51
|
+
exit_code: number;
|
|
52
|
+
status: "success" | "error";
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
output: string;
|
|
56
|
+
timed_out: boolean;
|
|
57
|
+
duration_ms: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function toJsonValue(value: unknown): JSONValue {
|
|
61
|
+
return JSON.parse(JSON.stringify(value)) as JSONValue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function trimOutput(output: string): string {
|
|
65
|
+
if (output.length <= MAX_OUTPUT_CHARS) {
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
return `${output.slice(0, MAX_OUTPUT_CHARS)}\n...[output truncated]`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function which(command: string): string | undefined {
|
|
72
|
+
const lookup = process.platform === "win32" ? "where" : "which";
|
|
73
|
+
const result = spawnSync(lookup, [command], {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (result.status !== 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const first = result.stdout
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.map((line) => line.trim())
|
|
85
|
+
.find(Boolean);
|
|
86
|
+
|
|
87
|
+
return first || undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function shellName(file: string): string {
|
|
91
|
+
const base =
|
|
92
|
+
process.platform === "win32"
|
|
93
|
+
? path.win32.parse(file).name
|
|
94
|
+
: path.basename(file);
|
|
95
|
+
return base.toLowerCase();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function pickShell(shell?: string): string {
|
|
99
|
+
if (shell?.trim()) {
|
|
100
|
+
return shell.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (process.platform === "win32") {
|
|
104
|
+
return (
|
|
105
|
+
which("pwsh.exe") ||
|
|
106
|
+
which("powershell.exe") ||
|
|
107
|
+
process.env.COMSPEC ||
|
|
108
|
+
"cmd.exe"
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
process.env.SHELL ||
|
|
114
|
+
(process.platform === "darwin" ? "/bin/zsh" : "/bin/sh")
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function shellConfig(command: string, shell?: string): ShellConfig {
|
|
119
|
+
const file = pickShell(shell);
|
|
120
|
+
const name = shellName(file);
|
|
121
|
+
|
|
122
|
+
if (process.platform === "win32") {
|
|
123
|
+
if (name === "pwsh" || name === "powershell") {
|
|
124
|
+
return {
|
|
125
|
+
file,
|
|
126
|
+
args: ["-NoProfile", "-NonInteractive", "-Command", command],
|
|
127
|
+
windowsHide: true,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (name === "bash" || name === "zsh" || name === "sh") {
|
|
132
|
+
return { file, args: ["-lc", command], windowsHide: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { file, args: ["/d", "/s", "/c", command], windowsHide: true };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { file, args: ["-lc", command] };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function killTree(
|
|
142
|
+
proc: ChildProcess,
|
|
143
|
+
opts?: { exited?: () => boolean },
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const pid = proc.pid;
|
|
146
|
+
if (!pid || opts?.exited?.()) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (process.platform === "win32") {
|
|
151
|
+
await new Promise<void>((resolve) => {
|
|
152
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], {
|
|
153
|
+
stdio: "ignore",
|
|
154
|
+
windowsHide: true,
|
|
155
|
+
});
|
|
156
|
+
killer.once("exit", () => resolve());
|
|
157
|
+
killer.once("error", () => resolve());
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
process.kill(-pid, "SIGTERM");
|
|
164
|
+
await sleep(SIGKILL_TIMEOUT_MS);
|
|
165
|
+
if (!opts?.exited?.()) {
|
|
166
|
+
process.kill(-pid, "SIGKILL");
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
proc.kill("SIGTERM");
|
|
170
|
+
await sleep(SIGKILL_TIMEOUT_MS);
|
|
171
|
+
if (!opts?.exited?.()) {
|
|
172
|
+
proc.kill("SIGKILL");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveWorkDir(baseDir: string, workDir?: string): string {
|
|
178
|
+
const raw = workDir?.trim();
|
|
179
|
+
if (!raw) {
|
|
180
|
+
return baseDir;
|
|
181
|
+
}
|
|
182
|
+
return path.resolve(baseDir, raw);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeCommands(
|
|
186
|
+
input: z.infer<ReturnType<typeof createShellInputSchema>>,
|
|
187
|
+
): NormalizedCommand[] {
|
|
188
|
+
const commands = Array.isArray(input.command)
|
|
189
|
+
? input.command
|
|
190
|
+
: [input.command];
|
|
191
|
+
|
|
192
|
+
return commands.map((entry) => {
|
|
193
|
+
if (typeof entry === "string") {
|
|
194
|
+
return {
|
|
195
|
+
command: entry,
|
|
196
|
+
timeoutSeconds: input.timeout ?? DEFAULT_TIMEOUT_SECONDS,
|
|
197
|
+
workDir: input.work_dir,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
command: entry.command,
|
|
203
|
+
timeoutSeconds: entry.timeout ?? input.timeout ?? DEFAULT_TIMEOUT_SECONDS,
|
|
204
|
+
workDir: entry.work_dir ?? input.work_dir,
|
|
205
|
+
stdin: entry.stdin,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function updateSequentialDir(currentDir: string, command: string): string {
|
|
211
|
+
const trimmed = command.trim();
|
|
212
|
+
const match = /^cd\s+(.+)$/.exec(trimmed);
|
|
213
|
+
if (!match) {
|
|
214
|
+
return currentDir;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const raw = match[1]!.trim().replace(/^['"]|['"]$/g, "");
|
|
218
|
+
const nextDir = path.resolve(currentDir, raw);
|
|
219
|
+
|
|
220
|
+
if (!existsSync(nextDir)) {
|
|
221
|
+
return currentDir;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return nextDir;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function executeOne(
|
|
228
|
+
item: NormalizedCommand,
|
|
229
|
+
cwd: string,
|
|
230
|
+
context?: ToolContext,
|
|
231
|
+
): Promise<CommandResult> {
|
|
232
|
+
const startedAt = Date.now();
|
|
233
|
+
const cfg = shellConfig(item.command);
|
|
234
|
+
|
|
235
|
+
let exited = false;
|
|
236
|
+
let timedOut = false;
|
|
237
|
+
let stdout = "";
|
|
238
|
+
let stderr = "";
|
|
239
|
+
|
|
240
|
+
const child = spawn(cfg.file, cfg.args, {
|
|
241
|
+
cwd,
|
|
242
|
+
env: process.env,
|
|
243
|
+
detached: process.platform !== "win32",
|
|
244
|
+
stdio: "pipe",
|
|
245
|
+
windowsHide: cfg.windowsHide ?? false,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
child.stdout?.on("data", (chunk: Buffer | string) => {
|
|
249
|
+
stdout += chunk.toString();
|
|
250
|
+
});
|
|
251
|
+
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
252
|
+
stderr += chunk.toString();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (item.stdin !== undefined) {
|
|
256
|
+
child.stdin?.write(item.stdin);
|
|
257
|
+
}
|
|
258
|
+
child.stdin?.end();
|
|
259
|
+
|
|
260
|
+
const timeoutMs = item.timeoutSeconds * 1000;
|
|
261
|
+
|
|
262
|
+
return await new Promise<CommandResult>((resolve) => {
|
|
263
|
+
const timeout = setTimeout(async () => {
|
|
264
|
+
timedOut = true;
|
|
265
|
+
await killTree(child, { exited: () => exited });
|
|
266
|
+
}, timeoutMs);
|
|
267
|
+
|
|
268
|
+
const abortHandler = async () => {
|
|
269
|
+
await killTree(child, { exited: () => exited });
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
context?.agent.cancelSignal.addEventListener("abort", abortHandler, {
|
|
273
|
+
once: true,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
child.once("error", (error) => {
|
|
277
|
+
exited = true;
|
|
278
|
+
clearTimeout(timeout);
|
|
279
|
+
context?.agent.cancelSignal.removeEventListener("abort", abortHandler);
|
|
280
|
+
resolve({
|
|
281
|
+
command: item.command,
|
|
282
|
+
cwd,
|
|
283
|
+
exit_code: 1,
|
|
284
|
+
status: "error",
|
|
285
|
+
stdout: trimOutput(stdout),
|
|
286
|
+
stderr: trimOutput(`${stderr}\n${error.message}`.trim()),
|
|
287
|
+
output: trimOutput(
|
|
288
|
+
[stdout, stderr, error.message].filter(Boolean).join("\n"),
|
|
289
|
+
),
|
|
290
|
+
timed_out: timedOut,
|
|
291
|
+
duration_ms: Date.now() - startedAt,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
child.once("close", (code) => {
|
|
296
|
+
exited = true;
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
context?.agent.cancelSignal.removeEventListener("abort", abortHandler);
|
|
299
|
+
|
|
300
|
+
const exitCode = code ?? (timedOut ? 124 : 1);
|
|
301
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
302
|
+
|
|
303
|
+
resolve({
|
|
304
|
+
command: item.command,
|
|
305
|
+
cwd,
|
|
306
|
+
exit_code: exitCode,
|
|
307
|
+
status: exitCode === 0 && !timedOut ? "success" : "error",
|
|
308
|
+
stdout: trimOutput(stdout),
|
|
309
|
+
stderr: trimOutput(stderr),
|
|
310
|
+
output: trimOutput(combined),
|
|
311
|
+
timed_out: timedOut,
|
|
312
|
+
duration_ms: Date.now() - startedAt,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function executeSequential(
|
|
319
|
+
commands: NormalizedCommand[],
|
|
320
|
+
baseDir: string,
|
|
321
|
+
ignoreErrors: boolean,
|
|
322
|
+
context?: ToolContext,
|
|
323
|
+
): Promise<CommandResult[]> {
|
|
324
|
+
const results: CommandResult[] = [];
|
|
325
|
+
let currentDir = baseDir;
|
|
326
|
+
|
|
327
|
+
for (const item of commands) {
|
|
328
|
+
const cwd = resolveWorkDir(currentDir, item.workDir);
|
|
329
|
+
const result = await executeOne(item, cwd, context);
|
|
330
|
+
results.push(result);
|
|
331
|
+
|
|
332
|
+
if (result.status === "success" && !item.workDir) {
|
|
333
|
+
currentDir = updateSequentialDir(currentDir, item.command);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (!ignoreErrors && result.status === "error") {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return results;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function executeParallel(
|
|
345
|
+
commands: NormalizedCommand[],
|
|
346
|
+
baseDir: string,
|
|
347
|
+
ignoreErrors: boolean,
|
|
348
|
+
context?: ToolContext,
|
|
349
|
+
): Promise<CommandResult[]> {
|
|
350
|
+
const tasks = commands.map(async (item) =>
|
|
351
|
+
executeOne(item, resolveWorkDir(baseDir, item.workDir), context),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const results = await Promise.all(tasks);
|
|
355
|
+
if (ignoreErrors) {
|
|
356
|
+
return results;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const firstError = results.findIndex((result) => result.status === "error");
|
|
360
|
+
return firstError === -1 ? results : results.slice(0, firstError + 1);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function createShellInputSchema() {
|
|
364
|
+
return z.object({
|
|
365
|
+
command: z
|
|
366
|
+
.union([CommandSchema, z.array(CommandSchema).min(1)])
|
|
367
|
+
.describe(
|
|
368
|
+
"Single command, command object, or list of commands/command objects.",
|
|
369
|
+
),
|
|
370
|
+
parallel: z
|
|
371
|
+
.boolean()
|
|
372
|
+
.optional()
|
|
373
|
+
.describe("Execute multiple commands in parallel."),
|
|
374
|
+
ignore_errors: z
|
|
375
|
+
.boolean()
|
|
376
|
+
.optional()
|
|
377
|
+
.describe("Continue executing even if a command fails."),
|
|
378
|
+
timeout: z
|
|
379
|
+
.number()
|
|
380
|
+
.positive()
|
|
381
|
+
.optional()
|
|
382
|
+
.describe("Default timeout in seconds for each command."),
|
|
383
|
+
work_dir: z
|
|
384
|
+
.string()
|
|
385
|
+
.optional()
|
|
386
|
+
.describe("Base working directory for command execution."),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function createShellTools() {
|
|
391
|
+
const inputSchema = createShellInputSchema();
|
|
392
|
+
|
|
393
|
+
return [
|
|
394
|
+
tool({
|
|
395
|
+
name: "shell",
|
|
396
|
+
description:
|
|
397
|
+
"Execute shell commands on the local machine. Supports single commands, multiple commands, per-command options, sequential or parallel execution, working directories, stdin, and timeouts.",
|
|
398
|
+
inputSchema,
|
|
399
|
+
callback: async (input, context?: ToolContext) => {
|
|
400
|
+
const commands = normalizeCommands(input);
|
|
401
|
+
const parallel = input.parallel ?? false;
|
|
402
|
+
const ignoreErrors = input.ignore_errors ?? false;
|
|
403
|
+
const baseDir = resolveWorkDir(getCwd(), input.work_dir);
|
|
404
|
+
|
|
405
|
+
const results = parallel
|
|
406
|
+
? await executeParallel(commands, baseDir, ignoreErrors, context)
|
|
407
|
+
: await executeSequential(commands, baseDir, ignoreErrors, context);
|
|
408
|
+
|
|
409
|
+
const successCount = results.filter(
|
|
410
|
+
(result) => result.status === "success",
|
|
411
|
+
).length;
|
|
412
|
+
const errorCount = results.length - successCount;
|
|
413
|
+
|
|
414
|
+
return toJsonValue({
|
|
415
|
+
status: errorCount === 0 || ignoreErrors ? "success" : "error",
|
|
416
|
+
execution_mode: parallel ? "parallel" : "sequential",
|
|
417
|
+
total_commands: results.length,
|
|
418
|
+
successful: successCount,
|
|
419
|
+
failed: errorCount,
|
|
420
|
+
base_dir: baseDir,
|
|
421
|
+
results,
|
|
422
|
+
});
|
|
423
|
+
},
|
|
424
|
+
}),
|
|
425
|
+
];
|
|
426
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { tool } from "@strands-agents/sdk";
|
|
2
|
+
import type { JSONValue, ToolContext } from "@strands-agents/sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
const THINKING_STATE_KEY = "thinking.sequential";
|
|
6
|
+
|
|
7
|
+
const coercedBoolean = z.preprocess((value) => {
|
|
8
|
+
if (typeof value === "boolean") {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
const normalized = value.toLowerCase();
|
|
13
|
+
if (normalized === "true") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (normalized === "false") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}, z.boolean());
|
|
22
|
+
|
|
23
|
+
type ThoughtEntry = {
|
|
24
|
+
thought: string;
|
|
25
|
+
thoughtNumber: number;
|
|
26
|
+
totalThoughts: number;
|
|
27
|
+
nextThoughtNeeded: boolean;
|
|
28
|
+
isRevision: boolean;
|
|
29
|
+
revisesThought: number | null;
|
|
30
|
+
branchFromThought: number | null;
|
|
31
|
+
branchId: string | null;
|
|
32
|
+
needsMoreThoughts: boolean;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ThinkingState = {
|
|
36
|
+
history: ThoughtEntry[];
|
|
37
|
+
branches: string[];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function toJsonValue(value: unknown): JSONValue {
|
|
41
|
+
return JSON.parse(JSON.stringify(value)) as JSONValue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readThinkingState(context: ToolContext): ThinkingState {
|
|
45
|
+
const current = context.agent.appState.get<{
|
|
46
|
+
[THINKING_STATE_KEY]: ThinkingState;
|
|
47
|
+
}>(THINKING_STATE_KEY);
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
current &&
|
|
51
|
+
typeof current === "object" &&
|
|
52
|
+
Array.isArray(current.history) &&
|
|
53
|
+
Array.isArray(current.branches)
|
|
54
|
+
) {
|
|
55
|
+
return current;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { history: [], branches: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeThinkingState(context: ToolContext, state: ThinkingState): void {
|
|
62
|
+
context.agent.appState.set(THINKING_STATE_KEY, state);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeThought(
|
|
66
|
+
input: z.infer<ReturnType<typeof createThinkingInputSchema>>,
|
|
67
|
+
): ThoughtEntry {
|
|
68
|
+
if (input.isRevision && input.revisesThought == null) {
|
|
69
|
+
throw new Error("`revisesThought` is required when `isRevision` is true.");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (input.branchFromThought != null && !input.branchId) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"`branchId` is required when `branchFromThought` is provided.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const totalThoughts =
|
|
79
|
+
input.needsMoreThoughts || input.thoughtNumber > input.totalThoughts
|
|
80
|
+
? Math.max(input.totalThoughts, input.thoughtNumber + 1)
|
|
81
|
+
: input.totalThoughts;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
thought: input.thought.trim(),
|
|
85
|
+
thoughtNumber: input.thoughtNumber,
|
|
86
|
+
totalThoughts,
|
|
87
|
+
nextThoughtNeeded: input.nextThoughtNeeded,
|
|
88
|
+
isRevision: input.isRevision ?? false,
|
|
89
|
+
revisesThought: input.revisesThought ?? null,
|
|
90
|
+
branchFromThought: input.branchFromThought ?? null,
|
|
91
|
+
branchId: input.branchId ?? null,
|
|
92
|
+
needsMoreThoughts: input.needsMoreThoughts ?? false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createThinkingInputSchema() {
|
|
97
|
+
return z.object({
|
|
98
|
+
thought: z.string().min(1).describe("Your current thinking step."),
|
|
99
|
+
nextThoughtNeeded: coercedBoolean.describe(
|
|
100
|
+
"Whether another thought step is needed.",
|
|
101
|
+
),
|
|
102
|
+
thoughtNumber: z.coerce
|
|
103
|
+
.number()
|
|
104
|
+
.int()
|
|
105
|
+
.min(1)
|
|
106
|
+
.describe("Current thought number."),
|
|
107
|
+
totalThoughts: z.coerce
|
|
108
|
+
.number()
|
|
109
|
+
.int()
|
|
110
|
+
.min(1)
|
|
111
|
+
.describe("Current estimate of total thoughts needed."),
|
|
112
|
+
isRevision: coercedBoolean
|
|
113
|
+
.optional()
|
|
114
|
+
.describe("Whether this thought revises previous thinking."),
|
|
115
|
+
revisesThought: z.coerce
|
|
116
|
+
.number()
|
|
117
|
+
.int()
|
|
118
|
+
.min(1)
|
|
119
|
+
.optional()
|
|
120
|
+
.describe("Which prior thought is being reconsidered."),
|
|
121
|
+
branchFromThought: z.coerce
|
|
122
|
+
.number()
|
|
123
|
+
.int()
|
|
124
|
+
.min(1)
|
|
125
|
+
.optional()
|
|
126
|
+
.describe("Thought number this branch diverges from."),
|
|
127
|
+
branchId: z
|
|
128
|
+
.string()
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Identifier for the current branch."),
|
|
131
|
+
needsMoreThoughts: coercedBoolean
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(
|
|
134
|
+
"Whether more thoughts are needed beyond the current estimate.",
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createThinkingTools() {
|
|
140
|
+
const inputSchema = createThinkingInputSchema();
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
tool({
|
|
144
|
+
name: "think",
|
|
145
|
+
description: `A sequential thinking tool for dynamic, reflective problem-solving.
|
|
146
|
+
Use it to break complex work into steps, revise earlier thinking, branch into alternatives,
|
|
147
|
+
and keep track of whether more analysis is still needed. Prefer this for planning,
|
|
148
|
+
debugging, design exploration, or other multi-step reasoning tasks.`,
|
|
149
|
+
inputSchema,
|
|
150
|
+
callback: async (input, context?: ToolContext) => {
|
|
151
|
+
if (!context) {
|
|
152
|
+
throw new Error("Think tool requires execution context.");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const state = readThinkingState(context);
|
|
156
|
+
|
|
157
|
+
if (input.thoughtNumber === 1 && !input.isRevision) {
|
|
158
|
+
state.history = [];
|
|
159
|
+
state.branches = [];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const normalized = normalizeThought(input);
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
normalized.branchId &&
|
|
166
|
+
!state.branches.includes(normalized.branchId)
|
|
167
|
+
) {
|
|
168
|
+
state.branches.push(normalized.branchId);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
state.history.push(normalized);
|
|
172
|
+
writeThinkingState(context, state);
|
|
173
|
+
|
|
174
|
+
return toJsonValue({
|
|
175
|
+
thoughtNumber: normalized.thoughtNumber,
|
|
176
|
+
totalThoughts: normalized.totalThoughts,
|
|
177
|
+
nextThoughtNeeded: normalized.nextThoughtNeeded,
|
|
178
|
+
branches: state.branches,
|
|
179
|
+
thoughtHistoryLength: state.history.length,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
];
|
|
184
|
+
}
|