core

package
v0.0.0-...-22905ba Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 15 Imported by: 0

README

Smoo AI license lom.smoo.ai

pkg.go.dev Go engine


The Go sibling of the Rust reference engine. Agents, tools, knowledge/RAG, memory, checkpointing, human-in-the-loop, cost budgets, and workflows — as one embeddable package. It's the engine, not a notebook demo.

github.com/SmooAI/smooth-operator-core/go/core is the native Go implementation of the Smoo AI agent engine — the in-process observe→think→act loop that powers lom.smoo.ai. It's a sibling of the Rust reference engine and one of the polyglot set (Rust, TypeScript, Python, Go, C#/.NET) whose behavior is held at parity by a shared eval suite.

It's a library, not a client to a remote server: it is the agent, running in your Go process. Every surface is covered by fast, offline tests built on a deterministic MockLlmProvider, so the loop is verified — not vibe-coded.

Install

go get github.com/SmooAI/smooth-operator-core/go/core

The engine is the core package; idiomatic alias core.

Quickstart

A complete agent — no credentials needed — using the deterministic mock provider the engine's own tests run on:

package main

import (
	"context"
	"fmt"

	core "github.com/SmooAI/smooth-operator-core/go/core"
)

func main() {
	provider := core.NewMockLlmProvider().PushText("the answer is 42")
	agent := core.NewSmoothAgent(provider, core.AgentOptions{Instructions: "You are a helpful assistant"})

	res, err := agent.Run(context.Background(), "what is the answer?", nil)
	if err != nil {
		panic(err)
	}
	fmt.Println(res.Text)
}

NewSmoothAgent(client, options) takes a ChatClient (the MockLlmProvider implements it — swap in any OpenAI-compatible client) and an AgentOptions struct. Run(ctx, message, history) — pass nil history for a fresh turn — returns (AgentRunResponse, error); res.Text is the final answer.

Features

The full parity surface — every engine in the polyglot set ships it:

  • Agentic tool-calling loop — observe→think→act, looping until the model answers.
  • Typed tools — register tools the model can call, with parallel dispatch.
  • Knowledge / RAG + vectors — ground the turn in retrieved documents.
  • Memory — long-term entries recalled into context each turn.
  • Compaction — a sliding-window token budget keeps the prompt under a ceiling.
  • Cost / budget — per-model pricing, token + USD accounting, early stop on budget.
  • Checkpointing — persist/resume a conversation via a checkpoint store.
  • Rerank — rerank retrieved hits before injection (lexical reranker built in).
  • Sub-agents / delegation — spawn child agents for sub-tasks.
  • Cast + clearance — roles with per-role tool-access policy.
  • Human-in-the-loop gate — require approval before designated tool calls run.
  • Conversation thread — carry a conversation across multiple Run calls.
  • LlmProvider seam + MockLlmProvider — inject any OpenAI-compatible client; the record/replay mock drives the offline tests.
  • Deferred tools + tool_search — hide rarely-used tool schemas behind a meta-tool the model calls to promote the ones it needs.
  • Typed workflow graph — a generic Workflow[S] node/edge engine alongside the agent loop.
  • Parallel tool calls — dispatch ≥2 tool calls concurrently (transcript order preserved).
  • Retry / backoff — retry transient model-call failures with exponential backoff.
  • Streaming — stream incremental text, tool calls, and tool results as the turn runs.

Streaming

RunStream is the streaming variant of Run: it yields incremental events — text deltas as the model produces them, each tool call before dispatch, each tool result after it finishes, and a terminal done event carrying the same response Run would have returned.

Part of Smoo AI

smooth-operator-core is built and open-sourced by Smoo AI — the AI-powered business platform with AI built into every product: CRM, customer support, campaigns, field service, observability, and developer tools.

License

MIT — see LICENSE.


Built by Smoo AI — AI built into every product.

Documentation

Overview

Package core is the native Go smooth-operator engine — an in-process agentic tool-calling loop over an OpenAI-compatible chat client, with in-memory knowledge grounding. The Go sibling of the Rust reference engine, the C# core, the Python core, and the TypeScript core. See docs/Architecture/Go Core.md.

Index

Constants

View Source
const END = "__end__"

END is the sentinel a conditional router returns to signal termination.

Variables

View Source
var DefaultPricing = map[string]ModelPricing{
	"claude-haiku-4-5":  {InputPerMTok: 1.0, OutputPerMTok: 5.0},
	"claude-sonnet-4-5": {InputPerMTok: 3.0, OutputPerMTok: 15.0},
}

DefaultPricing is approximate (USD / 1M tokens). Override via AgentOptions.Pricing.

Functions

This section is empty.

Types

type AgentOptions

