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
- type Handler
- type HandlerFuncs
- func (h HandlerFuncs) Materialize(ctx context.Context, ssn *model.Session, m MaterializePayload) error
- func (h HandlerFuncs) PostBusy(ctx context.Context, ssn *model.Session, info agent.BusyInfo) error
- func (h HandlerFuncs) Question(ctx context.Context, ssn *model.Session, q QuestionPayload) error
- func (h HandlerFuncs) RegisterTasks(ctx context.Context, tasks []TaskInfo)
- func (h HandlerFuncs) Trace(ctx context.Context, line string)
- func (h HandlerFuncs) TraceRound(ctx context.Context, roundKey, line string)
- func (h HandlerFuncs) TraceTask(ctx context.Context, taskID, line string)
- type MaterializePayload
- type QuestionItem
- type QuestionItemType
- type QuestionPayload
- type Result
- type Status
- type TaskInfo
- type Trigger
- type TurnRequest
- type UseCase
Constants ¶
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) 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) TraceRound ¶
func (h HandlerFuncs) TraceRound(ctx context.Context, roundKey, 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 ¶
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 ¶
PlannerLoopMax / SubAgentLoopMax expose the active budget so callers (e.g. tests) can assert on the wired configuration.
func (*UseCase) RunTurn ¶
RunTurn drives one open-mode planner round-trip on the supplied Session. The flow is:
- Acquire the per-thread turn lock (Phase A) and start the heartbeat goroutine. Return StatusBusy / StatusIdempotent if appropriate.
- Build a planner gollem agent (no tools, JSON response schema, WithLoopLimit(1)) bound to Session.ID via WithHistoryRepository.
- 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.
- 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.