chatops

package
v0.63.13 Latest Latest
Warning

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

Go to latest
Published: Aug 21, 2026 License: MIT Imports: 18 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewLLMIntentResolver

func NewLLMIntentResolver(client agent.Completer, model string) *llmIntentResolver

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 GateKind

type GateKind string

GateKind classifies how a human gate is presented in a thread.

const (
	GateChoice   GateKind = "choice"   // pick one of N labels (buttons)
	GateYesNo    GateKind = "yes_no"   // a fixed Yes/No decision
	GateFreeform GateKind = "freeform" // open-ended text reply
)

type GrammarResolver

type GrammarResolver struct{}

GrammarResolver understands the explicit form "[run] <workflow> [k=v ...]".

func (GrammarResolver) Resolve

func (GrammarResolver) Resolve(_ context.Context, text string) (Intent, error)

type Intent

type Intent struct {
	Workflow string
	Params   map[string]string
}

Intent is a parsed request: which workflow to run and any param overrides.

type IntentResolver

type IntentResolver interface {
	Resolve(ctx context.Context, text string) (Intent, error)
}

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

func (r *Runner) ActiveRuns() []RunView

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

func (r *Runner) OnMention(ctx context.Context, channel, threadTS, text string)

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

func (r *Runner) Resume(ctx context.Context, rec RunRecord)

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

func (r *Runner) SweepOrphans(keep []RunRecord)

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

type StatusNode struct {
	Label string
	State string // "pending" | "active" | "done" | "failed"
}

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

func OpenStore(path string) *Store

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).

func (*Store) List

func (s *Store) List() []RunRecord

list returns the recorded runs, ordered by thread_ts.

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

func (s *ThreadInterviewer) Ask(prompt string, choices []string, def string) (string, error)

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

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.

Jump to

Keyboard shortcuts

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