type AgentOptions struct {
	Instructions  string
	Model         string
	MaxIterations int
	MaxTokens     int
	Temperature   float64
	Knowledge     Knowledge
	KnowledgeTopK int
	// Reranker reorders retrieved hits before injection (nil = passthrough).
	Reranker Reranker
	// KnowledgeCandidateK is the pool size retrieved before reranking; when greater
	// than KnowledgeTopK, more docs are fetched, reranked, then trimmed to TopK.
	KnowledgeCandidateK int
	// Memory, if set, recalls relevant facts into context each turn.
	Memory Memory
	// MemoryTopK is how many memory entries to recall per turn (0 = default 4).
	MemoryTopK int
	Tools      []Tool
	// ParallelToolCalls, when true and an assistant turn returns >=2 tool calls,
	// dispatches them concurrently (goroutines + sync.WaitGroup) instead of
	// sequentially. Tool-result messages are still appended in the original
	// ToolCalls order, so the transcript stays deterministic regardless of
	// completion order. Default false preserves the sequential behaviour. Per-tool
	// semantics (clearance, human-gate approval, tool_search promotion, JSON
	// parsing, error handling) are unchanged — only the dispatch loop runs in parallel.
	ParallelToolCalls bool
	// DeferredTools are registered but with their schemas HIDDEN from the model.
	// When any are present, a built-in tool_search meta-tool is advertised in their
	// place; the model calls it to fuzzy-match and promote the ones it needs, which
	// then become visible + dispatchable on subsequent turns. Keeps the tool schema
	// payload small when there are many rarely-used tools. An unpromoted deferred
	// tool is NOT dispatchable.
	DeferredTools []Tool
	// MaxContextTokens is the approximate token budget for the context window.
	// Before each model call, older non-system messages are dropped (sliding
	// window) to stay under it. 0 uses the default (8000); negative disables.
	MaxContextTokens int
	// Budget, if set, stops the turn early once accumulated usage/cost hits it.
	Budget *CostBudget
	// Pricing overrides the per-model cost table (defaults to DefaultPricing).
	Pricing map[string]ModelPricing
	// CheckpointStore, with ConversationID, persists/resumes the conversation.
	CheckpointStore CheckpointStore
	// ConversationID keys the checkpoint store (required to use checkpointing).
	ConversationID string
	// Clearance, if set, gates which tools may be dispatched. A tool the clearance
	// forbids is not executed — a "tool not permitted" result is returned to the
	// model instead. Nil allows every tool (the prior behaviour).
	Clearance *Clearance
	// HumanGate, when set, is asked for approval before running any tool call for
	// which RequiresApproval returns true. A denied call is not executed; the model
	// is told it was denied and can adapt.
	HumanGate HumanGate
	// RequiresApproval reports which tool calls need human approval (e.g. writes /
	// destructive actions), given the tool name and parsed arguments. nil = none.
	// Only consulted when HumanGate is set. Example:
	//
	//	func(name string, _ map[string]any) bool { return name == "delete_record" }
	RequiresApproval func(name string, args map[string]any) bool
	// MaxRetries is the number of ADDITIONAL attempts after the first if the model
	// call returns a transient error (rate-limit, 5xx, dropped connection). 0 (the
	// default) preserves today's behaviour: a single attempt, error returned
	// immediately. Only the model call is retried — never tool execution.
	MaxRetries int
	// RetryBackoff is the base delay for exponential backoff between retries. The
	// wait before retry attempt n (1-indexed) is RetryBackoff * 2^(n-1). The zero
	// value means no real delay (retries fire immediately) — which is what tests use
	// so they don't sleep; production should set a small base such as 200ms.
	RetryBackoff time.Duration
}

AgentOptions configures a SmoothAgent turn. Mirrors the sibling cores' options.

type AgentRunResponse

type AgentRunResponse struct {
	Text       string
	Iterations int
	ToolCalls  int
	Usage      Usage
	CostUSD    float64
	// BudgetExceeded is true if the turn stopped because the cost/token budget was hit.
	BudgetExceeded bool
}

AgentRunResponse is the result of a turn.

type Cast

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

Cast is the registered set of roles a lead can dispatch to. Mirrors the reference engines' Cast.

func NewCast

func NewCast() *Cast

NewCast returns an empty cast.

func (*Cast) Count

func (c *Cast) Count() int

Count is the number of registered roles.

func (*Cast) Get

func (c *Cast) Get(name string) (OperatorRole, bool)

Get returns the role by name and whether it was found.

func (*Cast) IsEmpty

func (c *Cast) IsEmpty() bool

IsEmpty reports whether no roles are registered.

func (*Cast) List

func (c *Cast) List() []OperatorRole

List returns all roles in registration order.

func (*Cast) ListVisible

func (c *Cast) ListVisible() []OperatorRole

ListVisible returns the non-hidden roles in registration order.

func (*Cast) Register

func (c *Cast) Register(role OperatorRole) *Cast

