@dawn-ai/core 0.1.8 → 0.2.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/LICENSE +1 -1
- package/README.md +1 -1
- package/dist/capabilities/built-in/agents-md.d.ts +9 -0
- package/dist/capabilities/built-in/agents-md.d.ts.map +1 -0
- package/dist/capabilities/built-in/agents-md.js +54 -0
- package/dist/capabilities/built-in/frontmatter.d.ts +17 -0
- package/dist/capabilities/built-in/frontmatter.d.ts.map +1 -0
- package/dist/capabilities/built-in/frontmatter.js +47 -0
- package/dist/capabilities/built-in/plan-md-parser.d.ts +6 -0
- package/dist/capabilities/built-in/plan-md-parser.d.ts.map +1 -0
- package/dist/capabilities/built-in/plan-md-parser.js +18 -0
- package/dist/capabilities/built-in/planning.d.ts +7 -0
- package/dist/capabilities/built-in/planning.d.ts.map +1 -0
- package/dist/capabilities/built-in/planning.js +125 -0
- package/dist/capabilities/built-in/skills.d.ts +3 -0
- package/dist/capabilities/built-in/skills.d.ts.map +1 -0
- package/dist/capabilities/built-in/skills.js +114 -0
- package/dist/capabilities/built-in/subagents.d.ts +3 -0
- package/dist/capabilities/built-in/subagents.d.ts.map +1 -0
- package/dist/capabilities/built-in/subagents.js +99 -0
- package/dist/capabilities/built-in/workspace.d.ts +3 -0
- package/dist/capabilities/built-in/workspace.d.ts.map +1 -0
- package/dist/capabilities/built-in/workspace.js +187 -0
- package/dist/capabilities/registry.d.ts +21 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +35 -0
- package/dist/capabilities/types.d.ts +56 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -214
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/typegen/extract-tool-schema.js +58 -3
- package/dist/typegen/extract-tool-types.js +2 -2
- package/dist/types.d.ts +24 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -3
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
Filesystem-based route discovery, app config loading, state-field resolution, and typegen primitives that the Dawn CLI builds on.
|
|
8
8
|
|
|
9
|
-
This is an internal Dawn workspace package. For Dawn documentation, see <https://
|
|
9
|
+
This is an internal Dawn workspace package. For Dawn documentation, see <https://github.com/cacheplane/dawnai/tree/main/apps/web/content/docs>.
|
|
10
10
|
|
|
11
11
|
## License
|
|
12
12
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CapabilityMarker } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-injects the contents of <process.cwd()>/workspace/AGENTS.md into the
|
|
4
|
+
* agent's system prompt under a "# Memory" heading. Always-on: the presence
|
|
5
|
+
* of the file IS the opt-in. Re-reads the file on every model turn so the
|
|
6
|
+
* agent sees its own updated memory immediately after it calls writeFile.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createAgentsMdMarker(): CapabilityMarker;
|
|
9
|
+
//# sourceMappingURL=agents-md.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents-md.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/agents-md.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AASnD;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,gBAAgB,CAWvD"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const MAX_MEMORY_BYTES = 64 * 1024;
|
|
4
|
+
const MEMORY_HEADER = `# Memory
|
|
5
|
+
|
|
6
|
+
The block below is the live contents of \`workspace/AGENTS.md\`, re-read on every turn. This IS your persistent memory — do NOT re-read this file with any tool; the content here is always current. Update it by calling \`writeFile({ path: "AGENTS.md", content: "..." })\` when you learn something worth remembering.
|
|
7
|
+
|
|
8
|
+
---`;
|
|
9
|
+
/**
|
|
10
|
+
* Auto-injects the contents of <process.cwd()>/workspace/AGENTS.md into the
|
|
11
|
+
* agent's system prompt under a "# Memory" heading. Always-on: the presence
|
|
12
|
+
* of the file IS the opt-in. Re-reads the file on every model turn so the
|
|
13
|
+
* agent sees its own updated memory immediately after it calls writeFile.
|
|
14
|
+
*/
|
|
15
|
+
export function createAgentsMdMarker() {
|
|
16
|
+
return {
|
|
17
|
+
name: "agents-md",
|
|
18
|
+
detect: async (_routeDir, _context) => true,
|
|
19
|
+
load: async (_routeDir, _context) => ({
|
|
20
|
+
promptFragment: {
|
|
21
|
+
placement: "after_user_prompt",
|
|
22
|
+
render: () => renderMemoryFragment(workspaceAgentsMdPath()),
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function workspaceAgentsMdPath() {
|
|
28
|
+
return resolve(process.cwd(), "workspace", "AGENTS.md");
|
|
29
|
+
}
|
|
30
|
+
function renderMemoryFragment(path) {
|
|
31
|
+
if (!existsSync(path))
|
|
32
|
+
return "";
|
|
33
|
+
let size;
|
|
34
|
+
try {
|
|
35
|
+
size = statSync(path).size;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
if (size > MAX_MEMORY_BYTES) {
|
|
41
|
+
return `${MEMORY_HEADER}\n\n(workspace/AGENTS.md is ${size} bytes; exceeds 64 KiB limit — not loaded)`;
|
|
42
|
+
}
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = readFileSync(path, "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
if (trimmed.length === 0)
|
|
52
|
+
return "";
|
|
53
|
+
return `${MEMORY_HEADER}\n\n${trimmed}`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal YAML-frontmatter parser. Sufficient for Dawn's skill files,
|
|
3
|
+
* which use a flat `key: value` block at the top delimited by `---` lines.
|
|
4
|
+
*
|
|
5
|
+
* Supports: keys, double-quoted values, single-quoted values, `#` comments,
|
|
6
|
+
* blank lines, CRLF endings, leading/trailing whitespace.
|
|
7
|
+
*
|
|
8
|
+
* Does NOT support: nested objects, arrays, multi-line strings, anchors,
|
|
9
|
+
* any other real YAML feature. If a skill needs full YAML, swap to the
|
|
10
|
+
* `yaml` npm package without changing this module's contract.
|
|
11
|
+
*/
|
|
12
|
+
export interface ParsedFrontmatter {
|
|
13
|
+
readonly frontmatter: Readonly<Record<string, string>>;
|
|
14
|
+
readonly body: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function parseFrontmatter(input: string): ParsedFrontmatter;
|
|
17
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/frontmatter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAKD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,CAejE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const OPEN_MARKER = /^---\r?\n/;
|
|
2
|
+
const CLOSE_MARKER = /\r?\n---\r?\n?/;
|
|
3
|
+
export function parseFrontmatter(input) {
|
|
4
|
+
if (!OPEN_MARKER.test(input)) {
|
|
5
|
+
return { frontmatter: {}, body: input };
|
|
6
|
+
}
|
|
7
|
+
const openLen = OPEN_MARKER.exec(input)?.[0].length ?? 0;
|
|
8
|
+
const afterOpen = input.slice(openLen);
|
|
9
|
+
const closeMatch = CLOSE_MARKER.exec(afterOpen);
|
|
10
|
+
if (!closeMatch) {
|
|
11
|
+
return { frontmatter: {}, body: input };
|
|
12
|
+
}
|
|
13
|
+
const block = afterOpen.slice(0, closeMatch.index);
|
|
14
|
+
const bodyStart = closeMatch.index + closeMatch[0].length;
|
|
15
|
+
const body = afterOpen.slice(bodyStart).replace(/^\r?\n/, "");
|
|
16
|
+
const frontmatter = parseFrontmatterBlock(block);
|
|
17
|
+
return { frontmatter, body };
|
|
18
|
+
}
|
|
19
|
+
function parseFrontmatterBlock(block) {
|
|
20
|
+
const out = {};
|
|
21
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
22
|
+
const line = rawLine.trim();
|
|
23
|
+
if (line.length === 0)
|
|
24
|
+
continue;
|
|
25
|
+
if (line.startsWith("#"))
|
|
26
|
+
continue;
|
|
27
|
+
const colonIdx = line.indexOf(":");
|
|
28
|
+
if (colonIdx < 0)
|
|
29
|
+
continue;
|
|
30
|
+
const key = line.slice(0, colonIdx).trim();
|
|
31
|
+
if (key.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
const rawValue = line.slice(colonIdx + 1).trim();
|
|
34
|
+
out[key] = stripQuotes(rawValue);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
function stripQuotes(value) {
|
|
39
|
+
if (value.length >= 2) {
|
|
40
|
+
const first = value[0];
|
|
41
|
+
const last = value[value.length - 1];
|
|
42
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
43
|
+
return value.slice(1, -1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plan-md-parser.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/plan-md-parser.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,WAAW,CAAA;CACzC;AAID,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAc3D"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const CHECKLIST_LINE = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
|
|
2
|
+
export function parsePlanMarkdown(input) {
|
|
3
|
+
const todos = [];
|
|
4
|
+
for (const line of input.split(/\r?\n/)) {
|
|
5
|
+
const match = CHECKLIST_LINE.exec(line);
|
|
6
|
+
if (!match)
|
|
7
|
+
continue;
|
|
8
|
+
const checkChar = match[1] ?? " ";
|
|
9
|
+
const content = (match[2] ?? "").trim();
|
|
10
|
+
if (content.length === 0)
|
|
11
|
+
continue;
|
|
12
|
+
todos.push({
|
|
13
|
+
content,
|
|
14
|
+
status: checkChar === " " ? "pending" : "completed",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return todos;
|
|
18
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CapabilityMarker } from "../types.js";
|
|
2
|
+
export interface RuntimeTodo {
|
|
3
|
+
readonly content: string;
|
|
4
|
+
readonly status: "pending" | "in_progress" | "completed";
|
|
5
|
+
}
|
|
6
|
+
export declare function createPlanningMarker(): CapabilityMarker;
|
|
7
|
+
//# sourceMappingURL=planning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"planning.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/planning.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAqC,MAAM,aAAa,CAAA;AAMtF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,WAAW,CAAA;CACzD;AAkBD,wBAAgB,oBAAoB,IAAI,gBAAgB,CAwEvD"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { parsePlanMarkdown } from "./plan-md-parser.js";
|
|
5
|
+
const PLAN_MD = "plan.md";
|
|
6
|
+
const MAX_PLAN_BYTES = 64 * 1024;
|
|
7
|
+
const TODO_STATUS = z.enum(["pending", "in_progress", "completed"]);
|
|
8
|
+
const WRITE_TODOS_INPUT = z.object({
|
|
9
|
+
todos: z.array(z.object({
|
|
10
|
+
content: z.string().min(1),
|
|
11
|
+
status: TODO_STATUS,
|
|
12
|
+
})),
|
|
13
|
+
});
|
|
14
|
+
const PLANNING_PROMPT_HEADER = `# Planning
|
|
15
|
+
|
|
16
|
+
For tasks with multiple steps, maintain a plan using \`writeTodos({ todos: [...] })\`.
|
|
17
|
+
Mark items \`in_progress\` immediately before working on them and \`completed\` when
|
|
18
|
+
finished. Always include the full list — \`writeTodos\` is full-replace, not incremental.`;
|
|
19
|
+
export function createPlanningMarker() {
|
|
20
|
+
return {
|
|
21
|
+
name: "planning",
|
|
22
|
+
detect: async (routeDir, _context) => existsSync(join(routeDir, PLAN_MD)),
|
|
23
|
+
load: async (routeDir, _context) => {
|
|
24
|
+
const seedTodos = readSeedTodos(routeDir);
|
|
25
|
+
const writeTodos = {
|
|
26
|
+
name: "writeTodos",
|
|
27
|
+
description: "Replace the agent's plan with the given list of todos. Pass the full list every time; this tool is not incremental.",
|
|
28
|
+
schema: WRITE_TODOS_INPUT,
|
|
29
|
+
run: (input) => {
|
|
30
|
+
const validated = validateWriteTodosInput(input);
|
|
31
|
+
return {
|
|
32
|
+
result: { todos: validated },
|
|
33
|
+
state: { todos: validated },
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const promptFragment = {
|
|
38
|
+
placement: "after_user_prompt",
|
|
39
|
+
render: (state) => {
|
|
40
|
+
const todos = state.todos ?? [];
|
|
41
|
+
if (todos.length === 0) {
|
|
42
|
+
return `${PLANNING_PROMPT_HEADER}\n\nCurrent plan: (empty)`;
|
|
43
|
+
}
|
|
44
|
+
const lines = todos.map((t) => `- [${t.status}] ${t.content}`).join("\n");
|
|
45
|
+
return `${PLANNING_PROMPT_HEADER}\n\nCurrent plan:\n${lines}`;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const streamTransformer = {
|
|
49
|
+
observes: "tool_result",
|
|
50
|
+
transform: async function* (input) {
|
|
51
|
+
if (input.toolName !== "writeTodos")
|
|
52
|
+
return;
|
|
53
|
+
// toolOutput shape depends on what the tool returned:
|
|
54
|
+
// - Bare object `{ todos }` (legacy / direct test invocation).
|
|
55
|
+
// - LangGraph Command instance whose `update.todos` carries the new todos
|
|
56
|
+
// (the {result, state} wrapped return path goes through the langchain
|
|
57
|
+
// bridge and arrives here as a Command).
|
|
58
|
+
// Read defensively from both locations; future capability authors
|
|
59
|
+
// shouldn't have to remember which one they're in.
|
|
60
|
+
const out = input.toolOutput;
|
|
61
|
+
const todos = out?.update?.todos ?? out?.todos ?? [];
|
|
62
|
+
yield {
|
|
63
|
+
event: "plan_update",
|
|
64
|
+
data: { todos },
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
tools: [writeTodos],
|
|
70
|
+
stateFields: [
|
|
71
|
+
{
|
|
72
|
+
name: "todos",
|
|
73
|
+
reducer: "replace",
|
|
74
|
+
default: seedTodos,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
promptFragment,
|
|
78
|
+
streamTransformers: [streamTransformer],
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function readSeedTodos(routeDir) {
|
|
84
|
+
const planPath = join(routeDir, PLAN_MD);
|
|
85
|
+
if (!existsSync(planPath))
|
|
86
|
+
return [];
|
|
87
|
+
const size = statSync(planPath).size;
|
|
88
|
+
if (size > MAX_PLAN_BYTES)
|
|
89
|
+
return [];
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = readFileSync(planPath, "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
const parsed = parsePlanMarkdown(raw);
|
|
98
|
+
return parsed.map((t) => ({ content: t.content, status: t.status }));
|
|
99
|
+
}
|
|
100
|
+
function validateWriteTodosInput(input) {
|
|
101
|
+
if (!isRecord(input)) {
|
|
102
|
+
throw new Error("writeTodos: input must be an object with a `todos` array");
|
|
103
|
+
}
|
|
104
|
+
const todos = input.todos;
|
|
105
|
+
if (!Array.isArray(todos)) {
|
|
106
|
+
throw new Error("writeTodos: `todos` must be an array");
|
|
107
|
+
}
|
|
108
|
+
return todos.map((t, i) => {
|
|
109
|
+
if (!isRecord(t)) {
|
|
110
|
+
throw new Error(`writeTodos: todos[${i}] must be an object`);
|
|
111
|
+
}
|
|
112
|
+
const content = t.content;
|
|
113
|
+
const status = t.status;
|
|
114
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
115
|
+
throw new Error(`writeTodos: todos[${i}].content must be a non-empty string`);
|
|
116
|
+
}
|
|
117
|
+
if (status !== "pending" && status !== "in_progress" && status !== "completed") {
|
|
118
|
+
throw new Error(`writeTodos: todos[${i}].status must be one of pending, in_progress, completed`);
|
|
119
|
+
}
|
|
120
|
+
return { content, status };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function isRecord(value) {
|
|
124
|
+
return typeof value === "object" && value !== null;
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/skills.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAkB,MAAM,aAAa,CAAA;AAwBnE,wBAAgB,kBAAkB,IAAI,gBAAgB,CA2CrD"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { parseFrontmatter } from "./frontmatter.js";
|
|
5
|
+
const SKILLS_DIR = "skills";
|
|
6
|
+
const SKILL_FILE = "SKILL.md";
|
|
7
|
+
// Directory name must be a valid kebab-case-ish identifier. We exclude dotfiles,
|
|
8
|
+
// spaces, and other punctuation that would make a poor agent-facing name.
|
|
9
|
+
const VALID_DIR_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
10
|
+
const SKILLS_PROMPT_HEADER = `# Skills
|
|
11
|
+
|
|
12
|
+
The following skills are available. To use one, call \`readSkill({ name: "<name>" })\` to load its full instructions before acting.`;
|
|
13
|
+
const READ_SKILL_INPUT = z.object({
|
|
14
|
+
name: z.string().min(1),
|
|
15
|
+
});
|
|
16
|
+
export function createSkillsMarker() {
|
|
17
|
+
return {
|
|
18
|
+
name: "skills",
|
|
19
|
+
detect: async (routeDir, _context) => discoverSkillDirs(routeDir).length > 0,
|
|
20
|
+
load: async (routeDir, _context) => {
|
|
21
|
+
const skills = loadSkills(routeDir);
|
|
22
|
+
const readSkill = {
|
|
23
|
+
name: "readSkill",
|
|
24
|
+
description: "Load the full instructions for a named skill.",
|
|
25
|
+
schema: READ_SKILL_INPUT,
|
|
26
|
+
run: async (input) => {
|
|
27
|
+
const { name } = READ_SKILL_INPUT.parse(input);
|
|
28
|
+
const found = skills.find((s) => s.name === name);
|
|
29
|
+
if (!found) {
|
|
30
|
+
const available = skills
|
|
31
|
+
.map((s) => s.name)
|
|
32
|
+
.sort()
|
|
33
|
+
.join(", ");
|
|
34
|
+
return `Unknown skill: ${name}. Available: ${available}`;
|
|
35
|
+
}
|
|
36
|
+
return found.body;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const promptFragment = {
|
|
40
|
+
placement: "after_user_prompt",
|
|
41
|
+
render: () => {
|
|
42
|
+
const lines = skills
|
|
43
|
+
.slice()
|
|
44
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
45
|
+
.map((s) => `- **${s.name}** — ${s.description}`)
|
|
46
|
+
.join("\n");
|
|
47
|
+
return `${SKILLS_PROMPT_HEADER}\n\n${lines}`;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
tools: [readSkill],
|
|
52
|
+
promptFragment,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function discoverSkillDirs(routeDir) {
|
|
58
|
+
const skillsDir = join(routeDir, SKILLS_DIR);
|
|
59
|
+
if (!existsSync(skillsDir))
|
|
60
|
+
return [];
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = readdirSync(skillsDir);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return entries.filter((name) => {
|
|
69
|
+
if (!VALID_DIR_NAME.test(name))
|
|
70
|
+
return false;
|
|
71
|
+
const full = join(skillsDir, name);
|
|
72
|
+
let stat;
|
|
73
|
+
try {
|
|
74
|
+
stat = statSync(full);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (!stat.isDirectory())
|
|
80
|
+
return false;
|
|
81
|
+
return existsSync(join(full, SKILL_FILE));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function loadSkills(routeDir) {
|
|
85
|
+
const dirNames = discoverSkillDirs(routeDir);
|
|
86
|
+
const loaded = [];
|
|
87
|
+
const seenNames = new Set();
|
|
88
|
+
for (const dirName of dirNames) {
|
|
89
|
+
const path = join(routeDir, SKILLS_DIR, dirName, SKILL_FILE);
|
|
90
|
+
let raw;
|
|
91
|
+
try {
|
|
92
|
+
raw = readFileSync(path, "utf8");
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
throw new Error(`Failed to read ${path}: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
98
|
+
if (Object.keys(frontmatter).length === 0) {
|
|
99
|
+
throw new Error(`${path} is missing required frontmatter. Add a YAML block at the top with at least \`description: …\`.`);
|
|
100
|
+
}
|
|
101
|
+
const description = frontmatter.description;
|
|
102
|
+
if (!description || description.length === 0) {
|
|
103
|
+
throw new Error(`${path} frontmatter is missing required \`description\` field.`);
|
|
104
|
+
}
|
|
105
|
+
const name = frontmatter.name && frontmatter.name.length > 0 ? frontmatter.name : dirName;
|
|
106
|
+
if (seenNames.has(name)) {
|
|
107
|
+
const dupPath = loaded.find((s) => s.name === name)?.path;
|
|
108
|
+
throw new Error(`Duplicate skill name "${name}" — collision between ${dupPath} and ${path}. Each skill name must be unique.`);
|
|
109
|
+
}
|
|
110
|
+
seenNames.add(name);
|
|
111
|
+
loaded.push({ name, description, body, path });
|
|
112
|
+
}
|
|
113
|
+
return loaded;
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subagents.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/subagents.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAA2C,MAAM,aAAa,CAAA;AAuC5F,wBAAgB,qBAAqB,IAAI,gBAAgB,CAmFxD"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { isDawnAgent } from "@dawn-ai/sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const SUBAGENTS_PROMPT_HEADER = `# Subagents
|
|
5
|
+
|
|
6
|
+
The following subagents are available. Call \`task({ subagent, input })\` to dispatch a sub-task. Use the description to choose the right subagent for each piece of work.`;
|
|
7
|
+
function findConventionSubagents(routeDir, routeManifest) {
|
|
8
|
+
const prefix = `${routeDir}/subagents/`;
|
|
9
|
+
return routeManifest.routes.filter((r) => {
|
|
10
|
+
if (!r.routeDir.startsWith(prefix))
|
|
11
|
+
return false;
|
|
12
|
+
// immediate child of <routeDir>/subagents/ — no further slashes
|
|
13
|
+
const tail = r.routeDir.slice(prefix.length);
|
|
14
|
+
return tail.length > 0 && !tail.includes("/");
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async function loadDescription(route) {
|
|
18
|
+
try {
|
|
19
|
+
const mod = (await import(pathToFileURL(route.entryFile).href));
|
|
20
|
+
if (isDawnAgent(mod.default) && typeof mod.default.description === "string") {
|
|
21
|
+
return mod.default.description;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// fall through to default — never fail capability composition over a description
|
|
26
|
+
}
|
|
27
|
+
return "No description provided.";
|
|
28
|
+
}
|
|
29
|
+
export function createSubagentsMarker() {
|
|
30
|
+
return {
|
|
31
|
+
name: "subagents",
|
|
32
|
+
detect: async (routeDir, context) => {
|
|
33
|
+
if (findConventionSubagents(routeDir, context.routeManifest).length > 0)
|
|
34
|
+
return true;
|
|
35
|
+
return (context.descriptor?.subagents?.length ?? 0) > 0;
|
|
36
|
+
},
|
|
37
|
+
load: async (routeDir, context) => {
|
|
38
|
+
const conventionRoutes = findConventionSubagents(routeDir, context.routeManifest);
|
|
39
|
+
const overrideDescriptors = context.descriptor?.subagents ?? [];
|
|
40
|
+
const overrideRoutes = [];
|
|
41
|
+
for (const desc of overrideDescriptors) {
|
|
42
|
+
const routeId = context.descriptorRouteMap?.get(desc);
|
|
43
|
+
if (!routeId) {
|
|
44
|
+
console.warn(`subagents marker: could not resolve override descriptor for route at ${routeDir}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const found = context.routeManifest.routes.find((r) => r.id === routeId);
|
|
48
|
+
if (found)
|
|
49
|
+
overrideRoutes.push(found);
|
|
50
|
+
}
|
|
51
|
+
const allRoutes = [...conventionRoutes, ...overrideRoutes];
|
|
52
|
+
if (allRoutes.length === 0)
|
|
53
|
+
return {};
|
|
54
|
+
const conventionPrefix = `${routeDir}/subagents/`;
|
|
55
|
+
const discovered = [];
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const r of allRoutes) {
|
|
58
|
+
const lastSegment = r.segments.at(-1);
|
|
59
|
+
const lastSegmentName = typeof lastSegment === "string"
|
|
60
|
+
? lastSegment
|
|
61
|
+
: (lastSegment?.raw ?? r.id.replace(/^\//, ""));
|
|
62
|
+
const leafName = r.routeDir.startsWith(conventionPrefix)
|
|
63
|
+
? r.routeDir.slice(conventionPrefix.length)
|
|
64
|
+
: lastSegmentName;
|
|
65
|
+
if (seen.has(leafName)) {
|
|
66
|
+
throw new Error(`subagents marker: duplicate leaf name "${leafName}" (collision between convention and override). Rename one of the subagent routes.`);
|
|
67
|
+
}
|
|
68
|
+
seen.add(leafName);
|
|
69
|
+
const description = await loadDescription(r);
|
|
70
|
+
discovered.push({ leafName, routeId: r.id, description });
|
|
71
|
+
}
|
|
72
|
+
const leafNames = discovered.map((d) => d.leafName);
|
|
73
|
+
const taskSchema = z.object({
|
|
74
|
+
subagent: z.enum(leafNames),
|
|
75
|
+
input: z.string().describe("The task description for the subagent to handle."),
|
|
76
|
+
});
|
|
77
|
+
const task = {
|
|
78
|
+
name: "task",
|
|
79
|
+
description: "Dispatch a sub-task to a specialized subagent. See the # Subagents section of your system prompt for available agents and when to use each.",
|
|
80
|
+
schema: taskSchema,
|
|
81
|
+
run: async (_input) => {
|
|
82
|
+
throw new Error("subagents marker: task tool was invoked outside the langchain bridge (dispatcher not wired)");
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const promptFragment = {
|
|
86
|
+
placement: "after_user_prompt",
|
|
87
|
+
render: () => {
|
|
88
|
+
const lines = discovered
|
|
89
|
+
.slice()
|
|
90
|
+
.sort((a, b) => a.leafName.localeCompare(b.leafName))
|
|
91
|
+
.map((s) => `- **${s.leafName}** — ${s.description}`)
|
|
92
|
+
.join("\n");
|
|
93
|
+
return `${SUBAGENTS_PROMPT_HEADER}\n\n${lines}`;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
return { tools: [task], promptFragment };
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../../../src/capabilities/built-in/workspace.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,aAAa,CAAA;AAsMvE,wBAAgB,qBAAqB,IAAI,gBAAgB,CAoBxD"}
|