Skip to content

feat: observability via kit bootstrap - #296

Merged
vreshch merged 1 commit into
masterfrom
feature/obs-v1
Sep 13, 2026
Merged

vreshch merged 1 commit into
masterfrom
feature/obs-v1

Conversation

@vreshch

@vreshch vreshch commented Sep 13, 2026

Copy link
Copy Markdown
Member

Goal

Give the CLI and its local daemon the estate's observability, without breaking a single byte of stdout.

Dependency decision: a real one

@agentage/observability (+ its @opentelemetry/api peer, listed so one copy is hoisted) is a plain dependency here - agentage is installed globally, once (npm install -g @agentage/cli), so the kit's ~41 packages are a one-time cost, not a per-invocation one. That is the opposite call from @agentage/server-memory (agentage/server-memory#38), which is run through npx per MCP client start and takes the kit optionally instead. src/package-guard.test.ts, which pins the runtime dependency set exactly, is updated with that rationale.

Change

Both bins become loaders: the kit instruments node:http, fetch and MCP tool registration through module hooks, and a hook only sees modules imported after it registers - so nothing may be statically imported ahead of the bootstrap.

  • src/cli.ts - node-version guard, startObservability('agentage-cli'), then dynamically imports src/program.ts (the commander wiring, moved verbatim).
  • src/daemon-entry.ts - same shape; the daemon itself moved to src/daemon/entry.ts and exports boot() instead of self-invoking. The bin path is unchanged (dist/daemon-entry.js), which is what spawnDaemon resolves.
  • src/observability.ts - the guarded bootstrap. Wrapped in try/catch even though the kit is a dependency: a half-installed global must degrade to a working CLI.
  • spawnDaemon pins OTEL_SERVICE_NAME for the child, so the daemon reports as agentage-daemon and never inherits the name we defaulted into the CLI's env. A name the user set is passed through to both.

Inert until OTEL_EXPORTER_OTLP_ENDPOINT names a collector. What you get then: HTTP client/server spans, one kind: "tool" wide event per memory__* call served by the daemon (the embedded @agentage/server-memory surface), one error shape, crash capture.

stdout purity

@agentage/observability 1.0.0 announces an enabled tracer with a console.log (dist/internal/tracer.js:88) - which would land inside --json output or on the daemon's wire. startObservability diverts process.stdout.write to stderr for the duration of the bootstrap import and restores it in a finally; a unit test asserts both the silence and the restore. The real fix belongs in the kit (that line should be a log.info); drop the workaround when it ships.

Runtime evidence, built bin, collector configured (OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:9):

$ agentage --version          -> stdout "0.32.0", stderr empty
$ agentage --no-daemon status --json  -> stdout parses as JSON, stderr empty
$ node dist/daemon-entry.js    (foreground, then a tools/call over 127.0.0.1:PORT/mcp)
  stdout: (empty)
  stderr: otel: tracing enabled - service agentage-daemon -> http://127.0.0.1:9
          {"service":"agentage-daemon","trace_id":"88e5…","span_id":"46f0…",
           "kind":"tool","tool":"memory__list","duration_ms":8,"status":"ok","msg":"tool_call"}

Verified

  • npm run verify green: type-check (+ e2e) + lint + format:check + 462 tests / 46 files + build.
  • npm run test:e2e -- --grep @offline green: 35 passed, including the daemon /mcp tier that spawns dist/daemon-entry.js - the restructured entry path.

Notes, not fixed here

  • The daemon is spawned with stdio: 'ignore', so its stderr (kit lines included) is discarded in normal use; telemetry reaches a collector over OTLP regardless. Pre-existing, unchanged by this PR.
  • The daemon serves raw node:http, so it gets HTTP spans and tool events but no request-log line (the kit's request log wires itself onto express listen). Its /api/health is still the daemon's own envelope, not @agentage/observability/health.
  • CLI HTTP calls to the backend send User-Agent: agentage-cli/<v> + X-Agentage-*-Version but no x-client-type, so the edge classifies them by UA (service). The e2e suite does tag x-client-type: test (e2e/client-type.ts). Flagging only - out of scope here.

Not in this PR

No publish. npm publish is workflow-only in this estate; the release rides the Friday train and wants dogfooding first.

The CLI and its daemon start @agentage/observability before anything else loads, so
node:http, fetch and the embedded server-memory tools are instrumented. Both bins
become loaders (program.ts / daemon/entry.ts hold what they used to), because the
kit's module hooks only see modules imported after they register.

A real dependency here, unlike server-memory: `agentage` is installed globally once.
Inert without OTEL_EXPORTER_OTLP_ENDPOINT, stderr-only, and stdout is guarded while
the kit boots so its tracer banner cannot land in --json output.
@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

🎉 PR Validation ✅ PASSED

Commit: 53f057f0582a1884c7b8840341f2c95b17877a09
Branch: feature/obs-v1

Checks:

  • ✅ Release guard (no version/changelog changes)
  • ✅ Dependencies installed
  • ✅ Type check passed
  • ✅ Linting passed
  • ✅ Format check passed
  • ✅ Tests + coverage passed
  • ✅ Build successful

Ready to merge! ✨


🔗 View workflow run
⏰ Generated at: 2026-09-13T22:28:35.410Z

@vreshch
vreshch marked this pull request as ready for review September 13, 2026 22:29
@vreshch
vreshch merged commit 871531a into master Sep 13, 2026
3 checks passed
@vreshch
vreshch deleted the feature/obs-v1 branch September 13, 2026 22:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant