agent

package
v0.0.0-...-1c90dee Latest Latest
Warning

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

Go to latest
Published: Apr 27, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package agent drives the peer-side Claude session for hearsay's Phase-2 interactive tools.

Phase-2 originally shipped via the Anthropic Managed-Agents API; PR C pivoted to a subprocess driver around `claude --print` so peers can use their Claude Code subscription instead of needing an ANTHROPIC_API_KEY. Key design points after the pivot:

  • Tools execute on the peer's box. Hearsay invokes `claude --print --allowed-tools "Read Glob Grep"` with cwd set to the project root; Claude Code itself enforces the read-only allowlist and runs the tools natively.

  • The convID is the Claude Code session UUID. Conversation persistence lives in `~/.claude/projects/<encoded-cwd>/<uuid>.jsonl` — the same JSONL files Phase-1 reads via `read_session`. The hearsay-side conversation map is metadata only (lastActivityAt, turnCount, system-prompt preview, etc.) and doesn't survive a restart.

  • cli.go holds the subprocess driver + JSON parser. No event loop — `claude --print` is synchronous, returns one JSON result, and writes its session JSONL to disk. We replay the JSONL after the call to extract per-tool-call detail (the stdout JSON has no `tool_calls[]` field).

Index

Constants

This section is empty.

Variables

View Source
var AllowedToolNames = []string{"read", "glob", "grep"}

AllowedToolNames is the hardcoded read-only allowlist for Phase 2. Widening this list to include `bash`, `edit`, `write`, etc. is an intentional Tier-3 follow-up — never a Phase-2 knob.

View Source
var ErrAgentDisabled = errors.New("agent: not enabled (use --enable-agent)")

ErrAgentDisabled is returned when callers try to use an Agent instance that wasn't constructed (--enable-agent off). The tools layer prevents this by not registering the agent tools when the flag is off, but the type is here for defense-in-depth.

View Source
var ErrClaudeBinMissing = errors.New("agent: claude binary not found on PATH")

ErrClaudeBinMissing is returned by New() when the configured `claude` binary isn't on PATH (or the explicit override path doesn't exist or isn't executable). main.go translates this into the friendly startup-refusal message documented in the README.

View Source
var ErrConvCap = errors.New("agent: max conversations reached")

ErrConvCap is returned by StartConversation when --max-conversations is full. The tool layer translates this into errorSummary=max_conversations.

View Source
var ErrConvReaped = errors.New("agent: conversation reaped after idle timeout")

ErrConvReaped is returned by SendMessage when the named conversation existed but was reaped after the idle timeout.

View Source
var ErrUnknownConv = errors.New("agent: unknown conversation id")

ErrUnknownConv is returned by SendMessage / EndConversation when the convID has no matching live conversation (typo, idle-reaped, or already ended).

Functions

func DefaultAuditPath

func DefaultAuditPath() string

DefaultAuditPath returns the platform-appropriate agent.log path. On macOS, ~/Library/Logs/hearsay/agent.log. On other systems, $XDG_STATE_HOME/hearsay/agent.log (default ~/.local/state/hearsay/... per the XDG Base Directory spec).

Types

type Agent

type Agent interface {
	OneShot(ctx context.Context, req OneShotRequest) (Transcript, error)
	StartConversation(ctx context.Context, req StartReq) (ConvID, time.Time, Budget, error)
	SendMessage(ctx context.Context, convID ConvID, prompt string, budget Budget) (Transcript, error)
	ListConversations() []ConvMeta
	EndConversation(ctx context.Context, convID ConvID, reason EndReason) (EndSummary, error)
}

Agent is the interface every Phase-2 tool calls into. PR A landed OneShot; PR B added the conversation-lifecycle methods.

func New

func New(cfg Config) (Agent, error)

New constructs a CLI-driven Agent. Default-applies `ClaudeBin = "claude"`, validates it's on PATH (or that the override path exists and is executable), starts the idle reaper, and returns. A misconfigured peer fails fast at construction time rather than at first ask_peer_claude call.

type AuditEntry

type AuditEntry struct {
	Timestamp     time.Time         `json:"timestamp"`
	PeerName      string            `json:"peer"`
	ConvID        string            `json:"convId"` // "oneshot" for ask_peer_claude
	TurnIndex     int               `json:"turnIndex"`
	PromptBytes   int               `json:"promptBytes"`
	ResponseBytes int               `json:"responseBytes"`
	ToolCalls     []AuditToolInvoke `json:"toolCalls,omitempty"`
	ElapsedMs     int64             `json:"elapsedMs"`
	StopReason    StopReason        `json:"stopReason"`
	ErrorSummary  ErrorSummary      `json:"errorSummary,omitempty"`
}

AuditEntry is one line of agent.log. Sizes only — no prompt / response / tool-arg content, no hashes (per the conservative-privacy posture in the plan). An opt-in --agent-debug-log flag could carry raw content, but that's a future addition.

type AuditToolInvoke

type AuditToolInvoke struct {
	Name     string `json:"name"`
	ArgBytes int    `json:"argBytes"`
}

AuditToolInvoke records a single tool call without its arguments — just the name and the byte size of the JSON-encoded args.

type Auditor

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

Auditor is a line-atomic JSONL writer guarded by a mutex. Concurrent agent calls produce non-interleaved appends.

