planexec

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package planexec hosts the reusable plan-and-execute loop shared by the proposal (case-draft) host and the planexec-strategy Job host.

The loop is `plan → executePhase → replan → ... → final` and follows the vocabulary established by secmon-lab/warren's `pkg/usecase/chat/bluebell`: the planner emits a `Plan` (a list of `TaskPlan` to run in parallel), the sub-agents fan out via `executePhase`, and the planner is asked again (`replan`) with the observations. The loop exits when replan returns no further tasks and no question, after which the runtime invokes `generateFinalResponse` for the host-visible terminal output. No "terminal action" discriminator is involved.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BudgetConfig

type BudgetConfig struct {
	// PlannerLoopMax bounds the number of planner (or replan) LLM
	// invocations (rounds) within one run. Planner / replan output that fails
	// validation is retried within the same pool. (Final-output regeneration in
	// Run[T] is bounded separately by finalOutputMaxRetry and does NOT consume
	// planner rounds.)
	PlannerLoopMax int

	// SubAgentLoopMax bounds the inner gollem loop limit of each
	// sub-agent (passed through to gollem.WithLoopLimit). Granted fresh to
	// every sub-agent, so it recovers per round.
	SubAgentLoopMax int
}

BudgetConfig is the per-turn LLM call ceiling configuration handed to the Runner. All values are caller-controlled so the surrounding wiring (CLI flags) can vary them per environment without re-importing constants.

The budget model is two controls — there is deliberately NO running "total sub-agent task count" across a turn (see .claude/rules/architecture.md → "Agent runtime vocabulary" / "Budget"):

  • PlannerLoopMax bounds the number of rounds in a turn.
  • SubAgentLoopMax is the per-sub-agent budget, granted fresh every round (so the sub-agent budget recovers per round).

Per-round fan-out is already bounded by plan validation (≤ maxTasksPerPhase tasks per phase), so total sub-agent work is naturally bounded by PlannerLoopMax × maxTasksPerPhase × SubAgentLoopMax without a separate total cap.

func (*BudgetConfig) Validate

func (c *BudgetConfig) Validate() error

Validate enforces required-field invariants for BudgetConfig. Every value must be positive; zero / negative budgets would silently produce no work.

type DirectPlan

type DirectPlan struct {
	// Tools is the subset of RunRequest.KnownToolIDs the direct agent may
	// call. May be empty for a pure conversational reply that needs no tool.
	// Bounded by maxToolsPerTask.
	Tools []string `json:"tools,omitempty"`
}

DirectPlan is the round-1 "answer directly" payload. It carries only the tools the single direct ReAct agent is permitted to call; everything else about the direct path (system prompt, history, loop limit) is supplied by the runtime, and the response is always plain text — structured-final generation (Run[T]) is not consulted on this path.

func (*DirectPlan) Validate

func (d *DirectPlan) Validate(knownToolIDs []string) error

Validate enforces DirectPlan invariants. knownToolIDs is the host-supplied allowlist (RunRequest.KnownToolIDs); every entry in Tools must be a member. An empty Tools list is allowed — a direct reply need not call any tool.

type FinalizePlan

type FinalizePlan struct {
	// Reason is a 1-sentence rationale for terminating now (optional).
	Reason string `json:"reason,omitempty"`
}

FinalizePlan is the planner's explicit "I'm done" declaration. It carries an optional short rationale; the actual user-visible output is produced by the entry point (final text, or the validated structured object) after the loop exits.

type PhaseSummary

type PhaseSummary struct {
	Phase   int
	Tasks   []TaskPlan
	Results []TaskResult
}

PhaseSummary aggregates the results of one executePhase invocation so the planner has structured observations on the next round.

type PlanInfo

type PlanInfo struct {
	// Round is 1-based.
	Round int
	// Reasoning is the planner's `message` field (1-2 sentence rationale).
	Reasoning string
	// IsReplan is true when this round was a replan (i.e. Round > 1).
	IsReplan bool
	// Direct is true when this round chose the direct (no-investigation)
	// fast path instead of proposing tasks. Round is 1 in this case and no
	// PhaseStarted / TaskProgress events follow.
	Direct bool
}

PlanInfo is the per-round summary emitted via Sink.PlanProposed. The fields are deliberately minimal: the host renders a status card, not a full plan replay.

