capuchin

package module
v0.0.0-...-8911077 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

Capuchin SDK

Durable AI agents. Write a tool and a prompt — inherit durability, human approval gating, metering, and replay from the harness. The agent is a Temporal workflow: conversations survive restarts, deploys, and crashes, and risky tool calls pause durably for human approval.

Named for the capuchin — the helper monkey that runs errands and, historically, performed tasks for coins. A Simian Creative project.

Install

brew install capuchinhq/tap/sdk

Or grab a binary from Releases. No npm.

Quickstart

capuchin dev     # one command: embedded Temporal + web UI + agent worker + mock model
capuchin chat    # talk to the demo agent (in another terminal)

capuchin dev needs nothing pre-installed — no Docker, no Node, no API key. It embeds a Temporal dev server (downloaded on first run, state persisted to .capuchin/) and runs the agent worker in-process. Set ANTHROPIC_API_KEY for real model responses; without it a scripted mock keeps the whole loop working offline.

The demo agent handles refunds: lookupOrder runs automatically, issueRefund pauses for your approval in the terminal. Kill capuchin dev while an approval is pending, restart it, approve — the conversation resumes exactly where it was. That's the point.

Write your own agent

capuchin init my-agent && cd my-agent
capuchin dev     # builds and runs YOUR worker, hot reload on save
capuchin chat    # talk to it (another terminal)

An agent is tools + a prompt, in plain Go:

import "capuchin.dev/sdk" // package capuchin

var issueRefund = capuchin.Tool{
	Name:          "issueRefund",
	Input:         capuchin.Schema{"orderId": "string", "amountCents": "number"},
	NeedsApproval: true, // pauses durably for a human
	Run: func(a capuchin.Args, ctx capuchin.Ctx) (string, error) {
		return refund(a.String("orderId"), a.Float("amountCents"))
	},
}

var billing = capuchin.Agent{
	Name:   "billing",
	System: "You handle billing disputes.",
	Tools:  capuchin.Tools{issueRefund},
}

func main() { log.Fatal(capuchin.Serve(billing)) }

Durability, the approval gate, replayable history, and the event stream are inherited — your code is just the tools and the prompt. Building your own agent needs Go 1.24+ (go.dev/dl); the built-in demo doesn't.

Layout

This repo IS the Go module: capuchin.dev/sdk (package capuchin) at the root, the capuchin CLI at cmd/capuchin. go install capuchin.dev/sdk/cmd/capuchin@latest works too.

Development status

Early. The platform is Go-first; a TypeScript SDK follows. This repo is a read-only mirror published from a private monorepo — issues and discussions are welcome here; the code syncs one-way.

License

Apache-2.0

Documentation

Overview

Package capuchin is the Capuchin agent SDK: define tools and agents in plain Go and run them as durable Temporal workflows. Conversations survive restarts and deploys, and any tool marked NeedsApproval pauses durably until a human approves or denies it.

A minimal agent:

var refund = capuchin.Tool{
	Name:          "issueRefund",
	Input:         capuchin.Schema{"orderId": "string", "cents": "int"},
	NeedsApproval: true,
	Run: func(a capuchin.Args, ctx capuchin.Ctx) (string, error) {
		return doRefund(a.String("orderId"), a.Int("cents"))
	},
}

var billing = capuchin.Agent{
	Name:   "billing",
	System: "You handle billing disputes.",
	Tools:  capuchin.Tools{refund},
}

func main() { log.Fatal(capuchin.Serve(billing)) }

Run `capuchin dev` in the project directory to bring up the local stack (embedded Temporal + web UI) and build/run this worker with hot reload; `capuchin chat` talks to the agent.

Index

Constants

View Source
const DefaultAddress = "127.0.0.1:7233"

DefaultAddress is where `capuchin dev` runs Temporal; Serve dials it unless TEMPORAL_ADDRESS overrides.

View Source
const TaskQueue = "capuchin"

TaskQueue is the Temporal task queue every capuchin worker serves.

Variables

This section is empty.

Functions

func ActiveModelLabel

func ActiveModelLabel() string

ActiveModelLabel describes which model adapter the current environment selects — what the `capuchin dev` banner shows.

func Serve

func Serve(agents ...Agent) error

Serve is the agent-project entry point: dial Temporal (TEMPORAL_ADDRESS, or the `capuchin dev` default), serve the given agents, and block until interrupted.

