pi

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package pi implements event-level adapters for TWO harnesses that share ONE session format: Pi (earendil-works/pi, `@earendil-works/pi-coding-agent`) and OpenClaw (openclaw/openclaw), which ships pi's own session manager. The on-disk record shapes are identical — verified byte for byte against live sessions from both CLIs on 2026-08-16 — so one parser serves both and the only differences are where the files live and what tool id the rows carry.

SURFACE. An append-only JSONL tree, one file per session:

Pi:       ${PI_CODING_AGENT_DIR:-~/.pi/agent}/sessions/<encoded-cwd>/<ts>_<uuid>.jsonl
          (or ${PI_CODING_AGENT_SESSION_DIR} used directly as the sessions dir)
OpenClaw: ${OPENCLAW_STATE_DIR:-${OPENCLAW_HOME:-~}/.openclaw}/agents/<id>/sessions/<uuid>.jsonl

Line 1 is the session header (`{"type":"session","id":<uuid>,"cwd":...}`). Every later line is an entry with its own `id` and a `parentId`, forming a tree; branching appends rather than rewrites. Token usage lives on three entry kinds: `message` entries whose `.message.role == "assistant"` carry `.message.usage`, and `compaction` / `branch_summary` entries carry the usage of the LLM call that produced the summary.

TOKEN SEMANTICS, read off pi-ai's own normalisers rather than guessed: `input`, `output`, `cacheRead` and `cacheWrite` are DISJOINT (the OpenAI path computes `input = prompt_tokens - cached - cache_write`, and calculateCost tiers on `input + cacheRead + cacheWrite`), `totalTokens` is their sum, and `reasoning` is a SUBSET of `output` — adding it to a total would bill the same token twice. `cost` is pi's own per-call computation from its model catalogue, in USD.

WHAT IS CAPTURED: usage and activity. A `toolCall` content block sits in the SAME record as the usage object it cost, so tool calls attribute exactly, with the divisor counted from that one record. There is NO turn context: the entry shape is `{type,id,parentId,timestamp}` and carries no attribution field of any kind, and a subagent gets its own session FILE rather than a marker on the parent's turn, so there is nothing to record and nothing to infer.

Index

Constants

View Source
const (
	// AgentDirEnv moves Pi's agent directory, and with it the sessions tree
	// beneath it. It is `<APP_NAME>_CODING_AGENT_DIR` in pi's config.js, where
	// APP_NAME comes from the package's own piConfig — "PI" for
	// @earendil-works/pi-coding-agent. OpenClaw reads this variable too, as the
	// documented fallback for OpenClawAgentDirEnv.
	//
	// NOT `PI_AGENT_DIR`: that name appears in third-party tooling but pi itself
	// never reads it (verified against pi 0.84.2's dist/config.js), so honouring
	// it would point this adapter at a directory the harness does not use.
	AgentDirEnv = "PI_CODING_AGENT_DIR"
	// SessionDirEnv points Pi at a sessions directory directly, bypassing the
	// <agent dir>/sessions/<encoded-cwd> layout. It is the environment form of
	// --session-dir and wins over AgentDirEnv for session lookup.
	SessionDirEnv = "PI_CODING_AGENT_SESSION_DIR"

	// OpenClawStateDirEnv is OpenClaw's state root; everything else hangs off it.
	OpenClawStateDirEnv = "OPENCLAW_STATE_DIR"
	// OpenClawHomeEnv replaces the home directory OpenClaw derives `.openclaw`
	// from, ahead of HOME/USERPROFILE.
	OpenClawHomeEnv = "OPENCLAW_HOME"
	// OpenClawAgentDirEnv overrides the per-agent directory. The sessions tree is
	// its sibling, so an override is scanned via its parent.
	OpenClawAgentDirEnv = "OPENCLAW_AGENT_DIR"
	// OpenClawConfigPathEnv names the config file, which may itself relocate an
	// agent's directory (`agents.list[].agentDir`). This adapter does not parse
	// the config — it is declared because it can move the surface, and a run
	// under it must not be treated as the default install.
	OpenClawConfigPathEnv = "OPENCLAW_CONFIG_PATH"
	// OpenClawProfileEnv (and its `--profile` flag) relocates the whole state
	// root to `~/.openclaw-<name>`; only the "default" profile keeps
	// `~/.openclaw`. The CLI resolves it into OpenClawStateDirEnv inside its own
	// process, which this adapter never sees, so discovery ALSO globs sibling
	// `.openclaw-*` roots. That is best-effort by construction: a profile whose
	// state dir was set explicitly to somewhere else is only found through
	// OpenClawStateDirEnv.
	OpenClawProfileEnv = "OPENCLAW_PROFILE"
)

