loops

package
v0.12.0 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package loops will coordinate shared loop orchestration behavior.

Index

Constants

View Source
const (
	FailureKindRetryableAfterResume = "retryable_after_resume"
	FailureKindManualIntervention   = "manual_intervention"

	ResumePolicyAdvanceFromCheckpoint = "advance_from_checkpoint"
	ResumePolicyManualIntervention    = "manual_intervention"
	ResumePolicyReplayStep            = "replay_step"
	ResumePolicyRestartFromDiscover   = "restart_from_discover"
)
View Source
const DiscoveryFingerprintVersion = "v1"

DiscoveryFingerprintVersion is bumped whenever the canonical discovery fingerprint payload changes shape so old persisted fingerprints become non-comparable instead of silently mismatching.

Variables

This section is empty.

Functions

func AppendHumanMessage added in v0.10.0

func AppendHumanMessage(metadataJSON *string, m HumanMessage) (string, error)

AppendHumanMessage queues one human message (trimming to the most recent humanInboxCap), preserving all other metadata keys.

func AppendMilestone added in v0.10.0

func AppendMilestone(metadataJSON *string, m Milestone) (string, error)

AppendMilestone adds one milestone to a loop's metadata (trimming to the most recent milestonesCap), preserving all other keys, and returns the updated JSON. A milestone whose text duplicates the immediately-preceding one is dropped so a retried event doesn't stutter the log.

func CanonicalSortedStrings added in v0.7.2

func CanonicalSortedStrings(values []string) []string

CanonicalSortedStrings returns a copy of values trimmed, lowercased, sorted, and deduplicated. Used to normalize labels/assignees before fingerprinting.

func ClearHITLAsk added in v0.10.0

func ClearHITLAsk(metadataJSON *string) (string, error)

ClearHITLAsk removes the HITL ask state from a loop's metadata JSON.

func ClearHumanInbox added in v0.10.0

func ClearHumanInbox(metadataJSON *string) (string, error)

ClearHumanInbox drops all queued human messages (called after the agent drains them in a turn).

func ClearTakeoverResume added in v0.10.0

func ClearTakeoverResume(metadataJSON *string) (string, error)

ClearTakeoverResume removes the takeover-resume marker from a loop's metadata.

func ComputeDiscoveryFingerprint added in v0.7.2

func ComputeDiscoveryFingerprint(parts ...string) string

ComputeDiscoveryFingerprint produces a stable hash over the supplied parts. Input order is preserved so callers must pass a canonical, role-specific ordering. Slice values are sorted in place by the caller before hashing.

func DebounceSchedule added in v0.11.9

func DebounceSchedule(now time.Time, quietSeconds int, existingAvailableAt time.Time) time.Time

DebounceSchedule computes the next eligible time for quiet-period debounce.

Quiet period settles new actionable signals before work starts:

  • quietSeconds <= 0 → eligible immediately (now)
  • otherwise next = max(existingAvailableAt, now+quiet)

Existing AvailableAt is never shortened by debounce (extend/reset only). Retry backoff and no-op follow-up delays remain separate constraints; callers that need both should take the max of DebounceSchedule and those times.

func IsHardHold added in v0.7.2

func IsHardHold(failureKind, resumePolicy string) bool

func IsManualHoldResumePolicy added in v0.7.2

func IsManualHoldResumePolicy(resumePolicy string) bool

func LastFailedDiscoveryFingerprint added in v0.7.2

func LastFailedDiscoveryFingerprint(metadataJSON *string) string

LastFailedDiscoveryFingerprint returns the persisted fingerprint of the last terminal-failed discovery attempt, or empty string if none. metadataJSON may be nil.

func LockLoopRequeue added in v0.11.0

func LockLoopRequeue(loopID string) func()

LockLoopRequeue acquires the process-wide per-loop requeue mutex shared by:

  • API retry/start/reuse
  • runtime HITL free-text / answer requeues
  • runtime deferred/startup recovery requeues
  • reviewer/fixer discovery enqueue for an existing loop

Callers must unlock via the returned function (typically defer). Nested acquisitions on the same loop from the same goroutine deadlock — do not call requeue helpers while already holding this lock for that loopID. Call order with LockLoopTarget: take the per-loop lock first, then the target lock.

func LockLoopTarget added in v0.11.0

func LockLoopTarget(key string) func()

LockLoopTarget acquires the process-wide same-target mutex. An empty key is a no-op. Callers that also take LockLoopRequeue must acquire the per-loop lock first to avoid deadlocks with API discard+retry.