func StartWorker

func StartWorker(c client.Client, agents ...Agent) (worker.Worker, error)

StartWorker registers the harness workflow and activities for the given agents on a non-blocking worker attached to an existing client. Most programs want Serve; the capuchin CLI uses this to host its embedded demo.

Types

type Agent

type Agent struct {
	Name     string // how `capuchin chat <name>` addresses it; defaults to "agent" when serving a single agent
	System   string
	Tools    Tools
	Model    string // optional model override, passed through to the adapter (e.g. "claude-opus-4-8")
	MaxTurns int    // model calls per user message (default 8)
	Price    Price  // marketplace metadata only
}

Agent is tools + a prompt. Everything else — durability, approval gating, metering, replayable history — is inherited from the harness.

type AgentConfig

type AgentConfig struct {
	System         string         `json:"system"`
	Tools          []ToolDef      `json:"tools,omitempty"`
	ApprovalPolicy ApprovalPolicy `json:"approvalPolicy"`
	Injected       map[string]any `json:"injected,omitempty"` // model-hidden per-call context
	ConversationID string         `json:"conversationId"`
	// Model is an opaque pass-through: the workflow copies it into every
	// CallModelInput; the adapter interprets it (and supplies a default when empty).
	Model             string `json:"model,omitempty"`
	MaxTurns          int    `json:"maxTurns,omitempty"`          // model calls per user message
	ApprovalTimeoutMs int64  `json:"approvalTimeoutMs,omitempty"` // pending-approval abandon bound
}

AgentConfig is the uniform agent input contract: tools + a prompt.

type AgentState

type AgentState struct {
	ConversationID   string            `json:"conversationId"`
	Status           string            `json:"status"` // idle|thinking|awaiting_approval|cancelled|abandoned|max_turns
	Messages         []Message         `json:"messages"`
	Events           []Event           `json:"events"`
	TurnSeq          int               `json:"turnSeq"`
	PendingApprovals []PendingApproval `json:"pendingApprovals"`
}

AgentState is the full serializable state — the getState query payload and the workflow return value.

type ApprovalPolicy

type ApprovalPolicy struct {
	Type  string   `json:"type"`
	Tools []string `json:"tools,omitempty"`
}

ApprovalPolicy decides which tool calls pause for a human:

auto      — nothing gates
all       — every tool call gates
allowlist — listed tools run without approval; everything else gates
denylist  — listed tools gate; everything else runs

type ApproveToolInput

type ApproveToolInput struct {
	ToolUseID string `json:"toolUseId"`
	Decision  string `json:"decision"` // approve | deny
	Reason    string `json:"reason,omitempty"`
}

type ApproveToolResult

type ApproveToolResult struct {
	Ok bool `json:"ok"`
}

type Args

type Args map[string]any

Args is a tool call's model-provided input, decoded from JSON.

func (Args) Bool

func (a Args) Bool(k string) bool

Bool returns the named argument as a bool (false if absent or another type).

func (Args) Float

func (a Args) Float(k string) float64

Float returns the named argument as a float64 (JSON numbers decode as float64).

func (Args) Int

func (a Args) Int(k string) int

Int returns the named argument as an int (0 if absent or not a number).

func (Args) String

func (a Args) String(k string) string

String returns the named argument as a string ("" if absent or another type).

type CallModelInput

type CallModelInput struct {
	System         string    `json:"system"`
	Messages       []Message `json:"messages"`
	Tools          []ToolDef `json:"tools,omitempty"`
	TurnSeq        int       `json:"turnSeq"`
	ConversationID string    `json:"conversationId"`
	Model          string    `json:"model,omitempty"` // pass-through from AgentConfig.Model
}

CallModelInput / CallModelResult — the model activity contract. The harness defines the shape; an adapter (mock, Anthropic, …) implements it.

type CallModelResult

type CallModelResult struct {
	Message    Message `json:"message"`
	StopReason string  `json:"stopReason"` // end_turn | tool_use | max_tokens | refusal
}

type ContentBlock

