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.
Files changed (96) hide show
  1. package/.github/screenshot.png +0 -0
  2. package/.github/workflows/build-publish.yml +49 -0
  3. package/LICENSE +21 -0
  4. package/README.md +399 -0
  5. package/docker-compose.yml +13 -0
  6. package/package.json +78 -0
  7. package/src/acp/acp-agent.ts +803 -0
  8. package/src/acp/approvals.ts +147 -0
  9. package/src/acp/index.ts +1 -0
  10. package/src/acp/meta/system-prompt.ts +44 -0
  11. package/src/acp/meta/user-id.ts +44 -0
  12. package/src/acp/prompt-invoke.ts +149 -0
  13. package/src/acp/sessions/config-options.ts +56 -0
  14. package/src/acp/sessions/replay.ts +131 -0
  15. package/src/acp/sessions/store.ts +158 -0
  16. package/src/acp/sessions/title.ts +22 -0
  17. package/src/acp/utils/paths.ts +5 -0
  18. package/src/acp/utils/tool-kind.ts +38 -0
  19. package/src/acp/utils/tool-locations.ts +46 -0
  20. package/src/acp/utils/tool-result-content.ts +27 -0
  21. package/src/chat/app.tsx +428 -0
  22. package/src/chat/approvals.ts +96 -0
  23. package/src/chat/components/ApprovalPrompt.tsx +25 -0
  24. package/src/chat/components/ChatMessage.tsx +47 -0
  25. package/src/chat/components/Composer.tsx +39 -0
  26. package/src/chat/components/EmptyChatBanner.tsx +26 -0
  27. package/src/chat/components/ReasoningStrip.tsx +30 -0
  28. package/src/chat/components/Spinner.tsx +34 -0
  29. package/src/chat/components/StatusBar.tsx +65 -0
  30. package/src/chat/components/ThinkingStatus.tsx +128 -0
  31. package/src/chat/components/ToolEvent.tsx +34 -0
  32. package/src/chat/components/Transcript.tsx +34 -0
  33. package/src/chat/components/ascii-logo.ts +11 -0
  34. package/src/chat/components/shared.ts +70 -0
  35. package/src/chat/index.tsx +42 -0
  36. package/src/chat/types.ts +21 -0
  37. package/src/cli.ts +146 -0
  38. package/src/configure/app.tsx +911 -0
  39. package/src/configure/components/BusyScreen.tsx +22 -0
  40. package/src/configure/components/HomeScreen.tsx +43 -0
  41. package/src/configure/components/MenuScreen.tsx +44 -0
  42. package/src/configure/components/PromptForm.tsx +40 -0
  43. package/src/configure/components/SelectMenuItem.tsx +30 -0
  44. package/src/configure/index.tsx +43 -0
  45. package/src/configure/open-in-editor.ts +133 -0
  46. package/src/configure/types.ts +45 -0
  47. package/src/configure/utils.ts +113 -0
  48. package/src/core/agent/index.ts +76 -0
  49. package/src/core/config.ts +157 -0
  50. package/src/core/index.ts +54 -0
  51. package/src/core/mcp/config.ts +80 -0
  52. package/src/core/mcp/index.ts +13 -0
  53. package/src/core/mcp/manager.ts +109 -0
  54. package/src/core/mcp/prefixed-mcp-tool.ts +45 -0
  55. package/src/core/mcp/tools.ts +92 -0
  56. package/src/core/mcp/types.ts +37 -0
  57. package/src/core/memory/index.ts +17 -0
  58. package/src/core/memory/ltm/embed.ts +67 -0
  59. package/src/core/memory/ltm/index.ts +18 -0
  60. package/src/core/memory/ltm/store.ts +376 -0
  61. package/src/core/memory/ltm/tools.ts +146 -0
  62. package/src/core/memory/ltm/types.ts +111 -0
  63. package/src/core/memory/ltm/utils.ts +218 -0
  64. package/src/core/memory/stm/index.ts +17 -0
  65. package/src/core/models/anthropic.ts +53 -0
  66. package/src/core/models/bedrock.ts +54 -0
  67. package/src/core/models/google.ts +51 -0
  68. package/src/core/models/index.ts +16 -0
  69. package/src/core/models/ollama/index.ts +13 -0
  70. package/src/core/models/ollama/strands-ollama.ts +439 -0
  71. package/src/core/models/openai.ts +12 -0
  72. package/src/core/prompts/index.ts +23 -0
  73. package/src/core/prompts/skills.ts +66 -0
  74. package/src/core/prompts/static/fetch.md +33 -0
  75. package/src/core/prompts/static/filesystem.md +38 -0
  76. package/src/core/prompts/static/identity.md +22 -0
  77. package/src/core/prompts/static/ltm.md +39 -0
  78. package/src/core/prompts/static/memory.md +39 -0
  79. package/src/core/prompts/static/shell.md +34 -0
  80. package/src/core/prompts/static/skills.md +19 -0
  81. package/src/core/prompts/static/thinking.md +27 -0
  82. package/src/core/prompts/system.ts +109 -0
  83. package/src/core/skills/index.ts +2 -0
  84. package/src/core/skills/registry.ts +239 -0
  85. package/src/core/skills/tools.ts +80 -0
  86. package/src/core/toolkit.ts +13 -0
  87. package/src/core/tools/fetch.ts +288 -0
  88. package/src/core/tools/filesystem.ts +747 -0
  89. package/src/core/tools/index.ts +5 -0
  90. package/src/core/tools/shell.ts +426 -0
  91. package/src/core/tools/thinking.ts +184 -0
  92. package/src/core/tools/time.ts +121 -0
  93. package/src/core/utils/cwd-context.ts +11 -0
  94. package/src/core/utils/paths.ts +28 -0
  95. package/src/exec/approvals.ts +85 -0
  96. package/tsconfig.json +30 -0
@@ -0,0 +1,5 @@
1
+ export { createFilesystemTools } from "./filesystem.ts";
2
+ export { createFetchTools } from "./fetch.ts";
3
+ export { createShellTools } from "./shell.ts";
4
+ export { createThinkingTools } from "./thinking.ts";
5
+ export { createTimeTools } from "./time.ts";
@@ -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
+ }