func NewAuditor

func NewAuditor(path string) (*Auditor, error)

NewAuditor opens (or creates) the platform-appropriate agent.log, MkdirAll'ing the parent directory if needed. Returns a no-op Auditor if path is empty (e.g. tests that don't care).

func (*Auditor) Close

func (a *Auditor) Close() error

Close releases the underlying file. Safe to call on a nil-file Auditor (returned for empty-path construction).

func (*Auditor) Log

func (a *Auditor) Log(entry AuditEntry) error

Log appends one JSON-serialized AuditEntry as a single line. Concurrent callers serialize via a mutex; each Write is line-atomic so partial-line interleaving is impossible even without the mutex, but the mutex avoids the rare case of a Write breaking up across page boundaries on some filesystems.

type Budget

type Budget struct {
	MaxTokens    int
	MaxToolCalls int
	Timeout      time.Duration
}

Budget bounds a single turn. Zero means "use the server default from the CLI flags." See the plan for the cascade rules.

func (Budget) Resolve

func (b Budget) Resolve(defaults Budget) Budget

Resolve fills zero fields from the supplied default. Used for the per-call ⟶ per-conversation ⟶ server cascade.

type Closer

type Closer interface {
	Close()
}

Closer is implemented by cliAgent so the binary can shut the background reaper down on SIGTERM. Same shape as PR B.

type Config

type Config struct {
	ClaudeBin       string // default "claude" (validated via exec.LookPath at startup)
	PeerName        string
	DefaultBudget   Budget
	Auditor         *Auditor
	FallbackProject string
	KeepEnvAPIKey   bool   // mirrors --agent-keep-env-key; default false
	DataDir         string // Claude Code's data root for JSONL replay (defaults to ~/.claude in main.go)

	MaxConversations        int
	ConversationIdleTimeout time.Duration
}

Config bundles everything the CLI driver needs. Built once in main.go.

type ConvID

type ConvID string

ConvID is an opaque handle to a hearsay-managed conversation. We use the SDK's session ID directly so there's no map-lookup indirection inside the agent layer.

type ConvMeta

type ConvMeta struct {
	ConvID         ConvID
	StartedAt      time.Time
	LastActivityAt time.Time
	TurnCount      int
	// Preview is the first ~140 *runes* (not bytes) of the first user
	// message — rune-based truncation so a multi-byte codepoint at the
	// boundary doesn't yield invalid UTF-8.  When the conversation has
	// been started but no send_peer_message has happened yet, falls
	// back to the first ~140 runes of the system_prompt (or empty if
	// no system_prompt was set).
	Preview string
}

ConvMeta mirrors list_peer_conversations' output one-for-one.

type EndReason

type EndReason string

EndReason discriminates how a conversation ended; carried into the audit log + the end_peer_conversation tool's output.

const (
	EndedByCaller   EndReason = "caller"
	EndedByIdleReap EndReason = "idle_timeout"
	EndedByShutdown EndReason = "shutdown"
)

type EndSummary

type EndSummary struct {
	Ended        bool
	AlreadyEnded bool // true if the conv was already ended (idempotent re-end)
	TotalTurns   int
	EndedReason  EndReason
}

EndSummary mirrors end_peer_conversation's tool output.

type ErrorSummary

type ErrorSummary string

ErrorSummary categorizes an error so callers can branch without parsing free-text. Always populated when StopReason == "error".

const (
	ErrAPIUnavailable ErrorSummary = "api_unavailable"
	ErrAPIRateLimit   ErrorSummary = "api_rate_limited"
	ErrAPIAuth        ErrorSummary = "api_auth"
	ErrNetwork        ErrorSummary = "network"
	ErrTimeout        ErrorSummary = "timeout"
	ErrDisallowedTool ErrorSummary = "disallowed_tool"
	ErrInvalidProject ErrorSummary = "invalid_project"
	ErrClaudeMissing  ErrorSummary = "claude_missing"
	ErrOther          ErrorSummary = "other"
)

type OneShotRequest

type OneShotRequest struct {
	Prompt  string
	Project string // "" => most-recent session's cwd; falls back to hearsay's cwd
	Budget  Budget
}

OneShotRequest is the input to Agent.OneShot.

type StartReq

type StartReq struct {
	SystemPrompt string
	Project      string
	Budget       Budget // becomes the conversation's per-turn default
}

StartReq is the input to Agent.StartConversation.

type StopReason

type StopReason string

StopReason discriminates how a turn ended. Returned with every Transcript so callers can decide whether to follow up.

const (
	StopReasonEndTurn      StopReason = "end_turn"
	StopReasonMaxTokens    StopReason = "max_tokens"
	StopReasonMaxToolCalls StopReason = "max_tool_calls"
	StopReasonTimeout      StopReason = "timeout"
	StopReasonError        StopReason = "error"
	StopReasonShutdown     StopReason = "shutdown"
)

type Transcript

type Transcript struct {
	Markdown      string
	TurnCount     int
	ToolCallCount int
	StopReason    StopReason
	ElapsedMs     int64
	ErrorSummary  ErrorSummary // populated iff StopReason == "error"
}

Transcript is what every prompt-sending tool returns.

Jump to

Keyboard shortcuts

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