type ContentBlock struct {
	Type      string         `json:"type"`
	Text      string         `json:"text,omitempty"`
	ToolUseID string         `json:"toolUseId,omitempty"`
	Name      string         `json:"name,omitempty"`    // tool_use: which tool
	Input     map[string]any `json:"input,omitempty"`   // tool_use: model-visible args only
	Content   string         `json:"content,omitempty"` // tool_result payload (or deny reason)
	IsError   bool           `json:"isError,omitempty"` // tool_result: error / denied
}

ContentBlock is one span of a message. Go has no unions, so this is a flat struct; Type picks which fields are meaningful ("text" | "tool_use" | "tool_result").

type Ctx

type Ctx struct {
	Context  context.Context
	Injected map[string]any
}

Ctx is the worker-side context a tool runs with. Injected carries model-hidden per-call values (tenant, credentials) — the model can neither see nor forge them; they arrive through the serving seam, never through Args.

type Event

type Event struct {
	Offset     int            `json:"offset"`
	Type       string         `json:"type"` // turn_start|assistant_message|tool_start|tool_end|approval_required|max_turns_reached
	TurnSeq    int            `json:"turnSeq,omitempty"`
	Text       string         `json:"text,omitempty"`
	StopReason string         `json:"stopReason,omitempty"`
	ToolUseID  string         `json:"toolUseId,omitempty"`
	ToolName   string         `json:"toolName,omitempty"`
	Input      map[string]any `json:"input,omitempty"`
	IsError    bool           `json:"isError,omitempty"`
}

Event is one entry in the offset-keyed, replay-identical event log — the resume cursor for clients (the chat REPL polls getEvents(sinceOffset)).

type Message

type Message struct {
	Role    string         `json:"role"`
	Content []ContentBlock `json:"content"`
}

Message is one transcript entry. Roles: user | assistant | tool.

type PendingApproval

type PendingApproval struct {
	ToolUseID string         `json:"toolUseId"`
	ToolName  string         `json:"toolName"`
	Input     map[string]any `json:"input"`
}

PendingApproval surfaces a paused tool call so a client can prompt the human.

type Price

type Price struct {
	Type string  `json:"type"`
	USD  float64 `json:"usd"`
}

Price is marketplace listing metadata. Nothing is billed locally; this is recorded when the agent is pushed to a marketplace.

func PerCall

func PerCall(usd float64) Price

PerCall prices an agent at a flat USD amount per conversation-turn call.

type RunToolInput

type RunToolInput struct {
	Name     string         `json:"name"`
	Input    map[string]any `json:"input"`
	Injected map[string]any `json:"injected,omitempty"`
}

RunToolInput — the tool-dispatch activity contract. Injected params ride here, never in the model-visible Input.

type Schema

type Schema map[string]any

Schema declares a tool's input fields. Values are either a JSON type name ("string", "int", "number", "bool") or a full JSON-schema property map for anything richer. All declared fields are required.

type StartInput

type StartInput struct {
	Agent          string `json:"agent,omitempty"`
	ConversationID string `json:"conversationId"`
}

StartInput starts a conversation: which registered agent (empty = the worker's default), plus the conversation id. The workflow resolves the agent's full config through the getAgentConfig activity as its first step, which pins the config in durable history — replay stays deterministic even after the worker's code changes.

type Tool

type Tool struct {
	Name          string
	Description   string
	Input         Schema
	NeedsApproval bool
	Run           func(Args, Ctx) (string, error)
}

Tool is one capability an agent can call. Run executes worker-side as a Temporal activity, so I/O is fine. NeedsApproval gates the call behind a durable human approve/deny pause.

type ToolDef

type ToolDef struct {
	Name           string         `json:"name"`
	Description    string         `json:"description"`
	InputSchema    map[string]any `json:"inputSchema"`
	InherentlySafe bool           `json:"inherentlySafe,omitempty"`
}

ToolDef is the model-visible tool description plus gating metadata. Handlers are NOT here (functions don't serialize) — they live in the worker's tool registry and are dispatched by name through the runTool activity.

type Tools

type Tools []Tool

Tools is a list of Tool — sugar for agent literals.

type UserMessageResult

type UserMessageResult struct {
	Accepted bool `json:"accepted"`
}

Update payloads/acks.

Directories

Path Synopsis
cmd
capuchin command
The capuchin CLI: scaffold agent projects, run the local durable-agent stack, and talk to agents.
The capuchin CLI: scaffold agent projects, run the local durable-agent stack, and talk to agents.

Jump to

Keyboard shortcuts

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