agentops

package module
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jun 4, 2026 License: MIT Imports: 17 Imported by: 0

README

agentops-go

A small, importable Go library that turns a structured agent/tool error into a filed GitHub ticket and (optionally) tees up a fix PR via a coding agent — Cursor Cloud or a self-hosted container running the Claude Code CLI.

Capture → ticket → fix-PR, automated and reusable across services.

error event ──► Capture ──► fingerprint + dedupe ──► GitHub issue (templated)
                               │
                               └─(if enabled)─► Trigger ──► fix branch ──► PR
                                                  ├─ CursorCloudTrigger (hosted)
                                                  ├─ CommandTrigger → agentops-runner (container)
                                                  └─ NoopTrigger (default: file only)

Install

go get github.com/teslashibe/agentops-go

Quickstart

Requires the gh CLI on PATH and an authenticated session (gh auth login or GH_TOKEN).

cap, _ := agentops.New(agentops.Config{Repo: "teslashibe/smore"})

res, err := cap.Capture(ctx, agentops.ErrorEvent{
    Tool:     "themls_get_json",
    Platform: "themls",
    Code:     "internal_error",
    Message:  "themls: not found",
    Input:    json.RawMessage(`{"path":"/listings/123","token":"sekret"}`), // redacted
})
// res.Action ∈ {created, deduped, reopened}; res.IssueURL points at the ticket.

See examples/quickstart.

User requests → code-aware tickets

Beyond automated tool errors, CaptureRequest turns a free-text bug report or feature request into a ticket and dispatches a Cursor triage agent to enrich it with codebase context. Filing is deterministic + deduped; the agent only enriches the already-filed issue (it never races to create one).

cap.CaptureRequest(ctx, agentops.Request{
    Kind:     agentops.KindBug, // or KindFeature
    Body:     "themls search returns empty for valid MLS numbers",
    Reporter: "dev@local",
})

With EnablePipeline + a Trigger (e.g. CursorCloudTrigger):

  • bug → agent comments affected files, root-cause hypothesis, repro, then opens a fix PR that closes the issue.
  • feature → agent comments affected areas, a design sketch, and acceptance criteria (no PR).

Capture (structured ErrorEvent, templated) and CaptureRequest (free-text, agent-enriched) share the same dedupe + filer.

The ErrorEvent contract

This is the shared schema services emit (and the in-app feedback tools reuse):

type ErrorEvent struct {
    Tool        string          // "themls_get_json"
    Platform    string          // "themls"
    Code        string          // "internal_error"
    Message     string
    Input       json.RawMessage // tool input; sensitive keys auto-redacted
    SessionID   string
    Stack       string
    Labels      []string        // extra labels (defaults + platform added)
    Fingerprint string          // optional override; else derived
    Repo        string          // optional per-event repo override
    Env         map[string]string
    OccurredAt  time.Time
}

Dedupe

Each event gets a stable fingerprint (override via ErrorEvent.Fingerprint, else a digest of platform/tool/code/normalized-message). The message is normalized to drop volatile bits (numbers, ids, quoted values) so the same class of error collapses onto one ticket. The fingerprint is embedded in the issue body as an HTML comment and used to search for prior occurrences:

  • open match → comment "recurred" (no duplicate issue).
  • closed match → reopen + comment, and re-trigger the fix pipeline.
  • no match → file a new issue from the template.

Pipelines (fix PR)

The fix pipeline is gated by Config.EnablePipeline (off by default — file only). Pick a Trigger:

Cursor Cloud (hosted)
agentops.New(agentops.Config{
    Repo:           "teslashibe/smore",
    EnablePipeline: true,
    Trigger: &agentops.CursorCloudTrigger{
        APIKey:       os.Getenv("CURSOR_API_KEY"), // Cloud Agents key
        Model:        "composer-2",
        AutoCreatePR: true,
    },
})

Calls POST https://api.cursor.com/v1/agents, which clones the repo, works the prompt, and opens a PR. The Cloud Agents endpoint needs an agent-specific API key (Dashboard → API Keys), not a generic key.

Self-hosted container (agentops-runner)

cmd/agentops-runner is a containerized executor: it pulls a fresh checkout, runs a coding-agent CLI (Claude Code by default, via codegen-go), commits, pushes, and opens a PR. Wire it via CommandTrigger, or run the container directly:

docker build -f deploy/Dockerfile -t agentops-runner .
docker run --rm \
  -e AGENTOPS_REPO=teslashibe/smore \
  -e AGENTOPS_ISSUE_NUMBER=57 \
  -e GH_TOKEN=$GH_TOKEN \
  -e ANTHROPIC_API_KEY=$ANTHROPIC_API_KEY \
  agentops-runner < prompt.txt

Filer backends

Filer is an interface; two implementations ship:

  • RESTFiler (recommended for servers) — talks to the GitHub REST API with a token; no gh/git binary needed, so it runs in a minimal container:

    agentops.New(agentops.Config{
        Repo:  "teslashibe/smore",
        Filer: agentops.NewRESTFiler(os.Getenv("GH_TOKEN")),
    })
    

    Token needs Issues read/write on the repo (fine-grained PAT: Issues RW; classic: repo).

  • GHFiler (default) — shells out to the gh CLI; convenient for local/CLI use where gh is already authenticated.

Trigger (pipeline) is likewise an interface — swap in your own queue or agent.

Development

make test    # go test ./...
make lint    # go vet ./...
make build   # go build ./...
make runner  # build the container binary

MIT licensed.

Documentation

Overview

Package agentops turns a structured agent/tool error into a filed GitHub ticket and (optionally) tees up a fix PR via a coding agent.

The single entrypoint is Capture, which:

  1. Derives a stable fingerprint for the error (or uses an override).
  2. Dedupes against existing issues so a recurring error re-opens / comments on the original ticket instead of spamming duplicates.
  3. Files a bug issue from a template (title, repro, observed error, environment, labels "auto-filed" + the platform).
  4. Optionally triggers a fix pipeline (a Cursor Cloud agent, a local containerized executor, or any custom Trigger) that branches and opens a PR against an up-to-date pull of the target repo.

The library is importable across services. GitHub filing defaults to the `gh` CLI (GHFiler) and the pipeline defaults to a no-op (NoopTrigger); both are interfaces so callers can swap in their own backends.

Usage:

cap, err := agentops.New(agentops.Config{Repo: "teslashibe/smore"})
if err != nil { /* misconfigured */ }
res, err := cap.Capture(ctx, agentops.ErrorEvent{
    Tool:     "themls_get_json",
    Platform: "themls",
    Code:     "internal_error",
    Message:  "themls: not found",
})

All operations are best-effort from the caller's perspective: Capture returns an error only when it cannot file at all. A failed pipeline trigger never fails the capture — it is reported via Result.PipelineErr.

Index

Constants

This section is empty.

Variables

View Source
var DefaultLabels = []string{"auto-filed", "bug"}

DefaultLabels are applied to every filed ticket.

Functions

func Fingerprint

func Fingerprint(ev ErrorEvent) string

Fingerprint returns the dedupe key for an event: the explicit override when set, otherwise a 16-hex-char digest of platform/tool/code/normalized message. Exported so callers (and tests) can precompute it.

Types

type Action

type Action string

Action describes what Capture did with the event.

const (
	// ActionCreated means a new issue was filed.
	ActionCreated Action = "created"
	// ActionDeduped means an existing open issue was found and commented on.
	ActionDeduped Action = "deduped"
	// ActionReopened means a closed issue matching the fingerprint was
	// re-opened and commented on.
	ActionReopened Action = "reopened"
)

type Capturer

type Capturer struct {
	// contains filtered or unexported fields
}

Capturer files tickets and optionally triggers fix pipelines. Construct with New; safe for concurrent use as long as the Filer/Trigger are.

func New

func New(cfg Config) (*Capturer, error)

New validates cfg and returns a Capturer with defaults applied.

func (*Capturer) Capture

func (c *Capturer) Capture(ctx context.Context, ev ErrorEvent) (Result, error)

Capture files (or dedupes onto) a GitHub ticket for ev and, when the pipeline is enabled, triggers a fix run. It returns an error only when the ticket cannot be filed; a failed pipeline trigger is reported via Result.PipelineErr.

func (*Capturer) CaptureRequest added in v0.3.0

func (c *Capturer) CaptureRequest(ctx context.Context, req Request) (Result, error)

CaptureRequest files (or dedupes onto) a GitHub ticket for a user-submitted bug report or feature request, then — when the pipeline is enabled — dispatches a triage agent to enrich the ticket with codebase context (and, for bugs, open a fix PR). Filing is deterministic and deduped; the agent only ever enriches the already-filed issue. Returns an error only when the ticket cannot be filed.

type CommandTrigger

type CommandTrigger struct {
	// Name is the binary to run (e.g. "agentops-runner", "cursor-agent").
	Name string
	// Args are static arguments prepended to the invocation.
	Args []string
	// Background runs the command detached (fire-and-forget) when true.
	// When false, Trigger blocks until the command exits.
	Background bool
	// contains filtered or unexported fields
}