func LoopTargetGuardKey added in v0.11.0

func LoopTargetGuardKey(projectID, loopType, targetType, targetKey string) string

LoopTargetGuardKey builds the process-wide target mutex key shared by API discard/retry/start/create and runtime recovery/discovery/HITL requeues.

Empty string means no lock (concurrent project-scoped workers are exempt). Pull-request targets omit loop type so fixer/reviewer/worker share one key — they share the managed PR worktree (looper-fix-<project>-pr-N).

func LoopTargetGuardKeyFromRecord added in v0.11.0

func LoopTargetGuardKeyFromRecord(loop storage.LoopRecord) string

LoopTargetGuardKeyFromRecord derives LoopTargetGuardKey from a stored loop. Key formatting matches API loopTargetKeyFromRecordCompat / loopTargetKeyCompat so API and runtime share the same mutex entries.

func MaxTime added in v0.11.9

func MaxTime(left, right time.Time) time.Time

MaxTime returns the later of left and right, treating zero as absent.

func MergeLastFailedDiscoveryFingerprint added in v0.7.2

func MergeLastFailedDiscoveryFingerprint(metadataJSON *string, fingerprint string) (string, error)

MergeLastFailedDiscoveryFingerprint returns a JSON object encoded as a string containing the existing metadata merged with the supplied fingerprint under the autonomousRecovery namespace. The fingerprint may be empty, in which case the field is cleared so the next discovery is allowed to revive the loop.

func NormalizeResumePolicy added in v0.7.2

func NormalizeResumePolicy(failureKind, resumePolicy string) string

func PullRequestTargetGuardKey added in v0.11.0

func PullRequestTargetGuardKey(projectID, repo string, prNumber int64) string

PullRequestTargetGuardKey builds the shared PR worktree target mutex key from project + repo + PR number (loop type omitted). Use when discovery creates or requeues a reviewer/fixer for a PR before a loop record is available.

func ShouldRestartFromDiscover added in v0.7.2

func ShouldRestartFromDiscover(status, resumePolicy string) bool

func ShouldSuppressFailedRediscovery added in v0.7.2

func ShouldSuppressFailedRediscovery(loopStatus, storedFingerprint, currentFingerprint string) bool

ShouldSuppressFailedRediscovery reports whether autonomous discovery should leave a previously-failed loop in failed state instead of reviving it.

The contract is: suppress only when (a) loop is currently failed, (b) we have a recorded last-failed fingerprint, and (c) the current discovery fingerprint matches it exactly.

All other cases (no stored fingerprint, status not failed, mismatched fingerprint) return false so the loop is revived as before.

func SuppressesAutonomousRecovery added in v0.7.2

func SuppressesAutonomousRecovery(failureKind, resumePolicy string) bool

func TargetKeyFromLoopRecord added in v0.11.0

func TargetKeyFromLoopRecord(loop storage.LoopRecord) string

TargetKeyFromLoopRecord returns the canonical target key for a stored loop (project:/issue:/pull_request:...), matching API loopTargetKeyFromRecordCompat.

func WriteHITLAsk added in v0.10.0

func WriteHITLAsk(metadataJSON *string, ask HITLAsk) (string, error)

WriteHITLAsk merges the HITL ask state into a loop's metadata JSON, preserving all other keys, and returns the updated JSON string.

func WriteTakeoverResume added in v0.10.0

func WriteTakeoverResume(metadataJSON *string, tr TakeoverResume) (string, error)

WriteTakeoverResume merges the takeover-resume marker into a loop's metadata, preserving all other keys.

Types

type CreateInput

type CreateInput struct {
	ProjectID    string
	Type         domain.LoopType
	Target       domain.LoopTarget
	Status       domain.LoopStatus
	ConfigJSON   *string
	MetadataJSON *string
}

type HITLAsk added in v0.10.0

type HITLAsk struct {
	Question    string   `json:"question,omitempty"`
	Options     []string `json:"options,omitempty"`
	SessionID   string   `json:"sessionId,omitempty"`
	ExecutionID string   `json:"executionId,omitempty"`
	Vendor      string   `json:"vendor,omitempty"`
	Answer      string   `json:"answer,omitempty"`
	Status      string   `json:"status,omitempty"` // "awaiting" | "answered" | "consumed"
	AskedAt     string   `json:"askedAt,omitempty"`
	AnsweredAt  string   `json:"answeredAt,omitempty"`
	// Transport records how the ask was delivered ("github" | "feishu"). GitHub
	// asks carry the PR + ask-comment id so the answer-poll lane can find the human
	// reply that came after the ask and resolve/re-request on that PR.
	Transport    string `json:"transport,omitempty"`
	PRNumber     int64  `json:"prNumber,omitempty"`
	AskCommentID int64  `json:"askCommentId,omitempty"`

	// The agent's decision brief — research + recommendation surfaced on the ask
	// card so a human can confirm in seconds instead of researching from scratch.
	Recommendation    string            `json:"recommendation,omitempty"`
	RecommendedOption string            `json:"recommendedOption,omitempty"`
	Consequences      map[string]string `json:"consequences,omitempty"`
	Confidence        string            `json:"confidence,omitempty"`
}

