@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.
- package/CONTRACT.md +595 -7
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestration/ooda/agent.d.ts.map +1 -1
- package/dist/orchestration/ooda/agent.js +36 -0
- package/dist/orchestration/ooda/agent.js.map +1 -1
- package/dist/orchestration/ooda/types.d.ts +11 -0
- package/dist/orchestration/ooda/types.d.ts.map +1 -1
- package/dist/skills/_secrets.d.ts +16 -0
- package/dist/skills/_secrets.d.ts.map +1 -0
- package/dist/skills/_secrets.js +20 -0
- package/dist/skills/_secrets.js.map +1 -0
- package/dist/skills/alpaca-quote.d.ts +2 -2
- package/dist/skills/alpaca-quote.d.ts.map +1 -1
- package/dist/skills/alpaca-quote.js +51 -20
- package/dist/skills/alpaca-quote.js.map +1 -1
- package/dist/skills/send-email.d.ts +2 -2
- package/dist/skills/send-email.d.ts.map +1 -1
- package/dist/skills/send-email.js +1 -12
- package/dist/skills/send-email.js.map +1 -1
- package/dist/skills/slack-notify.d.ts.map +1 -1
- package/dist/skills/slack-notify.js +52 -21
- package/dist/skills/slack-notify.js.map +1 -1
- package/dist/skills/telegram-notify.d.ts.map +1 -1
- package/dist/skills/telegram-notify.js +48 -19
- package/dist/skills/telegram-notify.js.map +1 -1
- package/dist/skills/web-search.d.ts.map +1 -1
- package/dist/skills/web-search.js +85 -40
- package/dist/skills/web-search.js.map +1 -1
- package/dist/telemetry/bus.d.ts +54 -0
- package/dist/telemetry/bus.d.ts.map +1 -0
- package/dist/telemetry/bus.js +159 -0
- package/dist/telemetry/bus.js.map +1 -0
- package/dist/telemetry/index.d.ts +35 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +30 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/port.d.ts +121 -0
- package/dist/telemetry/port.d.ts.map +1 -0
- package/dist/telemetry/port.js +48 -0
- package/dist/telemetry/port.js.map +1 -0
- package/dist/telemetry/sinks/otlp.d.ts +45 -0
- package/dist/telemetry/sinks/otlp.d.ts.map +1 -0
- package/dist/telemetry/sinks/otlp.js +195 -0
- package/dist/telemetry/sinks/otlp.js.map +1 -0
- package/dist/telemetry/sinks/sqlite.d.ts +32 -0
- package/dist/telemetry/sinks/sqlite.d.ts.map +1 -0
- package/dist/telemetry/sinks/sqlite.js +170 -0
- package/dist/telemetry/sinks/sqlite.js.map +1 -0
- package/dist/telemetry/sinks/stdout.d.ts +22 -0
- package/dist/telemetry/sinks/stdout.d.ts.map +1 -0
- package/dist/telemetry/sinks/stdout.js +38 -0
- package/dist/telemetry/sinks/stdout.js.map +1 -0
- package/docs/telemetry/migration.md +155 -0
- package/docs/telemetry/overview.md +154 -0
- package/docs/telemetry/privacy.md +127 -0
- package/docs/telemetry/sinks/cc.md +155 -0
- package/docs/telemetry/sinks/otlp.md +146 -0
- package/docs/telemetry/sinks/sqlite.md +126 -0
- package/docs/telemetry/sinks/stdout.md +82 -0
- package/package.json +18 -19
- package/src/index.ts +30 -1
- package/src/orchestration/ooda/agent.ts +50 -0
- package/src/orchestration/ooda/types.ts +12 -0
- package/src/skills/_secrets.ts +25 -0
- package/src/skills/alpaca-quote.ts +68 -23
- package/src/skills/send-email.ts +1 -12
- package/src/skills/slack-notify.ts +73 -30
- package/src/skills/telegram-notify.ts +70 -24
- package/src/skills/web-search.ts +132 -50
- package/src/telemetry/bus.test.ts +231 -0
- package/src/telemetry/bus.ts +241 -0
- package/src/telemetry/index.ts +49 -0
- package/src/telemetry/port.ts +160 -0
- package/src/telemetry/sinks/otlp.test.ts +146 -0
- package/src/telemetry/sinks/otlp.ts +250 -0
- package/src/telemetry/sinks/sqlite.test.ts +121 -0
- package/src/telemetry/sinks/sqlite.ts +260 -0
- package/src/telemetry/sinks/stdout.test.ts +109 -0
- 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.
|
|
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
|
+
}
|