Register adds (or replaces) a role and returns the cast for chaining.

func (*Cast) Sidekicks

func (c *Cast) Sidekicks() []OperatorRole

Sidekicks returns the sidekick roles in registration order.

type ChatChunk

type ChatChunk struct {
	// ContentDelta is an incremental piece of assistant text ("" when this chunk
	// carries no text).
	ContentDelta string
	// ToolCallDeltas are incremental tool-call fragments in this chunk.
	ToolCallDeltas []ToolCallDelta
	// Usage, when non-nil, reports cumulative token usage (gateways send it last).
	Usage *Usage
}

ChatChunk is one streamed chunk from a streaming chat completion — the standard OpenAI streaming chunk shape (the slice the agent reads). Content deltas concatenate into the assistant text; tool-call fragments are assembled by their Index (ID + Function.Name appear when a call first opens, Function.Arguments arrives in fragments). Usage is non-nil on (typically) the final chunk.

type ChatClient

type ChatClient interface {
	Chat(ctx context.Context, req ChatRequest) (ChatResponse, error)
}

ChatClient is the minimal OpenAI-compatible surface the agent needs. The GatewayClient implements it against a live endpoint; tests inject a fake.

type ChatMessage

type ChatMessage struct {
	Role       string
	Content    string
	ToolCalls  []ToolCall
	ToolCallID string // set on role=="tool" messages
}

ChatMessage is one message in the OpenAI-shaped conversation.

type ChatRequest

type ChatRequest struct {
	Model       string
	Messages    []ChatMessage
	Tools       []ToolSpec
	Temperature float64
	MaxTokens   int
}

ChatRequest is a single model call.

type ChatResponse

type ChatResponse struct {
	Content   string
	ToolCalls []ToolCall
	Usage     Usage
}

ChatResponse is the assistant's reply (content and/or tool calls).

func TextResponse

func TextResponse(content string) ChatResponse

TextResponse builds a plain-text ChatResponse (no tool calls). Handy for scripting the mock and for assertions.

func ToolCallResponse

func ToolCallResponse(id, name, arguments string) ChatResponse

ToolCallResponse builds a ChatResponse that requests a single tool call. arguments is the raw JSON-string the model emits for the call's arguments.

func WithUsage

func WithUsage(resp ChatResponse, promptTokens, completionTokens int) ChatResponse

WithUsage returns a copy of resp with the given token usage attached. Handy when scripting the mock so the streaming final chunk / non-streaming Usage is non-zero.

type Checkpoint

type Checkpoint struct {
	ConversationID string
	Messages       []ChatMessage
}

Checkpoint is a saved conversation: its id and the non-system messages so far.

type CheckpointStore

type CheckpointStore interface {
	Save(cp Checkpoint)
	Load(conversationID string) (Checkpoint, bool)
}

CheckpointStore persists and restores conversations by id.

type Clearance

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

Clearance is a tool-access policy for a role. A deny always wins; a non-empty AllowTools is a whitelist; empty allow + empty deny means "all tools". DenyEverything blocks every tool regardless of the lists.

func AllowAllClearance

func AllowAllClearance() Clearance

AllowAllClearance permits every tool (the zero-value default).

func AllowClearance

func AllowClearance(tools ...string) Clearance

AllowClearance whitelists exactly the named tools.

func DenyAllClearance

func DenyAllClearance() Clearance

DenyAllClearance blocks every tool.

func DenyClearance

func DenyClearance(tools ...string) Clearance

DenyClearance blocks the named tools (everything else allowed).

func NewClearance

func NewClearance(allow, deny []string, denyEverything bool) Clearance

NewClearance builds a clearance from explicit allow/deny lists and the deny-everything flag.

func (Clearance) IsAllowed

func (c Clearance) IsAllowed(tool string) bool

IsAllowed reports whether tool is permitted under this clearance.

type CostBudget

type CostBudget struct {
	MaxUSD    float64
	MaxTokens int
}

CostBudget is a ceiling for a turn. A zero field means "unset"; the first non-zero limit that is hit stops the turn.

type CostTracker

type CostTracker struct {
	Usage   Usage
	CostUSD float64
}

CostTracker accumulates usage + cost across a turn's model calls.

func (*CostTracker) Exceeds

func (t *CostTracker) Exceeds(b *CostBudget) bool

Exceeds reports whether the accumulated usage/cost has hit the budget.

func (*CostTracker) Record

func (t *CostTracker) Record(model string, u Usage, pricing map[string]ModelPricing)

Record adds a model call's usage (and its cost, if the model is priced).

type Embedder

type Embedder interface {
	Embed(text string) []float64
}

Embedder turns text into a fixed-length vector.

type FuncTool

type FuncTool struct {
	ToolName string
	Desc     string
	Params   map[string]any
	Fn       func(ctx context.Context, args map[string]any) (string, error)
}

