summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/agents
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-28 22:51:47 +0900
committerAdam Malczewski <[email protected]>2026-05-28 22:51:47 +0900
commitd6609efd4e14101e77fb35a98ce597a32816862d (patch)
tree09ea404ce0a780ca6b8c380fdd93ad1ae9960986 /packages/core/src/agents
parent2eeabc95b78f6624c187e1e3892f9413266b4b9a (diff)
downloaddispatch-d6609efd4e14101e77fb35a98ce597a32816862d.tar.gz
dispatch-d6609efd4e14101e77fb35a98ce597a32816862d.zip
fix(core): normalize tool schemas for Anthropic, add toolChoice=auto; feat(summon): agent definition support; docs: cc/ research findings
- registry.ts: add normalizeForAnthropic() to strip , additionalProperties, default, nullable from zodToJsonSchema output so Anthropic doesn't silently reject tool definitions - agent.ts: add toolChoice=auto for Claude OAuth to prevent Opus thinking forever without calling tools - summon.ts: add agentSlug parameter, build agents catalog in description, add toAvailableAgents helper - agent-manager.ts: wire agent definition loading into spawnChildAgent, agent model fallback - loader.ts: export loadAgent, expandAgentToolNames, getAgentDirPaths; add getAgentDirPaths for permission gate - agent.ts: auto-allow read-only tools in agent definition directories - packaging/PKGBUILD: exclude ARM64 prebuilds from x86_64 package - cc/: research findings on Claude Opus tool calling issues - tests: loader tests, summon tool tests
Diffstat (limited to 'packages/core/src/agents')
-rw-r--r--packages/core/src/agents/index.ts12
-rw-r--r--packages/core/src/agents/loader.ts75
2 files changed, 84 insertions, 3 deletions
diff --git a/packages/core/src/agents/index.ts b/packages/core/src/agents/index.ts
index 13f6244..4931162 100644
--- a/packages/core/src/agents/index.ts
+++ b/packages/core/src/agents/index.ts
@@ -1 +1,11 @@
-export { deleteAgent, getAgentDirs, loadAgents, saveAgent } from "./loader.js";
+export {
+ deleteAgent,
+ expandAgentToolNames,
+ GLOBAL_AGENTS_DIR,
+ getAgentDirPaths,
+ getAgentDirs,
+ getProjectAgentsDir,
+ loadAgent,
+ loadAgents,
+ saveAgent,
+} from "./loader.js";
diff --git a/packages/core/src/agents/loader.ts b/packages/core/src/agents/loader.ts
index cf84381..333716e 100644
--- a/packages/core/src/agents/loader.ts
+++ b/packages/core/src/agents/loader.ts
@@ -20,9 +20,9 @@ function sanitizeSlug(slug: string): string {
// ─── Constants ───────────────────────────────────────────────────
-const GLOBAL_AGENTS_DIR = path.join(os.homedir(), ".config", "dispatch", "agents");
+export const GLOBAL_AGENTS_DIR = path.join(os.homedir(), ".config", "dispatch", "agents");
-function getProjectAgentsDir(projectDir: string): string {
+export function getProjectAgentsDir(projectDir: string): string {
return path.join(projectDir, ".dispatch", "agents");
}
@@ -49,6 +49,77 @@ export function getAgentDirs(
}
/**
+ * Return just the absolute filesystem paths of the agent directories.
+ * Used by the agent's permission gate to grant read-only access to
+ * these locations by default (so any agent can list/read agent
+ * definitions without prompting the user).
+ */
+export function getAgentDirPaths(projectDir?: string): string[] {
+ const paths = [GLOBAL_AGENTS_DIR];
+ if (projectDir) paths.push(getProjectAgentsDir(projectDir));
+ return paths;
+}
+
+/**
+ * Load a single agent definition by slug. Searches the project-scoped
+ * directory first (if `projectDir` is provided), then falls back to
+ * the global directory. Returns `null` if no match is found.
+ *
+ * Slug matching is exact and case-sensitive; sanitization mirrors
+ * `saveAgent` to keep loader and writer symmetric.
+ */
+export function loadAgent(slug: string, projectDir?: string): AgentDefinition | null {
+ const safeSlug = sanitizeSlug(slug);
+ const agents = loadAgents(projectDir);
+ return agents.find((a) => a.slug === safeSlug) ?? null;
+}
+
+/**
+ * Translate the short permission-group names used by `AgentDefinition.tools`
+ * (e.g. `"read"`, `"edit"`, `"bash"`) into the concrete tool-implementation
+ * names registered with the agent runtime (e.g. `"read_file"`,
+ * `"list_files"`, `"write_file"`, `"run_shell"`).
+ *
+ * The mapping mirrors the per-permission tool-creation paths in
+ * `AgentManager.getOrCreateAgentForTab` so a subagent summoned with a
+ * given agent definition ends up with the exact same set of registered
+ * tools as a top-level tab using that definition. Tool names that aren't
+ * group aliases (`summon`, `retrieve`, `web_search`, `youtube_transcribe`,
+ * `todo`) are passed through unchanged.
+ *
+ * `"todo"` is auto-included so the summoned agent always has its task list
+ * available, matching the parent-agent path which always registers `todo`.
+ */
+export function expandAgentToolNames(tools: string[]): string[] {
+ const expanded = new Set<string>();
+ for (const t of tools) {
+ switch (t) {
+ case "read":
+ expanded.add("read_file");
+ expanded.add("read_file_slice");
+ expanded.add("list_files");
+ break;
+ case "edit":
+ expanded.add("write_file");
+ break;
+ case "bash":
+ expanded.add("run_shell");
+ break;
+ default:
+ // Pass through tool names that aren't permission-group
+ // aliases (summon, retrieve, web_search, youtube_transcribe,
+ // todo, and the granular file tools themselves if a user
+ // hand-wrote them in a TOML).
+ expanded.add(t);
+ }
+ }
+ // Always include `todo` — every agent should be able to track its work,
+ // and the parent-agent path adds it unconditionally.
+ expanded.add("todo");
+ return Array.from(expanded);
+}
+
+/**
* Ensure the default global agent exists. Creates it if missing.
*/
function ensureDefaultAgent(): void {