proposal

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 27, 2026 License: Apache-2.0 Imports: 23 Imported by: 0

Documentation

Overview

Package draft contains the open-mode (case-draft) agent runtime: a plan/execute loop that coordinates a planner LLM and parallel sub-agent investigations to produce a CaseProposal for the host (Slack) to render. The plan schema and parsing live in this file.

Index

Constants

View Source
const (
	DefaultPlannerLoopMax  = 8
	DefaultSubAgentLoopMax = 20
)

Default budget values used when the caller passes 0.

Variables

This section is empty.

Functions

This section is empty.

Types

type Handler

type Handler interface {
	// Question renders the planner's terminal question payload. Items can
	// be 1-5 with each item carrying a select / multi-select control.
	Question(ctx context.Context, ssn *model.Session, q QuestionPayload) error
	// Materialize persists / refreshes the CaseProposal for ssn with the given
	// payload. The host validates against the workspace's FieldSchema.
	Materialize(ctx context.Context, ssn *model.Session, m MaterializePayload) error
	// Trace appends one progress line to the host's per-turn trace UI.
	// Used for phase-level updates (planner round start, planner.message).
	// Per-task transitions go through TraceTask instead.
	Trace(ctx context.Context, line string)
	// TraceRound posts or updates a per-round phase line. The first call
	// for a given roundKey posts a fresh thread reply; subsequent calls
	// with the same key REPLACE the prior content of that round's
	// message in place (no append). This collapses the
	// "Planning… → retry → Planning… → action" sequence within one
	// planner round into a single self-updating message instead of a
	// stack of three Slack posts. A new roundKey opens a new message;
	// passing an empty roundKey or empty line is a no-op.
	TraceRound(ctx context.Context, roundKey, line string)
	// RegisterTasks reserves a stable trace block per investigation task
	// before the sub-agents start. The host renders one block per task in
	// the supplied order so subsequent TraceTask calls overwrite that
	// block in place. Block creation is deliberately the host's job —
	// sub-agents only update their line via TraceTask, never create new
	// blocks. Calling RegisterTasks with an empty slice is a no-op.
	RegisterTasks(ctx context.Context, tasks []TaskInfo)
	// TraceTask updates the line associated with a task that was previously
	// registered via RegisterTasks. Calling with an unknown taskID is a
	// no-op; calling before RegisterTasks is also a no-op (the host has no
	// block to address). The line replaces the prior content for that task
	// — there is no append.
	TraceTask(ctx context.Context, taskID, line string)
	// PostBusy notifies the user that another turn is running on this
	// session and the new trigger is being dropped.
	PostBusy(ctx context.Context, ssn *model.Session, info agent.BusyInfo) error
}

Handler is the host-side surface the draft runtime calls into for all Slack-side side effects. The runtime never touches the Slack service directly; the host implements this interface and renders the terminal action / busy / trace into Slack messages.

PostMessage was retired with the post_message planner action: when the planner needs user input, it always uses Question instead. Internal fallback (planner budget exhausted, internal errors) is signalled via Result.Status=StatusFallback so the host can render whatever copy fits.

type HandlerFuncs

type HandlerFuncs struct {
	QuestionFn      func(ctx context.Context, ssn *model.Session, q QuestionPayload) error
	MaterializeFn   func(ctx context.Context, ssn *model.Session, m MaterializePayload) error
	TraceFn         func(ctx context.Context, line string)
	TraceRoundFn    func(ctx context.Context, roundKey, line string)
	RegisterTasksFn func(ctx context.Context, tasks []TaskInfo)
	TraceTaskFn     func(ctx context.Context, taskID, line string)
	PostBusyFn      func(ctx context.Context, ssn *model.Session, info agent.BusyInfo) error
}

HandlerFuncs is a struct-of-funcs adapter for tests / one-off wiring, letting callers supply only the methods they care about. Missing entries are treated as no-ops (or returning nil for methods that return error).

func (HandlerFuncs) Materialize

func (h HandlerFuncs) Materialize(ctx context.Context, ssn *model.Session, m MaterializePayload) error