type PlanResult

type PlanResult struct {
	// Message is a 1-2 sentence rationale shown to the user via
	// Sink.PlanProposed.
	Message string `json:"message,omitempty"`
	// Tasks is the parallel investigation phase emitted by the planner.
	// Empty / omitted when Direct is set — the two are mutually exclusive.
	Tasks []TaskPlan `json:"tasks,omitempty"`
	// Direct, when non-nil, signals the planner judged the request trivial
	// enough to answer without any investigation phase. The nil Direct (the
	// common case) means "investigate via Tasks". Mutually exclusive with
	// Tasks; rejected unless RunRequest.AllowDirect is true.
	Direct *DirectPlan `json:"direct,omitempty"`
}

PlanResult is the parsed shape of the first planner-round JSON output. The planner must choose exactly one of two shapes:

  • Tasks: the parallel investigation phase (the default path). At least one task is required; if the host wants the planner to terminate after a phase, that is a replan-round concern.
  • Direct: skip investigation entirely and answer the user directly (round-1 fast path). Only valid when the host set RunRequest.AllowDirect.

type Question

type Question struct {
	// Reason is the rationale shared across every item ("why am I
	// asking?").
	Reason string `json:"reason"`
	// Items is the ordered list of questions to ask (1..5 items,
	// enforced by Validate).
	Items []QuestionItem `json:"items"`
}

Question is the host-facing payload when the planner needs human input. proposal forwards it to the Slack question UI; the job host has AllowQuestion=false and therefore never sees one.

func (*Question) Validate

func (q *Question) Validate() error

Validate enforces Question invariants. Called from Validate on the containing ReplanResult.

type QuestionAnswer

type QuestionAnswer struct {
	ID       string   `json:"id"`
	Choice   string   `json:"choice,omitempty"`    // select
	Choices  []string `json:"choices,omitempty"`   // multi_select
	FreeText string   `json:"free_text,omitempty"` // free_text
}

QuestionAnswer is the host's reply payload for one QuestionItem.

type QuestionItem

type QuestionItem struct {
	ID      string           `json:"id"`
	Text    string           `json:"text"`
	Type    QuestionItemType `json:"type"`
	Options []string         `json:"options,omitempty"`
}

QuestionItem is one question within Question.Items.

func (*QuestionItem) Validate

func (i *QuestionItem) Validate() error

Validate enforces QuestionItem invariants. The free_text exemption applies: free_text items skip the Options ≥2 rule and ignore any supplied options as a discardable hint.

type QuestionItemType

type QuestionItemType string

QuestionItemType discriminates how the host should render the answer control. Closed-list types (select / multi_select) require non-empty Options; free_text is the last-resort prose-input shape and Options is ignored. The planner is told to prefer the closed-list types — see prompts/planner.md for the policy.

const (
	QuestionItemSelect      QuestionItemType = "select"
	QuestionItemMultiSelect QuestionItemType = "multi_select"
	QuestionItemFreeText    QuestionItemType = "free_text"
)

type QuestionResult

type QuestionResult struct {
	// Terminate, when true, signals planexec.Runner to stop the loop
	// immediately and return RunStatus=Completed without invoking the
	// final-response phase. Used by proposal to defer the conversation
	// to the next thread reply.
	Terminate bool
	// Items, when Terminate=false, supplies the user's answers and the
	// loop continues with these injected into the next planner round.
	// Empty when Terminate=true.
	Items []QuestionAnswer
}

QuestionResult is what the host returns from OnQuestion. The hecatoncheires proposal host uses the {Terminate=true} shape (the session ends after the planner asks) but the type is set up to also support warren-style in-loop continuation in the future.

type ReplanResult

type ReplanResult struct {
	Message  string        `json:"message,omitempty"`
	Tasks    []TaskPlan    `json:"tasks,omitempty"`
	Question *Question     `json:"question,omitempty"`
	Finalize *FinalizePlan `json:"finalize,omitempty"`
}

ReplanResult is the parsed shape of every subsequent planner round. The planner must choose EXACTLY ONE terminal-or-continuation action per round:

  • Tasks: run another investigation phase.
  • Question: ask the user (only when the host allows it).
  • Finalize: declare completion and produce the final output.

