build-agent
暂无简介
- License 许可
- MIT
- 所属仓库
- Johnny1110/EVVA
- 最近更新
- 2026-08-04
- Stars
- ★ 2
- Forks
- 3
03 SKILL.md 正文
build-agent Scaffold a Go program that embeds an evva agent with the evva-sdk (pkg/agent)
Use this skill when the user wants to BUILD THEIR OWN agent or app on top of evva — "embed evva in my Go program", "build an agent with the evva-sdk", "scaffold an evva host", "use pkg/agent", "make a custom agent like nono", "how do I host evva". This is about authoring a NEW downstream Go program that imports evva's public pkg/* packages. It is NOT about changing the evva repo itself, and NOT about configuring an already-running evva session (for hooks use the setup-hooks skill; for MCP servers use setup-mcp).
The mental model: a "host"
A program that embeds evva is a host. It owns main(), picks a UI (or none), and constructs an agent from evva's public pkg/* surface — it never imports internal/ (Go forbids that across modules anyway). Everything is wired through one of two constructors plus a handful of Options.
Authoritative API, always: run go doc github.com/johnny1110/evva/pkg/agent (and .../pkg/config, .../pkg/llm, .../pkg/tools, .../pkg/ui, .../pkg/permission) to read the EXACT API of the evva version the host imports. That godoc beats any snippet in this skill — snippets here may lag the version on disk. The full prose reference is docs/contributing/extending.md in the evva repo, and two runnable templates are the source of truth — read whichever matches the user's goal before writing code:
examples/full-host/main.go(~60 lines) — the batteries-included path: interactive TUI, persona catalog +/profileswitching, permission prompts,/resume,/compact, background tasks. Built on the one-callagent.New(Config). It is a SEPARATE Go module (its owngo.modwith areplace), which proves the public surface is self-sufficient.examples/minimal-host/main.go(~150 lines) — the à-la-carte path:agent.NewWithProfileplus a custom provider, a custom tool, and a custom skill, with a JSON event sink and no TUI.
If the evva source isn't on disk (the user is in their own repo), the patterns below are self-contained — scaffold from them directly, and fetch the example files from the evva repository only if you need more detail.
Step 1 — Clarify the goal (use ask_user_question if unclear)
Pin down four things before writing any code:
- Interactive or headless? A TUI app a human drives, or a one-shot / automation that returns text and exits.
- Which LLM provider? A built-in (
anthropic,deepseek,openai,ollama) or a custom one. - Default tools, a kit, or custom? The standard evva catalog, a named kit from
pkg/tools/kits, or bespoke tools. - One persona or several? Just the default
evva, or a custom persona (the evva→nono pattern), possibly several switchable ones.
The answers decide the constructor (Step 2) and which extension points you wire (Step 4). Don't wire anything the goal doesn't call for.
Step 2 — Pick the constructor
| Goal | Constructor | Why |
|---|---|---|
| Full app, fastest path | agent.New(agent.Config{...}, opts...) |
One call wires persona resolution (with an evva fallback), memory, the skill catalog, the permission store + mode, and the approval + question brokers — all from Config. Mirror examples/full-host. |
| Fine-grained / headless / custom wiring | agent.NewWithProfile(profile, opts...) |
À-la-carte: it wires only what you pass. Mirror examples/minimal-host. |
Default to agent.New(Config) unless the user needs à-la-carte control — it is dramatically less code (a full TUI app is ~40 lines).
Step 3 — Scaffold the project
- If it's a new program:
go mod init <module-path>. If the user already has a module, add to it — don't scaffold a competing one. - Add the dependency:
go get github.com/johnny1110/evva@latest(for local development against an evva checkout, use areplacedirective instead — seeexamples/full-host/go.mod). - Blank-import the providers so the built-ins register:
_ "github.com/johnny1110/evva/pkg/llm/builtins". - Write
main.gofrom the matching template below.
3a — Batteries-included host (most users)
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/johnny1110/evva/pkg/agent"
"github.com/johnny1110/evva/pkg/config"
_ "github.com/johnny1110/evva/pkg/llm/builtins" // register anthropic/deepseek/openai/ollama
"github.com/johnny1110/evva/pkg/ui/bubbletea"
)
func main() {
cfg := config.Get()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
tui := bubbletea.New(cfg.AppHome) // the UI is the agent's event sink
ag, err := agent.New(agent.Config{AppConfig: cfg},
agent.WithSink(tui),
agent.WithRootContext(ctx),
)
if err != nil {
fmt.Fprintln(os.Stderr, "host:", err)
os.Exit(1)
}
defer ag.Shutdown()
tui.Attach(ag.Controller()) // ag.Controller() is the ui.Controller view
if err := tui.Run(ctx); err != nil {
fmt.Fprintln(os.Stderr, "host:", err)
os.Exit(1)
}
}
3b — Headless host (automation, no UI)
A non-interactive host has no UI to resolve approval prompts, so it MUST opt out of the gate — otherwise every tool call that needs approval auto-denies and silently fails.
ag, err := agent.NewWithProfile(profile,
agent.WithConfig(cfg),
agent.WithSink(jsonSink{enc: json.NewEncoder(os.Stdout)}), // your event.Sink
agent.WithHeadlessBypass(), // REQUIRED for non-interactive hosts: auto-allow all tools
)
// ... then ag.Controller().Run(ctx, prompt) and consume events from your sink.
WithHeadlessBypass() auto-allows every tool — use ONLY in trusted environments (CI runners, sandboxes, ephemeral VMs). If the host has any approval UI, omit it and wire real approvals via Agent.RespondPermission.
Step 4 — Wire only the extension points the goal calls for
Each bullet is one section of docs/contributing/extending.md — read that section (or go doc the package) for the exact contract.
- Custom LLM provider — implement
pkg/llm.Client(Name,Model,Complete,Stream,Apply), register a factory onllm.DefaultRegistry().MustRegister(name, factory), then install creds:cfg.LLMProviderConfig[name] = config.APIConfig{ApiURL: ..., ApiSecret: os.Getenv(...)}. - Custom tool — implement
pkg/tools.Tool(Name,Description,Schema,Execute). For one agent:agent.WithCustomTool(name, func(s tools.State) (tools.Tool, error) { ... }). Process-wide:toolset.DefaultRegistry().MustRegister(name, factory)in aninit(). - Tool kits — instead of hand-assembling tool-name lists, use
kits.GeneralPurposeKit()/ReadOnlyKit()/CodingKit()/ResearchKit()frompkg/tools/kits; each returns(active, deferred)name slices you canappendto. - Custom persona (evva→nono) —
reg, _ := agent.BuildAgentRegistry(cfg.AppHome), thenreg.Register(agent.AgentDefinition{Name: "nono", WhenToUse: "...", As: []string{"main", "subagent"}, InjectMemory: true, SystemPrompt: "..."}). PassPersonas: reg, Persona: "nono"inagent.Config. Or drop<AppHome>/agents/<name>/{system_prompt.md,tools.yml,meta.yml}on disk —New(Config)loads them automatically. - Custom event sink — implement
pkg/event.Sink(one method,Emit(event.Event)); passagent.WithSink(s). Fan out withevent.Multi{Sinks: [...]}. Type-switch on a payload withe.Payload(). - Custom UI — implement
pkg/ui.UI(Emit,Attach,Run) and drive theController; or just embedpkg/ui/bubbletea. - Permissions —
permission.Load(cfg.WorkDir, cfg.AppHome)for disk rules (New(Config)does this for you). For a programmatic allow/deny policy: buildpermission.NewBroker(), registerpermission.SetOnRequest(func(req) Decision { ... }), passagent.WithPermissionBroker(b). - Custom skills —
reg := skill.NewRegistry(); reg.Add(skill.SkillMeta{Name, Description, BodyFunc}); passagent.WithSkillRegistry(reg). NOTE: an explicit registry SKIPS the disk + bundled auto-load, so include any built-ins you still want. - Custom AppHome / config —
config.Load(config.LoadOptions{AppName, AppHome, WorkDir, ProviderCredentials, EnvAliases, SeedEnvTemplate})for a fully branded app with its own home dir and declarative env wiring. - Hooks / MCP — config-driven via
settings.json;agent.New(Config)auto-loads both. To CONFIGURE them in a running session, the user wants thesetup-hooks/setup-mcpskills, not this one.
Step 5 — What you CANNOT change (say so up front)
These are fixed by design (see extending.md "What you can't change"):
- The set of
pkg/event.Kindconstants — fixed at the evva version you import; a new kind needs a change in evva itself. - The agent loop shape —
iter → LLM call → dispatch tools → fold results → repeatis internal and not configurable. - The bundled sysprompt builders — a custom persona injects its OWN full system prompt (via the
AgentDefinition/NewProfile), it doesn't edit evva's.
If the user needs one of these, the answer is "fork or file an issue", not "import internal/".
Step 6 — Verify it compiles and runs
Never hand the user code you haven't checked:
go build ./...— fix every error before continuing.- If the host is its own module AND embeds the bundled TUI (
pkg/ui/bubbletea), pin the charmbracelet versions to evva's (a different bubbletea major/minor makes the twotea.Programtypes fail to unify at theUI.Runboundary — seeextending.md"Charmbracelet version pinning"), or rungo mod tidyto resolve to evva's transitive pins. - Confirm a provider credential is set (the relevant
*_API_KEYenv var or.env) before the first run, or the first LLM call fails. - Run it:
go run .(TUI) or pipe a prompt to the headless binary. Confirm one full ReAct turn end to end.
Guardrails
- NEVER import
internal/— it won't compile across modules and isn't the supported surface. If a capability looks internal-only, it's almost certainly a public symbol you haven't found yet: check theextending.mdpackage table orgo doc, don't reach in. - Keep the host SMALL. The point of
agent.New(Config)is that ~40 lines buys a full app — don't re-hand-wire what the constructor already does. - Match the user's existing module and Go toolchain; add to their
go.modrather than creating a parallel module. - This skill scaffolds a NEW host program. It does not modify the evva repository, and it is not how you configure a running evva session.