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 ¶
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.
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.
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.
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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
Budget bounds a single turn. Zero means "use the server default from the CLI flags." See the plan for the cascade rules.
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.
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 ( 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.