An output that sets none of the three is rejected (parseReplanResult) and folded back into another replan round. This is deliberate: the previous design treated "empty tasks + no question" as an implicit completion signal, so a planner that merely forgot to emit tasks would silently terminate (and, in structured hosts, commit) a half-finished turn. Completion is now an explicit act.

type ResumeRequest

type ResumeRequest struct {
	// RunRequest carries the same identity / planner / tool / output
	// configuration as the original Run. HistoryKey MUST equal the
	// suspended run's key so the conversation continues. UserInput is
	// validated (non-empty) but not used to drive the first round — the
	// answers do; callers may set it to the original job prompt.
	RunRequest

	// Question is the question the planner asked before suspending,
	// reconstructed by the host from its persisted form. Used to label the
	// answers in the resumed planner input.
	Question Question

	// Answers is the user's reply, one entry per answered item. Required
	// (non-empty).
	Answers []QuestionAnswer
}

ResumeRequest re-enters a previously-suspended turn after the user has answered a Question. The turn ended earlier with QuestionResult{Terminate: true} (RunResult.EndedWithQuestion); the host persisted the question, collected the answer out-of-band, and now calls Resume.

Resume re-enters planexec at a replan round (NOT a fresh plan), folding the answers into the first planner input. It deliberately carries no plan snapshot: the gollem conversation history keyed by RunRequest.HistoryKey already holds every prior round's observations, so the planner sees them on Load. The planner budget is fresh — the human answer is a natural checkpoint, and a strict carry-over would risk a stillborn resume when the budget was already near-exhausted at the moment the question was asked.

func (*ResumeRequest) Validate

func (r *ResumeRequest) Validate() error

Validate enforces the resume contract: the embedded RunRequest must be valid and at least one answer must be present.

type RunRequest

type RunRequest struct {

	// HistoryKey is the gollem.WithHistoryRepository key. Required.
	// Used to bind every planner / replan call to one continuous
	// conversation history.
	HistoryKey string

	// TraceID is the trace.WithTraceID value handed to the trace.Recorder.
	// Required.
	TraceID string

	// TraceMetadata is attached to the trace.Recorder as labels.
	// Optional — zero value is fine.
	TraceMetadata trace.TraceMetadata

	// TraceHandler, when non-nil, is wired (in addition to the run's
	// internal archive recorder) into every gollem agent the run drives —
	// planner, sub-agents, direct, and final — so a host can capture
	// per-call LLM / tool events for its own timeline. The Job host
	// supplies its jobRunTraceHandler here so planexec Jobs populate the
	// JobRunEvent timeline the same way the single-loop executor does.
	// Optional; nil preserves the archive-only behaviour (the proposal
	// host passes nil).
	TraceHandler trace.Handler

	// LanguageLabel is interpolated into the planner system prompt's
	// "respond in" directive (e.g. "Japanese", "English"). Optional —
	// empty disables the directive.
	LanguageLabel string

	// UserInput is the very first user message handed to the planner.
	// Required.
	UserInput string

	// SystemPrompt is the planner's base system prompt. Required.
	// Host-specific guidance lives here; planexec appends only the
	// budget / action-shape boilerplate it owns.
	SystemPrompt string

	// PlannerTools are tools the planner itself is allowed to call
	// during a round (e.g. proposal's wsmeta tools). Optional — nil
	// means the planner is tool-less.
	PlannerTools []gollem.Tool

	// AllowDirect lets the planner short-circuit round 1: when the request
	// is trivially answerable without any investigation phase, the planner
	// may emit a `direct` payload instead of `tasks`, and the runtime
	// answers in a single tool-enabled ReAct loop. The direct path is plain
	// text only: even under Run[T] it returns RunResult.Text (not Data), since
	// side-effecting terminal actions must go through the investigate →
	// finalize loop. Hosts opt in (structured-only flows may still enable it
	// for a trivial text reply). Default false.
	AllowDirect bool

	// AllowSubAgentWrites lets this run's sub-agents perform writes /
	// side-effecting actions (post a message, change case status, ...) with
	// the tools the planner assigns them, instead of being restricted to
	// observation-only investigation. Default false → sub-agents are told
	// they are observation-only (the historical behaviour).
	//
	// The flag governs only the sub-agent prompt wording; the tools a
	// sub-agent can physically call are decided by ToolResolver. A caller
	// that sets this false MUST supply a read-only resolver so a sub-agent
	// is never handed a write tool it is then told not to use — that
	// prompt-vs-capability mismatch is exactly the failure this feature
	// fixes. A caller that sets it true supplies a resolver that includes
	// the write tools, and the planner assigns them per task.
	//
	// job sets true (Job deliverables are side effects, e.g. a Slack post);
	// threadcase sets true for mention turns (the sub-agent may close /
	// transition the case via case__update_case_status) and false for create
	// turns (no case to act on yet; the case is materialized by the host from
	// the structured final output).
	AllowSubAgentWrites bool

	// ToolResolver maps TaskPlan.Tools entries into concrete gollem
	// tool slices for each sub-agent. Required.
	ToolResolver ToolResolver

	// KnownToolIDs is the enum the planner sees in the JSON schema's
	// task.tools field. Required (must be non-empty).
	KnownToolIDs []string

	// AllowQuestion toggles the question section in the planner prompt
	// and schema. proposal sets true; job sets false.
	AllowQuestion bool

	// OnQuestion is the host callback fired when the planner emits a
	// Question. Required iff AllowQuestion is true.
	OnQuestion func(ctx context.Context, q Question) (QuestionResult, error)

	// Sink is the progress / state output channel. Required (use
	// SinkFuncs{} for a no-op).
	Sink Sink
}

