@vauban-org/agent-sdk 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CONTRACT.md +595 -7
  2. package/dist/index.d.ts +4 -1
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/orchestration/ooda/agent.d.ts.map +1 -1
  7. package/dist/orchestration/ooda/agent.js +36 -0
  8. package/dist/orchestration/ooda/agent.js.map +1 -1
  9. package/dist/orchestration/ooda/types.d.ts +11 -0
  10. package/dist/orchestration/ooda/types.d.ts.map +1 -1
  11. package/dist/skills/_secrets.d.ts +16 -0
  12. package/dist/skills/_secrets.d.ts.map +1 -0
  13. package/dist/skills/_secrets.js +20 -0
  14. package/dist/skills/_secrets.js.map +1 -0
  15. package/dist/skills/alpaca-quote.d.ts +2 -2
  16. package/dist/skills/alpaca-quote.d.ts.map +1 -1
  17. package/dist/skills/alpaca-quote.js +51 -20
  18. package/dist/skills/alpaca-quote.js.map +1 -1
  19. package/dist/skills/send-email.d.ts +2 -2
  20. package/dist/skills/send-email.d.ts.map +1 -1
  21. package/dist/skills/send-email.js +1 -12
  22. package/dist/skills/send-email.js.map +1 -1
  23. package/dist/skills/slack-notify.d.ts.map +1 -1
  24. package/dist/skills/slack-notify.js +52 -21
  25. package/dist/skills/slack-notify.js.map +1 -1
  26. package/dist/skills/telegram-notify.d.ts.map +1 -1
  27. package/dist/skills/telegram-notify.js +48 -19
  28. package/dist/skills/telegram-notify.js.map +1 -1
  29. package/dist/skills/web-search.d.ts.map +1 -1
  30. package/dist/skills/web-search.js +85 -40
  31. package/dist/skills/web-search.js.map +1 -1
  32. package/dist/telemetry/bus.d.ts +54 -0
  33. package/dist/telemetry/bus.d.ts.map +1 -0
  34. package/dist/telemetry/bus.js +159 -0
  35. package/dist/telemetry/bus.js.map +1 -0
  36. package/dist/telemetry/index.d.ts +35 -0
  37. package/dist/telemetry/index.d.ts.map +1 -0
  38. package/dist/telemetry/index.js +30 -0
  39. package/dist/telemetry/index.js.map +1 -0
  40. package/dist/telemetry/port.d.ts +121 -0
  41. package/dist/telemetry/port.d.ts.map +1 -0
  42. package/dist/telemetry/port.js +48 -0
  43. package/dist/telemetry/port.js.map +1 -0
  44. package/dist/telemetry/sinks/otlp.d.ts +45 -0
  45. package/dist/telemetry/sinks/otlp.d.ts.map +1 -0
  46. package/dist/telemetry/sinks/otlp.js +195 -0
  47. package/dist/telemetry/sinks/otlp.js.map +1 -0
  48. package/dist/telemetry/sinks/sqlite.d.ts +32 -0
  49. package/dist/telemetry/sinks/sqlite.d.ts.map +1 -0
  50. package/dist/telemetry/sinks/sqlite.js +170 -0
  51. package/dist/telemetry/sinks/sqlite.js.map +1 -0
  52. package/dist/telemetry/sinks/stdout.d.ts +22 -0
  53. package/dist/telemetry/sinks/stdout.d.ts.map +1 -0
  54. package/dist/telemetry/sinks/stdout.js +38 -0
  55. package/dist/telemetry/sinks/stdout.js.map +1 -0
  56. package/docs/telemetry/migration.md +155 -0
  57. package/docs/telemetry/overview.md +154 -0
  58. package/docs/telemetry/privacy.md +127 -0
  59. package/docs/telemetry/sinks/cc.md +155 -0
  60. package/docs/telemetry/sinks/otlp.md +146 -0
  61. package/docs/telemetry/sinks/sqlite.md +126 -0
  62. package/docs/telemetry/sinks/stdout.md +82 -0
  63. package/package.json +18 -19
  64. package/src/index.ts +30 -1
  65. package/src/orchestration/ooda/agent.ts +50 -0
  66. package/src/orchestration/ooda/types.ts +12 -0
  67. package/src/skills/_secrets.ts +25 -0
  68. package/src/skills/alpaca-quote.ts +68 -23
  69. package/src/skills/send-email.ts +1 -12
  70. package/src/skills/slack-notify.ts +73 -30
  71. package/src/skills/telegram-notify.ts +70 -24
  72. package/src/skills/web-search.ts +132 -50
  73. package/src/telemetry/bus.test.ts +231 -0
  74. package/src/telemetry/bus.ts +241 -0
  75. package/src/telemetry/index.ts +49 -0
  76. package/src/telemetry/port.ts +160 -0
  77. package/src/telemetry/sinks/otlp.test.ts +146 -0
  78. package/src/telemetry/sinks/otlp.ts +250 -0
  79. package/src/telemetry/sinks/sqlite.test.ts +121 -0
  80. package/src/telemetry/sinks/sqlite.ts +260 -0
  81. package/src/telemetry/sinks/stdout.test.ts +109 -0
  82. package/src/telemetry/sinks/stdout.ts +59 -0