HITLAsk is the persisted state of a mid-run human-in-the-loop question. It is written by the runner when the agent asks (Question/Options/SessionID), and the answer is filled in by POST /loops/{seq}/respond. On resume the runner reads Answer + SessionID to continue the same agent session.

func ReadHITLAsk added in v0.10.0

func ReadHITLAsk(metadataJSON *string) (HITLAsk, bool)

ReadHITLAsk extracts the HITL ask state from a loop's metadata JSON. The second return is false when no HITL state is present.

type HumanMessage added in v0.10.0

type HumanMessage struct {
	At   string `json:"at"`
	Text string `json:"text"`
}

HumanMessage is one free-text message a human sent into a loop's thread at any time — a follow-up question, a clarification, a new instruction — queued until the loop's next turn drains it and feeds it to the agent (same session). Unlike a button-click decision, a message does not by itself resolve a pending ask; the agent reads it in context and decides whether to proceed, answer, or re-ask.

func ReadHumanInbox added in v0.10.0

func ReadHumanInbox(metadataJSON *string) []HumanMessage

ReadHumanInbox returns a loop's queued human messages in arrival order.

type Milestone added in v0.10.0

type Milestone struct {
	At   string `json:"at"`   // ISO-8601 timestamp
	Text string `json:"text"` // already-formatted, human-facing (lark_md ok)
}

Milestone is one human-scannable event in a loop's story — a decision, a phase completing, a PR opening — timestamped so the anchor reads as a narrative (who decided what, how long each phase took, the PR link) instead of a single current-status line.

func ReadMilestones added in v0.10.0

func ReadMilestones(metadataJSON *string) []Milestone

ReadMilestones returns a loop's milestone log in chronological order.

type PauseResult

type PauseResult struct {
	Loop                storage.LoopRecord
	CancelledQueueItems int64
}

type Service

type Service struct {
	DB    *sql.DB
	Repos *storage.Repositories
	Now   func() time.Time
}

func (*Service) Create

func (s *Service) Create(ctx context.Context, input CreateInput) (storage.LoopRecord, error)

func (*Service) Get

func (s *Service) Get(ctx context.Context, id string) (*storage.LoopRecord, error)

func (*Service) GetBySeq

func (s *Service) GetBySeq(ctx context.Context, seq int64) (*storage.LoopRecord, error)

func (*Service) List

func (s *Service) List(ctx context.Context) ([]storage.LoopRecord, error)

func (*Service) Pause

func (s *Service) Pause(ctx context.Context, loopID string, reason *string) (PauseResult, error)

func (*Service) Resume

func (s *Service) Resume(ctx context.Context, loopID string) (storage.LoopRecord, error)

func (*Service) Terminate added in v0.9.5

func (s *Service) Terminate(ctx context.Context, loopID string, reason *string) (TerminateResult, error)

func (*Service) TransitionStatus

func (s *Service) TransitionStatus(ctx context.Context, loopID string, input TransitionInput) (storage.LoopRecord, error)

type TakeoverResume added in v0.10.0

type TakeoverResume struct {
	SessionID string `json:"sessionId,omitempty"`
	Prompt    string `json:"prompt,omitempty"`
}

TakeoverResume records that a loop was handed back after an interactive human takeover, carrying the native session id the human drove so the daemon's next worker run resumes THAT session (seeing the human's turns) rather than starting a fresh one. Consumed after one resume.

func ReadTakeoverResume added in v0.10.0

func ReadTakeoverResume(metadataJSON *string) (TakeoverResume, bool)

ReadTakeoverResume returns the pending takeover-resume marker, if any.

type TerminateResult added in v0.9.5

type TerminateResult struct {
	Loop                storage.LoopRecord
	CancelledQueueItems int64
}

type TransitionInput

type TransitionInput struct {
	Status    domain.LoopStatus
	NextRunAt *time.Time
	LastRunAt *time.Time
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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