FuncTool wraps a function as a Tool (the AIFunctionFactory analogue).

func (FuncTool) Description

func (t FuncTool) Description() string

func (FuncTool) Execute

func (t FuncTool) Execute(ctx context.Context, args map[string]any) (string, error)

func (FuncTool) Name

func (t FuncTool) Name() string

func (FuncTool) Parameters

func (t FuncTool) Parameters() map[string]any

type GatewayClient

type GatewayClient struct {
	BaseURL    string // e.g. https://llm.smoo.ai/v1
	APIKey     string
	HTTPClient *http.Client
}

GatewayClient is an OpenAI-compatible ChatClient over HTTP — the Go analogue of the other cores' OpenAI SDK client. Points at any /chat/completions endpoint (e.g. the SmooAI gateway) with a Bearer API key.

(Phase 0 uses net/http directly, mirroring how the sibling cores' OpenAI SDKs do their own HTTP. Adopting @smooai/fetch's Go client is a tracked follow-up.)

func NewGatewayClient

func NewGatewayClient(baseURL, apiKey string) *GatewayClient

NewGatewayClient builds a client for the given base URL + key.

func (*GatewayClient) Chat

Chat implements ChatClient.

func (*GatewayClient) ChatStream

func (g *GatewayClient) ChatStream(ctx context.Context, req ChatRequest) (<-chan ChatChunk, error)

ChatStream implements StreamingChatClient: it opens an SSE streaming completion and translates each `data: {...}` line into a ChatChunk on the returned channel. Connect-time failures (request build / HTTP / non-2xx) come back as the error; the channel is closed when the SSE stream ends (`data: [DONE]` or EOF).

type HashEmbedder

type HashEmbedder struct {
	Dim int
}

HashEmbedder is a deterministic, offline feature-hashing embedder. It hashes each token into one of dim buckets (signed) and L2-normalizes. No learned semantics, but a real vector with cosine geometry — docs sharing tokens land near each other.

func NewHashEmbedder

func NewHashEmbedder(dim int) HashEmbedder

NewHashEmbedder returns a HashEmbedder with the given dimension (default 256).

func (HashEmbedder) Embed

func (e HashEmbedder) Embed(text string) []float64

Embed hashes tokens into a normalized vector.

type HumanApprovalRequest

type HumanApprovalRequest struct {
	ToolName  string
	Arguments map[string]any
	Prompt    string
}

HumanApprovalRequest is sent before the agent executes a sensitive/write tool. Mirrors the C# HumanApprovalRequest / the Rust engine's HumanRequest::Confirm.

type HumanApprovalResponse

type HumanApprovalResponse struct {
	Decision HumanDecision
	Reason   string
}

HumanApprovalResponse is the answer to a HumanApprovalRequest. Mirrors the C# HumanApprovalResponse.

func Approve

func Approve() HumanApprovalResponse

Approve builds an approval.

func Deny

func Deny(reason string) HumanApprovalResponse

Deny builds a denial carrying a reason the model will see.

func (HumanApprovalResponse) IsApproved

func (r HumanApprovalResponse) IsApproved() bool

IsApproved reports whether the decision was HumanApproved.

type HumanDecision

type HumanDecision int

HumanDecision is the human's verdict on a tool call that required approval.

const (
	// HumanApproved lets the tool run.
	HumanApproved HumanDecision = iota
	// HumanDenied blocks the tool; the reason is fed back to the model.
	HumanDenied
)

type HumanGate

type HumanGate func(ctx context.Context, request HumanApprovalRequest) (HumanApprovalResponse, error)

HumanGate is the human-in-the-loop seam: the agent consults it before running any tool that AgentOptions.RequiresApproval flags. The implementation IS the pause point — wire it to a UI awaiting a real person, or decide programmatically. Mirrors the C# IHumanGate (here a func, the idiomatic Go seam).

type InMemoryCheckpointStore

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

InMemoryCheckpointStore is a process-local store backed by a map.

func NewInMemoryCheckpointStore

func NewInMemoryCheckpointStore() *InMemoryCheckpointStore

NewInMemoryCheckpointStore creates an empty in-memory store.

func (*InMemoryCheckpointStore) Load

func (s *InMemoryCheckpointStore) Load(conversationID string) (Checkpoint, bool)

Load returns the checkpoint for an id, and whether one was found.

func (*InMemoryCheckpointStore) Save

func (s *InMemoryCheckpointStore) Save(cp Checkpoint)

Save persists a copy of the checkpoint's messages.

type InMemoryKnowledge

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

InMemoryKnowledge is a tiny lexical-overlap knowledge base — Phase-0 parity with the reference engines' in-memory lexical store (not a vector store).

func (*InMemoryKnowledge) Ingest

func (k *InMemoryKnowledge) Ingest(content, source string)