RunRequest is the argument bundle handed to Runner.Run. Required and optional fields are documented per-field; Validate() enforces the required-field contract at the entry point so host wiring bugs (Sink nil, OnQuestion missing when AllowQuestion=true, ...) fail loud before the first LLM call.

func (*RunRequest) Validate

func (r *RunRequest) Validate() error

Validate enforces the required-field contract. Runner.Run MUST call this at the top before doing any work.

type RunResult

type RunResult[T any] struct {
	Status RunStatus
	// Data is the validated structured final output. Non-nil ONLY for a Run[T]
	// turn that completed via an explicit finalize (not direct, not question,
	// not fallback). Always nil for RunText / ResumeText.
	Data *T
	// Text is the plain-text terminal reply: the whole output of RunText /
	// ResumeText, OR the round-1 direct fast-path reply on any run
	// (Direct == true). Empty for a structured Run[T] that finalized normally
	// (the payload is in Data instead).
	Text string
	// Direct is true when the turn terminated via the round-1 direct fast path
	// (no investigation phase ran); Text holds the reply and Data is nil.
	Direct bool
	// EndedWithQuestion is true when the loop exited because OnQuestion returned
	// QuestionResult{Terminate: true}. Status is still StatusCompleted; the host
	// uses this to distinguish "we answered" from "we asked".
	EndedWithQuestion bool
	// AllResults is the per-phase observation trail.
	AllResults []PhaseSummary
	// FallbackReason is the human-readable cause for a StatusFallback* result.
	FallbackReason string
}

RunResult is the outcome of a run. T is the structured final-output type for Run[T]; RunText / ResumeText instantiate it as string (the reply travels in Text and Data is unused). AllResults carries the per-phase observation trail regardless of status so a fallback can still report what was learnt.

func ResumeText

func ResumeText(ctx context.Context, r *Runner, req ResumeRequest) (*RunResult[string], error)

ResumeText re-enters a suspended plain-text turn after the user answered the planner's Question. It mirrors RunText's setup (same system prompt, a fresh trace recorder bound to the same TraceID, a fresh budget) but enters the loop at a replan round (logicalRound 1) with the answers folded in as the first input. The conversation history keyed by HistoryKey already carries the prior observations, so the planner re-plans with full context without re-executing the completed phases.

func Run

func Run[T Validatable](ctx context.Context, r *Runner, req RunRequest, finalizers ...func(*T) error) (*RunResult[T], error)

Run drives a single structured plan-and-execute turn end-to-end and returns a validated *T as the terminal output. The flow is: build the planner prompt → loop (plan → sub-agents → replan) until the planner emits an explicit finalize → generate the final JSON, decode into T, run T.Validate() plus any host finalizers, and regenerate on failure (bounded). Side effects driven by the investigation (case status changes etc.) are performed by the sub-agents' tools inside the loop, never by planexec.