CommandTrigger launches the fix pipeline by executing an arbitrary command, e.g. the containerized agentops-runner or a `cursor-agent` CLI. The trigger spec is exported to the child process as environment variables:

AGENTOPS_REPO, AGENTOPS_ISSUE_NUMBER, AGENTOPS_ISSUE_URL,
AGENTOPS_FINGERPRINT, AGENTOPS_TITLE

and the prompt is piped to the command's stdin.

func (*CommandTrigger) Trigger

func (t *CommandTrigger) Trigger(ctx context.Context, spec TriggerSpec) (TriggerResult, error)

type Config

type Config struct {
	// Repo is the default target repository ("owner/name"). Required unless
	// every ErrorEvent sets Repo.
	Repo string
	// Filer is the GitHub backend. Defaults to NewGHFiler() (the `gh` CLI).
	Filer Filer
	// Trigger is the fix pipeline. Defaults to NoopTrigger{} (file only).
	Trigger Trigger
	// Labels override DefaultLabels when non-nil (the platform label is
	// always added on top).
	Labels []string
	// EnablePipeline gates the fix pipeline. When false (default) Capture
	// only files/dedupes tickets and never triggers Trigger. This is the
	// "behind a config flag" switch the integration shim toggles.
	EnablePipeline bool
	// RedactBytes caps the rendered tool-input size in the ticket body.
	RedactBytes int
	// DedupeTTL is how long a freshly filed fingerprint is remembered
	// in-process so rapid recurrences dedupe even before GitHub search
	// indexes the issue. Defaults to 5m.
	DedupeTTL time.Duration
	// Now overrides the clock (tests). Defaults to time.Now.
	Now func() time.Time
}

Config configures a Capturer.

type CursorCloudTrigger

type CursorCloudTrigger struct {
	// APIKey is the Cursor Cloud Agents API key. Required.
	APIKey string
	// BaseURL overrides the API host (default https://api.cursor.com).
	BaseURL string
	// Model is the agent model id (e.g. "composer-2"). Empty lets Cursor
	// choose its default.
	Model string
	// StartingRef is the branch the agent forks from (default "main").
	StartingRef string
	// AutoCreatePR asks the agent to open a PR when done (default true).
	AutoCreatePR bool
	// UsePrivateWorker routes the run to a self-hosted worker pool.
	UsePrivateWorker bool
	// HTTPClient overrides the default client (10s connect, no global
	// timeout so long runs aren't cut — context governs cancellation).
	HTTPClient *http.Client
	// RepoURL maps "owner/name" → full clone URL. Empty defaults to GitHub
	// https URLs.
	RepoURLFunc func(repo string) string
	// AuthScheme selects the Authorization header style: "basic" (default,
	// matches the documented v1 `-u KEY:` curl examples) or "bearer".
	AuthScheme string
}

CursorCloudTrigger launches a Cursor Cloud agent against the target repo via the Cloud Agents API. The agent clones the repo at startingRef, works the prompt, and (with AutoCreatePR) opens a PR.

Auth uses a Cursor Cloud Agents API key (Bearer). Note: the cloud-agents endpoint requires an agent-specific key, not a generic dashboard key.

func (*CursorCloudTrigger) Trigger

Trigger creates a Cloud Agent and returns its id/url as the run ref.

type ErrorEvent

type ErrorEvent struct {
	// Tool is the failing tool name, e.g. "themls_get_json".
	Tool string `json:"tool"`
	// Platform is the connected platform / integration, e.g. "themls".
	Platform string `json:"platform"`
	// Code is the machine error code, e.g. "internal_error".
	Code string `json:"code"`
	// Message is the human-readable error message.
	Message string `json:"message"`
	// Input is the (redacted) tool input that produced the error. Sensitive
	// keys are stripped by Capture before the value is written to a ticket.
	Input json.RawMessage `json:"input,omitempty"`
	// SessionID correlates the error to an agent session.
	SessionID string `json:"session_id,omitempty"`
	// Stack is an optional stack trace or diagnostic context.
	Stack string `json:"stack,omitempty"`
	// Labels are extra GitHub labels applied in addition to the defaults
	// ("auto-filed" and "platform:<Platform>").
	Labels []string `json:"labels,omitempty"`
	// Fingerprint overrides the derived dedupe key. Leave empty to derive
	// one from Platform/Tool/Code/normalized Message.
	Fingerprint string `json:"fingerprint,omitempty"`

	// Repo optionally overrides Config.Repo for this event ("owner/name").
	Repo string `json:"repo,omitempty"`
	// Env carries environment metadata rendered into the ticket's
	// Environment section (e.g. "app_version", "commit", "stack").
	Env map[string]string `json:"env,omitempty"`
	// OccurredAt is when the error happened. Zero means "now".
	OccurredAt time.Time `json:"occurred_at,omitempty"`
}

