pi

package
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package pi reads pi's session files: the JSONL files under ~/.pi/agent/sessions, one per session. It imports the standard library alone.

pi specifies the format: docs/session-format.md and docs/message-types.md in the @earendil-works/pi-coding-agent package, and its TypeScript types (session-manager.d.ts, pi-ai's types.d.ts). This package follows them at session version 3, and reads versions 1 and 2 through Migrate, which applies pi's own migrations. Every object keeps the members it does not declare and writes them back, a line of a shape it does not expect keeps its raw bytes, and a line that is not JSON is recorded and skipped, as pi skips it.

Read and ReadFile read; BuildTree links the entries as pi does. Converting to the Agent Session Format is the convert subpackage's; docs/formats/pi.md is the mapping.

Index

Constants

View Source
const (
	TypeMessage             = "message"
	TypeModelChange         = "model_change"
	TypeThinkingLevelChange = "thinking_level_change"
	TypeUsage               = "usage"
	TypeCompaction          = "compaction"
	TypeBranchSummary       = "branch_summary"
	TypeCustom              = "custom"
	TypeCustomMessage       = "custom_message"
	TypeContextEdit         = "context_edit"
	TypeLabel               = "label"
	TypeSessionInfo         = "session_info"
)

Entry types: lines of the tree, each with an id and a parentId.

View Source
const (
	RoleSystem            = "system"
	RoleUser              = "user"
	RoleAssistant         = "assistant"
	RoleToolResult        = "toolResult"
	RoleBashExecution     = "bashExecution"
	RoleCustom            = "custom"
	RoleBranchSummary     = "branchSummary"
	RoleCompactionSummary = "compactionSummary"
	// RoleHookMessage is version 2's name for RoleCustom.
	RoleHookMessage = "hookMessage"
)

Message roles.

View Source
const (
	StopPending  = "pending"
	StopStop     = "stop"
	StopLength   = "length"
	StopToolUse  = "toolUse"
	StopError    = "error"
	StopAborted  = "aborted"
	StopDeferred = "deferred"
)

Stop reasons of an assistant message.

View Source
const (
	BlockText     = "text"
	BlockImage    = "image"
	BlockThinking = "thinking"
	BlockToolCall = "toolCall"
)

Block types.

View Source
const CurrentVersion = 3

CurrentVersion is the session version pi writes, and the one Migrate brings a file to.

View Source
const MaxLineSize = 256 << 20

MaxLineSize bounds one line. A longer line is reported as malformed and skipped rather than read into memory.

View Source
const TypeSession = "session"

TypeSession is the header line's type.

Variables

This section is empty.

Functions

func FindSessions

func FindSessions(root string) ([]string, error)

FindSessions returns the session files under root, every *.jsonl at any depth, sorted. root may be ~/.pi/agent/sessions, one project directory, or a single file.

func IDFromFileName

func IDFromFileName(path string) string

IDFromFileName returns the session id a file name carries, from pi's <timestamp>_<session-id>.jsonl, or "" when the name is not of that form.

func StripLoneSurrogates

func StripLoneSurrogates(raw []byte) ([]byte, int)

StripLoneSurrogates removes the \u escapes of UTF-16 surrogates that are not half of a pair from JSON text, and returns how many it removed. JavaScript writes a string cut inside an emoji with such an escape; Go would read it as U+FFFD, and pi removes it from what it sends a model (pi-ai's sanitizeSurrogates).

Types

type Block

type Block struct {
	Type string `json:"type"`

	// text.
	Text          string `json:"text"`
	TextSignature string `json:"textSignature"`

	// image: base64 data.
	Data     string `json:"data"`
	MimeType string `json:"mimeType"`

	// thinking.
	Thinking          string `json:"thinking"`
	ThinkingSignature string `json:"thinkingSignature"`
	Redacted          bool   `json:"redacted"`

	// toolCall.
	ID               string          `json:"id"`
	Name             string          `json:"name"`
	Arguments        json.RawMessage `json:"arguments"`
	ThoughtSignature string          `json:"thoughtSignature"`
	Namespace        string          `json:"namespace"`

	Members
	// contains filtered or unexported fields
}

Block is a content block: the members of every block type; Type says which apply.

func (*Block) Invalid

func (b *Block) Invalid() bool

Invalid reports whether the block, as read, was not a JSON object: null, a number, a string or an array. Only its raw form is kept.

func (Block) MarshalJSON

func (b Block) MarshalJSON() ([]byte, error)

MarshalJSON writes the block back as it was read.

func (*Block) Raw

func (b *Block) Raw() json.RawMessage

Raw is the block as read, empty for a block built in Go.

func (*Block) UnmarshalJSON

func (b *Block) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a block, keeping undeclared members, and a member whose value does not fit its field among them.

type Content

type Content struct {
	// Text is the content when it was a string.
	Text string
	// IsText says the content was a string.
	IsText bool
	Blocks []*Block
}

Content is a message's content: a string, or an array of blocks.

func (Content) MarshalJSON

func (c Content) MarshalJSON() ([]byte, error)

MarshalJSON writes the string or the blocks.

func (*Content) TextOf

func (c *Content) TextOf(sep string) string

TextOf joins the text of the content's text blocks with sep, as pi does when a role needs one string; a string content is itself.

func (*Content) UnmarshalJSON

func (c *Content) UnmarshalJSON(data []byte) error

UnmarshalJSON accepts a string or an array of blocks. The blocks are decoded one by one, so one that is not an object, or is null, is kept as written (see Block.Invalid) and its neighbours still decode.

type File

type File struct {
	// Path is the file the session was read from; empty when it was read
	// from a reader.
	Path string
	// Lines are the lines that decoded as JSON, the header among them,
	// in file order.
	Lines []*Line
	// Errors lists the lines that could not be read, which the reader
	// skipped, as pi skips them.
	Errors []LineError
	// Surrogates lists the lines that held lone UTF-16 surrogate
	// escapes, which the reader removed before decoding; such a line
	// writes back without them.
	Surrogates []int
}

File is one session file.

func Read

func Read(r io.Reader) (*File, error)

Read reads a session file. A line that is not JSON is recorded in Errors and skipped, as pi skips it; a line that is JSON but not of the shapes this package declares is kept with Err set. Read fails only when r does.

func ReadFile

func ReadFile(path string) (*File, error)

ReadFile reads a session from a file.

func (*File) Entries

func (f *File) Entries() []*Line

Entries returns the lines that are entries of the tree, in file order: every line but the header. A line that did not decode as its type is among them, placed by its envelope.

func (*File) Header

func (f *File) Header() *Line

Header returns the session header: the first line of type session, or nil when the file has none.

func (*File) LineCount

func (f *File) LineCount() int

LineCount is the number of lines the file had that a reader accounts for: those read and those that could not be. Blank lines are not counted.

func (*File) Version

func (f *File) Version() int

Version is the header's version; a header with none, or no header, is version 1, as pi reads it.

type Line

type Line struct {
	Type string `json:"type"`
	// ID is the session's id on the header, the entry's id otherwise.
	ID string `json:"id"`
	// ParentID is empty for a root entry; the input wrote null.
	ParentID  string `json:"parentId"`
	Timestamp string `json:"timestamp"`

	// session (the header). Version is a number, as JavaScript writes
	// it; [File.Version] reads it.
	Version       float64 `json:"version"`
	CWD           string  `json:"cwd"`
	ParentSession string  `json:"parentSession"`

	// message.
	Message *Message `json:"message"`

	// model_change and usage.
	Provider string `json:"provider"`
	ModelID  string `json:"modelId"`
	// thinking_level_change.
	ThinkingLevel string `json:"thinkingLevel"`

	// usage.
	Kind  string `json:"kind"`
	Model string `json:"model"`
	Note  string `json:"note"`
	// usage, compaction and branch_summary.
	Usage *Usage `json:"usage"`

	// compaction and branch_summary.
	Summary  string          `json:"summary"`
	Details  json.RawMessage `json:"details"`
	FromHook *bool           `json:"fromHook"`
	// compaction.
	FirstKeptEntryID string `json:"firstKeptEntryId"`
	// FirstKeptEntryIndex is version 1's form of FirstKeptEntryID: an
	// index into the file's entries, the header counted.
	FirstKeptEntryIndex *int     `json:"firstKeptEntryIndex"`
	TokensBefore        int      `json:"tokensBefore"`
	SystemMessage       *Message `json:"systemMessage"`
	// branch_summary.
	FromID string `json:"fromId"`

	// custom and custom_message.
	CustomType string `json:"customType"`
	// custom.
	Data json.RawMessage `json:"data"`
	// custom_message.
	Content *Content `json:"content"`
	Display bool     `json:"display"`

	// context_edit and label.
	TargetID string `json:"targetId"`
	// context_edit: null omits the target from context, an object
	// replaces its content.
	Replacement json.RawMessage `json:"replacement"`
	// label: absent clears the label.
	Label *string `json:"label"`
	// session_info.
	Name *string `json:"name"`

	Members

	// No is the line's 1-based number in its file.
	No int `json:"-"`
	// Raw is the line as read, without NUL bytes.
	Raw json.RawMessage `json:"-"`
	// Err is set when the line is JSON but does not decode as a line of
	// this package's types; only Type, No, Raw and, when they read, ID,
	// ParentID and Timestamp are then set.
	Err error `json:"-"`
	// Synthetic says ID and ParentID were given by [Migrate] to a
	// version 1 entry, which had none on disk.
	Synthetic bool `json:"-"`
}

Line is one line of a session file: the header or an entry. It holds the members of every line type; Type says which apply. Members the type does not declare are kept in Extra and written back.

func ReadHeader

func ReadHeader(path string) (*Line, error)

ReadHeader reads only the header of the session file at path, as pi does to find a session: the first line that decodes as a session line, within the first lines of the file.

func (*Line) EditReplacement

func (l *Line) EditReplacement() (*Content, error)

EditReplacement decodes a context_edit's replacement: nil when the edit omits its target.

func (*Line) IsEntry

func (l *Line) IsEntry() bool

IsEntry reports whether the line is an entry of the tree, rather than the header. Every line that is not the header is one, whatever its type, as pi indexes them.

func (*Line) IsRetainNone

func (l *Line) IsRetainNone() bool

IsRetainNone reports whether a compaction kept no entry from before it, which pi writes as firstKeptEntryId naming the compaction itself.

func (Line) MarshalJSON

func (l Line) MarshalJSON() ([]byte, error)

MarshalJSON writes the line back as it was read.

func (*Line) MistypedMembers

func (l *Line) MistypedMembers() []string

MistypedMembers lists, as paths from the line, the members anywhere in the line whose value was not of the type pi defines for it, which the reader kept as written in their object's Extra rather than fail the line.

func (*Line) Time

func (l *Line) Time() time.Time

Time is the line's timestamp, zero when it has none or it does not parse.

func (*Line) UnmarshalJSON

func (l *Line) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a line, keeping undeclared members.

type LineError

type LineError struct {
	No  int
	Err error
}

LineError is a line the reader could not read.

func (LineError) Error

func (e LineError) Error() string

type Members

type Members = jsonline.Members

Members is embedded in every object this package decodes. It keeps the members the Go type does not declare, and remembers which declared members the input carried and which were null, so an object decoded and encoded again is the same JSON value.

type Message

type Message struct {
	Role    string   `json:"role"`
	Content *Content `json:"content"`
	// Timestamp is Unix milliseconds.
	Timestamp float64 `json:"timestamp"`

	// system.
	Sections     Sections    `json:"sections"`
	ToolsAdded   []*Tool     `json:"toolsAdded"`
	ToolsRemoved []*ToolName `json:"toolsRemoved"`
	Replace      bool        `json:"replace"`

	// assistant.
	API                   string          `json:"api"`
	Provider              string          `json:"provider"`
	Model                 string          `json:"model"`
	ResponseModel         string          `json:"responseModel"`
	ResponseID            string          `json:"responseId"`
	ThinkingLevel         string          `json:"thinkingLevel"`
	ProviderThinkingLevel string          `json:"providerThinkingLevel"`
	Diagnostics           json.RawMessage `json:"diagnostics"`
	StopReason            string          `json:"stopReason"`
	RawStopReason         string          `json:"rawStopReason"`
	Deferred              json.RawMessage `json:"deferred"`
	ErrorMessage          string          `json:"errorMessage"`
	EndTurn               *bool           `json:"endTurn"`
	// assistant and toolResult.
	Usage *Usage `json:"usage"`

	// toolResult.
	ToolCallID string `json:"toolCallId"`
	ToolName   string `json:"toolName"`
	IsError    bool   `json:"isError"`
	// toolResult and custom.
	Details json.RawMessage `json:"details"`

	// bashExecution.
	Command            string `json:"command"`
	Output             string `json:"output"`
	ExitCode           *int   `json:"exitCode"`
	Cancelled          bool   `json:"cancelled"`
	Truncated          bool   `json:"truncated"`
	FullOutputPath     string `json:"fullOutputPath"`
	ExcludeFromContext bool   `json:"excludeFromContext"`

	// custom.
	CustomType string `json:"customType"`
	Display    bool   `json:"display"`

	// branchSummary and compactionSummary, which pi derives in memory
	// and does not persist.
	Summary      string `json:"summary"`
	FromID       string `json:"fromId"`
	TokensBefore int    `json:"tokensBefore"`

	Members
}

Message is an AgentMessage: the members of every role; Role says which apply.

func (Message) MarshalJSON

func (m Message) MarshalJSON() ([]byte, error)

MarshalJSON writes the message back as it was read.

func (*Message) UnmarshalJSON

func (m *Message) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a message, keeping undeclared members.

type Migration

type Migration struct {
	// From is the version the file declared; To is [CurrentVersion].
	From, To int
	// IDs counts the version 1 entries given an id and a parentId,
	// chaining them in file order.
	IDs int
	// FirstKept counts the version 1 compactions whose
	// firstKeptEntryIndex became a firstKeptEntryId.
	FirstKept int
	// HookMessages counts the messages whose version 2 role hookMessage
	// became custom.
	HookMessages int
	// KeptIDs says the file read as version 1 but its entries carry ids,
	// which were kept rather than replaced.
	KeptIDs bool
}

Migration says what Migrate did to bring a file to the current version, as pi does in memory when it loads one.

func Migrate

func Migrate(f *File) Migration

Migrate brings f to CurrentVersion in place, applying pi's own migrations: version 1 entries get ids and are chained in file order, with a compaction's firstKeptEntryIndex turned into the id of the entry it indexed; version 2's hookMessage role becomes custom. An id it gives is "v1-<line>" and the line is marked Synthetic, since the file never held it. A file already current is not changed. The lines it changes no longer write back as they were read.

type Node

type Node struct {
	Line     *Line
	Parent   *Node
	Children []*Node
}

Node is an entry in the tree.

type Section

type Section struct {
	Name   string
	Text   string
	Remove bool
}

Section is one named prompt section a system message sets, or removes when Remove is set.

type Sections

type Sections []Section

Sections are a system message's section patches, in the order the object wrote them: pi replays them in that order, and a new section takes its place at the end.

func (Sections) MarshalJSON

func (s Sections) MarshalJSON() ([]byte, error)

MarshalJSON writes the object in order.

func (*Sections) UnmarshalJSON

func (s *Sections) UnmarshalJSON(data []byte) error

UnmarshalJSON reads the object's members in order.

type Tool

type Tool struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Parameters  json.RawMessage `json:"parameters"`

	Members
}

Tool is a tool declaration a system message adds.

func (Tool) MarshalJSON

func (t Tool) MarshalJSON() ([]byte, error)

MarshalJSON writes the tool back as it was read.

func (*Tool) UnmarshalJSON

func (t *Tool) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a tool, keeping undeclared members.

type ToolName

type ToolName struct {
	Name string `json:"name"`

	Members
}

ToolName names a tool a system message removes.

func (ToolName) MarshalJSON

func (t ToolName) MarshalJSON() ([]byte, error)

MarshalJSON writes the tool reference back as it was read.

func (*ToolName) UnmarshalJSON

func (t *ToolName) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a tool reference, keeping undeclared members.

type Tree

type Tree struct {
	// Roots are the entries with no parent, in file order.
	Roots []*Node
	// Nodes holds every entry in file order.
	Nodes []*Node
	// Leaf is the last entry in the file, where pi resumes; nil when
	// the file has no entry.
	Leaf *Node
	// Dangling lists the entries whose parentId names no entry; each
	// starts a root.
	Dangling []*Line
	// Cycles lists the entries whose parent links led back to
	// themselves; the link was cut and each starts a root.
	Cycles []*Line
	// Duplicates lists the entries whose id an earlier entry had. pi
	// indexes by id and the later entry wins, so a parentId naming the
	// id names the later one; the earlier keeps its place in the tree.
	Duplicates []*Line
	// contains filtered or unexported fields
}

Tree is a session's entries linked by parentId.

func BuildTree

func BuildTree(f *File) *Tree

BuildTree links the entries of f, as pi does: by id and parentId, the last entry with an id winning, children in file order. f should have been through Migrate.

func (*Tree) Node

func (t *Tree) Node(id string) *Node

Node returns the entry with the given id, the last one written when several have it.

type Usage

type Usage struct {
	Input        int64  `json:"input"`
	Output       int64  `json:"output"`
	CacheRead    int64  `json:"cacheRead"`
	CacheWrite   int64  `json:"cacheWrite"`
	CacheWrite1h *int64 `json:"cacheWrite1h"`
	Reasoning    *int64 `json:"reasoning"`
	TotalTokens  int64  `json:"totalTokens"`
	// Cost is in dollars, per kind of token and in total.
	Cost json.RawMessage `json:"cost"`

	Members
}

Usage is pi's token and cost accounting. Input excludes the tokens read from and written to the cache, which have their own counts; Reasoning, when present, is already in Output.

func (Usage) MarshalJSON

func (u Usage) MarshalJSON() ([]byte, error)

MarshalJSON writes usage back as it was read.

func (*Usage) UnmarshalJSON

func (u *Usage) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes usage, keeping undeclared members.

Directories

Path Synopsis
Package convert converts pi sessions into the Agent Session Format.
Package convert converts pi sessions into the Agent Session Format.

Jump to

Keyboard shortcuts

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