Ingest adds a document to the knowledge base.

func (*InMemoryKnowledge) Query

func (k *InMemoryKnowledge) Query(query string, topK int) []KnowledgeHit

Query returns up to topK documents, ranked by token overlap with the query. When nothing overlaps, the first topK documents are returned anyway so the agent still has context to ground (or honestly decline) against.

type InMemoryMemory

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

InMemoryMemory is a process-local memory pool with lexical-overlap recall.

func (*InMemoryMemory) Recall

func (m *InMemoryMemory) Recall(query string, topK int) []MemoryEntry

Recall returns up to topK entries that share terms with the query, best first.

func (*InMemoryMemory) Remember

func (m *InMemoryMemory) Remember(text string)

Remember adds a fact (blank entries are ignored).

type Knowledge

type Knowledge interface {
	Query(query string, topK int) []KnowledgeHit
}

Knowledge is a retriever: returns the most relevant documents for a query. Both the lexical InMemoryKnowledge and the embedding-backed VectorKnowledge satisfy this, so the agent accepts either.

type KnowledgeHit

type KnowledgeHit struct {
	Content string
	Source  string
	Score   float64
}

KnowledgeHit is one retrieved document with its lexical-overlap score.

type LexicalReranker

type LexicalReranker struct{}

LexicalReranker reorders by query-term coverage normalized by document length: coverage / log2(2 + docTokenCount), so coverage is rewarded but long documents are penalized relative to concise ones with the same coverage. Stable for ties.

func (LexicalReranker) Rerank

func (LexicalReranker) Rerank(query string, hits []KnowledgeHit) []KnowledgeHit

Rerank reorders hits by the normalized-coverage score.

type LlmProvider

type LlmProvider = ChatClient

LlmProvider is the LLM-call seam the agent loop depends on. It is the existing ChatClient interface under a name that names the role — formalizing the seam so the agent loop is unit-testable deterministically, without a live model or network. The GatewayClient already implements it; tests inject a MockLlmProvider.

This keeps backward compatibility: NewSmoothAgent still takes a ChatClient, and any LlmProvider is a ChatClient (and vice versa) since they are the same shape.

type Memory

type Memory interface {
	Remember(text string)
	Recall(query string, topK int) []MemoryEntry
}

Memory is a pool of remembered facts, recalled by relevance to a query.

type MemoryEntry

type MemoryEntry struct {
	Text string
}

MemoryEntry is one remembered fact.

type MockLlmProvider

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

MockLlmProvider is a deterministic LlmProvider for tests. Script the responses it should return (FIFO), drive your code, then assert on Calls. Build it up fluently with PushText / PushToolCall / PushError.

It replaces the ad-hoc fakeClient the tests rolled by hand, and mirrors the BEHAVIOR of the Rust reference's MockLlmClient (record + replay). It is not safe for concurrent use — a turn drives it serially, which is the intended use.

func NewMockLlmProvider

func NewMockLlmProvider() *MockLlmProvider

NewMockLlmProvider returns an empty mock. Script it with the Push* methods.

func (*MockLlmProvider) CallCount

func (m *MockLlmProvider) CallCount() int

CallCount returns the number of requests received.

func (*MockLlmProvider) Calls

func (m *MockLlmProvider) Calls() []ChatRequest

Calls returns every request the mock has received so far, in order.

func (*MockLlmProvider) Chat

Chat implements ChatClient / LlmProvider: it records the request, then replays the next scripted outcome. With an empty script it returns a benign empty text response so loops terminate cleanly.

func (*MockLlmProvider) ChatStream

func (m *MockLlmProvider) ChatStream(_ context.Context, req ChatRequest) (<-chan ChatChunk, error)

