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 +1 -1
- package/src/acp/utils/tool-kind.ts +15 -0
- package/src/configure/app.tsx +17 -0
- package/src/core/agent/index.ts +38 -15
- package/src/core/agents/definitions.ts +47 -0
- package/src/core/agents/index.ts +15 -0
- package/src/core/agents/registry.ts +108 -0
- package/src/core/agents/runner.ts +375 -0
- package/src/core/agents/tools.ts +100 -0
- package/src/core/approvals/allowed-tools.ts +29 -4
- package/src/core/config.ts +23 -0
- package/src/core/prompts/agents/plan.md +35 -0
- package/src/core/prompts/agents/research.md +32 -0
- package/src/core/prompts/environment.ts +62 -0
- package/src/core/prompts/static/environment.md +15 -0
- package/src/core/prompts/static/sleep.md +20 -0
- package/src/core/prompts/static/subagents.md +28 -0
- package/src/core/prompts/system.ts +9 -0
- package/src/core/tools/index.ts +1 -0
- package/src/core/tools/sleep.ts +51 -0
package/package.json
CHANGED
|
@@ -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 };
|
package/src/configure/app.tsx
CHANGED
|
@@ -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" }),
|
package/src/core/agent/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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[] {
|
package/src/core/config.ts
CHANGED
|
@@ -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,
|
package/src/core/tools/index.ts
CHANGED
|
@@ -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
|
+
}
|