| name | agenteye-python-sdk |
| description | The way to make an AI agent report what it did to AgentEye — planning what to record, writing the instrumentation, and proving the events land. Reach for it on vague phrasing too: "add observability to my agent", "why isn't my agent showing up?"
Trigger when the user wants to:
• plan an integration — which points in their agent loop to record, and what the platform must see before sessions, errors, and evals work at all;
• write or fix instrumentation — add the `agenteye` Python SDK to an agent codebase, thread session/agent identity through it, emit tool, model, hook, or human events;
• verify it — confirm events are being written, or debug an integration that looks correct and produces nothing.
Served by the `agenteye` Python SDK, inside the user's own agent.
NOT for reading telemetry that already landed or operating a deployment (that's `agenteye-cli`), or building the evaluator service that scores runs (that's `agenteye-evaluator`). |
AgentEye Python SDK
The SDK records what your agent did, from inside your agent. You call it at points
you choose; it appends structured events to local .jsonl files. A separate
collector ships those files to the platform.
your agent calls agenteye.event.*
→ SDK queues it in memory
→ flush thread writes <base_dir>/events/event-<timestamp>.jsonl
→ collector picks the file up and ships it
→ visible as sessions / events / errors / evals
The SDK's job ends at the file. That boundary is the most useful thing to know
about it: everything up to the .jsonl is yours to get right and yours to verify,
and it is verifiable on a laptop with no server, no API key, and no network.
The API is small — 13 event methods, all keyword-only. The hard parts are
deciding where to call them and knowing which silences are bugs, because
this SDK does not raise when you get it wrong. Sections 1-3 are the plan, 4 is the
code, 5-6 are the proof.
1. Install it — and do not trust the obvious command
pip install agenteye does not install this SDK. It installs the AgentEye
CLI, a different product that happens to publish under the same distribution
name on public PyPI. The result is quiet and confusing:
pip install agenteye → you get the CLI → import agenteye raises
ModuleNotFoundError, because the CLI ships the module agenteye_cli.
- Worse, if the SDK is already installed,
pip install agenteye removes it
and puts the CLI there instead. Same distribution name, higher version number.
Your agent's import agenteye breaks at the next deploy, and nothing in the
output says so.
The SDK ships as a private wheel from GitHub Releases, not from an index.
references/install.md has the ladder (token, download, install, pin).
Tell: if a coding agent proposes pip install agenteye to install the SDK,
this skill never loaded. Stop and re-read it.
Confirm what you actually have before writing a line of instrumentation:
python -c "import agenteye; print(agenteye.__version__)"
A version like 0.0.1b9 is the SDK. ModuleNotFoundError means you have the CLI
or nothing. The CLI is a fine thing to want — install it separately with pipx or
uv tool, never with pip into your agent's environment.
2. Plan before you instrument
Instrumentation lands in code that already exists and already works. Read it
first, then decide. Two questions settle most of the design, and only the user can
answer the first:
What is one run of this agent? That is your session_id — one value for the
whole run, generated by you at the point the run starts. A chat turn, a job, a
request, a workflow execution. If the agent handles concurrent runs, this must
be per-run, not per-process.
What are the distinguishable actors in a run? That is your agent_id — a
stable label, not a unique id. "planner", "researcher", "main". It is how
the platform tells sub-agents apart, so reuse the same string across runs.
Get these two named and agreed before writing code. They are the axes every
surface groups by, and changing them later splits the history: old runs keep the
old labels and the trends break.
The two events everything else hangs off
Most of the catalog is optional and incremental. These two are not:
| Event | Without it |
|---|
agent_start | The session does not exist. No row on Sessions, no timeline, no evaluation — while every other event you emit still lands fine and shows up in the event stream. |
agent_end | The run never closes, and it is not handed to the evaluator at the normal time. |
That first row is the single most common integration failure, and it is
completely silent: a run emitting 500 tool calls and no agent_start produces a
busy event stream and zero sessions. Sessions are defined as "something that
emitted agent_start". So:
Emit agent_start at the top of the run and agent_end at every exit, and get
those two working end-to-end before you instrument anything else. One event at
each end proves the whole path — install, identity, base dir, collector — with
almost no code to be wrong. Add tools, models, and hooks after that path is green.
Then map the rest onto the agent's shape
Walk the agent loop and pick the points that exist in this codebase. Skip what
doesn't apply; there is no requirement to emit every type.
| In the code | Emit | Buys you |
|---|
| every exit path of a run — success, exception, early return | agent_start / agent_end | the session itself |
| the tool dispatcher, both sides of the call | tool_use / tool_result | what ran, in what order, how long |
| the LLM client wrapper, both sides | model_request / model_response | model mix, token spend, stop reasons |
your except blocks | error | the Errors surface |
| a policy/guard/middleware layer | hook_triggered / hook_completed | hook behaviour |
| an approval gate or human handoff | human_wait / human_input, human_pause, human_interrupt | where runs sit waiting on people |
If the codebase has one tool dispatcher and one LLM wrapper, you have two edit
sites for the bulk of the value. If tool calls are scattered inline across the
codebase, say so — a wrapper (§4) is worth more than 40 call sites.
Full field-by-field catalog: references/events.md.
3. The contract
Work with these; none of them raise, so none of them show up in testing.
-
There is no ambient session. No decorator, no context manager, no
contextvar, no set_session(). Every one of the 13 methods takes session_id
and agent_id as required keyword args, and nothing propagates them for you.
This is what §4 exists to solve.
-
configure() is optional, and every call restates all of it. It is
keyword-only with exactly three settings:
| arg | default resolution |
|---|
base_dir | $AGENTEYE_HOME, else ~/.agenteye |
environment | $AGENTEYE_ENVIRONMENT, else "dev" |
flush_interval | 0.5 (seconds) |
Each call sets all three — omitted arguments are reset to default
resolution, not left alone. So a later configure(flush_interval=1.0)
silently moves your events back to the default directory and re-resolves the
environment. An explicit configure(environment=...) beats the env var; omit it
and the env var applies again. Call it once, at startup, before the first
event, passing every argument you care about.
-
environment defaults to "dev". An unconfigured production agent reports
its runs as dev and they are invisible wherever the team filters on
production. Set it explicitly via configure(environment=...) or the
AGENTEYE_ENVIRONMENT env var. This is a favourite: everything works, in the
wrong bucket.
-
Every value you pass must be JSON-serializable. This is the one that will
hurt you. Field names are unvalidated; field values are not. They are
serialized on a background thread ~0.5s after your call returned, so a
datetime, UUID, Decimal, set, bytes, or Pydantic model raises there
— where you cannot catch it. The writer thread dies, the whole batch it was in
is destroyed, and every later event in that process is lost, including the
at-exit flush. Nothing raises at your call site; no wrapper can catch it; the
process keeps serving traffic and recording nothing, forever.
A tool that returns a datetime is enough. Coerce at the boundary:
import json
safe = json.loads(json.dumps(value, default=str))
The wrapper in references/integration.md does this for you. If you write your
own, do it there — not at 40 call sites.
-
Field names are unvalidated — but only the optional ones. Every method
takes arbitrary **fields and stores them as-is, so a typo'd optional name
(inpt= for input=) is not an error, it is a new field, and nothing will tell
you. Typos in required names raise TypeError (they're real parameters), and
five reserved names — timestamp, session_id, agent_id, type,
environment — raise ValueError.
-
outcome="failed", not "failure". A run counts as failed only when
outcome (or status) is one of error, failed, timeout, rejected
(case-insensitive). "failure" is the natural antonym of the "success" in
every example — and it silently counts as not a failure. The run shows green.
-
You own correlation, and tool_call_id + hook_id share ONE flat map.
They are keyed bare, process-wide — not per session, not per type. So they must
be unique across every concurrent run and across each other: a
hook_completed(hook_id="x") will happily pair with a pending
tool_use(tool_call_id="x") and report a confident, wrong duration. (input_id
is the exception — it is scoped per session/agent.)
Reusing your framework's id is safe (Anthropic and OpenAI ids are globally
unique). Per-run counters — call_1, call_2, common in home-grown loops — are
not, and the failure is a plausible wrong number attributed to the wrong
run, not a missing one. A wrong duration is worse than a null.
-
duration_ms is computed for you on three methods only — tool_result,
hook_completed, human_input — from the matching earlier event. Passing it to
those three raises ValueError. Passing it to any of the other ten is
silently accepted as a custom field.
-
Events are fire-and-forget. event.* queues in memory and returns; a daemon
thread writes every 0.5s, plus once at interpreter exit. A clean exit flushes.
A hard kill (SIGKILL, os._exit, a container OOM) drops whatever is queued,
silently.
SIGTERM deserves its own line, because it is not exotic — it is every
rolling deploy, every docker stop, every Kubernetes eviction. Python's
default handler exits, so atexit does run; but if your app installs its own
handler and calls os._exit, or takes longer than the grace period, the queue
dies with it. What is in flight at shutdown is disproportionately agent_end,
so runs never close and never reach the evaluator. If you handle SIGTERM,
flush before you exit.
4. Write it
Threading session_id and agent_id through every call site by hand is the thing
that makes integrations ugly and abandoned. Don't. Bind identity once per run and
let the call sites read it.
references/integration.md has the canonical contextvars wrapper — one small
module, correct under asyncio and threads, adaptable to any codebase — plus
worked shapes for a tool dispatcher, an LLM client wrapper, and framework-specific
callback layers. Read it before writing your own; the naive version (a module
global, or a plain attribute) breaks the moment two runs overlap, and it breaks by
mixing two runs' events together rather than by failing.
Match the codebase you're in. If it's async, the wrapper is async. If it already
has a request context or a trace id, bind to that instead of inventing one.
5. Verify — watch the files
This is the whole point of the file boundary: you can prove the integration
without a server. Run the agent and look.
ls -la ~/.agenteye/events/
You are looking for event-<UTC timestamp>.jsonl files. Each line is one event.
Read them with a JSON parser, not grep — the exact spacing of the output is not
a contract, and a grep for "type":"agent_start" returns nothing on a perfectly
healthy integration:
cat ~/.agenteye/events/*.jsonl | python -m json.tool --json-lines | head -20
Then check, in this order — the first failure explains everything downstream:
- Any files at all — or do they stop mid-run? Look at stderr for
Exception in thread agenteye-flush. This is the first thing to check and
the worst thing to miss: one non-JSON-serializable value killed the writer,
and everything after it — including the at-exit flush — is gone (§3). The tell
is that events stop for every type at once, and nothing raised. If instead
there were never any files: did import agenteye succeed (§1)? Is the base dir
writable? Did the process die hard (SIGKILL, docker stop, an OOM) before a
flush?
- Is
agent_start there, once per run? No → you will see events on the
platform and no sessions, and you will spend an afternoon on it (§2).
- Sessions but no tool or model events? Your emit path is dropping them
before the SDK ever sees them — nearly always because they're emitted from a
thread the identity never reached. See
references/integration.md → "Threads
will drop your events". The SDK is silent here; only your own wrapper can warn.
- Is
environment what you expect? It is "dev" unless you set it (§3).
- Is
outcome on agent_end a word that counts? failed/error/timeout/
rejected — not "failure" (§3). Failed runs showing green is this, every
time.
- Run two overlapping runs. Confirm two
session_ids with no events
crossing between them. Do not check this with one run: a single run passes
even when identity is a module global, and mixing only appears once two runs
overlap — which is production, not your laptop (§4).
- Do
tool_use and tool_result share a tool_call_id? Unpaired means no
duration. Also confirm your ids are unique process-wide — a collision pairs
the wrong two events and reports a confident wrong duration (§3).
A test-mode loop that costs nothing:
export AGENTEYE_HOME=/tmp/agenteye-test
rm -rf /tmp/agenteye-test && python your_agent.py
cat /tmp/agenteye-test/events/*.jsonl | python -m json.tool --json-lines
AGENTEYE_HOME sends events somewhere disposable, so you can iterate on the
integration without touching the real directory or shipping test runs to the
platform. Note the SDK reads it late, per flush — so set it before you start the
process, not halfway through.
Do not verify by installing the CLI into your agent's environment. It will
uninstall the SDK you just integrated (§1). Reading back what landed on the
platform is the agenteye-cli skill's job, from a separate environment.
6. Production — the collector has to agree with you
The SDK writes files. It never talks to the network, so from its point of view a
completely unshipped integration looks perfect.
In production, the collector must be running and reading the same directory
the SDK is writing to. That is the whole contract, and both halves fail
silently:
- Collector not running → files pile up in
events/ forever. The SDK is fine.
- Collector reading a different base dir than the agent writes to — a different
AGENTEYE_HOME, a different user's ~, a container path that isn't mounted —
→ files pile up in a directory nobody reads. The SDK is fine.
So when events are on disk but not on the platform, the SDK is not the suspect.
Compare the two paths first: print the directory your agent is actually writing to
(python -c "import agenteye._resolver as r; print(r.get_base_dir())" in the
agent's own environment, with the agent's own env vars) and check the collector is
running and pointed at the same one. A .jsonl count that only grows is the tell.
Confirming events arrived on the platform is deliberately not this skill's job —
that is the agenteye-cli skill, from a separate environment (§1). Collector
setup and deployment are your platform's own documentation.
If the files look right (§5) and the collector is running against the same
directory, the integration is done.