| package | github.com/genai-io/san/internal/core |
|---|---|
| layer | 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.
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.
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 — 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))
}// 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 — inference. Streams Chunk; final chunk carries the aggregated InferResponse.
type LLM interface {
Infer(ctx context.Context, req InferRequest) (<-chan Chunk, error)
InputLimit() int
}The contracts here are mostly clean (this package is the design intent), but a few items deserve flagging:
- Rule 1 (small) —
Agenthas 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 yieldAgentIdentity,AgentIO,AgentMessages,AgentRunner— butAgentis the central primitive and downstream code treats it as one cohesive value. Document the trade-off; don't split. - Rule 1 —
Systemhas 6 methods,Toolshas 6. Same trade-off: observer + mutation + query on one type. Acceptable for now. - Rule 5 (constructors return concrete types).
NewAgentreturnsAgent(interface). The concrete*agentis 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.
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.
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.
internal/core/agent_impl_test.go — agent loop behavior, signals, drains.
internal/core/message_test.go — message value equality and copying.
- Code:
internal/core/ - Consumer:
packages/agent.md(internal/agentwrapscore.Agent) - Subsystem implementations:
packages/tool.md,packages/llm.md,packages/subagent.md - Layer:
core(seereference/dependency-rules.md)