finalizers are optional host-supplied validators for the terminal output. Each is called after T.Validate() on the decoded final output; a returned error is fed back to the model and the output regenerated (bounded by finalOutputMaxRetry), exactly like a Validate() failure. They exist because T.Validate() is a pure method on the type and cannot see host context a validation needs — e.g. a workspace field schema known only to the caller. A finalizer validates the output against that external context and MUST be side-effect-free, since a later attempt re-runs every finalizer. Committing the output (persisting the entity, posting a message) belongs AFTER the turn, where the host applies the returned *T — never inside a finalizer, so an infrastructure error the model cannot fix does not burn a regeneration cycle.

Run is a package function, not a Runner method, because Go methods cannot be generic. The Runner carries the shared backend deps; T is the caller's terminal-output type.

func RunText

func RunText(ctx context.Context, r *Runner, req RunRequest) (*RunResult[string], error)

RunText drives a plain-text turn: same loop as Run, but the terminal output is free-form text (RunResult.Text). Used by hosts whose deliverable is a message or a side effect performed by a sub-agent tool (e.g. the Job host), not a structured object the host must apply.

type RunStatus

type RunStatus int

RunStatus is the terminal classification of a run.

const (
	// StatusCompleted means the loop terminated cleanly: either the planner
	// declared completion (finalize → final output produced), the round-1
	// direct path replied, or OnQuestion returned QuestionResult{Terminate:
	// true} (the host acknowledged the question and will resume in a later
	// turn — RunResult.EndedWithQuestion distinguishes this case).
	StatusCompleted RunStatus = iota
	// StatusFallbackBudget means the planner budget was exhausted before the
	// loop could terminate. RunResult.Data / Text are empty; the host should
	// render a "couldn't reach a conclusion" message of its choice.
	StatusFallbackBudget
	// StatusFallbackError means an internal error path made the run give up
	// before terminating (e.g. the direct reply failed, or the structured
	// final output failed to validate after retries). FallbackReason carries
	// the human-readable cause.
	StatusFallbackError
)

type Runner

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

Runner is the reusable plan-and-execute engine. One Runner is wired up at process startup with the shared backend deps (LLM client, history repo, trace repo) and the global budget defaults; per-call inputs (prompts, sink, tool resolver, schema) come in via the RunRequest handed to the Run / RunText / ResumeText package functions. Runner is safe for concurrent use across goroutines — it holds no per-call state internally.

func NewRunner

func NewRunner(deps RunnerDeps) (*Runner, error)

NewRunner constructs a Runner. Returns a goerr if RunnerDeps is incomplete so wiring failures surface at startup rather than at the first Slack mention.

type RunnerDeps

type RunnerDeps struct {
	LLMClient   gollem.LLMClient
	HistoryRepo gollem.HistoryRepository
	TraceRepo   trace.Repository
	Budget      BudgetConfig
}

RunnerDeps is the constructor-time bundle. Every field is required.

func (*RunnerDeps) Validate

func (d *RunnerDeps) Validate() error

Validate enforces RunnerDeps' required-field contract. Surfaced as a method (not just baked into NewRunner) so the CLI wiring can sanity- check its config independent of constructor invocation.

type Sink

type Sink interface {
	// Notify is a free-form line aimed at the user. Examples:
	// planner.message at the top of a phase, "couldn't reach a
	// conclusion" on fallback, etc.
	Notify(ctx context.Context, line string)

	// PlanProposed fires once per planner round AFTER the JSON parses.
	// The host typically renders the round's reasoning / message into a
	// progress card that it can later update in place.
	PlanProposed(ctx context.Context, info PlanInfo)

	// PhaseStarted reserves a progress block per TaskPlan before the
	// sub-agents start. Tasks come in the order they will run; the host
	// renders one row each and addresses them via TaskProgress later.
	PhaseStarted(ctx context.Context, phase int, tasks []TaskInfo)

	// TaskProgress overwrites the line of a previously-registered task
	// (running / tool-call / final result snippet). The host MUST
	// no-op gracefully if taskID is unknown — that path fires when a
	// progress middleware races a phase boundary.
	TaskProgress(ctx context.Context, taskID, line string)

	// TaskFinished marks a single task as completed or failed. The host
	// updates the corresponding row to its terminal display.
	TaskFinished(ctx context.Context, result TaskResult)
}

