Imported from OliWoods-Org/agent-runtime (
AGENTS.md). Install upstream withnpx skills add OliWoods-Org/agent-runtime. Copyright stays with the author.
AGENTS.md — @oliwoods-org/agent-runtime
Governing ruleset: ~/Documents/OliwoodsHQ/Rules/GLOBAL-LLM-RULES.md.
What this is
The shared agentic tool-call loop for MAMA and CoFounder. It owns the tool contract and the execution loop, and nothing else. Published to GitHub Packages (npm.pkg.github.com) on version tags via .github/workflows/publish.yml.
Architecture
src/tool-loop.ts is the whole product; src/ports.ts defines the injection seams. Everything the loop needs from a host arrives as a port — LlmPort (required), plus optional LoggerPort, HooksPort, OptimizerPort. The loop imports no product code and must never start to. That is what lets MAMA inject its local-first gateway and CoFounder inject its 5-provider failover engine without either learning about the other.
Stack
TypeScript ESM, Node >= 20, tsc build to dist/, vitest. Ships .js and .d.ts — CoFounder's orchestrator is plain JS and needs both.
Rules an agent will otherwise get wrong
- Zero runtime dependencies is a hard constraint, not minimalism. MAMA runs zod 3, CoFounder runs zod 4, and their types are incompatible. Do not add zod — or any dep — to
dependencies. Structured output takes an injected validator, never a schema. - Nothing product-shaped belongs here: no agent roles, team configs, dispatchers, schedulers, persistence, MCP clients, or model selection. If a change needs to know what an agent is, it belongs in the product.
tests/tool-loop.test.tsis a conformance suite (17 tests), not incidental coverage. Both hosts run it against their ownLlmPortadapter; that is the only thing stopping the two loops from silently re-diverging. Adding loop behaviour without a conformance test defeats the point of the package.denyToolsalways beatsallowTools. An operator revoking a tool cannot be overridden by a caller that allowlists it. Preserve that ordering.- Callers must branch on
result.stopReason(final|structured|max_turns|budget|blocked|error). Never assume success."structured"is NOT proof of success — it is returned both when validation succeeded and when retries were exhausted. In the failure casefinalTextholds the validation error andresult.structuredis undefined. Checkstructured, not the stop reason. - A
pre-actionhook denial cancels the entire batch, not just the offending call. - Hooks default to permit-all (
allowAllHooks) and the logger defaults to silent. A host that injects noHooksPortgets an ungated loop —denyToolsis the only guard that needs no host support. Do not mistake the default for a safety net. - NodeNext ESM: relative imports carry the
.jsextension even from.tssources (import "./types.js"), andverbatimModuleSyntaxrequiresimport typefor type-only imports. isAuthErroris a string heuristic that matches the bare substringauthamong others, so it will false-positive on unrelated errors. Auth recovery runs at most once per loop, shared across concurrent failures.
Checks
npm test # vitest, the conformance suite
npm run lint # tsc --noEmit
npm run build # emits dist/*.js + dist/*.d.ts
prepublishOnly runs build + test, so a broken suite cannot publish.
⚠️ Tests do not run on push or PR. publish.yml is the only workflow and it
triggers on v* tags — a branch push runs nothing. Run npm test yourself; CI
will not catch it for you. Also note dist/ is gitignored but present on disk and
goes stale, so it is easy to grep into and "fix" the wrong file.