Environment variables that move what these adapters READ. Every one of them is exported because a supervised install must know that the collected surface was relocated by the environment: a unit does not inherit the installing shell, so an install made under one of these would collect somewhere else than the shell that installed it (see cmd.discoveryEnv).

Every lookup spells its variable with one of these constants AT the os.Getenv call, never through a helper's parameter: cmd.TestDiscoveryEnvCoversEveryAdapterVariable parses this file and resolves the ARGUMENT of each lookup through the package's own constants, so a name it cannot resolve is a variable the automatic install cannot suppress — and a unit installed under one of those would collect from the default location while the shell that installed it read somewhere else.

Variables

This section is empty.

Functions

func NewOpenClaw

func NewOpenClaw() adapter.Adapter

NewOpenClaw returns the adapter for OpenClaw.

func NewPi

func NewPi() adapter.Adapter

NewPi returns the adapter for Pi.

Types

type Adapter

type Adapter struct {
	// contains filtered or unexported fields
}

Adapter reads one harness's pi-format session transcripts. Read-only: it opens files for reading, takes no lock, and writes nothing.

func (Adapter) Capabilities

func (a Adapter) Capabilities() model.ToolCapability

Capabilities declares what this project can say about this adapter's harness.

Both harnesses declare the SAME values because they are one package over one byte-identical session format — what the code can observe does not depend on which of the two wrote the file. They remain two declarations under two tool ids because they are two tools, and their rows are never summed.

Cost is VENDOR-reported: pi.go stamps a.tool+"-reported" from the figure the harness itself recorded, which collect.stampCost is forbidden to overwrite. Activity is an EXACT join because a toolCall block sits in the SAME record as the usage object it cost.

func (Adapter) Collect

func (a Adapter) Collect(ctx context.Context, src adapter.Source) (adapter.Observation, error)

Collect reads one session transcript in full.

func (Adapter) CollectIncremental

func (a Adapter) CollectIncremental(ctx context.Context, src adapter.Source, cp *model.SourceCheckpoint) (adapter.Observation, error)

CollectIncremental reads only what is new since cp: an unchanged size+mtime skips the file; pure growth tail-reads from the stored offset with the persisted header/model state; anything else (a shrink, a same-size rewrite — pi rewrites a whole file when it migrates its session version) re-reads from zero, which is harmless because every dedup key is derived from content.

func (Adapter) Discover

func (a Adapter) Discover(ctx context.Context, cfg adapter.DiscoverConfig) ([]adapter.Source, error)

Discover locates the session transcripts of this adapter's harness.

Both harnesses are scanned by the same rule — every `*.jsonl` under a discovered sessions root — and both exclude the same sidecars. Results are sorted by path so a fork and its source are always visited in the same order, which makes which of the two duplicates lands in the ledger deterministic across passes (their dedup keys are equal, so only the first is stored).

func (Adapter) DisplayName

func (a Adapter) DisplayName() string

DisplayName returns the human-friendly name.

func (Adapter) ID

func (a Adapter) ID() string

ID returns the stable tool identifier.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL