1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87import { existsSync, readFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { join, basename } from "node:path";
import type { Capability } from "../config.ts";
export interface AgentSpec {
// Logical name without extension, e.g. "table".
name: string;
// File name as referenced in notes, e.g. "table.md".
file: string;
// Full markdown contents (system prompt + contract).
content: string;
// Capabilities declared in the "## Required capability" section.
capabilities: Capability[];
// git SHA of the file in the agents/ checkout, or null for session-built
// agents that have no upstream object (PRD §7.3 version pinning).
sha: string | null;
// True when this agent lives in tmp/<session>/agents (session-built, §7.5).
sessionBuilt: boolean;
}
const CAPABILITY_WORDS: Capability[] = ["text", "vision", "structured_output"];
function parseCapabilities(content: string): Capability[] {
const m = content.match(/##\s*Required capability\s*\n([^#]*)/i);
const found = new Set<Capability>();
if (m) {
for (const cap of CAPABILITY_WORDS) {
if (new RegExp(`\\b${cap}\\b`).test(m[1])) found.add(cap);
}
}
if (found.size === 0) found.add("vision");
return [...found];
}
function gitSha(dir: string, file: string): string | null {
try {
// `:./` resolves the path relative to `dir`, so this works whether agents/
// is its own checkout or a subdirectory of a larger repo.
const out = execFileSync("git", ["-C", dir, "rev-parse", `HEAD:./${file}`], {
stdio: ["ignore", "pipe", "ignore"],
});
return out.toString().trim() || null;
} catch {
return null; // not a git checkout, or file not committed
}
}
// Loads an agent by name, preferring a session-built agent in tmp/ over the
// upstream library (PRD §7.3: built agents are used for the rest of the
// session). Returns null when no agent exists for the type.
export function loadAgent(
name: string,
opts: { agentsDir: string; tmpAgentsDir: string },
): AgentSpec | null {
const file = name.endsWith(".md") ? name : `${name}.md`;
const logical = basename(file, ".md");
const sessionPath = join(opts.tmpAgentsDir, file);
if (existsSync(sessionPath)) {
const content = readFileSync(sessionPath, "utf8");
return {
name: logical,
file,
content,
capabilities: parseCapabilities(content),
sha: null,
sessionBuilt: true,
};
}
const libPath = join(opts.agentsDir, file);
if (existsSync(libPath)) {
const content = readFileSync(libPath, "utf8");
return {
name: logical,
file,
content,
capabilities: parseCapabilities(content),
sha: gitSha(opts.agentsDir, file),
sessionBuilt: false,
};
}
return null;
}