func (HandlerFuncs) PostBusy

func (h HandlerFuncs) PostBusy(ctx context.Context, ssn *model.Session, info agent.BusyInfo) error

func (HandlerFuncs) Question

func (h HandlerFuncs) Question(ctx context.Context, ssn *model.Session, q QuestionPayload) error

func (HandlerFuncs) RegisterTasks

func (h HandlerFuncs) RegisterTasks(ctx context.Context, tasks []TaskInfo)

func (HandlerFuncs) Trace

func (h HandlerFuncs) Trace(ctx context.Context, line string)

func (HandlerFuncs) TraceRound

func (h HandlerFuncs) TraceRound(ctx context.Context, roundKey, line string)

func (HandlerFuncs) TraceTask

func (h HandlerFuncs) TraceTask(ctx context.Context, taskID, line string)

type MaterializePayload

type MaterializePayload struct {
	WorkspaceID       string
	Title             string
	Description       string
	CustomFieldValues map[string]any
	// IsTest marks the proposed case as a test case. Defaults to false.
	IsTest bool
}

MaterializePayload is the pure-domain shape passed to Handler.Materialize.

type QuestionItem

type QuestionItem struct {
	// ID uniquely identifies the question within the payload; the host
	// uses it to correlate answers back when the user submits.
	ID string
	// Text is the prompt shown to the user.
	Text string
	// Type discriminates the answer control (select / multi_select /
	// free_text).
	Type QuestionItemType
	// Options is the allowed answer set for select / multi_select
	// (always ≥2 entries). Ignored for free_text.
	Options []string
}

QuestionItem is one question within QuestionPayload.Items.

type QuestionItemType

type QuestionItemType string

QuestionItemType is the host-rendering hint for a question item.

const (
	// QuestionItemSelect is a single-choice picker.
	QuestionItemSelect QuestionItemType = "select"
	// QuestionItemMultiSelect is a multi-choice picker.
	QuestionItemMultiSelect QuestionItemType = "multi_select"
	// QuestionItemFreeText is a multiline plain-text input. Reserved
	// for the last-resort case where neither investigation nor a
	// closed-list classification can capture what we need from the
	// user. See prompts/planner.md for the policy.
	QuestionItemFreeText QuestionItemType = "free_text"
)

type QuestionPayload

type QuestionPayload struct {
	// Reason explains the information gap (single rationale shared across
	// all items).
	Reason string
	// Items is the ordered list of questions to ask in this turn. Always
	// non-empty (validation guarantees ≥1).
	Items []QuestionItem
}

QuestionPayload is the pure-domain shape passed to Handler.Question.

type Result

type Result struct {
	Status Status
	// EndedWith is the SessionEndReason recorded on Session when the turn
	// hit a terminal action. Zero-valued for StatusBusy / StatusIdempotent.
	EndedWith model.SessionEndReason
	// FallbackReason describes why StatusFallback was returned (e.g.
	// "planner budget exhausted"). Non-empty only when Status==Fallback.
	FallbackReason string
}

Result is the outcome of RunTurn.

type Status

type Status int

Status discriminates the terminal shapes RunTurn can return to the host.

const (
	// StatusCompleted means the turn ran end-to-end and the host has been
	// called with a terminal action (Question or Materialize).
	StatusCompleted Status = iota
	// StatusBusy means another turn was running on this Session;
	// Handler.PostBusy was invoked. The host should not re-post on the
	// same trigger.
	StatusBusy
	// StatusIdempotent means the trigger duplicates a turn already in
	// flight (Slack event re-delivery). Drop silently.
	StatusIdempotent
	// StatusFallback means the planner exhausted its budget or hit an
	// internal error before reaching a terminal action. The host should
	// post a system fallback message (e.g. "I couldn't reach a conclusion;
	// please mention me again with more context"). The runtime does NOT
	// post anything itself in this case.
	StatusFallback
)

type TaskInfo

type TaskInfo struct {
	ID    string
	Title string
}