ErrorEvent is the structured tool-error contract shared across services (the same schema feeds the in-app feedback tools). Tool, Platform, Code and Message are the minimum required to file a useful ticket; everything else is optional enrichment.

type Filer

type Filer interface {
	// Find returns the most recent issue (any state) whose body carries the
	// fingerprint marker, or nil when none exists.
	Find(ctx context.Context, repo, fingerprint string) (*Issue, error)
	// Create files a new issue and returns it.
	Create(ctx context.Context, repo string, spec IssueSpec) (*Issue, error)
	// Comment adds a comment to an existing issue.
	Comment(ctx context.Context, repo string, number int, body string) error
	// Reopen transitions a closed issue back to open. A no-op on open issues.
	Reopen(ctx context.Context, repo string, number int) error
}

Filer abstracts the GitHub backend so callers can swap the default `gh` CLI implementation for the REST API, an in-memory fake (tests), or a queue.

type GHFiler

type GHFiler struct {
	// Bin is the gh binary name/path. Empty defaults to "gh".
	Bin string
	// contains filtered or unexported fields
}

GHFiler files issues through the GitHub CLI (`gh`). It requires `gh` on PATH and an authenticated session (GH_TOKEN / `gh auth login`).

func NewGHFiler

func NewGHFiler() *GHFiler

NewGHFiler returns a GHFiler using the `gh` binary on PATH.

func (*GHFiler) Comment

func (g *GHFiler) Comment(ctx context.Context, repo string, number int, body string) error

func (*GHFiler) Create

func (g *GHFiler) Create(ctx context.Context, repo string, spec IssueSpec) (*Issue, error)

Create files a new issue. Labels are best-effort: gh fails the call if a label doesn't exist, so missing labels are created on demand first.

func (*GHFiler) Find

func (g *GHFiler) Find(ctx context.Context, repo, fingerprint string) (*Issue, error)

Find searches issues (any state) for the fingerprint marker embedded in the body, then confirms the exact marker is present before treating it as a match. The confirmation step is essential: `gh --search` is tokenized full-text, so it can surface issues that merely contain similar tokens; a false positive would silently merge an unrelated bug onto the wrong ticket.

func (*GHFiler) Reopen

func (g *GHFiler) Reopen(ctx context.Context, repo string, number int) error

type Issue

type Issue struct {
	Number int    `json:"number"`
	URL    string `json:"url"`
	State  string `json:"state"` // "open" or "closed" (lowercased)
	Title  string `json:"title"`
}

Issue is the minimal view of a GitHub issue the dedupe logic needs.

type IssueSpec

type IssueSpec struct {
	Title  string
	Body   string
	Labels []string
}

IssueSpec is the request to create an issue.

type NoopTrigger

type NoopTrigger struct{}

NoopTrigger does nothing. It is the default so Capture only files tickets unless a real pipeline is wired.

func (NoopTrigger) Trigger

type RESTFiler added in v0.2.0

type RESTFiler struct {
	// Token is the GitHub token. Required.
	Token string
	// BaseURL overrides the API host (e.g. GitHub Enterprise). Defaults to
	// https://api.github.com.
	BaseURL string
	// HTTPClient overrides the default client (10s timeout).
	HTTPClient httpDoer
}

RESTFiler files issues through the GitHub REST API using a token — no `gh` CLI or git binary required, so it works inside a minimal container image. This is the recommended backend for server deployments.

The token needs Issues read/write on the target repo (fine-grained PAT: "Issues" read & write; classic PAT: `repo`). A service-account token is ideal so filings are attributed to a bot.

func NewRESTFiler added in v0.2.0

func NewRESTFiler(token string) *RESTFiler

NewRESTFiler returns a RESTFiler for the public GitHub API.

func (*RESTFiler) Comment added in v0.2.0

func (f *RESTFiler) Comment(ctx context.Context, repo string, number int, body string) error

func (*RESTFiler) Create added in v0.2.0

func (f *RESTFiler) Create(ctx context.Context, repo string, spec IssueSpec) (*Issue, error)

func (*RESTFiler) Find added in v0.2.0

func (f *RESTFiler) Find(ctx context.Context, repo, fingerprint string) (*Issue, error)

