Agent runtime: majordomo in-process, Ollama Cloud config, chat endpoint (#56)
Everything below the run loop already existed. This is the thing that runs a model. The build tag is gone, deliberately. internal/agent's doc comment promised two separations — cmd/pansy not importing the package, and the tool wiring behind //go:build majordomo — and both have been rewritten rather than left as a stale aspiration. A tag that keeps the agent out of the binary only earns its keep if you would ever ship a build without the agent, and the agent is the point; keeping it meant an untagged CI that never compiled the code that matters. majordomo is a real dependency now, resolved from the Gitea instance as a pseudo-version with no replace directive, so the Docker build (which has no sibling checkout) resolves it the same way this machine does. It is stdlib-first and pure Go, so CGO_ENABLED=0 and the single static binary survive. A TURN IS ONE CHANGE SET. That is the whole reason acting without a confirmation prompt is defensible: "empty the garlic bed and plant cucumbers" is one object edit and a dozen planting inserts, and it has to undo as one action rather than thirteen. The scope is opened even for a turn that turns out to be a question, because a change set with no revisions is never written — so asking costs nothing and history isn't littered with empty entries. The model spec goes to majordomo.Parse verbatim. That grammar, including comma-separated failover chains, is majordomo's; re-implementing any of it here would only mean two places to update when it grows. The key needs a bridge though: majordomo's ollama-cloud preset reads OLLAMA_API_KEY while pansy (like gadfly) is configured with OLLAMA_CLOUD_API_KEY, so the provider is registered explicitly on a private registry rather than depending on ambient environment. Runs are bounded by a step cap, a timeout and majordomo's loop guards. This is loop safety, not cost control — pansy is a personal tool and spend caps are explicitly not a v2 concern. A capped run does NOT fail: it kept whatever it managed to do, that work is recorded and undoable, and the reply says it stopped early rather than going silent. The chat endpoint streams. A turn that clears a bed and replants it makes a dozen tool calls over tens of seconds, and without streaming that is a long silence followed by everything at once — which reads as a hang, and defeats a design that rests on watching the canvas change as it happens. Conversations persist per (user, garden). Client-held history would be lost on a refresh, which is exactly when someone reloads to check whether the agent's change landed. Only the user/assistant TEXT is stored, not the model's full transcript: continuity needs what was said and what came back, and replaying a stored tool call would replay a decision made against a garden that has since moved on. It also keeps majordomo's message shape out of the schema. An instance with no key starts, serves the app, and doesn't advertise the agent — the routes aren't registered at all, the same shape as OIDC 404ing when unconfigured. A configured-but-unresolvable model logs and disables the assistant rather than refusing to boot: a garden planner that won't start because of a chat feature is worse than one without chat. Tool refusals reach the model as tool results it can explain, not 500s. The ACL story only works if it can narrate the refusal. Closes #56 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
This commit is contained in:
@@ -19,6 +19,9 @@ const (
|
||||
RegistrationClosed = "closed"
|
||||
)
|
||||
|
||||
// DefaultAgentModel is the assistant's model when PANSY_AGENT_MODEL is unset.
|
||||
const DefaultAgentModel = "ollama-cloud/glm-5.2:cloud"
|
||||
|
||||
// Config is the fully-resolved runtime configuration.
|
||||
type Config struct {
|
||||
// Port is the TCP port the HTTP server listens on (PANSY_PORT, default 8080).
|
||||
@@ -37,6 +40,8 @@ type Config struct {
|
||||
LocalAuth bool
|
||||
// OIDC holds the optional OpenID Connect provider settings.
|
||||
OIDC OIDCConfig
|
||||
// Agent holds the garden-assistant settings.
|
||||
Agent AgentConfig
|
||||
// TrustedProxies is the set of proxy CIDRs/IPs gin trusts for client IP
|
||||
// resolution (PANSY_TRUSTED_PROXIES, comma-separated). Empty trusts none.
|
||||
TrustedProxies []string
|
||||
@@ -51,6 +56,33 @@ type OIDCConfig struct {
|
||||
ButtonLabel string // PANSY_OIDC_BUTTON_LABEL — login-page button text
|
||||
}
|
||||
|
||||
// AgentConfig holds the garden assistant's settings.
|
||||
type AgentConfig struct {
|
||||
// Model is the majordomo model spec, passed VERBATIM to majordomo.Parse
|
||||
// (PANSY_AGENT_MODEL). The grammar is majordomo's, not pansy's — parsing or
|
||||
// validating it here would only mean two places to update when it grows. A
|
||||
// comma-separated spec is a failover chain, so
|
||||
// "ollama-cloud/glm-5.2:cloud,ollama-cloud/kimi-k2.6:cloud" gets a fallback
|
||||
// for free.
|
||||
Model string
|
||||
// OllamaCloudAPIKey authenticates against Ollama Cloud
|
||||
// (OLLAMA_CLOUD_API_KEY — the same secret name gadfly uses, which is why
|
||||
// it isn't majordomo's own OLLAMA_API_KEY; pansy passes it explicitly rather
|
||||
// than relying on ambient environment).
|
||||
OllamaCloudAPIKey string
|
||||
// Enabled turns the assistant on (PANSY_AGENT_ENABLED). Defaults to on when
|
||||
// a key is present, so an instance with no key starts cleanly and simply
|
||||
// doesn't offer the agent — the same shape as OIDC 404ing when unconfigured.
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// Ready reports whether the assistant can actually be offered. Both the route
|
||||
// registration and whatever advertises capabilities gate on this, so what's
|
||||
// advertised always matches what's live.
|
||||
func (a AgentConfig) Ready() bool {
|
||||
return a.Enabled && a.OllamaCloudAPIKey != "" && a.Model != ""
|
||||
}
|
||||
|
||||
// Enabled reports whether enough OIDC config is present to attempt discovery.
|
||||
func (o OIDCConfig) Enabled() bool {
|
||||
return o.Issuer != "" && o.ClientID != ""
|
||||
@@ -87,6 +119,16 @@ func Load() *Config {
|
||||
TrustedProxies: envList("PANSY_TRUSTED_PROXIES"),
|
||||
}
|
||||
|
||||
agentKey := envStr("OLLAMA_CLOUD_API_KEY", "")
|
||||
cfg.Agent = AgentConfig{
|
||||
Model: envStr("PANSY_AGENT_MODEL", DefaultAgentModel),
|
||||
OllamaCloudAPIKey: agentKey,
|
||||
// Default on when a key is present: having configured the key IS the
|
||||
// opt-in, and making people set a second flag to use what they just
|
||||
// configured is a papercut with no upside.
|
||||
Enabled: envBool("PANSY_AGENT_ENABLED", agentKey != ""),
|
||||
}
|
||||
|
||||
if cfg.Registration != RegistrationOpen && cfg.Registration != RegistrationClosed {
|
||||
slog.Warn("config: invalid PANSY_REGISTRATION, defaulting to open", "value", cfg.Registration)
|
||||
cfg.Registration = RegistrationOpen
|
||||
|
||||
Reference in New Issue
Block a user