@agent-live/sdk
v0.0.7
Published
Fail-soft agent run observability with Strands hooks, redaction, and tenant-scoped event recording.
Maintainers
Readme
@agent-live/sdk
Fail-soft agent run observability for applications that need to record what an AI agent did, what required approval, and whether work completed. The SDK currently provides a generic run recorder, tenant-scoped event bus, SQLite storage, redaction helpers, and a Strands Agents model/tool lifecycle adapter.
Install
npm install @agent-live/sdkFor Strands hook instrumentation, install the optional peer dependency too:
npm install @strands-agents/sdkUsage
import { openDb, createSqliteStore, withRunRecording } from '@agent-live/sdk'
const store = createSqliteStore(openDb('./.agent-live/agent-live.db'))
await withRunRecording(
store,
{
tenantId: 'default',
agentId: 'agt_wa_support',
agentName: 'WhatsApp Support Bot',
sessionKey: session.id,
channel: 'whatsapp',
model: 'claude-sonnet-5',
},
async (recorder) => {
recorder.emit('run.received', 'Inbound WhatsApp message')
const reply = await handleMessage(session)
recorder.attachCompletionExtras({ outputPreview: reply.summary })
return reply
},
)Point npx @agent-live/cli (see the root README) at the same database file to see
these runs in a live dashboard.
Strands Agents SDK wiring
withRunRecording (above) creates the run; RunObservabilityPlugin hooks
into a Strands Agent to record its model/tool calls and approval interrupts
within that run. The two are meant to be combined — here's the full wiring:
import { openDb, createSqliteStore, withRunRecording } from '@agent-live/sdk'
import { RunObservabilityPlugin } from '@agent-live/sdk/adapters/strands'
import {
Agent,
BeforeModelCallEvent,
AfterModelCallEvent,
BeforeToolCallEvent,
AfterToolCallEvent,
InterruptEvent,
} from '@strands-agents/sdk'
const store = createSqliteStore(openDb('./.agent-live/agent-live.db'))
// Pass your own installed SDK's event classes — never import them from
// @agent-live/sdk itself. Strands dispatches hooks by class identity, so a
// different physical install of the SDK would silently never fire.
const hookEvents = { BeforeModelCallEvent, AfterModelCallEvent, BeforeToolCallEvent, AfterToolCallEvent, InterruptEvent }
await withRunRecording(
store,
{
tenantId: 'default',
agentId: 'agt_wa_support',
agentName: 'WhatsApp Support Bot',
sessionKey: session.id,
channel: 'whatsapp',
model: 'claude-sonnet-5',
},
async (recorder) => {
// fromActiveContext() reads the run withRunRecording just started — no
// need to thread runId/tenantId/store down as parameters, even if the
// Agent is actually constructed several function calls deeper than this.
// It also disposes itself automatically once this callback ends, so for
// this common case — one Agent, invoked once per run — that's the whole
// integration: no explicit plugin.dispose() call needed.
const plugin = RunObservabilityPlugin.fromActiveContext(hookEvents)
const agent = new Agent({ model: 'claude-sonnet-5', tools: [/* ... */], plugins: plugin ? [plugin] : [] })
const result = await agent.invoke(session.message)
recorder.attachCompletionExtras({ outputPreview: result.lastMessage.content[0]?.text })
return result
},
)fromActiveContext returns undefined outside any withRunRecording call
(fail-soft, not a throw) — useful when the same agent-construction code path
is also reachable from a non-run context (a one-off script, a test helper),
so it can push the plugin conditionally without special-casing that case.
If a run invokes more than one Agent (e.g. a retry/failover loop that
builds a fresh Agent per attempt), auto-dispose is a safety net for the
run, not each attempt — every prior attempt's hooks stay attached until the
whole run ends otherwise. Dispose each attempt's plugin explicitly as soon as
that invocation finishes:
const plugin = RunObservabilityPlugin.fromActiveContext(hookEvents)
try {
return await agent.invoke(session.message)
} finally {
plugin?.dispose() // safe even though auto-dispose will also run later
}If your own call chain between withRunRecording and the actual
new Agent(...) has several layers in between (a queue, an executor, a
"build agent" helper, etc.), none of them need to know about agent-live at
all — only the two ends do: the withRunRecording call and whatever
constructs the Agent.
Even less code: createInstrumentedAgent
If you'd rather not touch the agent-construction site at all, wrap the
Agent class itself once and use the wrapped version everywhere instead:
import { Agent } from '@strands-agents/sdk'
import { createInstrumentedAgent } from '@agent-live/sdk/adapters/strands'
const InstrumentedAgent = createInstrumentedAgent(Agent, hookEvents)Then construct new InstrumentedAgent(config) wherever you used
new Agent(config) before — nothing else about that call changes, and every
instance automatically gets the plugin (again a no-op outside an active run,
again auto-disposing). This is the one-import-swap version of the pattern
above, not true zero-code auto-instrumentation — doing that for real would
mean hooking Node's module loader itself (the mechanism Sentry/Datadog/
OpenTelemetry use under the hood, via import-in-the-middle), which is a
bigger, separate piece of work this package doesn't attempt yet.
A tool that raises a human-in-the-loop interrupt (event.interrupt({ name, reason })
inside a BeforeToolCallEvent hook, or context.interrupt(...) from a tool
callback) is recorded as approval.waiting automatically — no extra wiring
needed. Resuming from an interrupt (approval.approved/approval.cancelled)
isn't recorded automatically yet; call recorder.emit(...) for that
transition yourself in the meantime.
Status
Version 0.0.7 is an early SDK release. agentId is required on every
recorded run. It records to a host-provided local store and is designed to
fail soft when observability storage is unavailable. withRunRecording
tracks the active run ambiently (via node:async_hooks), so deeply-nested
agent construction code doesn't need runId/tenantId/store threaded through
it as parameters — see getActiveRunContext/RunObservabilityPlugin.fromActiveContext.
A plugin built via fromActiveContext also disposes itself automatically
when its run ends, so integrating Strands observability into a run that
invokes one Agent once is two lines: build the plugin, pass it to Agent.