Find searches issues for the fingerprint marker, then confirms the literal marker is present in the body (search is tokenized; a rank-1 hit can be a false positive that would otherwise merge unrelated bugs).

func (*RESTFiler) Reopen added in v0.2.0

func (f *RESTFiler) Reopen(ctx context.Context, repo string, number int) error

type Request added in v0.3.0

type Request struct {
	// Kind is "bug" or "feature". Required.
	Kind RequestKind `json:"kind"`
	// Title is an optional short summary. Derived from Body when empty.
	Title string `json:"title,omitempty"`
	// Body is the user's request text. Required.
	Body string `json:"body"`
	// Reporter identifies who submitted it (email / user id), for attribution.
	Reporter string `json:"reporter,omitempty"`
	// SessionID correlates to the originating agent session.
	SessionID string `json:"session_id,omitempty"`
	// Labels are extra GitHub labels (defaults + kind + needs-triage added).
	Labels []string `json:"labels,omitempty"`
	// Fingerprint overrides the derived dedupe key.
	Fingerprint string `json:"fingerprint,omitempty"`
	// Repo optionally overrides Config.Repo for this request ("owner/name").
	Repo string `json:"repo,omitempty"`
	// Env carries metadata rendered into the ticket (e.g. app_version).
	Env map[string]string `json:"env,omitempty"`
	// OccurredAt is when it was submitted. Zero means now.
	OccurredAt time.Time `json:"occurred_at,omitempty"`
}

Request is a free-text user bug report or feature request. Unlike ErrorEvent (structured, machine-emitted), a Request is human prose that a triage agent turns into a code-aware ticket.

type RequestKind added in v0.3.0

type RequestKind string

RequestKind classifies a user-submitted request.

const (
	// KindBug is a bug report; triage enriches the ticket and opens a fix PR.
	KindBug RequestKind = "bug"
	// KindFeature is a feature request; triage enriches the ticket only.
	KindFeature RequestKind = "feature"
)

type Result

type Result struct {
	// Fingerprint is the dedupe key used for this event.
	Fingerprint string `json:"fingerprint"`
	// Action is what happened to the ticket (created/deduped/reopened).
	Action Action `json:"action"`
	// IssueNumber and IssueURL identify the filed/matched issue.
	IssueNumber int    `json:"issue_number"`
	IssueURL    string `json:"issue_url"`
	// PipelineTriggered reports whether a fix pipeline was kicked off.
	PipelineTriggered bool `json:"pipeline_triggered"`
	// PipelineRef is an opaque handle to the triggered run (e.g. a Cursor
	// Cloud agent ID or a PR URL), when available.
	PipelineRef string `json:"pipeline_ref,omitempty"`
	// PipelineErr holds a non-fatal trigger error. A failed trigger never
	// fails Capture; the ticket is still filed.
	PipelineErr string `json:"pipeline_err,omitempty"`
}

Result is the outcome of a Capture call.

type Trigger

type Trigger interface {
	Trigger(ctx context.Context, spec TriggerSpec) (TriggerResult, error)
}

Trigger kicks off the fix pipeline for a filed ticket. Implementations should be non-blocking where possible (enqueue and return a handle) and must never panic; Capture treats trigger errors as non-fatal.

type TriggerResult

type TriggerResult struct {
	Triggered bool
	// Ref is an opaque handle to the run (Cursor Cloud agent ID, PR URL, …).
	Ref string
}

TriggerResult reports the outcome of a pipeline trigger.

type TriggerSpec

type TriggerSpec struct {
	Repo        string // "owner/name"
	IssueNumber int
	IssueURL    string
	Fingerprint string
	Title       string
	// Kind classifies the run: "error" (automated tool-error fix), "bug" or
	// "feature" (user request triage/enrich). Implementations may use it for
	// routing/labels; it is also exported to CommandTrigger child env.
	Kind string
	// OpenPR requests that the agent open a PR (bugs/errors) vs. enrich-only
	// (features). Drives Cursor Cloud's autoCreatePR per call.
	OpenPR bool
	// Prompt is a ready-to-run instruction for the agent. Built by the
	// capturer from the event/request.
	Prompt string
}

TriggerSpec is the payload handed to a pipeline once a ticket is filed.

Directories

Path Synopsis
cmd
agentops-runner command
Command agentops-runner is the containerized fix executor for the agentops-go pipeline.
Command agentops-runner is the containerized fix executor for the agentops-go pipeline.
examples
quickstart command
Quickstart: capture a tool error into a GitHub ticket.
Quickstart: capture a tool error into a GitHub ticket.

Jump to

Keyboard shortcuts

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