Sink is the host-side output channel for progress, task state, and free-form notifications during a planexec run. The planexec package never touches Slack / Firestore directly — it pushes events here, and the host (proposal / job) renders them onto its transport. Sink is defined inside planexec on purpose: it describes what THIS package wants to emit; consumers implement the interface in their own packages rather than re-defining a parallel interface elsewhere.

type SinkFuncs

type SinkFuncs struct {
	NotifyFn       func(ctx context.Context, line string)
	PlanProposedFn func(ctx context.Context, info PlanInfo)
	PhaseStartedFn func(ctx context.Context, phase int, tasks []TaskInfo)
	TaskProgressFn func(ctx context.Context, taskID, line string)
	TaskFinishedFn func(ctx context.Context, result TaskResult)
}

SinkFuncs is a struct-of-funcs adapter for tests and minimal hosts that only care about a subset of the Sink methods. Missing entries are treated as no-ops, so an all-zero SinkFuncs is safe to pass as Sink.

func (SinkFuncs) Notify

func (s SinkFuncs) Notify(ctx context.Context, line string)

func (SinkFuncs) PhaseStarted

func (s SinkFuncs) PhaseStarted(ctx context.Context, phase int, tasks []TaskInfo)

func (SinkFuncs) PlanProposed

func (s SinkFuncs) PlanProposed(ctx context.Context, info PlanInfo)

func (SinkFuncs) TaskFinished

func (s SinkFuncs) TaskFinished(ctx context.Context, result TaskResult)

func (SinkFuncs) TaskProgress

func (s SinkFuncs) TaskProgress(ctx context.Context, taskID, line string)

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.

type TaskPlan

type TaskPlan struct {
	ID                 string   `json:"id"`
	Title              string   `json:"title"`
	Description        string   `json:"description"`
	AcceptanceCriteria string   `json:"acceptance_criteria"`
	Tools              []string `json:"tools"`
}

TaskPlan is one parallel investigation task within a PlanResult or ReplanResult. The fields follow warren bluebell's TaskPlan shape:

  • ID: stable identifier the host uses to correlate progress lines
  • Title: short label rendered to the user
  • Description: full instruction handed to the sub-agent
  • AcceptanceCriteria: the measurable bar against which the next replan judges whether the goal has been met
  • Tools: the allowed ToolResolver IDs for this task (subset of RunRequest.KnownToolIDs)

func (*TaskPlan) Validate

func (t *TaskPlan) Validate(knownToolIDs []string) error

Validate enforces TaskPlan invariants. KnownToolIDs is the host-supplied allowlist (RunRequest.KnownToolIDs); every entry in TaskPlan.Tools must be a member.

type TaskResult

type TaskResult struct {
	TaskID             string
	Title              string
	AcceptanceCriteria string
	Status             TaskStatus
	Summary            string
	Error              string

	// InnerLoopsUsed / InnerLoopsMax surface in the trace UI but are not
	// fed back into the planner (the planner doesn't need them for
	// decisions).
	InnerLoopsUsed int64
	InnerLoopsMax  int
}

TaskResult is the per-task summary surfaced via Sink.TaskFinished and folded into the next planner round's observations.

type TaskStatus

type TaskStatus string

TaskStatus marks the outcome of a single sub-agent task.

const (
	TaskStatusCompleted TaskStatus = "completed"
	TaskStatusFailed    TaskStatus = "failed"
)

type ToolResolver

type ToolResolver interface {
	// Resolve returns the concatenated tool list for the requested IDs.
	// Unknown IDs MUST be silently dropped — they should already have
	// been rejected at plan validation, and we never want to crash a
	// running sub-agent because the planner emitted a stray name.
	Resolve(ids []string) []gollem.Tool
}

ToolResolver translates the symbolic Tools list inside a TaskPlan into a concrete []gollem.Tool slice for one sub-agent. The interface is the only piece of "what tools exist" planexec needs to know about; the concrete catalogue (workspace-scoped Slack / Notion / GitHub clients, the read-only core toolset, etc.) lives in the host.

type Validatable

type Validatable interface {
	Validate() error
}

Validatable is the constraint on Run's structured final-output type. The generic layer decodes the planner's terminal JSON into T, then calls Validate() and regenerates on failure: gollem's response-schema check verifies the JSON shape only, so Validate() is where a host enforces its domain invariants (required fields, allowed values). RunText / ResumeText produce plain text and do not use this.

Jump to

Keyboard shortcuts

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