pi-agent-observer
v0.1.0-alpha.1
Published
Prompt-neutral runtime observability for pi agents
Maintainers
Readme
pi-agent-observer
read-only, prompt-neutral local observability for pi runtimes.
pi-agent-observer is a general-purpose observability package for users and pi package authors who need to see which pi agents are running, how spawned agents relate to their parents, and what high-level lifecycle events occurred without retaining model-visible payloads.
use it to answer questions like:
- what child agents did this session spawn?
- which runtime is active, stale, completed, failed, or aborted?
- which package/orchestrator launched this runtime?
- what model/session/cwd/runtime metadata is attached?
- what bounded event previews exist for debugging a run?
it is intentionally not a tracing/profiling/security/audit system for prompt contents. it stores structured runtime metadata and bounded previews only.
compatibility
this alpha targets pi 0.80.6 exactly. unknown observer state/event schema versions are ignored rather than interpreted. macos and linux are supported; storage and discovery are host-local.
install and local development
this alpha is not published to npm yet. install from a local checkout:
git clone https://github.com/almogdepaz/pi-agent-observer.git
cd pi-agent-observer
bun install
bun run build
pi install "$PWD"local development without installing globally:
bun install
bun run check
pi -e src/extension.tsordinary package installs load dist/extension.js. the executable is dist/cli.js and is exposed as pi-agents. if you install from a source checkout, build first so dist/ exists.
user-facing tools
interactive agent browser
in interactive pi, type / and select /agents. use ↑/↓ to choose a descendant runtime and enter to open its durable event tail. escape returns to the picker, then closes it.
/agents all includes unrelated local roots; /agents <runtime-prefix> opens one matching descendant directly. the browser remains read-only and shows the same bounded previews as the cli—full payloads are unavailable.
after at least one descendant is observed, interactive sessions show a, c, f, and s counts for active, completed, failed/aborted, and stale descendants. sessions with no descendants show no observer status; unrelated roots are excluded.
cli
pi-agents list [--json] [--runtime ID] [--session ID] [--cwd PATH] [--launcher pi|edc|pi-subagent] [--origin NAME] [--parent ID|none] [--status STATUS]
pi-agents tree [--json] [--launcher pi|edc|pi-subagent] [--origin NAME] [--parent ID|none] [--status STATUS]
pi-agents tail <runtime-or-session-prefix> [--json] [--follow] [--event TYPE] [--since ISO_TIME]missing tail targets exit 2; ambiguous targets exit 3. json list/tree output includes both persisted state and heartbeat-derived observed status. tail json is jsonl. full prompt/tool payloads are not retained and therefore cannot be requested by the cli.
observability data model
for each observed pi runtime, agent observer stores prompt-neutral runtime state:
- runtime id, parent runtime id, pid, cwd, session id, and optional session file
- pi session display name, if set through
--name,/name, rpc, orpi.setSessionName() - model provider/id after selection events
- launcher classification (
pi,edc, orpi-subagent) - structured launcher hints such as
EDC_PI_SUBPROCESSorPI_SUBAGENT_* - optional explicit runtime origin for orchestrators/packages
- start, heartbeat, end timestamps, terminal status, shutdown reason, and bounded error preview
it also writes durable jsonl events for session, agent, turn, message, tool, model, compaction, and tree/info changes. message/tool events retain only bounded previews and structured metadata such as tool name, tool-call id, error flag, and argument key names.
agent observer does not retain:
- full prompts
- full messages
- full tool arguments
- full tool results
- provider request payloads
- credentials
- model-visible resources
if a preview is truncated, the full payload is intentionally unavailable.
integration contract for third-party tools
third-party tools means pi packages, cli wrappers, ci jobs, custom uis, rpc clients, and sdk applications that launch or embed pi. treat pi-agent-observer as an optional observability layer: your tool must keep working when it is not installed.
stable integration surface:
- inherit observer environment variables into child
piprocesses - set pi session names for human-readable task labels
- set
PI_OBSERVER_RUNTIME_ORIGINfor orchestrator attribution - consume
pi-agents ... --jsonoutput for automation
non-contract surface:
- private files under the observer directory
- human text from the interactive
/agentsbrowser - prompt text, tool arguments, or tool results; full payloads are intentionally not retained
integration levels
| level | use case | required work |
|---|---|---|
| zero-config | your tool spawns normal pi children | install observer globally, inherit process.env, do not pass --no-extensions |
| labeled children | users need readable task names | pass --name <task-name> or use rpc/sdk session-name APIs |
| origin attribution | users need to filter by orchestrator | set PI_OBSERVER_RUNTIME_ORIGIN=<package-name> |
| sdk embedding | your app creates AgentSession directly | use pi's normal resource loader and bind extensions for each active session |
| automation dashboards | another process reads observer state | call pi-agents list --json, tree --json, or tail --json |
child processes launched with pi
when customizing child process environments, preserve the inherited observer variables instead of rebuilding a tiny env map.
import { spawn } from "node:child_process";
const child = spawn("pi", ["--name", "workflow-review-analysis", "-p", "review this workflow"], {
env: {
...process.env,
PI_OBSERVER_RUNTIME_ORIGIN: "looper-ai",
},
stdio: ["ignore", "pipe", "pipe"],
});
await new Promise((resolve, reject) => {
child.once("error", reject);
child.once("exit", resolve);
});shell wrappers can do the same thing:
export PI_OBSERVER_RUNTIME_ORIGIN=my-tool
pi --name "my-tool: dependency review" -p "review dependencies"python subprocess wrappers should copy os.environ:
import os
import subprocess
env = os.environ.copy()
env["PI_OBSERVER_RUNTIME_ORIGIN"] = "my-python-runner"
subprocess.run(["pi", "--name", "my-python-runner: task", "-p", "do the task"], env=env, check=True)be real: env={"PATH": ...} breaks ancestry unless you also copy PI_OBSERVER_CURRENT_ID. prefer copying the full environment, then overriding only your tool's own keys.
sdk-created sessions
sdk-created sessions are visible only after extensions are loaded and bound. sessions that never bind extensions are invisible to the observer.
recommended sdk behavior:
- use pi's
DefaultResourceLoaderor equivalent package-aware loader so globally installed packages are discovered - bind extensions for the initial session
- after
newSession(),switchSession(),fork(), or reload-style replacement, bind extensions again for the new active session - set a useful name with
pi.setSessionName(), rpcset_session_name, or cli--name
browser labels prefer:
- session display name
- explicit runtime origin
- safe cwd basename/runtime id fallback
machine-readable consumption
use json output; do not parse human tree/list rows.
pi-agents list --json --origin looper-ai
pi-agents tree --json --parent <runtime-id>
pi-agents tail <runtime-or-session-prefix> --json --event agent --since 2026-01-01T00:00:00Zlist --json returns an array of records with:
state: persisted runtime metadata, includingruntimeId,parentId,sessionId,sessionName,cwd,launcher,origin, timestamps, and terminal outcomeobservedStatus: heartbeat/event-derived status:active,completed,failed,aborted, orstaleobservedStatusReason: why that status was chosen
tree --json returns the same runtime records nested by parentId, with relation markers for roots, orphans, cycles, and children. tail --json emits observer events as jsonl.
prefix matching is supported only for tail targets. if a prefix is missing, exit code is 2; if ambiguous, exit code is 3.
runtime origin metadata
set PI_OBSERVER_RUNTIME_ORIGIN=<your-package-name> when your package is the orchestrator. accepted values are a-z0-9._-, must start with a-z0-9, and are limited to 64 chars.
examples:
PI_OBSERVER_RUNTIME_ORIGIN=looper-ai
PI_OBSERVER_RUNTIME_ORIGIN=branchout
PI_OBSERVER_RUNTIME_ORIGIN=my-package.subrunnerruntime origin is not launcher classification. launcher kind is low-level process context (pi, edc, or pi-subagent); origin tells users which package intentionally created the runtime.
edc and compatibility hints
EDC_PI_SUBPROCESS=1 is treated as an edc launcher hint for compatibility. if your package sets EDC_PI_SUBPROCESS=1 only to disable an edc extension inside isolated child calls, also set PI_OBSERVER_RUNTIME_ORIGIN. then the runtime remains queryable with the edc hint without being semantically labeled as edc-owned.
package author checklist
- inherit
process.env/os.environwhen spawning child pi processes - do not pass
--no-extensionsfor children you want observed - set
--name, rpcset_session_name, orpi.setSessionName()for readable labels - set
PI_OBSERVER_RUNTIME_ORIGINfor package/orchestrator attribution - bind extensions for sdk-created sessions, and re-bind after session replacement
- consume
pi-agents --json; do not scrape display text or private storage - never rely on retained prompt/tool payloads; only bounded previews exist
- treat
EDC_PI_SUBPROCESSas a compatibility hint, not ownership metadata
privacy and retention defaults
observer data is stored under ~/.pi/agent/observer unless PI_OBSERVER_DIR is set. directories are owner-only (0700) and files are owner-only (0600). traces retain structured metadata and bounded previews only; full prompts, tool arguments, and tool results are unavailable.
heartbeats default to 1 second and become stale after 5 seconds. cleanup at session startup removes terminal/stale records older than 7 days and trims eligible records toward a 100 mib total ceiling. these defaults can be changed with PI_OBSERVER_HEARTBEAT_MS, PI_OBSERVER_STALE_MS, PI_OBSERVER_PREVIEW_BYTES, PI_OBSERVER_RETENTION_AGE_MS, PI_OBSERVER_RETENTION_BYTES, and PI_OBSERVER_STATUS_MS.
this package registers no tools, commands, skills, prompt templates, or model-visible resources. it has no install or postinstall scripts.
alpha limits
- no visibility into
--no-extensionsor sdk sessions that never bind extensions - no standalone tui, sockets, control, abort/steer/follow-up commands, or resume
- no remote host aggregation or semantic phase inference
- filesystem ownership is the privacy boundary, not a sandbox