TaskInfo describes one investigation task at the moment its trace block is being reserved. Title is the short, ID-free label the host renders; it should already be human-readable (the planner is asked to keep it under ~40 characters in planner.md).

type Trigger

type Trigger int

Trigger discriminates how the host invoked RunTurn for a given Session. The planner prompt may use it (e.g. WSSwitch should yield materialize without further investigation).

const (
	// TriggerAppMention — the user @-mentioned the bot.
	TriggerAppMention Trigger = iota
	// TriggerThreadReply — the user replied in the thread without a mention,
	// while the prior turn ended on action=question.
	TriggerThreadReply
	// TriggerWSSwitch — the user switched the active workspace via the
	// preview UI, requiring a re-materialise on the existing proposal.
	TriggerWSSwitch
)

type TurnRequest

type TurnRequest struct {
	// Session is the per-thread Session row. The runtime reads / writes
	// the turn-lock fields via deps.StartTurn.
	Session *model.Session

	// UserInput is the planner's first user message. For app_mention this
	// is the mention text; for thread_reply it is the reply text; for
	// ws_switch it is a synthetic system event sentence (see spec §2.11).
	UserInput string

	// Trigger discriminates the entry point — drives prompt hints.
	Trigger Trigger

	// TriggerTS is the Slack TS used as both the trace ID and the lock
	// trigger key. For ws_switch, the host generates a UUID and uses it
	// here.
	TriggerTS string

	// ActorUserID is the Slack user who initiated the turn. Pass to
	// sub-agent tools that need actor authorisation.
	ActorUserID string

	// ExistingProposal is the prior draft persisted by the host (when this
	// turn is resuming an existing draft, e.g. ws-switch or thread reply).
	// The planner does not consume it directly — the host uses it to keep
	// preview state coherent across turns.
	ExistingProposal *model.CaseProposal

	// Handler implements the host-side terminal action dispatchers and
	// trace updates.
	Handler Handler
}

TurnRequest is the input for one open-mode turn.

type UseCase

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

UseCase orchestrates a single open-mode (case-draft) turn: planner LLM loop → parallel sub-agent investigations → terminal action via Handler. Construction is via New, which validates the configurable budget knobs and embeds the shared agent CommonDeps.

func New

func New(deps *agent.CommonDeps, plannerLoopMax, subAgentLoopMax int) (*UseCase, error)

New builds a proposal.UseCase.

plannerLoopMax / subAgentLoopMax are the budget knobs. They are caller-controlled — the recommended defaults are 8 / 20 wired via CLI flags. Pass 0 to use the package defaults. There is no per-turn total sub-agent count: the round-count limit (plannerLoopMax) plus the per-sub-agent budget (subAgentLoopMax) are the only knobs, with per-round fan-out bounded by plan validation.

func (*UseCase) PlannerLoopMax

func (uc *UseCase) PlannerLoopMax() int

PlannerLoopMax / SubAgentLoopMax expose the active budget so callers (e.g. tests) can assert on the wired configuration.

func (*UseCase) RunTurn

func (uc *UseCase) RunTurn(ctx context.Context, req TurnRequest) (*Result, error)

RunTurn drives one open-mode planner round-trip on the supplied Session. The flow is:

  1. Acquire the per-thread turn lock (Phase A) and start the heartbeat goroutine. Return StatusBusy / StatusIdempotent if appropriate.
  2. Build a planner gollem agent (no tools, JSON response schema, WithLoopLimit(1)) bound to Session.ID via WithHistoryRepository.
  3. Loop while the planner budget allows: call agent.Execute with the next user input (initial UserInput on round 1, formatted observations after each investigate phase), parse + validate the JSON plan, and either continue (investigate) or invoke a terminal Handler method.
  4. When the planner budget is exhausted without a terminal action, fall back to a PostMessage with the "couldn't reach a conclusion" copy.

Errors from validation / planner / sub-agent failures are surfaced; the caller decides whether to post a Slack-side error message.

func (*UseCase) SubAgentLoopMax

func (uc *UseCase) SubAgentLoopMax() int

Jump to

Keyboard shortcuts

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