Documentation
¶
Overview ¶
Package agent implements the local Agent-loop: the canonical "while the model requests tools, execute them and feed results back" cycle (Phase 0 — read-only). It is deliberately model-backend-agnostic (any OpenAI-compatible tool-calling Client) and side-effect-agnostic (tools are injected), so the loop logic is unit-testable without a live model or real tools. Stop conditions and the step budget are owned HERE, in code — never by the model — and an erroring or unknown tool is fed back as data (defer-not- crash), never a panic or an aborted run.
Index ¶
- func Mem0KeyFromEnvOrFile() string
- func SystemPrompt(allowWrite, allowFetch, allowShell bool) string
- type Action
- type ActionKind
- type Allowlist
- type AuditLog
- type BuildConfig
- type BuildResult
- type Client
- type Completion
- type Decision
- type LLMClient
- type Loop
- type Memory
- type MemoryClient
- type Msg
- type OffloadFunc
- type Policy
- type Recalled
- type Result
- type Tool
- type ToolCall
- type ToolSpec
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Mem0KeyFromEnvOrFile ¶
func Mem0KeyFromEnvOrFile() string
Mem0KeyFromEnvOrFile resolves the mem0 API key: $MEM0_API_KEY first, then the on-disk key file (~/.mem0/api-key, mode 600 — the mem0 server's own key source). Returns "" if neither is present (memory then stays disabled rather than crashing).
func SystemPrompt ¶
SystemPrompt is the loop's capability-aware system prompt: it advertises only the tools actually granted (read-only by default, +write/+web_fetch/+run_shell when enabled) and labels fetched web content as untrusted data. It is shared by every drive mode (headless CLI, MCP front door, standalone) so the agent's behavior is identical regardless of who supplies the goal.
Types ¶
type Action ¶
type Action struct {
Kind ActionKind
Path string
Exists bool
}
Action is a proposed mutating operation. Path is worktree-relative (the worktree FS boundary itself is enforced separately by os.Root); Exists tells the broker whether the target already exists (new vs overwrite).
type ActionKind ¶
type ActionKind string
ActionKind is the class of mutating action the broker gates. P2 covers file mutations; later phases extend this (shell, network) behind the same broker.
const ( ActWrite ActionKind = "write" ActDelete ActionKind = "delete" ActFetch ActionKind = "fetch" // P3: outbound HTTP(S) GET to a host (egress-allowlist gated) ActShell ActionKind = "shell" // P4.6: run a command inside the OS sandbox (opt-in, audited) )
type Allowlist ¶
type Allowlist struct {
// contains filtered or unexported fields
}
Allowlist is the P3 egress host allowlist: a set of canonical hosts plus opt-in, boundary-anchored wildcard suffixes. It is built once and never mutated, so the policy broker that reads it stays deterministic. The zero value permits NOTHING (default-deny) — exactly how P0/P1 grant no network tool at all. The host-matching rule is the security boundary; see canonHost.
func NewAllowlist ¶
NewAllowlist parses bare-hostname entries and "*.host" wildcards into an Allowlist, rejecting any entry that carries a scheme, port, path, userinfo, or backslash (bare hostnames only — the Managed-Agents rule). Empty input yields the deny-all zero value. A "*." prefix becomes a boundary-anchored suffix; any other "*" (mid-label, bare) is rejected by canonHost.
type AuditLog ¶
type AuditLog struct {
// contains filtered or unexported fields
}
AuditLog is an append-only JSONL record of every brokered decision — kept separate from the savings ledger, never co-mingled.
func NewAuditLog ¶
NewAuditLog returns a logger writing to path (created on first record).
type BuildConfig ¶
type BuildConfig struct {
PlannerBase string // local model base URL (no /v1); required
Model string // planner model id (must support tool-calling); required
Timeout time.Duration // per planner-call timeout; default 180s
MaxSteps int // hard step budget; default 12
MaxTokens int // planner max tokens per call; default 1024
ReadRoot string // directory the agent may read (P0 scope); required
Offload OffloadFunc // in-process offload (record=false); nil => no offload tools
// SystemPromptOverride, when set, replaces the capability-aware system prompt
// (P6 flywheel replay evaluates a CANDIDATE planner prompt). Empty => the normal
// SystemPrompt. The tool set is still capability-gated as usual, so a read-only
// build stays side-effect-free regardless of the prompt.
SystemPromptOverride string
Unattended bool // true => every broker "ask" deny-and-queues (no human in the loop)
AuditPath string // append-only broker audit JSONL; must live OUTSIDE the worktree
AskQueuePath string // P5b: reviewable queue of asks deferred on an unattended run (optional)
AllowWrite bool // P2: write_file/delete_file in the worktree
AllowFetch bool // P3: web_fetch behind the egress allowlist
AllowShell bool // P4.6: run_shell in the OS cage (granted only if sandbox.Available)
Worktree string // RW worktree for write/shell; default = ReadRoot
EgressHosts []string // web_fetch allowlist (AllowFetch)
Memory Memory // optional mem0 layer; nil => no memory
}
BuildConfig declares everything a drive mode needs to construct the agent loop identically. The caller supplies the local planner endpoint, the read scope, an in-process offload closure (record=false — e.g. pipeline.NewRecordlessOffload), and which capabilities are opt-in enabled. Build wires the tools + the single deny→ask→allow broker + the loop the SAME way for every mode (CLI, MCP front door, standalone), so the three modes cannot drift.
type BuildResult ¶
type BuildResult struct {
Loop *Loop
Tools []Tool
Policy *Policy
Worktree string // resolved RW worktree (empty if no write/shell)
ShellGranted bool
Notes []string
}
BuildResult is the assembled loop plus what was actually granted. ShellGranted is false when --allow-shell was requested but the OS cage is unavailable here (fail-closed); Notes are human-readable capability lines the caller can log.
func Build ¶
func Build(cfg BuildConfig) (*BuildResult, error)
Build assembles the agent loop for any drive mode. It is the SINGLE place the tool set + broker + loop are constructed, so the CLI, the MCP front door, and the standalone runner stay at parity by construction. Operational failures (bad paths, audit-inside-worktree, worktree creation) are returned as errors, never os.Exit — the caller decides how to surface them.
type Client ¶
type Client interface {
Chat(ctx context.Context, msgs []Msg, tools []ToolSpec, maxTokens int) (Completion, error)
}
Client is the minimal OpenAI-compatible tool-calling chat interface the loop needs. A concrete implementation targets llama-swap / NIM; tests use a fake.
type Completion ¶
Completion is one model turn: the assistant message plus the finish reason ("tool_calls" => the loop must execute tools and continue; anything else => the loop stops).
type LLMClient ¶
type LLMClient struct {
// contains filtered or unexported fields
}
LLMClient is the concrete OpenAI-compatible tool-calling Client. It targets any /v1/chat/completions endpoint — llama-swap (:11436, keyless) by default, or an NIM endpoint (apiKey set) for the hybrid "ask" escalation. One type, base_url swap. It implements the agent.Client interface.
func NewLLMClient ¶
NewLLMClient builds a client. base is the server root (no /v1); apiKey "" => no Authorization header (local).
type Loop ¶
type Loop struct {
// contains filtered or unexported fields
}
Loop runs the canonical agent loop over a fixed tool set.
func NewLoop ¶
NewLoop builds a loop. maxSteps is the hard budget guard (owned in code, not the prompt). A non-positive maxSteps defaults to 1.
func (*Loop) Run ¶
Run executes the loop for objective until the model stops, the step budget is exhausted, or the context is cancelled.
func (*Loop) WithMaxTokens ¶
func (*Loop) WithMemory ¶
WithMemory attaches a memory layer: the loop recalls relevant context before planning and persists the run outcome when it finishes. nil = no memory.
func (*Loop) WithSystem ¶
WithSystem sets an optional system prompt. WithMaxTokens overrides the per-call completion cap.
type Memory ¶
type Memory interface {
Recall(ctx context.Context, query string, limit int) ([]Recalled, error)
Persist(ctx context.Context, text string, meta map[string]string) (string, error)
}
Memory is the loop's optional memory layer: recall relevant context before planning, persist a run outcome after. nil => the loop runs without memory.
type MemoryClient ¶
type MemoryClient struct {
// contains filtered or unexported fields
}
MemoryClient talks to the mem0 REST server (the agentic-memory system). It is WRITE-ISOLATED by construction: it recalls across readUsers (e.g. the agent's own namespace for run-to-run continuity + the canonical "dmmdea" namespace for Daniel's knowledge) but only ever WRITES under writeUser, and only at the "evidence" tier — the mem0 server independently rejects any attempt to write the canonical tier (403), so the agent cannot pollute the canonical anchor.
func NewMemoryClient ¶
func NewMemoryClient(base, apiKey string, readUsers []string, writeUser, agentID string, timeout time.Duration) *MemoryClient
NewMemoryClient builds a client. readUsers are the namespaces to recall from; writeUser is the (isolated) namespace to persist to. A trailing slash on base is trimmed.
func (*MemoryClient) Persist ¶
func (c *MemoryClient) Persist(ctx context.Context, text string, meta map[string]string) (string, error)
Persist writes text as an EVIDENCE-tier memory under the isolated writeUser namespace, tagged source=local-agent. It NEVER sends tier=canonical (the server would 403 anyway) — the agent cannot reach the canonical anchor.
func (*MemoryClient) Recall ¶
Recall searches each readUser namespace CONCURRENTLY under one short deadline and returns the merged, score-sorted, de-duplicated top-`limit` memories. A per-namespace error is tolerated (that namespace contributes nothing); a total failure returns the last error with whatever was gathered — the caller treats an empty recall as "no memory". Concurrency + the deadline mean a single slow/ wedged namespace can't stall planning.
type Msg ¶
Msg is one chat message in the loop's running transcript. Role is "system" | "user" | "assistant" | "tool". A tool result carries ToolCallID (matching the originating ToolCall.ID) and IsError when the tool failed.
type OffloadFunc ¶
type OffloadFunc func(ctx context.Context, task, input string, params map[string]any) (result string, err error)
OffloadFunc is the in-process offload capability injected by cmd/local-agent (wired to pipeline.RunTier, which is record=false → no ledger/cache/shadow/ exemplar writes). Keeping it an injected func keeps this package free of any pipeline import and unit-testable with a fake. nil => no offload tools.
type Policy ¶
type Policy struct {
// contains filtered or unexported fields
}
Policy is the deterministic deny→ask→allow broker — the cage that must exist BEFORE any mutating tool is granted. It is the single chokepoint for every effectful action. For an UNATTENDED run, "ask" resolves to deny-and-queue (never proceed without a human), making approval-before-destructive a mechanism rather than a hope.
func NewPolicy ¶
NewPolicy builds a broker. unattended=true makes every "ask" deny-and-queue. audit may be nil (decisions then aren't logged). Egress is deny-all (no allowlist) — use NewPolicyWithEgress to grant outbound hosts.
func NewPolicyWithEgress ¶
NewPolicyWithEgress is NewPolicy plus a P3 egress allowlist (the set of hosts web_fetch may reach). An empty allowlist is default-deny. The allowlist is set once and never mutated afterward, so classify stays pure and deterministic.
func (*Policy) Decide ¶
Decide returns the EFFECTIVE decision (resolving ask→deny for unattended runs) plus a reason, and records it to the audit log. This is what tools call. If an ALLOW cannot be audited, it is downgraded to DENY — an unauditable mutation is not performed (a mutating agent must leave a trail).
func (*Policy) WithAskQueue ¶
WithAskQueue attaches a reviewable queue that records every ask DEFERRED on an unattended run (so a human can review/approve them later). Off by default; the standalone runner sets it. The action is still denied in the moment (unattended ask → deny-and-queue) — this just parks the pending request for review.
type Recalled ¶
type Recalled struct {
ID string
Text string
Score float64
Source string // which namespace (user_id) it came from
}
Recalled is one retrieved memory.
type Result ¶
type Result struct {
Output string // the final assistant content
Steps int // model turns taken
StopReason string // "done" (model finished) | "budget" (hit maxSteps) | "error"
Transcript []Msg
}
Result is the outcome of a loop run.
type Tool ¶
Tool is a ToolSpec plus its executor. Exec receives the raw JSON args and returns a result string; an error is fed back to the model as an is_error tool result (the loop does not abort).
func FetchTools ¶
FetchTools builds the P3 web_fetch tool, registered ONLY when egress is opted in. With no allowlist the broker denies every host, so the network capability is inert by default — mirroring how P0/P1 advertise no network tool at all.
func ReadOnlyTools ¶
func ReadOnlyTools(root string, offload OffloadFunc) ([]Tool, error)
ReadOnlyTools builds the Phase-0 tool set, scoped to root (the worktree): list_dir + read_file (both confined to root), plus the offload_* tools when an offloader is wired. It registers NO write/shell/network tool — P0 is read-only by construction; broader capability is gated to later phases behind the cage.
Containment is OS-ENFORCED via os.Root: every file access goes through a root handle that refuses to escape — `..`, absolute paths, AND reparse points (symlinks / Windows directory junctions) are all rejected by the kernel-level openat-style traversal, and it FAILS CLOSED (any resolution error => the tool errors, never a silent fall-through). This replaces the earlier hand-rolled EvalSymlinks check, which a fresh-context review proved bypassable by a non-privileged Windows junction.
func ShellTools ¶
ShellTools builds the P4.6 run_shell tool: it executes a command inside the OS sandbox (no network, filesystem confined to the worktree (RW) + scratch, dangerous syscalls blocked, i386 ABI closed). It is registered ONLY when the caller has opted in AND sandbox.Available() is true; it is gated AGAIN at runtime by the deny→ask→allow broker, which audits every command. worktree is the RW working directory; scratch is a RW temp dir.
func WriteTools ¶
WriteTools builds the Phase-2 MUTATING tools (write_file, delete_file), confined to worktreeRoot via os.Root (the same kernel-enforced, fail-closed, reparse-safe containment used for reads) AND gated by the deny→ask→allow policy broker (the cage). Every mutation is brokered + audited; a non-Allow decision returns a "NOT performed" tool result (defer-not-crash) — the file is untouched and the model can react. These tools are registered ONLY when the caller opts into write capability; P0/P1 stay read-only.