Documentation
¶
Overview ¶
ABOUTME: Posts a run's outcome to its thread — diagnosis on failure, adaptive on success (D3).
Package chatops is the transport-neutral core of a chat-shaped Tracker front-end: session-to-run routing (Runner), the human-gate interviewer (ThreadInterviewer over a ThreadUI seam), the event-to-notification filter (notifier), result delivery, durable resume (Store), and free-text intent resolution. A concrete transport (Slack, and later Discord/Teams/Email) supplies a ThreadUI plus its inbound event loop and reuses everything here — so a new chat transport is a ThreadUI + auth, not a rewrite.
See docs/plans/2026-07-21-transport-implementation-plans.md (Phase 0).
ABOUTME: Transport-neutral gate types for the Slack bot's human-gate bridge. ABOUTME: ThreadUI is the seam between gate logic and the Slack Block Kit layer.
ABOUTME: Turns @mention text into a workflow + params (decision D1). ABOUTME: An LLM classifier routes free text onto a built-in workflow; a grammar is the fast-path.
ABOUTME: ThreadInterviewer implements the tracker human-gate interfaces over a Slack thread. ABOUTME: Each gate is posted to the thread and blocks until the thread resolves it.
ABOUTME: Turns a run's pipeline event stream into concise Slack thread updates. ABOUTME: describeEvent is the "what's worth posting" policy (decision D2).
ABOUTME: Runner maps Slack threads to concurrent tracker runs via the RunManager. ABOUTME: OnMention starts a run; OnInteraction routes a reply/click to the run's gate.
ABOUTME: Runner control verbs (help/status/cancel/runs/retry/workflows) and the ABOUTME: live-status registry that lets `status` report a run's in-flight progress.
ABOUTME: The live run status card — one message per run, updated in place as ABOUTME: the pipeline event stream flows, so a thread shows the run happening.
ABOUTME: Persists active runs so they can be resumed after a bot process restart. ABOUTME: A JSON file maps thread_ts → the info needed to re-launch from checkpoint.
ABOUTME: Read-only run snapshots for a transport dashboard (e.g. Slack App Home).
Index ¶
- func NewLLMIntentResolver(client agent.Completer, model string) *llmIntentResolver
- type Deliverable
- type Gate
- type GateAnswer
- type GateKind
- type GrammarResolver
- type Intent
- type IntentResolver
- type PendingClearer
- type RunRecord
- type RunView
- type Runner
- func (r *Runner) ActiveRuns() []RunView
- func (r *Runner) OnInteraction(threadTS, gateID string, answer GateAnswer) bool
- func (r *Runner) OnMention(ctx context.Context, channel, threadTS, text string)
- func (r *Runner) Resume(ctx context.Context, rec RunRecord)
- func (r *Runner) SweepOrphans(keep []RunRecord)
- type RunnerDeps
- type StatusCard
- type StatusNode
- type StatusRenderer
- type Store
- type ThreadInterviewer
- func (s *ThreadInterviewer) Actor() pipeline.Actor
- func (s *ThreadInterviewer) Ask(prompt string, choices []string, def string) (string, error)
- func (s *ThreadInterviewer) AskFreeform(prompt string) (string, error)
- func (s *ThreadInterviewer) AskFreeformWithLabels(prompt string, labels []string, def string) (string, error)
- func (s *ThreadInterviewer) AskInterview(questions []handlers.Question, _ *handlers.InterviewResult) (*handlers.InterviewResult, error)
- func (s *ThreadInterviewer) Cancel()
- func (s *ThreadInterviewer) Resolve(gateID string, ans GateAnswer) bool
- func (s *ThreadInterviewer) SetPipelineContext(ctx context.Context)
- type ThreadUI
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewLLMIntentResolver ¶
Types ¶
type Deliverable ¶
type Deliverable struct {
URL string // a deploy/PR/preview URL surfaced from the run's output
Summary string // a workflow-provided delivery summary (ctx["delivery"])
}
Deliverable describes what a successful run produced, for presentation.
type Gate ¶
type Gate struct {
ID string
Kind GateKind
Prompt string
Choices []string // GateChoice / GateYesNo: selectable labels
Default string // preferred label, if any
}
Gate describes a human decision the pipeline is blocked on, for presentation in a conversation thread. One ThreadInterviewer serves one thread, so a Gate carries no thread id — the ThreadUI it is posted through is already bound to its thread. ID correlates the eventual answer back to the blocked call. Interview gates are decomposed into a sequence of these by the interviewer.
type GateAnswer ¶
type GateAnswer struct {
Choice string // GateChoice / GateYesNo / labeled freeform
Freeform string // GateFreeform / labeled freeform "other"
Canceled bool
}
GateAnswer carries a human's response back to the blocked pipeline. Exactly one field is meaningful per gate kind; Canceled short-circuits both.
type GrammarResolver ¶
type GrammarResolver struct{}
GrammarResolver understands the explicit form "[run] <workflow> [k=v ...]".
type IntentResolver ¶
IntentResolver turns the free text of an @mention into an Intent.
type PendingClearer ¶
type PendingClearer interface {
ClearPending(gateID string)
}
PendingClearer is an optional ThreadUI capability: clear a thread's pending freeform gate when it stops waiting (resolved or abandoned), so a later unrelated reply isn't consumed by a stale gate.
type RunRecord ¶
type RunRecord struct {
ThreadTS string `json:"thread_ts"`
Channel string `json:"channel"`
Workflow string `json:"workflow"`
Params map[string]string `json:"params,omitempty"`
}
RunRecord is what's needed to resume a run after a restart: which thread and channel it belongs to, and which workflow (+ params) it ran. The workdir and checkpoint path are derived deterministically from the thread_ts.
type RunView ¶
type RunView struct {
Key string // external id — a Slack thread_ts
State string // "starting" | "running" | terminal status
}
RunView is a read-only snapshot of an active run, for a transport that renders a standing dashboard (Slack App Home, a web status page). It is decoupled from the engine's *ManagedRun so the transport layer needs no pipeline types.
type Runner ¶
type Runner struct {
// contains filtered or unexported fields
}
Runner maps Slack threads to tracker runs. It owns a RunManager and, per active thread, the ThreadInterviewer that inbound interactions resolve against.
func NewRunner ¶
func NewRunner(rm *tracker.RunManager, deps RunnerDeps) *Runner
NewRunner builds a Runner over an existing RunManager.
func (*Runner) ActiveRuns ¶
ActiveRuns returns a snapshot of every run the Runner currently tracks, sorted by key (RunManager.List sorts). Safe to call from any goroutine.
func (*Runner) OnInteraction ¶
func (r *Runner) OnInteraction(threadTS, gateID string, answer GateAnswer) bool
OnInteraction routes an inbound button/modal/reply to the right run's pending gate, using thread_ts (which run) and gateID (which gate). Returns false if no run/gate matched.
func (*Runner) OnMention ¶
OnMention starts a run for a fresh @mention. thread_ts is the run's identity: the RunManager keys on it, and every message for this run routes by it.
func (*Runner) Resume ¶
Resume re-launches an interrupted run after a restart. Each thread has a deterministic workdir + checkpoint path; because the checkpoint file still exists, launching again replays from it (the engine loads a checkpoint at its configured path automatically). No run-id bookkeeping required.
func (*Runner) SweepOrphans ¶
SweepOrphans removes workdirs under RunsBase that no live store record references — left by a crash between a run's store.remove and its reap. Runs referenced by keep (the current store records, which Resume will replay) are preserved. Best-effort; the state file (not a dir) is skipped.
type RunnerDeps ¶
type RunnerDeps struct {
// NewThreadUI returns a ThreadUI bound to one (channel, thread) — supplied by
// the transport (slack.go) so the runner stays Slack-agnostic.
NewThreadUI func(channel, threadTS string) ThreadUI
// WorkDir is where ResolveSource looks for local .dip files (built-ins
// resolve regardless).
WorkDir string
// RunsBase is the parent directory for per-thread isolated run workdirs
// (base/<sanitized thread_ts>), each holding that run's checkpoint.
RunsBase string
// NewID returns a fresh unique gate id per call.
NewID func() string
// Intent resolves @mention text to a workflow + params. Nil falls back to the
// deterministic grammar ("[run] <workflow> [k=v ...]").
Intent IntentResolver
// Store persists active runs for resume-after-restart. Nil disables it.
Store *Store
// KeepWorkdirs retains a run's workdir after it finishes (for later
// inspection) instead of reclaiming the disk. Default false: reap on
// terminal, bounding disk under sustained multi-run load.
KeepWorkdirs bool
// ConfirmOverUSD requires a human to confirm a run whose expected cost meets
// or exceeds this dollar amount before it starts. 0 disables confirmation.
ConfirmOverUSD float64
// ConfigBase carries provider/budget/backend config; the runner overlays the
// per-run Interviewer, EventHandler, and Params onto a copy of it.
ConfigBase tracker.Config
}
RunnerDeps are the transport-provided hooks and per-run config a Runner needs.
type StatusCard ¶
type StatusCard struct {
Workflow string
State string // "running" | "success" | "fail" | "budget_exceeded" | "validation_overridden"
Nodes []StatusNode
CurrentNode string
DoneCount int
TotalCount int
CostUSD float64
BudgetUSD float64 // 0 = no ceiling
Tokens int
Elapsed time.Duration
}
StatusCard is the transport-neutral snapshot the renderer draws. It is a plain value so a renderer can hold it without racing the tracker.
type StatusNode ¶
StatusNode is one node's lamp in the card.
type StatusRenderer ¶
type StatusRenderer interface {
UpsertStatus(card StatusCard) error
}
StatusRenderer is an optional ThreadUI capability: post-or-update a single status message for the run. A transport that implements it (Slack via chat.update; a future Discord via message edit) gets the live card; one that doesn't just falls back to the discrete notifier posts.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
store is a small JSON-file-backed set of active runs, keyed by thread_ts. A nil *Store is a valid no-op (persistence disabled).
func OpenStore ¶
OpenStore loads (or starts) the store at path. A missing file yields an empty store (fresh start). A *corrupt* file is not silently dropped — that would lose every resumable run without a trace; it is preserved aside and logged loudly, then the bot starts with no resumable runs (an operator can recover the file and restart).
type ThreadInterviewer ¶
type ThreadInterviewer struct {
// contains filtered or unexported fields
}
ThreadInterviewer implements tracker's human-gate interviewer interfaces by presenting each gate in a Slack thread and blocking until the thread resolves it (a button click, modal submit, or reply). One instance serves one run.
It implements the full family (Interviewer, FreeformInterviewer, LabeledFreeformInterviewer, InterviewInterviewer) plus Actor()/Cancel()/ SetPipelineContext, so tracker's human handler picks the richest mode.
func NewThreadInterviewer ¶
func NewThreadInterviewer(ui ThreadUI, newID func() string) *ThreadInterviewer
NewThreadInterviewer builds an interviewer bound to a thread's UI. newID must return a fresh unique id per call (used to correlate answers).
func (*ThreadInterviewer) Actor ¶
func (s *ThreadInterviewer) Actor() pipeline.Actor
Actor marks answers as human-driven for override auditing.
func (*ThreadInterviewer) Ask ¶
Ask presents a choice (or yes/no) gate and returns the chosen label.
func (*ThreadInterviewer) AskFreeform ¶
func (s *ThreadInterviewer) AskFreeform(prompt string) (string, error)
AskFreeform presents an open-ended gate and returns the reply text.
func (*ThreadInterviewer) AskFreeformWithLabels ¶
func (s *ThreadInterviewer) AskFreeformWithLabels(prompt string, labels []string, def string) (string, error)
AskFreeformWithLabels presents selectable labels alongside a freeform "other" escape hatch; a typed reply wins over a selected label.
func (*ThreadInterviewer) AskInterview ¶
func (s *ThreadInterviewer) AskInterview(questions []handlers.Question, _ *handlers.InterviewResult) (*handlers.InterviewResult, error)
AskInterview presents a structured form as a sequence of one-question-at-a-time thread gates (buttons for options / yes-no, a reply for open-ended), matching the TUI's flow and reusing the same button/reply machinery — no Slack modal needed. A cancelled interview returns a Canceled result (not an error) so the pipeline routes on cancellation.
func (*ThreadInterviewer) Cancel ¶
func (s *ThreadInterviewer) Cancel()
Cancel abandons every waiting gate (idempotent). The Slack transport calls it on run teardown; tracker's Engine.Close also calls it.
func (*ThreadInterviewer) Resolve ¶
func (s *ThreadInterviewer) Resolve(gateID string, ans GateAnswer) bool
Resolve delivers a human's answer to the gate identified by gateID. The Slack event loop calls it on a button click / modal submit / reply. Returns false if no such gate is pending (already answered, unknown, or torn down).
func (*ThreadInterviewer) SetPipelineContext ¶
func (s *ThreadInterviewer) SetPipelineContext(ctx context.Context)
SetPipelineContext lets a run cancellation unblock a waiting gate. Guarded because parallel-branch human gates can drive one interviewer concurrently.
type ThreadUI ¶
type ThreadUI interface {
// PostGate renders a gate to the thread; gate.ID correlates the answer.
PostGate(gate Gate) error
// Post sends a plain notification/message to the thread.
Post(text string) error
}
ThreadUI presents gates and messages in a single conversation thread. It is the seam between the transport-neutral gate logic (ThreadInterviewer, runner) and the Slack Block Kit layer (slack.go) — so the logic is testable without a live Slack connection. Implementations are bound to one thread.