Tool Resolution
Tools are NOT pre-discovered into a global registry. Instead, the runtime resolves tools lazily on demand using findTool() and listTools().
findTool(ref, globals)
Resolves a tool reference in three forms:
- URI (
file://,mcp://,inline://) → searches programmatic tools bysrcfield. Forfile://URIs, converts to a filesystem path and loads directly vialoadSkillsFromFile(). Supports#fragmentfor multi-skill files. - Path (any string that is not a valid bare name — contains
/,\, uppercase, underscores, or exceeds 64 characters) → resolves to an absolute path and loads directly vialoadSkillsFromFile(). Bypasses the search path entirely, like/usr/bin/foobypasses$PATHin bash. Supports#fragmentfor multi-skill files. - Bare name → checks
globals.tools.programmaticToolsfirst, then searchespathsleft-to-right on disk. For each directory, checks the build-time cache (.agent-apps.json) first, then builds a full name index (cached per directory) by loading all skill files and mappingmeta.name→ToolDef. On index miss, rebuilds from disk in case the index was stale. First path entry with a match wins.
Results are cached on globals.cache.tool.
listTools(globals)
Enumerates all tools visible from globals.config.paths. Programmatic tools first, then disk tools from each path entry. Used by agent tool selection and skill-list. Results are cached on globals.cache.list.
Cache
Caches live for the process lifetime. Explicit invalidation only — clearToolCache(name) after compile, clearAllCaches() after clean. No TTL complexity.
The Cache interface holds these maps:
| Cache | Key | Value | Purpose |
|---|---|---|---|
tool |
name\0pathsKey |
ToolDef | null |
findTool results. Null means "not found." |
list |
pathsKey |
ToolDef[] |
listTools results. |
dir |
dir\0ignoreKey |
string[] |
listDir results (file paths in a directory). Keyed by directory path and ignore patterns. |
cascade |
name\0pathsKey |
{ middleware, order, hook, toolKeys, selfDefaults?, resolvedChain? } |
Cascade results per tool. |
file |
absolute path | ToolDef[] |
Parsed tool definitions from a file. Multi-skill files return multiple entries. Avoids re-reading the same file. |
authority |
src | name |
ToolDef |
Authority resolution results. Avoids redundant directory walks. |
cyclicPeers |
(singleton) | Map<name, Set<name>> | null |
SCC peer sets for auto-static cycle detection. |
cyclicPeersSeededDirs |
(singleton) | Set<string> | null |
Directories whose cyclicPeers data was seeded from .agent-apps.json. |
mainInDir |
absolute dir | ToolDef | null |
Main skill per directory. Avoids redundant readdir+stat. |
nameIndex |
dir |
Map<name, filePath> |
Per-directory name→file path index for O(1) bare-name lookup. |
promotions |
(singleton) | { local, global } | null |
Resolved $local/$global promotions from the main skill's metadata. Computed once, invalidated by clearAllCaches. |
cascadeDeps |
name\0pathsKey |
string[] |
Volatile interpolation dependency paths per tool. Paths that change between invocations (e.g., args.*) are tracked so the cascade cache key includes their values. |
Programmatic Tools
globals.tools.programmaticTools holds tools registered by code rather than discovered from disk: bootstrap, MCP tools, inline delegates, callback refs, and test mocks. findTool checks programmatic tools before disk search, so they shadow disk tools. Callback refs (created via createCallbackRef) are programmatic tools with generated names ({label}-{8-char-uuid}) and are subject to allowed-tools enforcement like any other tool.
For authority resolution, programmatic tools have src: null or synthetic URIs (mcp://, inline://). No filesystem walk is possible, so findAuthority returns the project's main skill or the library's main skill.
Utility Helpers
The runtime exports helper functions that eliminate common multi-field fallback patterns. All are importable from agent-apps.
| Helper | Signature | Returns |
|---|---|---|
ctxTarget(ctx) |
(ctx: Context) => Context |
ctx.envelope.target, throwing if target === ctx (not middleware). Used by all middleware to access the served context. |
ctxPaths(ctx) |
(ctx: Context) => PathEntry[] |
ctx.locals.config.paths ?? ctx.globals.config.paths |
ctxCwd(ctx) |
(ctx: Context) => string |
ctx.locals.config.cwd ?? ctx.globals.config.cwd ?? process.cwd() |
ctxHookTrigger(ctx) |
(ctx: Context) => string | undefined |
Read the hook trigger from the current chain entry ("$next", "$invoke", or undefined). |
ctxHookTarget(ctx) |
(ctx: Context) => string | undefined |
Find the next non-hook entry name in the chain (the real entry a $next hook wraps). |
pathEntryDir(entry) |
(entry: PathEntry) => string |
Resolve the raw directory string from a path entry. Throws on { ignore } entries — use iterPaths() instead. |
iterPaths(paths) |
(paths: PathEntry[]) => Generator<DirEntry> |
Iterate directory entries from a paths array, skipping { ignore } entries. Yields { dir, external }. |
extractIgnore(paths) |
(paths: PathEntry[]) => string[] |
Extract ignore patterns from a paths array. Returns the pattern strings from { ignore } entries. |
mapPaths(paths, fn) |
(paths: PathEntry[], fn: (dir, external, entry) => PathEntry[]) => PathEntry[] |
FlatMap over directory entries, preserving { ignore } entries. Callback returns [] to remove, [entry] to keep, [a, b] to expand. |
filterPaths(paths, fn) |
(paths: PathEntry[], fn: (dir, external) => boolean) => PathEntry[] |
Filter directory entries from a paths array, preserving { ignore } entries. |
registerTool(globals, opts) |
(globals: Globals, opts: RegisterToolOptions) => void |
Registers a programmatic tool on globals.tools.programmaticTools |
unregisterTool(globals, name) |
(globals, name) => void |
Removes a programmatic tool and clears its cache entry |
matchPattern(pattern, name, tags?) |
(string, string, string[]?) => boolean |
Glob match with #tag selector support. See Pattern Syntaxes. |
pathGet(obj, expr) |
(unknown, string) => unknown |
Read a value by path expression. See Pattern Syntaxes. |
pathSet(obj, expr, value) |
(unknown, string, unknown) => void |
Write a value by path expression, creating intermediates. |
pathQuery(obj, expr) |
(unknown, string) => unknown[] |
All matches for a path expression. |
pathRemove(obj, expr) |
(unknown, string) => void |
Delete matched paths. |
pathKeys(obj, exprs) |
(Record, string[]) => Set<string> |
Resolve expressions to top-level key names. |
isMetaKey(key) |
(string) => key is MetaKey |
True if key starts with $. See Pattern Syntaxes. |
safeStringify(value, indent?) |
(unknown, number?) => string |
JSON.stringify with safe handling of functions, promises, and circular references. |
serialize(value, opts?) |
(unknown, SerializeOptions?) => unknown |
Transform any value into a JSON-safe structure. Handles cycles, functions, Promises, Maps, depth, size. Returns a plain value — use stringify() for a JSON string. |
deserialize(value, opts?) |
(unknown, DeserializeOptions?) => unknown |
Transform a JSON-safe structure back to a rich value. Reconstructs $fn-encoded functions when opts.functions is true. |
stringify(value, opts?) |
(unknown, SerializeOptions?) => string |
Serialize to a JSON string. Drop-in replacement for JSON.stringify. Safe by default. |
parse(text, opts?) |
(string, DeserializeOptions?) => unknown |
Parse a JSON string and optionally deserialize $fn conventions. Drop-in replacement for JSON.parse. |
safeClone(value, opts?) |
(unknown, SafeCloneOptions?) => unknown |
Deep-clone any value into a plain, JSON-safe structure. Handles circular references, functions, Promises, Maps. Depth-limited and optionally size-limited. |
safeSnapshot(value, maxDepth?, maxBytes?) |
(unknown, number?, number?) => unknown |
Size-limited deep clone for diagnostic/debug event data. Defaults: depth=4, maxBytes=10KB. |
emptyGlobals() |
() => Globals |
Create a fresh Globals object with empty caches and default config. Useful for testing. |
transferableSerializerSource(names?) |
(names?) => string |
Self-contained JS source for $fn-aware stringify, parse, and reviveFns. Designed for injection into environments that cannot import this module (e.g., isolated-vm sandboxes). |
serializeContext(ctx, maxDepth?) |
(Context, number?) => Record | null |
Serialize a context for logging or debugging. Replaces functions with undefined, circular refs with "[Circular]", depth-limited. |
resolveJsonSchema(schema, ctx) |
(JsonObject | undefined, Context) => JsonObject | undefined |
Resolve a JSON Schema with optional $resolve path expression. If $resolve is present and the path resolves to a non-null object on ctx, returns the resolved object. Otherwise returns the static schema unchanged. |
registerTool accepts a single RegisterToolOptions object with name, fn, and optional fields: description, params, returns, tags, visibility, metadata, role, allowedTools, src, module, content. unregisterTool also calls clearToolCache to ensure subsequent lookups don't return stale results.
Glob Matching
See Pattern Syntaxes for the full syntax reference. Summary: real globs via picomatch with #tag selectors and (specifier) grammar.
# Allow all internal tools, plus a specific custom tool
allowed-tools: "#meta-internal custom-tool"
# Allow everything except destructive tools
allowed-tools: "* #destructive($deny)"
# Deny specific operations with brace expansion
allowed-tools: "* context-manager(op={finish,fail},$deny)"
# Constrain subagent tool access via arg constraint
allowed-tools: "file-read agent(allowedTools=file-read)"
# Allow callback refs (programmatic tools with generated names)
allowed-tools: "file-read callback-*"
# Trust: auto-approve all internal tools, prompt for others
trust:
rules: "#meta-internal($policy=allow) *"
The $ prefix also escapes into the internal tool namespace. The context-manager skill's operations are always allowed unless explicitly denied via arg constraints. For example, context-manager(op=finish,$deny) denies the finish operation. Normal tools follow the opposite rule: if allowed-tools is set, only explicitly listed tools are allowed.
Visibility
Visibility is controlled by metadata.visibility and determines how a tool appears in listings and to agents.
metadata.visibility |
In skill-list (default) |
Available to agents | Callable by code |
|---|---|---|---|
| (absent) | Yes | Yes | Yes |
hidden |
No | No | Yes |
By default, skill-list returns tools that are not hidden. With includeHidden: true, it returns all tools including hidden ones. Tools without params accept no arguments but are still visible — only visibility: hidden controls listing.
Tags
Tools can declare user-defined tags for categorization and querying:
metadata:
tags: [api, destructive, slow]
Tags live inside metadata (alongside role, params, etc.). In code tools, they go in the metadata object of the exported frontmatter. Tags are normalized when loaded: lowercased, non-alphanumeric characters replaced with hyphens, consecutive hyphens collapsed, leading/trailing hyphens stripped, duplicates removed.
The skill-list tool accepts a tags argument (array of strings, AND logic) to filter results. The skill-describe tool includes tags in its output.
Tags have no effect on tool behavior — they are purely for organization and querying.
Reserved Tags
All library skills use meta- prefixed tags. These are reserved — user skills should not use the meta- prefix.
Ownership:
| Tag | Meaning |
|---|---|
meta-internal |
Shipped with the framework in skills/. Every library skill has this tag. |
Role (how the skill integrates with the framework):
| Tag | Meaning |
|---|---|
meta-middleware |
Participates in the middleware pipeline via ctx.manager.next(). |
meta-metadata |
Implements a frontmatter metadata: key. |
meta-directive-inline |
Implements an inline remark directive (:name[arg]). |
meta-directive-block |
Implements a block remark directive (:::name{attrs}). |
Usage context (where/how the skill is invoked):
| Tag | Meaning |
|---|---|
meta-runnable |
Can be run as a standalone command. |
meta-action |
Generic toolbar action. |
meta-action-text |
Toolbar action for markdown skills. |
meta-action-code |
Toolbar action for code skills. |
Tool References
Anywhere the spec says "tool reference" — $inherit, metadata.delegate, ctx.manager.invoke(), CLI arguments, :skill[], :tool[] directives — the string is resolved through findTool():
- URI (
file://,mcp://,inline://) → matched bysrcin programmatic tools. Forfile://URIs, converted to a filesystem path and loaded directly.inline://is used by thedelegatemiddleware for inline code delegates registered as programmatic tools. - Path (any string that is not a valid bare name — contains
/,\, uppercase, underscores, or exceeds 64 characters) → resolved to an absolute path, loaded directly. Bypasses the search path entirely. - Bare name (1–64 lowercase alphanumeric characters and hyphens, no leading/trailing/consecutive hyphens) → searched by name across paths. Programmatic tools first, then disk.