Skip to content

Latest commit

 

History

History
150 lines (119 loc) · 4.75 KB

File metadata and controls

150 lines (119 loc) · 4.75 KB
package github.com/genai-io/san/internal/core
layer core

core

The agent primitive: the Agent interface, its surrounding System / Tools / LLM contracts, and the message/event types they exchange. No implementations live here — only the contracts every feature package shares.

Purpose

Everything in feature and above depends on this package; nothing here depends on anything outside internal/log, context, and stdlib. Keeping the surface small and stable is the whole point.

This is also the only package that gets multiple interfaces on one page — Agent, System, Tools, Tool, LLM are the system's primitives and move together when they move at all.

Contract

Agent

package core

// Agent — an LLM in a loop. Three capabilities: System (WHO), Tools (WHAT),
// Inbox/Outbox (HOW it communicates).
type Agent interface {
    ID() string
    System() System
    Tools() Tools
    Inbox() chan<- Message    // caller owns and closes
    Outbox() <-chan Event     // agent owns and closes on Run() return
    Messages() []Message
    SetMessages(msgs []Message)
    Append(ctx context.Context, msg Message)
    ThinkAct(ctx context.Context) (*Result, error)
    Run(ctx context.Context) error
}

func NewAgent(cfg Config) Agent  // returns interface — see Note below

System

// System — the composable, mutable system prompt.
type System interface {
    Prompt() string
    Use(sec Section, caller string)
    Drop(name, caller string)
    Refresh(name, caller string)
    Sections() []Section
    SetObserver(fn func(SystemChange))
}

Tools

// Tool — one capability the agent can execute. Pure (no hooks, no permissions).
type Tool interface {
    Name() string
    Description() string
    Schema() ToolSchema
    Execute(ctx context.Context, input map[string]any) (string, error)
}

// Tools — mutable collection of Tool.
type Tools interface {
    Get(name string) Tool
    All() []Tool
    Add(tool Tool, caller string)
    Remove(name, caller string)
    Schemas() []ToolSchema
    SetObserver(fn func(ToolsChange))
}

LLM

// LLM — inference. Streams Chunk; final chunk carries the aggregated InferResponse.
type LLM interface {
    Infer(ctx context.Context, req InferRequest) (<-chan Chunk, error)
    InputLimit() int
}

Known Violations

The contracts here are mostly clean (this package is the design intent), but a few items deserve flagging:

  • Rule 1 (small) — Agent has 8 methods. Borderline. The methods cluster into identity (ID/System/Tools), I/O (Inbox/Outbox), state (Messages/SetMessages/Append), and execution (ThinkAct/Run). A clean split would yield AgentIdentity, AgentIO, AgentMessages, AgentRunner — but Agent is the central primitive and downstream code treats it as one cohesive value. Document the trade-off; don't split.
  • Rule 1 — System has 6 methods, Tools has 6. Same trade-off: observer + mutation + query on one type. Acceptable for now.
  • Rule 5 (constructors return concrete types). NewAgent returns Agent (interface). The concrete *agent is unexported, so callers must use the interface — there is no concrete type to return. Acceptable: this is the only place an interface return is the right call, because hiding the implementation is the whole point of the primitive.

Tool (4 methods) and LLM (2 methods) are model-citizen interfaces and need no changes.

Internals

There are no business-logic internals to document — implementations live in internal/agent (for core.Agent), internal/core/system/ (for System), internal/tool/ (for Tool/Tools), and internal/llm/<provider>/ (for LLM).

The single implementation file here is agent_impl.go (the *agent struct backing NewAgent) — kept inside core because the run loop is inseparable from the contract.

Lifecycle

NewAgent panics if LLM, System, or Tools is nil. After construction, callers own the Inbox channel (must close when done sending) and read the Outbox until it closes (agent owns it).

Run returns when the context is cancelled or a SigStop message is received. After Run returns, sending to the inbox blocks indefinitely.

Tests

internal/core/agent_impl_test.go    — agent loop behavior, signals, drains.
internal/core/message_test.go       — message value equality and copying.

See Also