ChatStream implements StreamingChatClient: it records the request, then replays the SAME next scripted outcome as Chat, but as chunked deltas. Text is split into a few content-delta chunks; each tool call is split into an opening chunk (ID + name + first half of arguments) and a second chunk with the rest of the arguments (exercising the agent's index-keyed accumulator); a final chunk carries usage. A scripted error is returned synchronously (connect-time), mirroring a failed open.

func (*MockLlmProvider) LastCall

func (m *MockLlmProvider) LastCall() (ChatRequest, bool)

LastCall returns the most recent request and true, or a zero request and false if none have been received.

func (*MockLlmProvider) PushError

func (m *MockLlmProvider) PushError(message string) *MockLlmProvider

PushError queues an error to be returned from the next Chat call.

func (*MockLlmProvider) PushResponse

func (m *MockLlmProvider) PushResponse(resp ChatResponse) *MockLlmProvider

PushResponse queues a raw ChatResponse for the next Chat call.

func (*MockLlmProvider) PushText

func (m *MockLlmProvider) PushText(content string) *MockLlmProvider

PushText queues a plain-text response for the next Chat call.

func (*MockLlmProvider) PushToolCall

func (m *MockLlmProvider) PushToolCall(id, name, arguments string) *MockLlmProvider

PushToolCall queues a single-tool-call response for the next Chat call.

type ModelPricing

type ModelPricing struct {
	InputPerMTok  float64
	OutputPerMTok float64
}

ModelPricing is USD per 1,000,000 tokens, input and output.

func (ModelPricing) Cost

func (p ModelPricing) Cost(u Usage) float64

Cost converts usage to USD at this pricing.

type NodeFn

type NodeFn[S any] func(ctx context.Context, state S) (S, error)

NodeFn transforms state into a new state. It receives a context so nodes can do async/IO work, and may return an error to abort the run.

type NoopReranker

type NoopReranker struct{}

NoopReranker returns the hits unchanged — the zero-cost default.

func (NoopReranker) Rerank

func (NoopReranker) Rerank(_ string, hits []KnowledgeHit) []KnowledgeHit

Rerank returns hits unchanged.

type OperatorRole

type OperatorRole struct {
	Name          string
	Kind          RoleKind
	Instructions  string
	Permissions   Clearance
	MaxIterations int
	// Hidden roles are omitted from ListVisible (still dispatchable by name).
	Hidden bool
}

OperatorRole is a named role in the cast — its kind, instructions, tool clearance, and iteration budget. Mirrors the reference engines' OperatorRole.

func NewOperatorRole

func NewOperatorRole(name string, kind RoleKind, instructions string) OperatorRole

NewOperatorRole builds a role with the reference-engine defaults applied (allow-all clearance, 8 iterations).

type Reranker

type Reranker interface {
	Rerank(query string, hits []KnowledgeHit) []KnowledgeHit
}

Reranker reorders retrieved hits by relevance to the query.

type RoleKind

type RoleKind int

RoleKind is a role's place in a multi-agent cast.

const (
	// RoleLead is the orchestrator that delegates to sidekicks.
	RoleLead RoleKind = iota
	// RoleSidekick is a focused specialist a lead can dispatch a sub-task to.
	RoleSidekick
	// RoleShadow is a passive observer (e.g. for logging/critique); not directly dispatchable.
	RoleShadow
)

type Router

type Router[S any] func(state S) string

Router inspects the current state and returns the next node name (or END).

type SmoothAgent

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

SmoothAgent is a native, in-process agent.

func NewSmoothAgent

func NewSmoothAgent(client ChatClient, options AgentOptions) *SmoothAgent

NewSmoothAgent constructs an agent over an OpenAI-compatible ChatClient.

func (*SmoothAgent) Run

func (a *SmoothAgent) Run(ctx context.Context, message string, history []ChatMessage) (AgentRunResponse, error)

Run executes a single turn. history is prior conversation messages (multi-turn).

func (*SmoothAgent) RunStream

func (a *SmoothAgent) RunStream(ctx context.Context, message string, thread *SmoothAgentThread) (*Stream, error)

RunStream streams a single turn, delivering incremental StreamEvents on the returned channel. It drives the SAME agentic loop as Run (system/knowledge/memory build, seed messages, per-iteration compaction, cost tracking, budget early-stop, deferred-tool specs, clearance + human-gate on dispatch, checkpoint/thread persistence on exit) — but calls the model in STREAMING mode and emits events as work happens:

  • a StreamText event per non-empty content delta as it streams in;
  • a StreamToolCall event per requested tool call, after that iteration's model stream ends, BEFORE the call is dispatched;
  • a StreamToolResult event per tool, after it finishes (in original call order even when ParallelToolCalls runs them concurrently);
  • exactly one terminal StreamDone event carrying the same AgentRunResponse Run would return for the same script.

Error contract (idiomatic Go): the client must implement StreamingChatClient — if it does not, RunStream returns a nil channel and a non-nil error synchronously and runs nothing. Once the turn is running, a model-call error aborts it: the channel is closed WITHOUT a StreamDone and the error is stored, retrievable via the returned *Stream's Err() after the channel drains. So a caller ranges the channel to completion, then checks Err(); a clean turn ends with a StreamDone and Err()==nil.

NOTE: retry-with-backoff (MaxRetries/RetryBackoff) is intentionally NOT applied to the streaming model call — re-running it after a mid-stream failure would re-emit already-yielded chunks. Retry stays scoped to non-streaming Run (see callModel); this mirrors the C# RunStreamingAsync decision.

func (*SmoothAgent) RunThread

func (a *SmoothAgent) RunThread(ctx context.Context, message string, thread *SmoothAgentThread) (AgentRunResponse, error)

RunThread executes a single turn carried by a SmoothAgentThread: the turn is seeded from the thread's messages, and this turn's new user + assistant (+ tool) messages are appended back to the thread before returning. The thread takes precedence over any history as the prior context. Run (single-shot/history) keeps working unchanged.

type SmoothAgentThread

type SmoothAgentThread struct {
	// ID is the stable id for this conversation (the key checkpoints store under).
	ID string
	// contains filtered or unexported fields
}

SmoothAgentThread is a conversation thread: a stable id plus the ordered non-system messages so far.

func NewThread

func NewThread() *SmoothAgentThread

NewThread creates a fresh thread with an auto-generated id.

func NewThreadWithID

func NewThreadWithID(id string) *SmoothAgentThread

NewThreadWithID resumes a thread under a known id (e.g. one recovered from a checkpoint). An empty id falls back to a fresh auto-generated one.

func (*SmoothAgentThread) Add

func (t *SmoothAgentThread) Add(m ChatMessage)

Add appends one message, skipping any system message (rebuilt per-run).

func (*SmoothAgentThread) Extend

func (t *SmoothAgentThread) Extend(msgs []ChatMessage)

Extend appends several messages, skipping any system messages.

func (*SmoothAgentThread) Len

func (t *SmoothAgentThread) Len() int

Len returns the number of messages currently in the thread.

func (*SmoothAgentThread) Messages

func (t *SmoothAgentThread) Messages() []ChatMessage

Messages returns the accumulated history, oldest first (no system prompt).

type Stream

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

Stream is the handle RunStream returns: range Events() to consume the turn's StreamEvents, then call Err() (after the channel drains) to see whether the turn aborted with a model error.

func (*Stream) Err

func (s *Stream) Err() error

Err returns the error that aborted the turn, or nil if it completed cleanly. Call it only after Events() has been fully drained (the channel closed).

func (*Stream) Events

func (s *Stream) Events() <-chan StreamEvent

Events returns the channel of streamed events. It is closed when the turn ends.

type StreamEvent

type StreamEvent struct {
	Kind      StreamEventKind
	Text      string           // StreamText
	Name      string           // StreamToolCall / StreamToolResult
	Arguments string           // StreamToolCall
	Result    string           // StreamToolResult
	Response  AgentRunResponse // StreamDone
}

StreamEvent is one event from RunStream. The Kind field selects which payload fields are populated, mirroring the C# RunStreamingAsync update sequence and the Rust reference engine's event stream:

  • StreamText: Text holds the content delta.
  • StreamToolCall: Name + Arguments hold the requested call.
  • StreamToolResult: Name + Result hold a finished tool's result.
  • StreamDone: Response holds the final AgentRunResponse (the same value Run would return for the same script). Exactly one StreamDone is emitted, last, UNLESS the turn ends in an error (see RunStream's error contract).

type StreamEventKind

type StreamEventKind int

StreamEventKind tags a StreamEvent.

const (
	// StreamText is an incremental assistant content delta as it streams in.
	StreamText StreamEventKind = iota
	// StreamToolCall is a tool call the model requested, emitted once before dispatch.
	StreamToolCall
	// StreamToolResult is a tool's result, emitted after it finishes.
	StreamToolResult
	// StreamDone is the single terminal event, carrying the final AgentRunResponse.
	StreamDone
)

type StreamingChatClient

type StreamingChatClient interface {
	ChatClient
	ChatStream(ctx context.Context, req ChatRequest) (<-chan ChatChunk, error)
}

StreamingChatClient is the OPTIONAL streaming surface. A ChatClient that also implements it can drive RunStream; the GatewayClient and MockLlmProvider both do. ChatStream opens a streaming model call and returns a receive-only channel of chunks. The channel is closed when the stream ends; any error is delivered via the returned error (for connect-time failures) or — for a mid-stream failure — stored and reported as documented by the implementation. Production wires this to the OpenAI `create(..., stream=True)` surface.

type Tool

type Tool interface {
	Name() string
	Description() string
	Parameters() map[string]any
	Execute(ctx context.Context, args map[string]any) (string, error)
}

Tool is a callable the agent may invoke.

func DelegateTool

func DelegateTool(name, description string, child *SmoothAgent) Tool

DelegateTool builds a Tool that delegates a subtask to a child SmoothAgent.

A sub-agent is just a tool backed by another agent: the model calls this tool with a "task" argument, the child agent runs that task, and the child's final reply becomes the tool result — composing with the existing tool loop, no special wiring. The child can have its own instructions, tools, knowledge, etc.

type ToolCall

type ToolCall struct {
	ID        string
	Name      string
	Arguments string // raw JSON
}

ToolCall is a model-requested tool invocation.

type ToolCallDelta

type ToolCallDelta struct {
	Index        int    // which tool call this fragment belongs to
	ID           string // set when the call first opens ("" in later fragments)
	Name         string // set when the call first opens ("" in later fragments)
	ArgsFragment string // a fragment of the JSON arguments to append
}

ToolCallDelta is one tool-call fragment within a streamed chunk.

type ToolSearch

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

ToolSearch drives deferred-tool promotion for one agent run. It implements Tool so the agent can advertise + dispatch it like any other tool. It holds the deferred tools (by name) and the mutable set of promoted names; the agent consults PromotedTools each iteration to decide which deferred schemas are now visible/dispatchable.

func NewToolSearch

func NewToolSearch(deferred []Tool) *ToolSearch

NewToolSearch builds a ToolSearch over the given deferred tools.

func (*ToolSearch) Description

func (s *ToolSearch) Description() string

func (*ToolSearch) Execute

func (s *ToolSearch) Execute(_ context.Context, args map[string]any) (string, error)

Execute fuzzy-matches the query, promotes matches, and returns their schemas as JSON.

func (*ToolSearch) HasDeferred

func (s *ToolSearch) HasDeferred() bool

HasDeferred reports whether any tool was registered deferred (the meta-tool is advertised only then).

func (*ToolSearch) IsPromoted

func (s *ToolSearch) IsPromoted(name string) bool

IsPromoted reports whether a deferred tool has been promoted and is now dispatchable.

func (*ToolSearch) Name

func (s *ToolSearch) Name() string

func (*ToolSearch) Parameters

func (s *ToolSearch) Parameters() map[string]any

func (*ToolSearch) Promote

func (s *ToolSearch) Promote(name string) bool

Promote marks a deferred tool promoted. Returns false if no such deferred tool.

func (*ToolSearch) PromotedTools

func (s *ToolSearch) PromotedTools() []Tool

PromotedTools returns the deferred tools that have been promoted — their schemas join the visible set. Returned in registration order for determinism.

func (*ToolSearch) ToolByName

func (s *ToolSearch) ToolByName(name string) (Tool, bool)

ToolByName resolves a promoted deferred tool for dispatch. Unpromoted deferred tools are invisible (returns nil, false).

type ToolSpec

type ToolSpec struct {
	Name        string
	Description string
	Parameters  map[string]any // JSON Schema
}

ToolSpec is a tool advertised to the model.

type Usage

type Usage struct {
	PromptTokens     int
	CompletionTokens int
}

Usage holds exact token counts reported by the model API.

func (Usage) TotalTokens

func (u Usage) TotalTokens() int

TotalTokens is prompt + completion.

type VectorKnowledge

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

VectorKnowledge is an embedding-backed knowledge store with cosine retrieval.

func NewVectorKnowledge

func NewVectorKnowledge(embedder Embedder) *VectorKnowledge

NewVectorKnowledge constructs a store with the given embedder (default HashEmbedder).

func (*VectorKnowledge) Ingest

func (v *VectorKnowledge) Ingest(content, source string)

Ingest embeds and stores a document.

func (*VectorKnowledge) Query

func (v *VectorKnowledge) Query(query string, topK int) []KnowledgeHit

Query embeds the query and returns up to topK docs by cosine similarity.

type Workflow

type Workflow[S any] struct {
	// contains filtered or unexported fields
}

Workflow is a typed workflow graph: named nodes connected by static/conditional edges. Build with AddNode, AddEdge / AddConditionalEdge, SetEntry, and SetEnd; the builder methods return the receiver so they chain. Run executes the graph.

func NewWorkflow

func NewWorkflow[S any](maxSteps int) *Workflow[S]

NewWorkflow creates an empty workflow with the given max-steps cap (use a value <= 0 for the default of 100).

func (*Workflow[S]) AddConditionalEdge

func (w *Workflow[S]) AddConditionalEdge(from string, router Router[S]) *Workflow[S]

AddConditionalEdge adds a conditional edge whose router picks the next node at runtime. The router returns the target node name, or END to terminate.

func (*Workflow[S]) AddEdge

func (w *Workflow[S]) AddEdge(from, to string) *Workflow[S]

AddEdge adds a static edge from → to.

func (*Workflow[S]) AddNode

func (w *Workflow[S]) AddNode(name string, fn NodeFn[S]) *Workflow[S]

AddNode registers a node under name (used to reference it in edges).

func (*Workflow[S]) Run

func (w *Workflow[S]) Run(ctx context.Context, initialState S) (S, error)

Run executes the workflow from the entry node, returning the final state.

It returns an error if no entry node was set, a referenced node does not exist, a node fails, or the maxSteps cap is exceeded (e.g. an unbroken cycle).

func (*Workflow[S]) SetEnd

func (w *Workflow[S]) SetEnd(from string) *Workflow[S]

SetEnd marks from as terminal — reaching it ends the workflow.

func (*Workflow[S]) SetEntry

func (w *Workflow[S]) SetEntry(name string) *Workflow[S]

SetEntry sets the entry node (first node to execute).

Jump to

Keyboard shortcuts

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