Documentation
¶
Overview ¶
Package coord houses the workflow coordinator surface: the small behavioural contract a coord-side AgentRunner must implement (RunnerHandle), the four goai.Tool factories the coord LLM uses to drive a workflow (forward_to_agent / send_message / narrate / finalize), and the factory + default system prompt that wire those tools onto a pre-configured *AgentRunner. All public coord symbols (tool factories, prompts, factory) are re-exported by package zenflow's coord_facade.go so external SDK consumers keep their `import "github.com/zendev-sh/zenflow"` working unchanged. Edit here, not in root.
Index ¶
- Constants
- Variables
- func BuildUnknownStepHint(rt *router.Router, attempted string) string
- func FinalizeToolDef(runner RunnerHandle) goai.Tool
- func ForwardToAgentToolDef(runner RunnerHandle) goai.Tool
- func NarrateToolDef(runner RunnerHandle) goai.Tool
- func SendMessageToolDef(runner RunnerHandle) goai.Tool
- type RunnerHandle
Constants ¶
const CoordRouterInboxID = "coordinator"
CoordRouterInboxID is the canonical mailbox key reverse replies and agent send_message calls route to. Mirrors the root-package constant of the same name; kept duplicated here so internal/coord has no import edge to root (which would create a cycle once root facades re-export coord symbols).
Variables ¶
var ( // ErrForwardTargetRequired is returned by forward_to_agent when the // target_step_id argument is empty. ErrForwardTargetRequired = errors.New("zenflow: forward_to_agent: target_step_id is required") // ErrSendMessageEmpty is returned by send_message when text is empty. ErrSendMessageEmpty = errors.New("zenflow: send_message: text is required and must be non-empty") // ErrNarrateEmpty is returned by narrate when text is empty. ErrNarrateEmpty = errors.New("zenflow: narrate: text is required and must be non-empty") )
Sentinel errors for required-argument validation in coord tools. Promoted from inline errors.New calls so callers (and tests) can match against stable values via errors.Is rather than substring matching on Error output.
Functions ¶
func BuildUnknownStepHint ¶
BuildUnknownStepHint formats a helpful suffix listing currently registered step IDs after a forward_to_agent unknown-step drop. + - error feedback for the coord LLM to self-correct. Returns an empty string when no steps are registered (defensive; caller wraps it harmlessly). message is action-oriented: tells LLM the EXACT next steps to take in the SAME response (retry with valid ID OR narrate as fallback) rather than just listing available IDs and hoping. Without action guidance, weak models (Gemini-Flash observed) may retry with wrapper or exit the turn entirely, leaving content dropped. The system DOES auto-preserve dropped content as narration (server-side safety net), but coaching the LLM to act in the same turn produces cleaner UX.
func FinalizeToolDef ¶
func FinalizeToolDef(runner RunnerHandle) goai.Tool
FinalizeToolDef returns the `finalize` tool. The coord LLM calls it to signal that coordination is complete - the caller's outer Run loop should exit. Stable. IMPORTANT - the tool does NOT itself stop AgentRunner.Run; finalization is explicit and the loop lifecycle is the caller's responsibility. The tool ONLY signals exit by: 1. Storing the optional summary via runner.SetFinalSummary so FinalSummary observers see it. 2. Calling runner.MarkFinalized which atomically flips the finalized flag AND closes the finalize channel exactly once (idempotent on repeated calls). The caller's Run loop selects on runner.EnsureFinalizeCh (or polls runner.Finalized) and exits its outer-loop iteration. Per CLI usage the loop also surfaces runner.FinalSummary as EventCoordinatorSynthesis before disposing the runner.
func ForwardToAgentToolDef ¶
func ForwardToAgentToolDef(runner RunnerHandle) goai.Tool
ForwardToAgentToolDef returns the `forward_to_agent` tool. The coord LLM calls it to route a message into a running step agent's mailbox for context injection, follow-up questions, or instructions. Stable. Routes via runner.Router.Send (NOT direct mailbox.Append) so the MessageRouter's lifecycle checks (closed-mailbox detection, pending-senders accounting, drop emission via the executor's installed OnDrop callback) all fire normally. Drops surface BOTH as EventMessageDropped (via OnDrop) AND as the tool's returned result string (`"dropped: <reason>"`) so the LLM observes the failure on the same turn the call was made. Result format: - success: `"queued: msg-fwd-<n>"` (n = monotonic per-runner seq) - drop: `"dropped: <reason>"` (reason = canonical DropReason.String, e.g. "unknown-step", "target-terminal", "mailbox-full") Safety: when runner.Router is nil (coord wired into an executor that has not yet installed a Router, OR a unit-test runner with no messaging stack) the tool returns a clear error so the LLM observes the misconfiguration instead of a silent no-op. The nil-Router case is distinct from a router-side drop: nil-Router is an operator configuration bug (the coord was misconfigured at construction) and surfaces as an Execute error; a router drop is a runtime routing outcome and surfaces as a non-error tool-result string.
func NarrateToolDef ¶
func NarrateToolDef(runner RunnerHandle) goai.Tool
NarrateToolDef returns the `narrate` tool. The coord LLM calls it to emit a user-facing narration message - e.g. explaining its routing decision, summarising a recently-finished step, or surfacing reasoning context for downstream observers. Stable. Emits one EventCoordinatorNarration per call via runner.Progress. Does NOT route messages to step agents (use forward_to_agent for that). Empty text returns an error rather than emitting an empty event - a no-op narration always indicates a coord-side bug worth surfacing to the model. Safety: when runner.Progress is nil the tool returns a clear error. The coord must be wired with a sink before narrate can fire.
func SendMessageToolDef ¶
func SendMessageToolDef(runner RunnerHandle) goai.Tool
SendMessageToolDef returns the `send_message` tool. Step agents call it to send a message back to the workflow coordinator. The target is hardcoded to the canonical coord inbox key (CoordRouterInboxID = "coordinator") - there is no `to` parameter on the input schema. Stable. Hub-only routing: - Agents can only message the hub (coord); siblings are unreachable. - The coord is the sole router - it decides forwarding via `forward_to_agent`. This eliminates peer discovery and mesh complexity (DL-6 anti-pattern). Result format: - success: `"queued: msg-send-<n>"` (n = monotonic per-runner seq) - drop: `"dropped: <reason>"` where <reason> is the canonical DropReason.String value, e.g. "unknown-step", "target-terminal", "mailbox-full". Drops surface in BOTH the tool result string AND EventMessageDropped (via Router's OnDrop), preserving the "zero silent drops" contract while making the failure visible to the LLM on the same turn. No-coordinator drop: - When runner.Router is nil (coord not wired into a workflow, OR a unit-test runner with no messaging stack), the tool returns `"dropped: no-coordinator"` as the result string with NIL error. This is distinct from `forward_to_agent`'s nil-Router path which surfaces as an Execute error: send_message is callable from steps in any zenflow context (including non-workflow RunAgent), so a missing coord is a runtime routing outcome (the user opted out of a coord), not a coord-side configuration bug.
Types ¶
type RunnerHandle ¶
type RunnerHandle interface {
Router() *router.Router
Progress() types.ProgressSink
StepID() string
RunID() string
// NextForwardSeq returns the next monotonic per-runner forward
// sequence number. Used to mint `msg-fwd-N` / `msg-send-N`
// correlation IDs on the tool result string. Atomic + monotonic.
NextForwardSeq() uint64
// EnsureFinalizeCh returns the lazily-allocated finalize channel.
// Read-only; the channel is closed exactly once by MarkFinalized.
// Safe to call concurrently with MarkFinalized - both observe the
// same channel via internal synchronisation on the runner.
EnsureFinalizeCh() <-chan struct{}
// MarkFinalized sets the finalized flag AND closes the finalize
// channel exactly once. Idempotent: subsequent calls are no-ops.
MarkFinalized()
// SetFinalSummary stores the optional synthesis text passed to
// FinalizeToolDef. Last-writer-wins on repeated calls.
SetFinalSummary(summary string)
// FinalSummary returns the most recently stored synthesis text,
// or empty string if SetFinalSummary was never called.
FinalSummary() string
// Finalized snapshots whether MarkFinalized has been invoked.
Finalized() bool
}
RunnerHandle is the behaviour contract the four coord tool factories (ForwardToAgentToolDef, SendMessageToolDef, NarrateToolDef, FinalizeToolDef) demand of the *AgentRunner they close over. It is the seam that lets the tools live in internal/coord while the concrete *AgentRunner stays in internal/exec - both packages can be imported without forming a cycle. Methods come in three groups: - Wiring accessors (Router, Progress, StepID, RunID): supply the plumbing the tools need to deliver messages and emit events. - Forward-correlation (NextForwardSeq): atomically allocates the monotonic counter that backs `msg-fwd-N` / `msg-send-N`. - Finalize plumbing (EnsureFinalizeCh, MarkFinalized, SetFinalSummary, FinalSummary, Finalized): the close-once / atomic-flag dance FinalizeToolDef performs to signal the caller's Run loop to exit. MarkFinalized handles BOTH the flag flip AND the close-once on the channel so callers do not need direct chan access. Any *AgentRunner-shaped type that satisfies these methods is a valid RunnerHandle. The concrete *exec.AgentRunner provides the canonical implementation.