@@ -0,0 +1,127 @@
1
+ # Privacy & Sovereignty
2
+
3
+ Per [ADR-ECO-039 §5](https://github.com/vauban-org/vauban-gouvernance/blob/main/governance/decisions/ADR-ECO-039-sdk-telemetry-port.md),
4
+ these are non-negotiable guarantees of the telemetry pipeline.
5
+
6
+ ## Six guarantees
7
+
8
+ ### 1. Zero phone-home by default
9
+
10
+ ```ts
11
+ createOODAAgent({
12
+ agentId: "my-agent",
13
+ // no `telemetry` field — sink is NOOP_TELEMETRY_SINK
14
+ });
15
+ ```
16
+
17
+ No network calls. No DNS lookups. No "anonymous usage statistics". The
18
+ sink is `NOOP_TELEMETRY_SINK` and silently no-ops.
19
+
20
+ ### 2. Local sink always available
21
+
22
+ When you opt in to ANY sink via `createTelemetryBus`, you can also
23
+ include `localSqliteTelemetrySink()` — there is no scenario in which
24
+ opting in to a remote sink requires giving up the local mirror.
25
+
26
+ The local file is the *exit plan* for every external dependency
27
+ ([Sovereignty principle](https://github.com/anthropics/claude-code/blob/main/docs/sovereignty.md)).
28
+
29
+ ### 3. PII never crosses sinks unless opted in
30
+
31
+ OODA `step` events carry an optional `metadata` field that may contain
32
+ raw prompts, completions, tool inputs, or addresses. By default the SDK
33
+ **redacts** known-sensitive fields and hashes the rest before passing to
34
+ sinks.
35
+
36
+ To opt in to raw payloads (e.g. for debugging in a private deployment) :
37
+
38
+ ```ts
39
+ createOODAAgent({
40
+ telemetry: createTelemetryBus({
41
+ sinks: [...],
42
+ includePayloads: true, // ← explicit opt-in
43
+ }),
44
+ });
45
+ ```
46
+
47
+ A startup warning is logged to make this choice visible.
48
+
49
+ ### 4. Tenant isolation
50
+
51
+ The CC backend enforces row-level security : every `agent_run` and
52
+ `telemetry_run_step` row carries the `tenant_id` resolved server-side
53
+ from the API key. **No cross-tenant SELECTs are possible** — even with
54
+ a compromised key from another tenant, queries are scoped by RLS policy
55
+ to that key's tenant only.
56
+
57
+ Verified by the test suite (`tests/routes/telemetry-ingest.test.ts`,
58
+ test "tenant_id from API key").
59
+
60
+ ### 5. Audit log of active sinks
61
+
62
+ Upcoming in v1.4 :
63
+
64
+ ```bash
65
+ vauban-agent telemetry status
66
+
67
+ Active sinks:
68
+ - stdout (1 of 1 healthy)
69
+ - sqlite ~/.vauban/runs.db (1 of 1 healthy, 1.2 MB)
70
+ - command-center https://command.vauban.tech (1 of 1 healthy, last successful POST 12s ago)
71
+ ```
72
+
73
+ Allows immediate auditing of "where my data is being sent right now".
74
+
75
+ ### 6. Sink failures never block the agent
76
+
77
+ ```ts
78
+ const bus = createTelemetryBus({
79
+ sinks: [
80
+ networkSink_thatThrows,
81
+ sqliteSink_thatWorks,
82
+ ],
83
+ logger: pinoLogger,
84
+ });
85
+
86
+ // runCycle() succeeds. SQLite captures. Network sink failure is logged.
87
+ await agent.triggerCycle();
88
+ ```
89
+
90
+ This is enforced at the bus level via per-sink try/catch + log. The
91
+ host agent's `runCycle` is untouched by sink failures.
92
+
93
+ ## Things we do NOT do
94
+
95
+ - We do NOT collect anonymous telemetry about your SDK usage. Period.
96
+ - We do NOT phone home to check for SDK updates.
97
+ - We do NOT silently retry telemetry after the user revokes their API
98
+ key — 401s are not retried.
99
+ - We do NOT keep deleted data. The cron at `command-center-prod`
100
+ namespace deletes runs older than the tenant's retention window (7
101
+ days free, 30 days Team, 1 year Pro, indefinite Sovereign).
102
+
103
+ ## Things we DO do (transparently)
104
+
105
+ - We log a warning at startup if you configure a remote sink AND opt
106
+ out of the local sink. Sovereignty preserved by signal.
107
+ - We embed the SDK version in the User-Agent of every POST. This lets
108
+ the CC backend tell users when they're running a SDK version below
109
+ the minimum supported (e.g. critical security patch).
110
+ - We record `last_used_at` on the API key server-side. Visible to the
111
+ tenant only.
112
+
113
+ ## How to verify
114
+
115
+ - Run with `node --inspect` and check the network panel — only the
116
+ hosts you configured should appear.
117
+ - Run `tcpdump` filtered on your agent's PID — confirm zero traffic
118
+ outside the configured sinks.
119
+ - Read the source — it's MIT, 700 LOC in `packages/agent-sdk/src/telemetry/`.
120
+
121
+ ## How to report a violation
122
+
123
+ If you observe behavior that violates any of these six guarantees —
124
+ files transmitted you didn't authorize, fields appearing in dashboards
125
+ you redacted, etc. — please open a GitHub security advisory at
126
+ [github.com/vauban-org/command-center/security](https://github.com/vauban-org/command-center/security/advisories/new).
127
+ We treat this as P0.
@@ -0,0 +1,155 @@
1
+ # `commandCenterTelemetrySink`
2
+
3
+ Pushes agent runs to **Vauban Command Center** — free → sovereign tiers,
4
+ with optional Vauban Claim Algebra (VPSF) signatures on Pro+.
5
+
6
+ Shipped as a separate npm package to keep the core SDK MIT and zero-dep.
7
+
8
+ ```bash
9
+ pnpm add @vauban-org/cc-telemetry
10
+ ```
11
+
12
+ ## Setup walkthrough (free tier)
13
+
14
+ ### 1. Sign up at `command.vauban.tech`
15
+
16
+ GitHub OAuth, takes 30 seconds. Free tier auto-issued. No credit card.
17
+
18
+ ### 2. Copy your API key
19
+
20
+ Settings → API Keys → "Generate". Format `vauban_pk_…` (free tier prefix).
21
+
22
+ !!! warning
23
+ The key is shown **once**. Store securely. Compromised keys can be
24
+ revoked from the same UI ; new key issuance is unlimited.
25
+
26
+ ### 3. Configure the sink
27
+
28
+ ```ts
29
+ import {
30
+ createOODAAgent,
31
+ createTelemetryBus,
32
+ localSqliteTelemetrySink,
33
+ } from "@vauban-org/agent-sdk";
34
+ import { commandCenterTelemetrySink } from "@vauban-org/cc-telemetry";
35
+
36
+ createOODAAgent({
37
+ agentId: "my-agent",
38
+ telemetry: createTelemetryBus({
39
+ sinks: [
40
+ localSqliteTelemetrySink(), // sovereign mirror
41
+ commandCenterTelemetrySink({
42
+ apiKey: process.env.VAUBAN_API_KEY!,
43
+ }),
44
+ ],
45
+ }),
46
+ });
47
+ ```
48
+
49
+ ### 4. Run a cycle, watch the dashboard
50
+
51
+ Every run shows up at `command.vauban.tech/runs` within 5 seconds. Filter
52
+ by agent, status, cost, time window. SSE-driven, no manual refresh needed.
53
+
54
+ ## Options
55
+
56
+ ```ts
57
+ commandCenterTelemetrySink({
58
+ apiKey: string; // REQUIRED — Bearer token from CC SaaS
59
+ baseUrl?: string; // default: "https://command.vauban.tech"
60
+ batchSize?: number; // events per HTTP batch (default 10, max 100)
61
+ batchMs?: number; // max wait before flush (default 5000)
62
+ timeoutMs?: number; // HTTP timeout (default 10_000)
63
+ fetchImpl?: typeof fetch; // test override
64
+ retry?: RetryConfig; // default: RETRY_TRANSIENT (3 attempts, exp + jitter)
65
+ logger?: { warn, error }; // default: console.warn / console.error
66
+ });
67
+ ```
68
+
69
+ ## Tiers
70
+
71
+ | Tier | Key prefix | Monthly runs | Retention | Extra |
72
+ |---|---|---|---|---|
73
+ | Free | `vauban_pk_*` | 1 000 | 7 days | Dashboard read-only |
74
+ | Team €49/mo | `vauban_team_*` | 10 000 | 30 days | + Slack/Discord HITL hooks |
75
+ | Pro €490/mo | `vauban_pro_*` | 100 000 | 1 year | + VPSF signed runs + STARK per-call |
76
+ | Sovereign €180k/yr | mTLS + air-gap | unlimited | indefinite | + TDX/SEV-SNP enclave + L3 anchor |
77
+
78
+ Free tier upgrade : no SDK code change. Just rotate the API key.
79
+
80
+ Per [Brain entry 7e18f454](https://command.vauban.tech/brain/7e18f454),
81
+ the free tier policy (1000/mo + 7d) is intentionally narrow enough to
82
+ funnel adoption toward paid tiers without being unusable for solo devs
83
+ exploring the platform.
84
+
85
+ ## Self-hosted Command Center
86
+
87
+ If you run the AGPL CC backend yourself ([ADR-ECO-014](https://github.com/vauban-org/vauban-gouvernance/blob/main/governance/decisions/ADR-ECO-014-cc-oss-release.md)),
88
+ override `baseUrl` :
89
+
90
+ ```ts
91
+ commandCenterTelemetrySink({
92
+ apiKey: "internal-key",
93
+ baseUrl: "https://cc.myorg.internal",
94
+ });
95
+ ```
96
+
97
+ No revenue for Vauban. Sovereignty respected.
98
+
99
+ ## Batching behavior
100
+
101
+ Events accumulate in an in-memory queue until **either** `batchSize` events
102
+ collected **or** `batchMs` elapses **or** a `finish` event arrives
103
+ (which always force-flushes — dashboards see results immediately).
104
+
105
+ Single-flight : while a batch POST is in flight, new events queue locally.
106
+ Subsequent `flush()` calls await the in-flight request before sending the
107
+ next batch.
108
+
109
+ ## Retry semantics
110
+
111
+ Errors are classified at the HTTP layer :
112
+
113
+ - **5xx + network errors** → retried per `retry` config (default 3 attempts,
114
+ exponential backoff with ±25 % jitter via SDK's `retry()` helper).
115
+ - **4xx** (401 unauthorized, 429 quota exceeded) → NOT retried. Logged
116
+ and dropped. Burning quota on retries would be net-negative.
117
+ - **Network timeout** → counted as 5xx (retryable).
118
+
119
+ After retry exhaustion, the batch is **dropped with a warning log**. The
120
+ sink itself never throws — failures are isolated by the SDK's TelemetryBus.
121
+
122
+ ## Security
123
+
124
+ - API key sent as `Authorization: Bearer <key>`. **Always use HTTPS.**
125
+ - Keys are stored hashed (SHA-256) server-side ; the plaintext is shown
126
+ only once at issuance.
127
+ - Revocation is immediate — the next POST returns 401.
128
+ - The server stamps every row with the resolved `tenant_id` from the
129
+ key. **No cross-tenant write authority possible**, even with a
130
+ compromised key from another tenant.
131
+ - Rate-limited at 60 req/min per key (Redis sliding window) plus the
132
+ monthly quota.
133
+
134
+ ## Failure mode runbook
135
+
136
+ | Symptom | Likely cause | Remediation |
137
+ |---|---|---|
138
+ | All POSTs return 401 | Key revoked or typo | Re-issue from /settings/api-keys |
139
+ | All POSTs return 429 immediately | Monthly quota exhausted | Upgrade tier or wait for month rollover |
140
+ | Sporadic 429 | Per-minute rate limit hit | Reduce agent cycle frequency or batch upstream |
141
+ | 502/503 transient | CC backend brief restart | Auto-retry; if persistent, check status page |
142
+ | Local SQLite has rows but dashboard empty | Sink not receiving events | Verify `VAUBAN_API_KEY` env var, check pod logs |
143
+
144
+ ## Observability of the sink itself
145
+
146
+ If you wrap with `createTelemetryBus`, inspect counters :
147
+
148
+ ```ts
149
+ const bus = createTelemetryBus({ sinks: [commandCenterTelemetrySink({...})] });
150
+ // later…
151
+ console.log(bus.counters);
152
+ // { dispatched: 142, sinkErrors: 0, dropped: 0 }
153
+ ```
154
+
155
+ Surface `sinkErrors` / `dropped` to Prometheus for alerting.
@@ -0,0 +1,146 @@
1
+ # `otlpTelemetrySink`
2
+
3
+ Pushes agent runs to any **OpenTelemetry Protocol over HTTP** receiver,
4
+ JSON-encoded. Works with Langfuse self-host, Grafana Tempo, Jaeger,
5
+ Honeycomb, Datadog, any OTLP-compliant collector — no vendor lock-in.
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import { otlpTelemetrySink } from "@vauban-org/agent-sdk";
11
+
12
+ otlpTelemetrySink({
13
+ url: "https://langfuse.vauban.tech/api/public/otel",
14
+ headers: { Authorization: "Basic <base64(pub:sec)>" },
15
+ });
16
+ ```
17
+
18
+ ## Options
19
+
20
+ ```ts
21
+ otlpTelemetrySink({
22
+ url: string; // base URL ; `/v1/traces` is appended
23
+ headers?: Record<string, string>; // static, includes auth
24
+ serviceName?: string; // OTel resource attribute (default: "vauban-agent-sdk")
25
+ fetchImpl?: typeof fetch; // test override
26
+ timeoutMs?: number; // request timeout (default: 5000)
27
+ });
28
+ ```
29
+
30
+ ## OTel semantic conventions
31
+
32
+ The sink maps SDK events to OpenTelemetry spans using **GenAI semantic
33
+ conventions** (`gen_ai.*` namespace) plus Vauban-specific extensions :
34
+
35
+ | Span attribute | Source | Convention |
36
+ |---|---|---|
37
+ | `service.name` | `serviceName` option | OTel resource |
38
+ | `gen_ai.system` | `event.provider` | OTel GenAI |
39
+ | `gen_ai.request.model` | `event.model` | OTel GenAI |
40
+ | `gen_ai.usage.input_tokens` | `event.totalInputTokens` | OTel GenAI |
41
+ | `gen_ai.usage.output_tokens` | `event.totalOutputTokens` | OTel GenAI |
42
+ | `vauban.run_id` | `event.runId` | Vauban ext |
43
+ | `vauban.agent.id` | `event.agentId` | Vauban ext |
44
+ | `vauban.agent.version` | `event.agentVersion` | Vauban ext |
45
+ | `vauban.run.status` | `event.status` | Vauban ext |
46
+ | `vauban.run.stop_reason` | `event.stopReason` | Vauban ext |
47
+ | `vauban.run.cost_usd` | `event.totalCostUsd` | Vauban ext |
48
+ | `vauban.run.steps` | step count | Vauban ext |
49
+
50
+ Step events emit child spans named `ooda.<kind>` with attributes
51
+ `vauban.step.{index,kind,status,tool_calls,cost_usd}` + tokens.
52
+
53
+ ## Span structure
54
+
55
+ ```
56
+ trace (single)
57
+ └── span: ooda.cycle.<agentId> [parent]
58
+ ├── span: ooda.observe [step 0]
59
+ ├── span: ooda.orient [step 1]
60
+ ├── span: ooda.decide [step 2]
61
+ └── span: ooda.act [step 3]
62
+ ```
63
+
64
+ trace_id is preserved from the caller — pass `event.traceId` to link to
65
+ upstream traces (e.g. an inbound HTTP request).
66
+
67
+ ## Wire format
68
+
69
+ JSON-encoded OTLP/HTTP (not protobuf). Reasoning : avoids pulling the
70
+ `@opentelemetry/exporter-trace-otlp-proto` dependency. JSON is a
71
+ first-class OTLP transport since v1.0.
72
+
73
+ Sample payload :
74
+
75
+ ```json
76
+ {
77
+ "resourceSpans": [{
78
+ "resource": {
79
+ "attributes": [
80
+ { "key": "service.name", "value": { "stringValue": "vauban-agent-sdk" } }
81
+ ]
82
+ },
83
+ "scopeSpans": [{
84
+ "scope": { "name": "vauban.agent.ooda", "version": "1.3.0" },
85
+ "spans": [{
86
+ "traceId": "abc...32 hex chars",
87
+ "spanId": "def...16 hex chars",
88
+ "name": "ooda.cycle.my-agent",
89
+ "startTimeUnixNano": "1747414800000000000",
90
+ "endTimeUnixNano": "1747414805012000000",
91
+ "kind": 1,
92
+ "attributes": [...],
93
+ "status": { "code": 1 }
94
+ }]
95
+ }]
96
+ }]
97
+ }
98
+ ```
99
+
100
+ ## Failure modes
101
+
102
+ - **5xx** : the bus retries (default RETRY_TRANSIENT). After exhaustion,
103
+ warned and dropped.
104
+ - **4xx** : NOT retried (auth/quota errors). Logged and dropped.
105
+ - **Timeout** : `AbortController` after `timeoutMs`. Counted as transient.
106
+
107
+ ## Performance
108
+
109
+ One HTTP request per `step` and `finish` event. Not batched at the sink
110
+ level. For high-volume agents, wrap with a batcher or use the CC sink
111
+ (which batches).
112
+
113
+ ## Common backends
114
+
115
+ ### Langfuse self-host (Vauban's choice)
116
+
117
+ ```ts
118
+ otlpTelemetrySink({
119
+ url: "https://langfuse.vauban.tech/api/public/otel",
120
+ headers: { Authorization: `Basic ${btoa("pk_xxx:sk_xxx")}` },
121
+ });
122
+ ```
123
+
124
+ Per [Brain entry 426c92f5](https://command.vauban.tech/brain/426c92f5),
125
+ `langfuse.vauban.tech` is the self-hosted Langfuse already deployed on
126
+ K3s. Free for ecosystem usage.
127
+
128
+ ### Grafana Tempo
129
+
130
+ ```ts
131
+ otlpTelemetrySink({
132
+ url: "http://tempo.observability.svc.cluster.local:4318",
133
+ });
134
+ ```
135
+
136
+ ### Jaeger (dev)
137
+
138
+ ```bash
139
+ docker run -p 16686:16686 -p 4318:4318 jaegertracing/all-in-one
140
+ ```
141
+
142
+ ```ts
143
+ otlpTelemetrySink({ url: "http://localhost:4318" });
144
+ ```
145
+
146
+ UI at `http://localhost:16686`.
@@ -0,0 +1,126 @@
1
+ # `localSqliteTelemetrySink`
2
+
3
+ **Sovereign local mirror** of every agent run. Per
4
+ [ADR-ECO-039 §5](https://github.com/vauban-org/vauban-gouvernance/blob/main/governance/decisions/ADR-ECO-039-sdk-telemetry-port.md),
5
+ this sink is **ON by default** when you compose with `createTelemetryBus` —
6
+ it is the *exit plan* for any remote sink.
7
+
8
+ ## Why a local mirror, always ?
9
+
10
+ If `command.vauban.tech` goes down, if your OTLP collector is unreachable,
11
+ if a network partition cuts the link to your observability stack — the
12
+ local SQLite file retains the full history. No data loss. No vendor lock-in.
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { localSqliteTelemetrySink } from "@vauban-org/agent-sdk";
18
+
19
+ localSqliteTelemetrySink({
20
+ path?: string; // default: ~/.vauban/runs.db
21
+ readonly?: boolean; // default: false
22
+ });
23
+ ```
24
+
25
+ ### Inspect via standard tools
26
+
27
+ ```bash
28
+ sqlite3 ~/.vauban/runs.db
29
+ sqlite> .tables
30
+ agent_run agent_run_step
31
+ sqlite> SELECT agent_id, status, COUNT(*) FROM agent_run GROUP BY agent_id, status;
32
+ ```
33
+
34
+ The upcoming `vauban-agent runs list` CLI wraps this with a nicer UI.
35
+
36
+ ## Schema
37
+
38
+ The sink auto-creates two tables on first use :
39
+
40
+ ```sql
41
+ CREATE TABLE agent_run (
42
+ run_id TEXT PRIMARY KEY,
43
+ agent_id TEXT NOT NULL,
44
+ agent_version TEXT NOT NULL,
45
+ model TEXT,
46
+ provider TEXT,
47
+ tenant_id TEXT,
48
+ trace_id TEXT,
49
+ started_at TEXT NOT NULL,
50
+ finished_at TEXT,
51
+ status TEXT,
52
+ stop_reason TEXT,
53
+ error_message TEXT,
54
+ total_input_tokens INTEGER DEFAULT 0,
55
+ total_output_tokens INTEGER DEFAULT 0,
56
+ total_cost_usd REAL DEFAULT 0,
57
+ total_tool_calls INTEGER DEFAULT 0
58
+ );
59
+
60
+ CREATE TABLE agent_run_step (
61
+ run_id TEXT NOT NULL,
62
+ step_index INTEGER NOT NULL,
63
+ kind TEXT NOT NULL,
64
+ status TEXT NOT NULL,
65
+ input_tokens INTEGER DEFAULT 0,
66
+ output_tokens INTEGER DEFAULT 0,
67
+ tool_calls INTEGER DEFAULT 0,
68
+ cost_usd REAL DEFAULT 0,
69
+ duration_ms INTEGER,
70
+ metadata TEXT,
71
+ recorded_at TEXT NOT NULL,
72
+ PRIMARY KEY (run_id, step_index)
73
+ );
74
+ ```
75
+
76
+ Plus indices on `(agent_id, started_at DESC)` and `(status, started_at DESC)`.
77
+
78
+ ## Optional dependency
79
+
80
+ `better-sqlite3` is declared as **peerDependencyOptional**. Install :
81
+
82
+ ```bash
83
+ pnpm add better-sqlite3
84
+ ```
85
+
86
+ If absent, the sink **degrades to no-op** and logs one warning at startup
87
+ ("better-sqlite3 not installed — sink degraded to no-op"). Your agent
88
+ continues without local persistence. This is intentional — SDK consumers
89
+ who only want the OTLP or CC sink shouldn't be forced to compile a native
90
+ addon.
91
+
92
+ ## Disk footprint
93
+
94
+ | Metric | Value |
95
+ |---|---|
96
+ | Per agent_run row | ~250 bytes |
97
+ | Per agent_run_step row | ~150 bytes |
98
+ | 100 000 runs × 5 steps each | ≈ 100 MB |
99
+ | WAL journal during writes | ≤ 10 MB |
100
+
101
+ Append-only. No cleanup required for normal usage. For aggressive retention,
102
+ manual DELETE WHERE `started_at < date('now', '-7 days')` works.
103
+
104
+ ## Tests
105
+
106
+ The unit test suite skips SQLite-specific assertions if `better-sqlite3`
107
+ is not installed in the test environment, so CI passes both with and
108
+ without the addon.
109
+
110
+ ## Failure modes
111
+
112
+ - **Disk full** : the next `start` throws a `SQLITE_FULL` error, isolated
113
+ by the bus. The agent continues.
114
+ - **WAL corruption** : never observed in the field. Recovery is to
115
+ delete the `.db-wal` + `.db-shm` files and re-open ; the main DB file
116
+ is rebuilt from the checkpoint.
117
+ - **Concurrent access from multiple agents** : safe — WAL mode (set by
118
+ the sink via `journal_mode = WAL`) supports concurrent readers + one
119
+ writer per file.
120
+
121
+ ## Migration to remote sinks
122
+
123
+ The local SQLite is the audit trail. Once your remote sink is wired up
124
+ and stable, the local file is **archival** — keep it around as the
125
+ sovereign backup. Some teams sync it nightly to S3 or Backblaze B2 as
126
+ a tier-2 archive.
@@ -0,0 +1,82 @@
1
+ # `stdoutTelemetrySink`
2
+
3
+ Emits one **JSON line per event** to `process.stderr` (by default). Designed
4
+ for dev visibility — pipe through `jq`, `pino-pretty`, or any structured-log
5
+ collector.
6
+
7
+ ## Why stderr, not stdout ?
8
+
9
+ Many agents print their *application output* to stdout. Mixing telemetry
10
+ JSON lines with that output corrupts both pipelines. stderr is the
11
+ conventional "diagnostics" channel that doesn't interfere.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { stdoutTelemetrySink } from "@vauban-org/agent-sdk";
17
+
18
+ createOODAAgent({
19
+ agentId: "my-agent",
20
+ telemetry: stdoutTelemetrySink(), // shorthand — single sink
21
+ });
22
+ ```
23
+
24
+ Or compose with the bus :
25
+
26
+ ```ts
27
+ import { createTelemetryBus, stdoutTelemetrySink, localSqliteTelemetrySink } from "@vauban-org/agent-sdk";
28
+
29
+ createOODAAgent({
30
+ agentId: "my-agent",
31
+ telemetry: createTelemetryBus({
32
+ sinks: [stdoutTelemetrySink(), localSqliteTelemetrySink()],
33
+ }),
34
+ });
35
+ ```
36
+
37
+ ## Options
38
+
39
+ ```ts
40
+ stdoutTelemetrySink({
41
+ stream?: NodeJS.WritableStream; // default: process.stderr
42
+ json?: boolean; // default: true (else key=value)
43
+ });
44
+ ```
45
+
46
+ ## Output sample
47
+
48
+ ```bash
49
+ node my-agent.ts 2>&1 | jq -c
50
+ ```
51
+
52
+ ```json
53
+ {"telemetry":"run.start","runId":"abc-…","agentId":"my-agent","agentVersion":"1.0.0","model":"unknown","provider":"unknown","startedAt":"2026-05-16T17:00:00.000Z"}
54
+ {"telemetry":"run.step","runId":"abc-…","stepIndex":0,"kind":"observe","status":"completed","inputTokens":127,"outputTokens":42,"costUsd":0.0008}
55
+ {"telemetry":"run.finish","runId":"abc-…","status":"success","finishedAt":"2026-05-16T17:00:05.012Z"}
56
+ ```
57
+
58
+ ## Tail-friendly recipes
59
+
60
+ ```bash
61
+ # Live error monitoring
62
+ node my-agent.ts 2> >(jq -c 'select(.telemetry == "run.finish" and .status != "success")')
63
+
64
+ # Cost per agent (last 100 runs)
65
+ node my-agent.ts 2>&1 | jq -s 'map(select(.telemetry == "run.finish")) | group_by(.agentId) | map({agent: .[0].agentId, runs: length})'
66
+ ```
67
+
68
+ ## Failure modes
69
+
70
+ - **Stream closed mid-write** : Node's default behavior is to emit an
71
+ `error` event on the stream. The sink does NOT swallow this — wire up
72
+ `stream.on('error', …)` if you need custom recovery.
73
+ - **stderr disconnected** (`> /dev/null 2>&1`) : silently no-ops, as
74
+ expected.
75
+
76
+ ## Performance
77
+
78
+ Synchronous `stream.write()` — ~1 µs per event. Negligible. Safe to keep
79
+ enabled in production for low-rate agents.
80
+
81
+ For high-rate (>1000 events/sec) deployments, prefer `otlpTelemetrySink`
82
+ or the CC sink, which batch internally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vauban-org/agent-sdk",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Vauban agent primitives: loop, budget, routing, HITL, permissions, tracking, durable execution",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -113,23 +113,6 @@
113
113
  "registry": "https://registry.npmjs.org",
114
114
  "access": "public"
115
115
  },
116
- "scripts": {
117
- "build": "tsc",
118
- "test": "vitest run",
119
- "lint": "biome check src/ tests/",
120
- "size": "size-limit",
121
- "size:legacy": "node scripts/measure-size.mjs",
122
- "bench": "node --import tsx/esm bench/run.ts",
123
- "bench:sdk": "node --import tsx/esm benchmarks/sdk.bench.ts",
124
- "bench:baseline": "node --import tsx/esm benchmarks/sdk.bench.ts --write-baseline",
125
- "mutation": "stryker run",
126
- "stryker": "stryker run --concurrency 2",
127
- "api:extract": "api-extractor run --local --verbose",
128
- "api:check": "api-extractor run",
129
- "depcruise": "depcruise src",
130
- "docs": "typedoc",
131
- "prepublishOnly": "pnpm build && pnpm test"
132
- },
133
116
  "dependencies": {
134
117
  "@anthropic-ai/sdk": "^0.39.0",
135
118
  "@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
@@ -181,5 +164,21 @@
181
164
  },
182
165
  "engines": {
183
166
  "node": ">=20"
167
+ },
168
+ "scripts": {
169
+ "build": "tsc",
170
+ "test": "vitest run",
171
+ "lint": "biome check src/ tests/",
172
+ "size": "size-limit",
173
+ "size:legacy": "node scripts/measure-size.mjs",
174
+ "bench": "node --import tsx/esm bench/run.ts",
175
+ "bench:sdk": "node --import tsx/esm benchmarks/sdk.bench.ts",
176
+ "bench:baseline": "node --import tsx/esm benchmarks/sdk.bench.ts --write-baseline",
177
+ "mutation": "stryker run",
178
+ "stryker": "stryker run --concurrency 2",
179
+ "api:extract": "api-extractor run --local --verbose",
180
+ "api:check": "api-extractor run",
181
+ "depcruise": "depcruise src",
182
+ "docs": "typedoc"
184
183
  }
185
- }
184
+ }