hoomanjs 1.14.0 → 1.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hoomanjs",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "Hackable Bun-powered AI agent toolkit for building local CLI, ACP, MCP, and channel-driven workflows.",
5
5
  "author": {
6
6
  "name": "Vaibhav Pandey",
@@ -14,6 +14,7 @@ const KNOWN_TOOL_KINDS = new Map<string, ToolKind>([
14
14
  ["search_files", "search"],
15
15
  ["get_file_info", "read"],
16
16
  ["shell", "execute"],
17
+ ["sleep", "other"],
17
18
  ["fetch", "fetch"],
18
19
  ["wiki_list_files", "read"],
19
20
  ["wiki_read_file", "read"],
@@ -22,9 +23,23 @@ const KNOWN_TOOL_KINDS = new Map<string, ToolKind>([
22
23
  ["wiki_stats", "read"],
23
24
  ["wiki_search", "search"],
24
25
  ["think", "think"],
26
+ ["run_agents", "other"],
25
27
  ["update_todos", "other"],
26
28
  ["get_current_time", "other"],
27
29
  ["convert_time", "other"],
30
+ ["list_skills", "read"],
31
+ ["search_skills", "search"],
32
+ ["install_skill", "edit"],
33
+ ["delete_skill", "edit"],
34
+ ["store_memory", "edit"],
35
+ ["search_memory", "search"],
36
+ ["update_memory", "edit"],
37
+ ["archive_memory", "edit"],
38
+ ["list_mcp_servers", "read"],
39
+ ["get_mcp_server", "read"],
40
+ ["add_mcp_server", "edit"],
41
+ ["update_mcp_server", "edit"],
42
+ ["delete_mcp_server", "edit"],
28
43
  ]);
29
44
 
30
45
  export { INTERNAL_ALWAYS_ALLOWED };
@@ -499,6 +499,23 @@ export function ConfigureApp({
499
499
  setScreen({ kind: "config" });
500
500
  },
501
501
  },
502
+ {
503
+ label: `Sleep tool • ${configData.tools.sleep.enabled ? "Enabled" : "Disabled"}`,
504
+ value: () => {
505
+ updateConfig(
506
+ {
507
+ tools: {
508
+ ...config.tools,
509
+ sleep: {
510
+ enabled: !configData.tools.sleep.enabled,
511
+ },
512
+ },
513
+ },
514
+ `Sleep tool ${configData.tools.sleep.enabled ? "disabled" : "enabled"}.`,
515
+ );
516
+ setScreen({ kind: "config" });
517
+ },
518
+ },
502
519
  {
503
520
  label: `Long-term memory • ${configData.tools.ltm.enabled ? "Enabled" : "Disabled"} • ${configData.tools.ltm.chroma.collection.memory}`,
504
521
  value: () => setScreen({ kind: "config-ltm" }),
@@ -1,4 +1,5 @@
1
1
  import { Agent, BeforeInvocationEvent } from "@strands-agents/sdk";
2
+ import type { Tool } from "@strands-agents/sdk";
2
3
  import type { Config } from "../config.ts";
3
4
  import { modelProviders } from "../models";
4
5
  import {
@@ -14,10 +15,15 @@ import {
14
15
  createLongTermMemoryTools,
15
16
  } from "../memory";
16
17
  import { createSkillsTools, type Registry } from "../skills";
18
+ import {
19
+ createRunAgentsTools,
20
+ loadBuiltInAgentDefinitions,
21
+ } from "../agents/index.ts";
17
22
  import {
18
23
  createTodoTools,
19
24
  createFetchTools,
20
25
  createFilesystemTools,
26
+ createSleepTools,
21
27
  createShellTools,
22
28
  createThinkingTools,
23
29
  createTimeTools,
@@ -49,7 +55,7 @@ export async function create(
49
55
  const skills = config.tools.skills.enabled
50
56
  ? (await createSkillsPrompt(registry)).content
51
57
  : "";
52
- const tools = config.tools.mcp.enabled
58
+ const prefixed = config.tools.mcp.enabled
53
59
  ? await mcp.manager.listPrefixedTools()
54
60
  : [];
55
61
  const append = config.tools.mcp.enabled
@@ -58,27 +64,44 @@ export async function create(
58
64
  const prompt = [system.content, meta.systemPrompt, ...append, skills]
59
65
  .filter((x) => !!x)
60
66
  .join(SECTION_BREAK);
67
+ const model = llm.create(config.llm.model, config.llm.params);
68
+ const tools: Tool[] = [
69
+ ...createTimeTools(),
70
+ ...(config.tools.sleep.enabled ? createSleepTools() : []),
71
+ ...(config.tools.todo.enabled ? createTodoTools() : []),
72
+ ...(config.tools.fetch.enabled ? createFetchTools() : []),
73
+ ...(ltm ? createLongTermMemoryTools(ltm) : []),
74
+ ...(config.tools.filesystem.enabled ? createFilesystemTools() : []),
75
+ ...(config.tools.shell.enabled ? createShellTools() : []),
76
+ ...(config.tools.wiki.enabled ? createWikiTools(config) : []),
77
+ ...(config.tools.mcp.enabled ? createMcpTools(mcp.config) : []),
78
+ ...(config.tools.skills.enabled ? createSkillsTools(registry) : []),
79
+ ...createThinkingTools(),
80
+ ...prefixed,
81
+ ];
82
+ if (config.tools.agents.enabled) {
83
+ const definitions = loadBuiltInAgentDefinitions(config, {
84
+ knownTools: tools.map((entry) => entry.name),
85
+ });
86
+ tools.push(
87
+ ...createRunAgentsTools({
88
+ parent: config.name,
89
+ definitions,
90
+ tools,
91
+ createModel: () => llm.create(config.llm.model, config.llm.params),
92
+ defaultConcurrency: config.tools.agents.concurrency,
93
+ }),
94
+ );
95
+ }
61
96
  const agent = new Agent({
62
97
  name: config.name,
63
98
  systemPrompt: prompt,
64
- model: llm.create(config.llm.model, config.llm.params),
99
+ model,
65
100
  appState: {
66
101
  ...(userId ? { userId } : {}),
67
102
  ...(sessionId ? { sessionId } : {}),
68
103
  },
69
- tools: [
70
- ...createTimeTools(),
71
- ...(config.tools.todo.enabled ? createTodoTools() : []),
72
- ...(config.tools.fetch.enabled ? createFetchTools() : []),
73
- ...(ltm ? createLongTermMemoryTools(ltm) : []),
74
- ...(config.tools.filesystem.enabled ? createFilesystemTools() : []),
75
- ...(config.tools.shell.enabled ? createShellTools() : []),
76
- ...(config.tools.wiki.enabled ? createWikiTools(config) : []),
77
- ...(config.tools.mcp.enabled ? createMcpTools(mcp.config) : []),
78
- ...(config.tools.skills.enabled ? createSkillsTools(registry) : []),
79
- ...createThinkingTools(),
80
- ...tools,
81
- ],
104
+ tools,
82
105
  printer: print,
83
106
  ...stm,
84
107
  });
@@ -0,0 +1,47 @@
1
+ export const BUILTIN_AGENT_KINDS = ["research", "plan"] as const;
2
+
3
+ export type AgentKind = (typeof BUILTIN_AGENT_KINDS)[number];
4
+
5
+ export type AgentConfig = {
6
+ id: AgentKind;
7
+ instructions: string;
8
+ description: string;
9
+ tools: readonly string[];
10
+ };
11
+
12
+ export type AgentDefinition = AgentConfig & {
13
+ instructionsText: string;
14
+ };
15
+
16
+ export const BUILTIN_AGENT_CONFIGS: readonly AgentConfig[] = [
17
+ {
18
+ id: "research",
19
+ instructions: "research.md",
20
+ description: "Investigates code, docs, and context before implementation.",
21
+ tools: [
22
+ "read_file",
23
+ "read_multiple_files",
24
+ "list_directory",
25
+ "directory_tree",
26
+ "search_files",
27
+ "get_file_info",
28
+ "fetch",
29
+ "think",
30
+ ],
31
+ },
32
+ {
33
+ id: "plan",
34
+ instructions: "plan.md",
35
+ description:
36
+ "Produces implementation plans, tradeoffs, risks, and validation steps.",
37
+ tools: [
38
+ "read_file",
39
+ "read_multiple_files",
40
+ "list_directory",
41
+ "directory_tree",
42
+ "search_files",
43
+ "get_file_info",
44
+ "think",
45
+ ],
46
+ },
47
+ ];
@@ -0,0 +1,15 @@
1
+ export {
2
+ BUILTIN_AGENT_CONFIGS,
3
+ BUILTIN_AGENT_KINDS,
4
+ type AgentConfig,
5
+ type AgentDefinition,
6
+ type AgentKind,
7
+ } from "./definitions.ts";
8
+ export { loadBuiltInAgentDefinitions } from "./registry.ts";
9
+ export {
10
+ runAgentJobs,
11
+ type AgentJob,
12
+ type AgentJobResult,
13
+ type RunAgentJobsResult,
14
+ } from "./runner.ts";
15
+ export { RUN_AGENTS_TOOL_NAME, createRunAgentsTools } from "./tools.ts";
@@ -0,0 +1,108 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { compile } from "handlebars";
5
+ import type { Config } from "../config.ts";
6
+ import { getEnvironmentPromptContext } from "../prompts/environment.ts";
7
+ import {
8
+ BUILTIN_AGENT_CONFIGS,
9
+ type AgentConfig,
10
+ type AgentDefinition,
11
+ } from "./definitions.ts";
12
+
13
+ function promptsDir(): string {
14
+ return join(dirname(fileURLToPath(import.meta.url)), "../prompts/agents");
15
+ }
16
+
17
+ function validateConfigs(configs: readonly AgentConfig[]): void {
18
+ const seen = new Set<string>();
19
+ for (const config of configs) {
20
+ if (!config.id.trim()) {
21
+ throw new Error("Agent config id cannot be empty.");
22
+ }
23
+ if (seen.has(config.id)) {
24
+ throw new Error(`Duplicate agent config id: '${config.id}'.`);
25
+ }
26
+ seen.add(config.id);
27
+ if (!config.instructions.trim()) {
28
+ throw new Error(
29
+ `Agent '${config.id}' instructions file cannot be empty.`,
30
+ );
31
+ }
32
+ if (!config.description.trim()) {
33
+ throw new Error(`Agent '${config.id}' description cannot be empty.`);
34
+ }
35
+ if (!Array.isArray(config.tools) || config.tools.length === 0) {
36
+ throw new Error(`Agent '${config.id}' must declare at least one tool.`);
37
+ }
38
+ for (const toolName of config.tools) {
39
+ if (!toolName.trim()) {
40
+ throw new Error(`Agent '${config.id}' has an empty tool name.`);
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ function assertKnownTools(
47
+ definitions: readonly AgentDefinition[],
48
+ knownTools: readonly string[],
49
+ ): void {
50
+ const known = new Set(knownTools);
51
+ for (const definition of definitions) {
52
+ for (const toolName of definition.tools) {
53
+ if (!known.has(toolName)) {
54
+ throw new Error(
55
+ `Agent '${definition.id}' references unknown tool '${toolName}'.`,
56
+ );
57
+ }
58
+ }
59
+ }
60
+ }
61
+
62
+ function context(config: Config): Record<string, unknown> {
63
+ return {
64
+ name: config.name,
65
+ llm: config.llm,
66
+ environment: getEnvironmentPromptContext(),
67
+ ltm: config.tools.ltm,
68
+ wiki: config.tools.wiki,
69
+ compaction: config.compaction,
70
+ };
71
+ }
72
+
73
+ export function loadBuiltInAgentDefinitions(
74
+ config: Config,
75
+ options?: { knownTools?: readonly string[] },
76
+ ): AgentDefinition[] {
77
+ validateConfigs(BUILTIN_AGENT_CONFIGS);
78
+ const dir = promptsDir();
79
+ const definitions = BUILTIN_AGENT_CONFIGS.map((entry) => {
80
+ const fullPath = join(dir, entry.instructions);
81
+ if (!existsSync(fullPath)) {
82
+ throw new Error(
83
+ `Agent '${entry.id}' instructions file not found: ${entry.instructions}`,
84
+ );
85
+ }
86
+ const raw = readFileSync(fullPath, "utf8").trim();
87
+ if (!raw) {
88
+ throw new Error(
89
+ `Agent '${entry.id}' instructions file is empty: ${entry.instructions}`,
90
+ );
91
+ }
92
+ const template = compile(raw);
93
+ const instructionsText = template(context(config)).trim();
94
+ if (!instructionsText) {
95
+ throw new Error(
96
+ `Agent '${entry.id}' instructions rendered to empty content.`,
97
+ );
98
+ }
99
+ return {
100
+ ...entry,
101
+ instructionsText,
102
+ };
103
+ });
104
+ if (options?.knownTools) {
105
+ assertKnownTools(definitions, options.knownTools);
106
+ }
107
+ return definitions;
108
+ }
@@ -0,0 +1,375 @@
1
+ import { Agent, TextBlock } from "@strands-agents/sdk";
2
+ import { Graph, Node, Status } from "@strands-agents/sdk/multiagent";
3
+ import type {
4
+ BaseModelConfig,
5
+ Model,
6
+ Tool,
7
+ ContentBlock,
8
+ } from "@strands-agents/sdk";
9
+ import type {
10
+ MultiAgentInput,
11
+ MultiAgentState,
12
+ MultiAgentStreamEvent,
13
+ NodeInputOptions,
14
+ NodeResultUpdate,
15
+ } from "@strands-agents/sdk/multiagent";
16
+ import type { AgentDefinition, AgentKind } from "./definitions.ts";
17
+
18
+ export type AgentJob = {
19
+ id: string;
20
+ kind: AgentKind;
21
+ description: string;
22
+ prompt: string;
23
+ };
24
+
25
+ export type AgentJobResult = {
26
+ id: string;
27
+ kind: AgentKind;
28
+ description: string;
29
+ status: "completed" | "failed";
30
+ content: string;
31
+ durationMs: number;
32
+ error: string | null;
33
+ stopReason: string | null;
34
+ };
35
+
36
+ export type RunAgentJobsResult = {
37
+ results: AgentJobResult[];
38
+ };
39
+
40
+ type RunAgentJobsOptions = {
41
+ jobs: readonly AgentJob[];
42
+ definitions: readonly AgentDefinition[];
43
+ tools: readonly Tool[];
44
+ createModel: () => Model<BaseModelConfig>;
45
+ concurrency: number;
46
+ parent: string;
47
+ appState: {
48
+ userId?: string;
49
+ sessionId?: string;
50
+ };
51
+ cancelSignal?: AbortSignal;
52
+ };
53
+
54
+ class JobNode extends Node {
55
+ override readonly type = "agentJobNode";
56
+ private readonly execute: () => Promise<AgentJobResult>;
57
+ private readonly cancelSignal?: AbortSignal;
58
+
59
+ constructor(
60
+ id: string,
61
+ execute: () => Promise<AgentJobResult>,
62
+ description: string,
63
+ cancelSignal?: AbortSignal,
64
+ ) {
65
+ super(id, { description });
66
+ this.execute = execute;
67
+ this.cancelSignal = cancelSignal;
68
+ }
69
+
70
+ override async *handle(
71
+ _input: MultiAgentInput,
72
+ _state: MultiAgentState,
73
+ _options?: NodeInputOptions,
74
+ ): AsyncGenerator<MultiAgentStreamEvent, NodeResultUpdate, undefined> {
75
+ if (this.cancelSignal?.aborted) {
76
+ return {
77
+ status: Status.CANCELLED,
78
+ content: [new TextBlock("Cancelled before job execution.")],
79
+ error: new Error("Cancelled before job execution."),
80
+ };
81
+ }
82
+ const result = await this.execute();
83
+ if (result.status === "completed") {
84
+ const content = result.content.trim()
85
+ ? [new TextBlock(result.content.trim())]
86
+ : [];
87
+ return {
88
+ status: Status.COMPLETED,
89
+ content,
90
+ };
91
+ }
92
+ return {
93
+ status:
94
+ result.stopReason === "cancelled" ? Status.CANCELLED : Status.FAILED,
95
+ content: result.error ? [new TextBlock(result.error)] : [],
96
+ error: result.error ? new Error(result.error) : undefined,
97
+ };
98
+ }
99
+ }
100
+
101
+ function buildJobPrompt(job: AgentJob): string {
102
+ return `Task: ${job.description}\n\nUser request:\n${job.prompt}`;
103
+ }
104
+
105
+ function selectTools(
106
+ definition: AgentDefinition,
107
+ tools: readonly Tool[],
108
+ ): Tool[] {
109
+ const byName = new Map<string, Tool>();
110
+ for (const tool of tools) {
111
+ byName.set(tool.name, tool);
112
+ }
113
+ const selected: Tool[] = [];
114
+ for (const name of definition.tools) {
115
+ const tool = byName.get(name);
116
+ if (!tool) {
117
+ throw new Error(
118
+ `Agent '${definition.id}' cannot access missing tool '${name}'.`,
119
+ );
120
+ }
121
+ selected.push(tool);
122
+ }
123
+ return selected;
124
+ }
125
+
126
+ async function runSingleJob(
127
+ job: AgentJob,
128
+ definition: AgentDefinition,
129
+ options: Omit<RunAgentJobsOptions, "jobs" | "definitions" | "concurrency">,
130
+ ): Promise<AgentJobResult> {
131
+ const started = Date.now();
132
+ if (options.cancelSignal?.aborted) {
133
+ return {
134
+ id: job.id,
135
+ kind: job.kind,
136
+ description: job.description,
137
+ status: "failed",
138
+ content: "",
139
+ durationMs: 0,
140
+ error: "Cancelled before execution.",
141
+ stopReason: "cancelled",
142
+ };
143
+ }
144
+ try {
145
+ const child = new Agent({
146
+ name: `${options.parent}-${definition.id}-${job.id}`,
147
+ systemPrompt: definition.instructionsText,
148
+ model: options.createModel(),
149
+ appState: {
150
+ ...(options.appState.userId ? { userId: options.appState.userId } : {}),
151
+ ...(options.appState.sessionId
152
+ ? { sessionId: options.appState.sessionId }
153
+ : {}),
154
+ agentKind: definition.id,
155
+ },
156
+ tools: selectTools(definition, options.tools),
157
+ printer: false,
158
+ });
159
+ const result = await child.invoke(buildJobPrompt(job), {
160
+ ...(options.cancelSignal ? { cancelSignal: options.cancelSignal } : {}),
161
+ });
162
+ return {
163
+ id: job.id,
164
+ kind: job.kind,
165
+ description: job.description,
166
+ status: result.stopReason === "cancelled" ? "failed" : "completed",
167
+ content: result.toString().trim(),
168
+ durationMs: Date.now() - started,
169
+ error: result.stopReason === "cancelled" ? "Cancelled." : null,
170
+ stopReason: result.stopReason,
171
+ };
172
+ } catch (error) {
173
+ const message = error instanceof Error ? error.message : String(error);
174
+ const cancelled = options.cancelSignal?.aborted ?? false;
175
+ return {
176
+ id: job.id,
177
+ kind: job.kind,
178
+ description: job.description,
179
+ status: "failed",
180
+ content: "",
181
+ durationMs: Date.now() - started,
182
+ error: message,
183
+ stopReason: cancelled ? "cancelled" : null,
184
+ };
185
+ }
186
+ }
187
+
188
+ function contentToText(content: readonly ContentBlock[]): string {
189
+ return content
190
+ .map((block) => {
191
+ if ("text" in block && typeof block.text === "string") {
192
+ return block.text;
193
+ }
194
+ return "";
195
+ })
196
+ .filter(Boolean)
197
+ .join("\n")
198
+ .trim();
199
+ }
200
+
201
+ function cancelledResult(job: AgentJob, message: string): AgentJobResult {
202
+ return {
203
+ id: job.id,
204
+ kind: job.kind,
205
+ description: job.description,
206
+ status: "failed",
207
+ content: "",
208
+ durationMs: 0,
209
+ error: message,
210
+ stopReason: "cancelled",
211
+ };
212
+ }
213
+
214
+ export async function runAgentJobs(
215
+ options: RunAgentJobsOptions,
216
+ ): Promise<RunAgentJobsResult> {
217
+ if (options.jobs.length === 0) {
218
+ return { results: [] };
219
+ }
220
+ if (options.cancelSignal?.aborted) {
221
+ return {
222
+ results: options.jobs.map((job) => ({
223
+ id: job.id,
224
+ kind: job.kind,
225
+ description: job.description,
226
+ status: "failed",
227
+ content: "",
228
+ durationMs: 0,
229
+ error: "Cancelled before execution.",
230
+ stopReason: "cancelled",
231
+ })),
232
+ };
233
+ }
234
+ const defsByKind = new Map<AgentKind, AgentDefinition>(
235
+ options.definitions.map((entry) => [entry.id, entry]),
236
+ );
237
+ const ordered = [...options.jobs];
238
+ const results: Array<AgentJobResult | null> = ordered.map(() => null);
239
+ const graphNodes: JobNode[] = [];
240
+ const graphSources: string[] = [];
241
+ const nodeToIndex = new Map<string, number>();
242
+ const graphNodeResults = new Map<string, AgentJobResult>();
243
+
244
+ for (const [index, job] of ordered.entries()) {
245
+ const definition = defsByKind.get(job.kind);
246
+ if (!definition) {
247
+ results[index] = {
248
+ id: job.id,
249
+ kind: job.kind,
250
+ description: job.description,
251
+ status: "failed",
252
+ content: "",
253
+ durationMs: 0,
254
+ error: `Unknown agent kind '${job.kind}'.`,
255
+ stopReason: null,
256
+ };
257
+ continue;
258
+ }
259
+ const nodeId = `${job.id}__${index + 1}`;
260
+ nodeToIndex.set(nodeId, index);
261
+ graphSources.push(nodeId);
262
+ graphNodes.push(
263
+ new JobNode(
264
+ nodeId,
265
+ async () => {
266
+ const jobResult = await runSingleJob(job, definition, {
267
+ tools: options.tools,
268
+ createModel: options.createModel,
269
+ parent: options.parent,
270
+ appState: options.appState,
271
+ cancelSignal: options.cancelSignal,
272
+ });
273
+ graphNodeResults.set(nodeId, jobResult);
274
+ return jobResult;
275
+ },
276
+ `${job.kind} :: ${job.description}`,
277
+ options.cancelSignal,
278
+ ),
279
+ );
280
+ }
281
+
282
+ if (graphNodes.length > 0) {
283
+ try {
284
+ const graph = new Graph({
285
+ id: "run_agents_graph",
286
+ nodes: graphNodes,
287
+ edges: [],
288
+ sources: graphSources,
289
+ maxConcurrency: Math.max(
290
+ 1,
291
+ Math.min(options.concurrency, graphNodes.length),
292
+ ),
293
+ });
294
+ const graphRun = graph.invoke("run jobs");
295
+ const graphResult = options.cancelSignal
296
+ ? await Promise.race([
297
+ graphRun,
298
+ new Promise<null>((resolve) => {
299
+ const onAbort = () => resolve(null);
300
+ options.cancelSignal?.addEventListener("abort", onAbort, {
301
+ once: true,
302
+ });
303
+ }),
304
+ ])
305
+ : await graphRun;
306
+ if (graphResult === null) {
307
+ for (const [nodeId, index] of nodeToIndex.entries()) {
308
+ if (results[index]) {
309
+ continue;
310
+ }
311
+ const recorded = graphNodeResults.get(nodeId);
312
+ results[index] =
313
+ recorded ?? cancelledResult(ordered[index]!, "Cancelled.");
314
+ }
315
+ return {
316
+ results: results.filter(
317
+ (entry): entry is AgentJobResult => entry !== null,
318
+ ),
319
+ };
320
+ }
321
+ const nodeResultsById = new Map(
322
+ graphResult.results.map((entry) => [entry.nodeId, entry] as const),
323
+ );
324
+ for (const [nodeId, index] of nodeToIndex.entries()) {
325
+ const recorded = graphNodeResults.get(nodeId);
326
+ if (recorded) {
327
+ results[index] = recorded;
328
+ continue;
329
+ }
330
+ const nodeResult = nodeResultsById.get(nodeId);
331
+ const job = ordered[index]!;
332
+ const cancelled = options.cancelSignal?.aborted ?? false;
333
+ results[index] = {
334
+ id: job.id,
335
+ kind: job.kind,
336
+ description: job.description,
337
+ status:
338
+ nodeResult?.status === Status.COMPLETED ? "completed" : "failed",
339
+ content: nodeResult ? contentToText(nodeResult.content) : "",
340
+ durationMs: nodeResult?.duration ?? 0,
341
+ error:
342
+ nodeResult?.error?.message ??
343
+ "Job did not produce a result from Graph execution.",
344
+ stopReason:
345
+ nodeResult?.status === Status.CANCELLED || cancelled
346
+ ? "cancelled"
347
+ : null,
348
+ };
349
+ }
350
+ } catch (error) {
351
+ const message =
352
+ error instanceof Error ? error.message : "Graph execution failed.";
353
+ for (const [nodeId, index] of nodeToIndex.entries()) {
354
+ if (results[index]) {
355
+ continue;
356
+ }
357
+ const job = ordered[index]!;
358
+ results[index] = {
359
+ id: job.id,
360
+ kind: job.kind,
361
+ description: job.description,
362
+ status: "failed",
363
+ content: "",
364
+ durationMs: 0,
365
+ error: message,
366
+ stopReason: options.cancelSignal?.aborted ? "cancelled" : null,
367
+ };
368
+ }
369
+ }
370
+ }
371
+
372
+ return {
373
+ results: results.filter((entry): entry is AgentJobResult => entry !== null),
374
+ };
375
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ tool,
3
+ type JSONValue,
4
+ type Tool,
5
+ type ToolContext,
6
+ } from "@strands-agents/sdk";
7
+ import type { BaseModelConfig, Model } from "@strands-agents/sdk";
8
+ import { z } from "zod";
9
+ import { BUILTIN_AGENT_KINDS, type AgentDefinition } from "./definitions.ts";
10
+ import { runAgentJobs } from "./runner.ts";
11
+
12
+ export const RUN_AGENTS_TOOL_NAME = "run_agents";
13
+
14
+ const JobSchema = z.object({
15
+ kind: z.enum(BUILTIN_AGENT_KINDS),
16
+ description: z.string().trim().min(1),
17
+ prompt: z.string().trim().min(1),
18
+ });
19
+
20
+ const RunAgentsInputSchema = z.object({
21
+ jobs: z.array(JobSchema).min(1),
22
+ maxConcurrency: z.coerce.number().int().min(1).optional(),
23
+ });
24
+
25
+ type RunAgentsToolOptions = {
26
+ parent: string;
27
+ definitions: readonly AgentDefinition[];
28
+ tools: readonly Tool[];
29
+ createModel: () => Model<BaseModelConfig>;
30
+ defaultConcurrency: number;
31
+ };
32
+
33
+ function toJsonValue(value: unknown): JSONValue {
34
+ return JSON.parse(JSON.stringify(value)) as JSONValue;
35
+ }
36
+
37
+ function readAppStateString(
38
+ context: ToolContext,
39
+ key: "userId" | "sessionId",
40
+ ): string | undefined {
41
+ const value = context.agent.appState.get(key);
42
+ return typeof value === "string" && value.trim() ? value : undefined;
43
+ }
44
+
45
+ export function createRunAgentsTools(options: RunAgentsToolOptions) {
46
+ const kinds = options.definitions.map(
47
+ (entry) => `- ${entry.id}: ${entry.description}`,
48
+ );
49
+ const baseTools = options.tools.filter(
50
+ (entry) => entry.name !== RUN_AGENTS_TOOL_NAME,
51
+ );
52
+ return [
53
+ tool({
54
+ name: RUN_AGENTS_TOOL_NAME,
55
+ description: `Run one or more specialized child agents in parallel and return their outputs.
56
+ Use this for deeper research or planning work that can be split into independent jobs.
57
+ Available agent kinds:
58
+ ${kinds.join("\n")}`,
59
+ inputSchema: RunAgentsInputSchema,
60
+ callback: async (input, context?: ToolContext) => {
61
+ if (!context) {
62
+ throw new Error(
63
+ `${RUN_AGENTS_TOOL_NAME} requires execution context.`,
64
+ );
65
+ }
66
+ const concurrency = Math.max(
67
+ 1,
68
+ Math.min(
69
+ input.maxConcurrency ?? options.defaultConcurrency,
70
+ input.jobs.length,
71
+ ),
72
+ );
73
+ const jobs = input.jobs.map((job, index) => ({
74
+ id: `job-${index + 1}`,
75
+ kind: job.kind,
76
+ description: job.description,
77
+ prompt: job.prompt,
78
+ }));
79
+ const result = await runAgentJobs({
80
+ jobs,
81
+ definitions: options.definitions,
82
+ tools: baseTools,
83
+ createModel: options.createModel,
84
+ concurrency,
85
+ parent: options.parent,
86
+ appState: {
87
+ userId: readAppStateString(context, "userId"),
88
+ sessionId: readAppStateString(context, "sessionId"),
89
+ },
90
+ cancelSignal: context.agent.cancelSignal,
91
+ });
92
+ return toJsonValue({
93
+ requestedJobs: jobs.length,
94
+ concurrency,
95
+ ...result,
96
+ });
97
+ },
98
+ }),
99
+ ];
100
+ }
@@ -10,17 +10,42 @@ type AgentLike = {
10
10
  const SESSION_ALLOWED_TOOLS_KEY = "allowedTools";
11
11
 
12
12
  export const INTERNAL_ALWAYS_ALLOWED = new Set([
13
+ // Strands / runtime
13
14
  "strands_structured_output",
15
+ // Todos
14
16
  "update_todos",
17
+ // Thinking
15
18
  "think",
16
- "get_current_time",
19
+ // Agent orchestration
20
+ "run_agents",
21
+ // Sleep
22
+ "sleep",
23
+ // Time
17
24
  "convert_time",
25
+ "get_current_time",
26
+ // Wiki
27
+ "wiki_knowledge_graph",
18
28
  "wiki_list_files",
19
29
  "wiki_read_file",
20
- "wiki_write_file",
21
- "wiki_knowledge_graph",
22
- "wiki_stats",
23
30
  "wiki_search",
31
+ "wiki_stats",
32
+ "wiki_write_file",
33
+ // Long-term memory
34
+ "archive_memory",
35
+ "search_memory",
36
+ "store_memory",
37
+ "update_memory",
38
+ // Skills
39
+ "list_skills",
40
+ "search_skills",
41
+ // MCP config
42
+ "get_mcp_server",
43
+ "list_mcp_servers",
44
+ // Filesystem (list / search / metadata)
45
+ "directory_tree",
46
+ "get_file_info",
47
+ "list_directory",
48
+ "search_files",
24
49
  ]);
25
50
 
26
51
  function normalizeAllowedTools(value: unknown): string[] {
@@ -71,15 +71,22 @@ const ToolTogglePartialSchema = z.object({
71
71
  enabled: z.boolean().optional(),
72
72
  });
73
73
 
74
+ const AgentsPartialSchema = z.object({
75
+ enabled: z.boolean().optional(),
76
+ concurrency: z.number().int().min(1).optional(),
77
+ });
78
+
74
79
  const ToolsPartialSchema = z.object({
75
80
  todo: ToolTogglePartialSchema.optional(),
76
81
  fetch: ToolTogglePartialSchema.optional(),
77
82
  filesystem: ToolTogglePartialSchema.optional(),
78
83
  shell: ToolTogglePartialSchema.optional(),
84
+ sleep: ToolTogglePartialSchema.optional(),
79
85
  ltm: LtmPartialSchema.optional(),
80
86
  wiki: WikiPartialSchema.optional(),
81
87
  mcp: ToolTogglePartialSchema.optional(),
82
88
  skills: ToolTogglePartialSchema.optional(),
89
+ agents: AgentsPartialSchema.optional(),
83
90
  });
84
91
 
85
92
  const ConfigSchema = z
@@ -111,6 +118,9 @@ const ConfigSchema = z
111
118
  shell: {
112
119
  enabled: input.tools?.shell?.enabled ?? true,
113
120
  },
121
+ sleep: {
122
+ enabled: input.tools?.sleep?.enabled ?? true,
123
+ },
114
124
  ltm: {
115
125
  enabled: ltm?.enabled ?? false,
116
126
  chroma: {
@@ -139,6 +149,10 @@ const ConfigSchema = z
139
149
  skills: {
140
150
  enabled: input.tools?.skills?.enabled ?? false,
141
151
  },
152
+ agents: {
153
+ enabled: input.tools?.agents?.enabled ?? true,
154
+ concurrency: input.tools?.agents?.concurrency ?? 3,
155
+ },
142
156
  },
143
157
  compaction: input.compaction,
144
158
  };
@@ -171,6 +185,9 @@ const defaultConfigData = (): ConfigData => ({
171
185
  shell: {
172
186
  enabled: true,
173
187
  },
188
+ sleep: {
189
+ enabled: true,
190
+ },
174
191
  ltm: {
175
192
  enabled: false,
176
193
  chroma: {
@@ -191,6 +208,10 @@ const defaultConfigData = (): ConfigData => ({
191
208
  skills: {
192
209
  enabled: false,
193
210
  },
211
+ agents: {
212
+ enabled: true,
213
+ concurrency: 2,
214
+ },
194
215
  },
195
216
  compaction: {
196
217
  ratio: 0.75,
@@ -222,6 +243,7 @@ export class Config {
222
243
  fetch: { ...this.data.tools.fetch },
223
244
  filesystem: { ...this.data.tools.filesystem },
224
245
  shell: { ...this.data.tools.shell },
246
+ sleep: { ...this.data.tools.sleep },
225
247
  ltm: {
226
248
  ...this.data.tools.ltm,
227
249
  chroma: {
@@ -238,6 +260,7 @@ export class Config {
238
260
  },
239
261
  mcp: { ...this.data.tools.mcp },
240
262
  skills: { ...this.data.tools.skills },
263
+ agents: { ...this.data.tools.agents },
241
264
  };
242
265
  }
243
266
 
@@ -0,0 +1,35 @@
1
+ ## Plan Agent
2
+
3
+ You are a specialized planning sub-agent for {{ name }}.
4
+
5
+ Your job is to design a practical, low-risk implementation plan that the parent agent can execute.
6
+
7
+ This is a strict read-only role:
8
+
9
+ - Do not create, edit, move, or delete files.
10
+ - Do not run commands that change system state.
11
+ - Do not write final implementation code; focus on strategy.
12
+
13
+ Planning process:
14
+
15
+ 1. Clarify the objective and constraints from the task.
16
+ 2. Inspect existing architecture and patterns before proposing changes.
17
+ 3. Choose an approach that fits current code conventions and minimizes regressions.
18
+ 4. Break work into ordered, reviewable steps.
19
+ 5. Identify dependencies, migration concerns, and rollback or fallback considerations.
20
+
21
+ What good plans include:
22
+
23
+ - Why this approach is preferred over obvious alternatives.
24
+ - Exact files/modules likely to change.
25
+ - Key data flow, API, or interface impacts.
26
+ - Edge cases, failure modes, and compatibility concerns.
27
+ - A concrete verification plan (tests, manual checks, and expected outcomes).
28
+
29
+ Return format:
30
+
31
+ 1. **Proposed Approach** - concise rationale and trade-offs.
32
+ 2. **Implementation Steps** - numbered sequence, each step actionable.
33
+ 3. **Critical Files / Areas** - paths and why they matter.
34
+ 4. **Risks and Mitigations** - specific, not generic.
35
+ 5. **Validation Plan** - how the parent agent should confirm correctness.
@@ -0,0 +1,32 @@
1
+ ## Research Agent
2
+
3
+ You are a specialized research sub-agent for {{ name }}.
4
+
5
+ Your job is to investigate the task, gather high-signal evidence, and return findings that help the parent agent decide what to do next.
6
+
7
+ This is a strict read-only role:
8
+
9
+ - Do not create, edit, move, or delete files.
10
+ - Do not run commands that change system state.
11
+ - Do not propose speculative conclusions as facts.
12
+
13
+ How to work:
14
+
15
+ 1. Understand the exact question and identify what evidence is required.
16
+ 2. Explore efficiently: start broad, then narrow to the most relevant files/symbols.
17
+ 3. Prefer concrete code evidence over assumptions.
18
+ 4. Surface contradictions, unknowns, and risks early.
19
+ 5. Stop exploring once confidence is high enough to answer the question.
20
+
21
+ Quality bar:
22
+
23
+ - Be precise, not verbose.
24
+ - Include file paths, symbol names, and behavior-level findings.
25
+ - Differentiate between "confirmed", "likely", and "unknown".
26
+ - If information is missing, state what additional check would resolve it.
27
+
28
+ Return format:
29
+
30
+ 1. **Findings** - short bullets with evidence.
31
+ 2. **Open Questions / Uncertainties** - only if relevant.
32
+ 3. **Recommended Next Step for Parent Agent** - one concise action.
@@ -0,0 +1,62 @@
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ release as osRelease,
5
+ type as osType,
6
+ version as osVersion,
7
+ } from "node:os";
8
+
9
+ type EnvironmentPromptContext = {
10
+ cwd: string;
11
+ platform: string;
12
+ osVersion: string;
13
+ shell: string;
14
+ isGitRepo: boolean;
15
+ };
16
+
17
+ function detectPlatform(): string {
18
+ return ["darwin", "linux", "win32"].includes(process.platform)
19
+ ? process.platform
20
+ : process.platform;
21
+ }
22
+
23
+ function detectOsVersion(): string {
24
+ if (process.platform === "win32") {
25
+ return `${osVersion()} ${osRelease()}`;
26
+ }
27
+ return `${osType()} ${osRelease()}`;
28
+ }
29
+
30
+ function detectShell(): string {
31
+ const shell =
32
+ process.env.SHELL ||
33
+ process.env.ComSpec ||
34
+ process.env.COMSPEC ||
35
+ "unknown";
36
+ return shell.trim() || "unknown";
37
+ }
38
+
39
+ function detectGitRepo(startDir: string): boolean {
40
+ let current = startDir;
41
+ while (true) {
42
+ if (existsSync(join(current, ".git"))) {
43
+ return true;
44
+ }
45
+ const parent = dirname(current);
46
+ if (parent === current) {
47
+ return false;
48
+ }
49
+ current = parent;
50
+ }
51
+ }
52
+
53
+ export function getEnvironmentPromptContext(): EnvironmentPromptContext {
54
+ const cwd = process.cwd();
55
+ return {
56
+ cwd,
57
+ platform: detectPlatform(),
58
+ osVersion: detectOsVersion(),
59
+ shell: detectShell(),
60
+ isGitRepo: detectGitRepo(cwd),
61
+ };
62
+ }
@@ -0,0 +1,15 @@
1
+ ## Environment
2
+
3
+ You are running in the following runtime environment:
4
+
5
+ - Primary working directory: `{{ environment.cwd }}`
6
+ - Platform: `{{ environment.platform }}`
7
+ - Shell: `{{ environment.shell }}`
8
+ - OS version: `{{ environment.osVersion }}`
9
+ - Is git repository: `{{ environment.isGitRepo }}`
10
+
11
+ ### How To Use This
12
+
13
+ - Use this information to choose correct path handling, shell syntax, and platform-specific behavior
14
+ - Treat this section as runtime context, not real-time clock data
15
+ - If the task needs the current date or time, call `get_current_time` instead of inferring from this section
@@ -0,0 +1,20 @@
1
+ ## Sleep
2
+
3
+ You have access to a `sleep` tool that waits for a specified duration.
4
+
5
+ ### When To Use It
6
+
7
+ - Use `sleep` when the user explicitly asks you to wait, pause, rest, or retry later
8
+ - Use `sleep` while waiting for external events where polling immediately would be wasteful
9
+ - Prefer this over shell-based sleep commands to avoid holding a shell process
10
+
11
+ ### How To Use It
12
+
13
+ - Pass `seconds` as a positive number
14
+ - Choose the shortest useful delay for responsiveness and cost
15
+ - Keep waits intentional; do not sleep if there is useful work to do now
16
+
17
+ ### Cancellation
18
+
19
+ - Sleep can be interrupted by user cancellation
20
+ - If cancellation happens, report that the wait was cancelled and continue with next best action
@@ -0,0 +1,28 @@
1
+ ## Sub Agents
2
+
3
+ You can delegate specific work using the `run_agents` tool.
4
+
5
+ Use this tool when delegation makes the response better:
6
+
7
+ - The task has independent parts that can run in parallel.
8
+ - You need deeper investigation before writing a final answer.
9
+ - You want one agent focused on research and another on planning.
10
+
11
+ Use delegation thoughtfully:
12
+
13
+ - Split jobs by clear goals and scopes.
14
+ - Write each job prompt with enough context to be actionable.
15
+ - Prefer concise descriptions that state the expected output.
16
+ - Run only as many jobs as needed for quality and speed.
17
+
18
+ Do not use `run_agents` when:
19
+
20
+ - The task is simple and can be handled directly.
21
+ - The work is tightly coupled and cannot be split cleanly.
22
+ - You already have enough evidence to answer confidently.
23
+
24
+ Output expectations:
25
+
26
+ - Child agents are read-only and return findings or plans.
27
+ - You are responsible for synthesizing child outputs into one coherent response.
28
+ - If child outputs conflict, resolve the conflict explicitly and explain why.
@@ -3,18 +3,22 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { compile } from "handlebars";
5
5
  import type { Config } from "../config.ts";
6
+ import { getEnvironmentPromptContext } from "./environment.ts";
6
7
 
7
8
  /** Bundled markdown next to this module (`prompts/static/`). */
8
9
  const STATIC_PROMPT_FILES = [
9
10
  "identity.md",
11
+ "environment.md",
10
12
  "ltm.md",
11
13
  "todo.md",
12
14
  "thinking.md",
13
15
  "filesystem.md",
14
16
  "fetch.md",
15
17
  "shell.md",
18
+ "sleep.md",
16
19
  "wiki.md",
17
20
  "skills.md",
21
+ "subagents.md",
18
22
  ] as const;
19
23
 
20
24
  const SECTION_BREAK = "\n\n---\n\n";
@@ -46,10 +50,14 @@ export class System {
46
50
  return this.config.tools.filesystem.enabled;
47
51
  case "shell.md":
48
52
  return this.config.tools.shell.enabled;
53
+ case "sleep.md":
54
+ return this.config.tools.sleep.enabled;
49
55
  case "wiki.md":
50
56
  return this.config.tools.wiki.enabled;
51
57
  case "skills.md":
52
58
  return this.config.tools.skills.enabled;
59
+ case "subagents.md":
60
+ return this.config.tools.agents.enabled;
53
61
  case "thinking.md":
54
62
  default:
55
63
  return true;
@@ -101,6 +109,7 @@ export class System {
101
109
  return {
102
110
  name: this.config.name,
103
111
  llm: this.config.llm,
112
+ environment: getEnvironmentPromptContext(),
104
113
  ltm: this.config.tools.ltm,
105
114
  wiki: this.config.tools.wiki,
106
115
  compaction: this.config.compaction,
@@ -1,5 +1,6 @@
1
1
  export { createFilesystemTools } from "./filesystem.ts";
2
2
  export { createFetchTools } from "./fetch.ts";
3
+ export { createSleepTools } from "./sleep.ts";
3
4
  export { createShellTools } from "./shell.ts";
4
5
  export { createThinkingTools } from "./thinking.ts";
5
6
  export { createTimeTools } from "./time.ts";
@@ -0,0 +1,51 @@
1
+ import { setTimeout as sleepTimer } from "node:timers/promises";
2
+ import { tool } from "@strands-agents/sdk";
3
+ import type { JSONValue, ToolContext } from "@strands-agents/sdk";
4
+ import { z } from "zod";
5
+
6
+ const MAX_SLEEP_SECONDS = 60 * 60;
7
+
8
+ function toJsonValue(value: unknown): JSONValue {
9
+ return JSON.parse(JSON.stringify(value)) as JSONValue;
10
+ }
11
+
12
+ export function createSleepTools() {
13
+ return [
14
+ tool({
15
+ name: "sleep",
16
+ description:
17
+ "Wait for a specified number of seconds without using shell processes. Use when the user asks you to pause or retry after a delay.",
18
+ inputSchema: z.object({
19
+ seconds: z.coerce
20
+ .number()
21
+ .positive()
22
+ .max(MAX_SLEEP_SECONDS)
23
+ .describe(
24
+ `How long to wait, in seconds. Must be greater than 0 and at most ${MAX_SLEEP_SECONDS}.`,
25
+ ),
26
+ }),
27
+ callback: async (input, context?: ToolContext) => {
28
+ const startedAt = Date.now();
29
+ try {
30
+ await sleepTimer(input.seconds * 1000, undefined, {
31
+ signal: context?.agent.cancelSignal,
32
+ });
33
+ return toJsonValue({
34
+ status: "completed",
35
+ requested_seconds: input.seconds,
36
+ slept_seconds: (Date.now() - startedAt) / 1000,
37
+ });
38
+ } catch (error) {
39
+ if (error instanceof Error && error.name === "AbortError") {
40
+ return toJsonValue({
41
+ status: "cancelled",
42
+ requested_seconds: input.seconds,
43
+ slept_seconds: (Date.now() - startedAt) / 1000,
44
+ });
45
+ }
46
+ throw error;
47
+ }
48
+ },
49
+ }),
50
+ ];
51
+ }