A library that gets installed into someone else's backend is judged partly on what its own install pulls down. Three changes tighten that, and two questions turned out to need no change at all. The line this draws is what ships. Every dependency that stays inside the repository is now pinned to the exact version the lockfile already resolved: all 27 devDependencies across the four workspaces, plus the private release bot's own zod. The lockfile change is 28 spec lines with no resolution moved, no entry added or removed, and all 61 platform-specific entries intact. A new root .npmrc sets save-exact=true so the next one arrives pinned rather than as a range nobody notices. What a consumer installs keeps its range: `dependencies` in packages/core, the only published runtime block here, every `peerDependencies`, and otel's core compatibility range. An exact version in those blocks overrides the consumer's own resolution and stops their `npm audit fix` from reaching a patched build until we cut a release, which is a worse trade than the install-time drift it prevents. save-exact will still pin a new dependency added to one of them, so the package job now asserts all three published manifests and fails when one is left exact, next to the assertion that already covers otel's range. min-release-age is deliberately not set. It first shipped in npm 11.10.0, and the CI matrix runs Node 20 and 22, which bundle npm 10.8.2 and 10.9.8. Those two accept the key, echo it back from `npm config get`, and never act on it, so a quarantine window would hold on one leg of the matrix while silently doing nothing on the other two. It also has no effect on `npm ci`, which installs the lockfile verbatim. Every `npm ci` that installs this repository's own tree now passes --ignore-scripts, so a compromised dependency cannot run an install hook on a runner that holds release credentials. Only three dependencies declare a postinstall at all, and the full lint, test, build, scaffold, and example suites pass on a tree installed that way. The `npm install` calls that stand in for a real consumer are deliberately left alone: their job is to reproduce what a user's install actually does, and hardening them would make the smoke tests prove less than they claim. A new scheduled Supply chain audit workflow asks two questions about the dependencies that reach a consumer, an hour before the weekly release bot so a finding lands before the release PR is drafted. --omit=dev is the point of running it separately: auditing the full tree reports eighteen advisories, every one of them in test and fixture tooling that never ships, and a permanently red job is one nobody reads. The two steps fail differently. `npm audit` reports into the run summary, because a new upstream advisory is not this repository misbehaving and may have no fixed version yet. `npm audit signatures` fails, because it compares served bytes against the registry's own signature, so a mismatch is tampering rather than churn; all 27 production packages verify today. Two things needed no change. npm provenance is already live: publish.yml publishes through npm trusted publishing with id-token: write, which attaches SLSA attestations on its own, and every version since core 1.16.0, otel 0.1.2, and create-oma-app 0.8.1 carries one. And .npmrc left .gitignore in #198 because it only restated the default registry; it comes back tracked, with the ignore rule narrowed so a contributor's own nested .npmrc stays out.
14 KiB
AGENTS.md
This file is the canonical working agreement for AI agents editing this repository. Keep durable execution rules, high-risk invariants, and verification expectations here. Keep conceptual architecture and subsystem behavior in packages/core/README.md and docs/ so this file stays concise and accurate.
Repository map
This is a private npm-workspaces root. Run the commands below from the repository root unless a workspace-specific command is shown.
| Workspace | Purpose | Main paths |
|---|---|---|
@open-multi-agent/core |
Multi-agent orchestration framework and oma CLI |
packages/core/src/, packages/core/tests/, packages/core/examples/ |
@open-multi-agent/otel |
Optional OpenTelemetry adapter; versioned independently from core | packages/otel/src/, packages/otel/tests/ |
create-oma-app |
Published scaffolder and starter templates | packages/create-oma-app/src/, packages/create-oma-app/templates/, packages/create-oma-app/tests/ |
@open-multi-agent/release-bot |
Private OMA-powered release planning and deterministic publication automation; never published | packages/release-bot/src/, packages/release-bot/tests/, .github/workflows/release-bot.yml, .github/workflows/publish.yml |
Root-level README.md, docs/, .github/, and scripts/ apply across workspaces. Paths in this file are repository-relative; do not assume an unprefixed src/ or tests/ means the workspace you intend.
Commands
npm run build # Compile every workspace
npm run lint # Type-check every workspace
npm test # Run unit tests in every workspace (no API keys required)
npm run test:scaffold # End-to-end create-oma-app scaffold smoke test
npm run test:example-catalog # Validate example catalog metadata and coverage
npm run dev # Watch-mode compilation for @open-multi-agent/core
npm run test:watch # Core Vitest watch mode
npm run test:coverage # Core coverage suite
npm run test:e2e # Core provider E2E; requires real API keys
node packages/core/dist/cli/oma.js help # After build; `oma` when installed from npm
Examples and core E2E tests may require real provider credentials. Unit tests mock provider SDKs and external processes and should run without network access or API keys.
Working rules
- Use strict TypeScript and ESM imports with
.jsextensions:import { X } from './foo.js'even when the source file is TypeScript. There is no eslint/prettier configuration; match nearby style. - Change source files, tests, templates, or docs rather than generated
dist/output. - Add or update tests for behavior changes. Update user-facing docs and examples when public behavior changes, or state why they are not applicable.
- Keep dependency ownership explicit. Core must remain importable and runnable without optional integrations.
- Pin what stays in the repository; keep ranges on what a consumer installs. Every
devDependenciesentry and the private release bot's dependencies are pinned exactly, and the root.npmrcsetssave-exact=trueso a new one arrives pinned. The published blocks stay semver ranges — core'sdependencies, everypeerDependencies, and otel's@open-multi-agent/core— because an exact version there overrides a consumer's own resolution and blocks theirnpm audit fixuntil we cut a release.save-exactwill pin a new dependency added to one of those blocks, so widen it by hand; thepackagejob in.github/workflows/ci.ymlfails when one is left exact. - Add optional provider SDKs as peer dependencies and load them lazily with dynamic
import(). Do not maintain a fixed dependency or adapter count in documentation. - OpenTelemetry APIs, SDKs, semantic-convention packages, and exporters belong in
@open-multi-agent/otel, never in the core root import. The application owns its tracer/provider lifecycle unless an API explicitly says otherwise. - Treat
docs/as the source of truth for subsystem behavior. Keep this file to rules and concise invariants; link to docs instead of copying long explanations. - The package READMEs are landing pages, not guides. A new feature earns at most one short paragraph plus a link to its
docs/page inpackages/core/README.md; anything that needs more belongs indocs/. Updatepackages/core/README_zh.mdin the same change or the translation drifts. Per-contributor credits live inCONTRIBUTORS.md, not in a README. - Follow conventional commits when a commit is requested. Reference a PR or issue when one exists. The full contribution flow is in
.github/CONTRIBUTING.md. - Publishing spans three packages with independent version tracks, a fixed publish order, and template pins that must move with the core version. Do not infer any of that from this file:
.github/RELEASING.mdis the source of truth. - Managed Git worktrees share repository metadata. Never repair access by changing
.gitownership or permissions. Use the current tool's normal Git and approval flow. - Keep worktree dependencies isolated. Run
npm ciin each worktree and never symlinknode_modulesfrom another checkout.
Validation by change type
Always inspect the focused diff and run git diff --check. Run the smallest relevant checks first, then broaden in proportion to the change.
- Documentation-only:
git diff --check; tests are not required unless commands, generated artifacts, or executable examples changed. - Core code: relevant core tests, then
npm run lint -w @open-multi-agent/coreandnpm run test -w @open-multi-agent/core. Runnpm run build -w @open-multi-agent/corewhen public entry points, declarations, package output, or CLI output may be affected. - OpenTelemetry adapter: relevant tests, then
npm run lint -w @open-multi-agent/otel,npm run test -w @open-multi-agent/otel, and build when package output or public types may be affected. - Scaffolder or templates: relevant tests, then
npm run lint -w create-oma-app,npm run test -w create-oma-app, andnpm run typecheck:template -w create-oma-app. Runnpm run test:scaffold -w create-oma-appwhen generated-project behavior changes. - Examples or catalog metadata:
npm run test:example-catalogandnpm run lint -w @open-multi-agent/core, which type-checkspackages/core/examples/alongsidesrc/viapackages/core/tsconfig.lint.json; add a runnable example smoke test when executable behavior changes. That config excludesexamples/fixtures/and the self-contained example projects that carry their ownpackage.json/tsconfig.json; add a new example of that shape to itsexcludelist, since core lint cannot resolve a package-name import of@open-multi-agent/corebeforedistexists. - Cross-workspace or dependency changes:
npm run lint,npm test, andnpm run build; add the package/import/template smoke checks relevant to the changed surface. - Provider E2E: run only when the changed surface requires real-provider verification and the necessary credentials are safely available. Never expose credential values.
Before finishing, report every command run and its outcome. If a relevant check was skipped or could not run, state the reason and residual risk. CI remains the source of truth for the complete Node 20/22/24 pre-merge matrix.
Architecture entry points
packages/core/src/ is organized one directory per subsystem (orchestrator/, agent/, team/, task/, tool/, llm/, memory/, observability/, eval/, dashboard/, cli/); run ls packages/core/src/ to locate code, and use the linked docs below for behavior and contracts. Inside orchestrator/, orchestrator.ts is the entry point that the peer modules in that directory hang off.
The published surface is packages/core/src/index.ts plus the dedicated subpath entry points declared under exports in packages/core/package.json: /observability, /observability/file, /eval, /eval/file, /mcp, /ai-sdk, /acp, /process, and /classifiers. Keep that declaration and the backing source files in sync when adding or renaming one.
OpenMultiAgent exposes three primary modes: runAgent() for a one-shot agent, runTeam() for coordinator-generated task DAGs, and runTasks() for explicit dependency pipelines. See the core package README for the conceptual architecture.
Non-obvious invariants
These constraints span multiple files and can cause behavioral or compatibility bugs when missed:
- Tool errors are values: tool failures are returned as
ToolResultwithisError: true; they do not throw through the runner. LLM API failures propagate. Task failures cascade to dependents while independent tasks may continue. - Built-in tools are default-deny: a built-in is granted only through
AgentConfig.tools,toolPreset, orOrchestratorConfig.defaultToolPreset. Registered custom/runtime tools are granted by registration but still honordisallowedTools. Ungranted calls return an error rather than executing. See tool configuration. - Per-call gates run below grants:
onToolCallruns after Zod validation and before execution. Denial returns an errorToolResult; throwing or invalid gates fail closed. Ungranted tools never reach the gate.AgentConfig.onToolCalloverrides the orchestrator default. The optional shell classifier is exported from/classifiers. - Delegation is orchestration-only and separately granted:
delegate_to_agentexists only inrunTeam()andrunTasks()workers and must be explicitly granted. StandalonerunAgent()and the simple-goal short circuit do not register it. Self-delegation, cycles, unknown targets, excess depth, and unavailable pool capacity are rejected; delegated usage counts against the parent budget. - Filesystem tools are sandboxed;
bashis not: filesystem built-ins resolve paths and symlinks withinAgentConfig.cwdorOrchestratorConfig.defaultCwd, defaulting to<cwd>/.agent-workspace.nulldisables that sandbox andprocess.cwd()widens it. Shell execution has no equivalent filesystem boundary. - Reasoning is dropped unless opted in: provider-native reasoning blocks that the target adapter cannot echo are discarded unless
preserveReasoningAsTextis enabled. Inline<thinking>text is never reconstructed into a signed reasoning block. See context management. - Native tool calls win: the local-model text extractor runs only when a server emits no native tool calls.
- External backends replace the LLM runner: process and ACP backends perform their own work in
cwd; the runner tool loop, sandbox, and context strategy do not apply, while queue, scheduler, memory, and budget behavior remain backend-agnostic. ACP permissions default to auto-approve and its cumulative context usage is recorded as per-turn deltas when updates exist. See external agents. - Telemetry is not execution state: losing telemetry must not roll back a durable run. Deleting traces must not delete checkpoints, shared memory, or remotely exported OpenTelemetry data. Observability delivery/export failures do not become agent, task, or run failures. See observability.
- Evaluation observes results: offline evaluation is separate; online sampling, scoring, and persistence are best-effort and isolated from the business response. Scorer failures become
scorer_errorand are excluded from score aggregates rather than converted to zero. See evaluation. - Secrets and PII are redacted best-effort: traces, shell output, and dashboard payloads pass through redaction, but callers must still avoid deliberately persisting or logging secrets.
Subsystem documentation
This table is a curated selection, not a complete index. docs/ contains more files than are listed here. Run ls docs/ for the full set before concluding that a topic is undocumented.
| Topic | Source of truth |
|---|---|
| Context strategies and reasoning round-tripping | docs/context-management.md |
| Tool grants, presets, sandbox, delegation, MCP, and gates | docs/tool-configuration.md |
| Providers, environment variables, local servers, and AI SDK | docs/providers.md |
| Shared memory and custom stores | docs/shared-memory.md |
| Checkpoint and restore | docs/checkpoint.md |
| Run event journal, lineage, and the model-visible boundary | docs/run-journal.md |
| Tracing, stores, progress, Run Viewer, privacy, and OpenTelemetry | docs/observability.md |
| Evaluation, scorers, stores, reports, sampling, and gates | docs/evaluation.md |
| CLI commands and JSON schemas | docs/cli.md |
| Process and ACP backends | docs/external-agents.md |
| Routing, scheduling, consensus, recovery, and replay | model-routing, execution-routing, task-scheduling, consensus, adaptive-recovery, plan-replay |
Adding an LLM adapter
Implement LLMAdapter.chat() and LLMAdapter.stream(), add the provider to SupportedProvider, and register it in packages/core/src/llm/adapter.ts through dynamic import() so unused SDKs never resolve. OpenAI-compatible providers should accept baseURL and reuse openai-common.ts. Add focused adapter tests and update docs/providers.md without introducing a hard-coded provider count.