react

package
v1.1.6 Latest Latest
Warning

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

Go to latest
Published: May 26, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package react ships Harbor's reference LLM-driven planner concrete (Phase 45 — RFC §6.2 + RFC §3.2 — the first concrete sitting on the `internal/planner.Planner` seam).

Each ReActPlanner.Next call:

  1. Honours ctx.Err() and the run's identity quadruple (§6 rule 9 + D-001 — identity is mandatory; the runtime fails closed).
  2. Checks the [MaxSteps] circuit breaker. When the run's prior trajectory carries ≥ MaxSteps recorded steps, the planner emits planner.EventTypePlannerMaxStepsExceeded AND returns planner.Finish{Reason: planner.FinishNoPath, Metadata["max_steps_exceeded"]=true}. Fail-loudly per §13.
  3. Observes [planner.RunContext.Control.Cancelled]; returns planner.Finish{Reason: planner.FinishCancelled} on a CANCEL observation (the planner's step-boundary contract per RFC §6.3).
  4. Builds the llm.CompleteRequest via the configured PromptBuilder. The default builder (Phase 83a — brief 13 §2.1) assembles the twelve XML-tagged structured sections and asks the LLM for a JSON envelope `{"tool":"<name>","args":{...}}` per step OR `{"tool":"_finish","args":{"answer":"..."}}` to signal completion. The envelope carries NO `reasoning` field — reasoning is captured from the provider channel, never required in the structured output (brief 13 §2.6).
  5. Delegates the response → planner.Decision mapping to Phase 44's repair.RepairLoop.Run (salvage → schema repair → graceful failure → multi-action salvage). The repair loop is OUTSIDE the LLM call; the Phase 36 retry-with-feedback wrapper is INSIDE — composition stays at the registry edge (D-043 + D-050).
  6. Maps the repair loop's planner.Decision to the planner's final shape: - `Finish{NoPath}` from the loop → propagate verbatim (planner.EventTypePlannerRepairExhausted already emitted by the loop's [repair.RepairLoop.gracefulFailure]). - `CallTool` with `Tool == "_finish"` → translate to `Finish{Reason: FinishGoal, Payload: <args.answer>}`. The reserved name is a prompt-time convention, NOT a magic- string opcode in the Decision sum (D-047 + D-051; the predecessor's `next_node` anti-pattern is rejected). - `CallTool` with `Tool == "_spawn_task"` → translate to planner.SpawnTask with the args decoded into planner.SpawnSpec (Phase 47, D-056). Wake mode is push: a non-retain-turn SpawnTask returns control to the runtime; on tasks.GroupCompletion the runtime re-enters Next with the resolved MemberOutcome surfaced through `RunContext.Trajectory.Background`. - `CallTool` with `Tool == "_await_task"` → translate to planner.AwaitTask keyed on the args' `task_id` (Phase 47, D-056). - `CallTool` with another tool name → return verbatim. - `CallParallel` (multi-action salvage from Phase 44) → pass through verbatim. Phase 47 (D-056) ships the runtime parallel executor that consumes this shape; the V1 single-tool-call- per-step collapse override (the Phase 45 D-051 stop-gap) is DELETED — Harbor's V1 ceiling lifts here.

**Wake-on-resolution (D-032).** ReActPlanner implements planner.WakeAware returning planner.WakePush. Phase 47 wires the emission path end-to-end (D-056): a non-retain-turn `_spawn_task` emission returns control to the runtime; the runtime registers the planner against tasks.TaskRegistry.WatchGroup; on the tasks.GroupCompletion delivery the runtime re-invokes `Next` with the resolved `MemberOutcome` slice surfaced through `RunContext.Trajectory.Background`. The conformance pack (Phase 49) asserts the round-trip:

planner.ResolveWakeMode(reactPlanner) == planner.WakePush

**Concurrent-reuse (D-025).** ReActPlanner is a reusable artifact: one constructed instance is safe to share across N concurrent runs. The receiver is read-only after construction; per-call state lives on the stack and in the run's planner.RunContext. `d025_test.go` pins N=128 invocations under `-race`.

**Import-graph contract (§13).** The react package MUST NOT import `internal/runtime/...`. The Phase 42 internal/planner/conformance.TestImportGraph_PlannerDoesNotImportRuntime covers the new package by construction (it walks the whole planner subtree). The Phase 45 smoke script asserts the same via grep.

Index

Constants

View Source
const (
	// ReminderFinishGuidance — finish-repair counter == 1.
	ReminderFinishGuidance = "reminder: your previous `_finish` action failed validation. " +
		"When you finish, emit exactly `{\"tool\": \"_finish\", \"args\": {\"answer\": \"...\"}}` " +
		"with `answer` as the only field — plain text, no metadata."
	// WarningFinishGuidance — finish-repair counter == 2.
	WarningFinishGuidance = "warning: your `_finish` action has failed validation twice. " +
		"Re-read the <finishing> section. The `args` object must contain ONLY the `answer` key, " +
		"and `answer` must be a plain-text string. Do not add status, confidence, or route fields."
	// CriticalFinishGuidance — finish-repair counter >= 3.
	CriticalFinishGuidance = "critical: your `_finish` action has failed validation three or more times. " +
		"Stop and emit this exact shape, substituting only the answer text: " +
		"`{\"tool\": \"_finish\", \"args\": {\"answer\": \"<your full plain-text answer here>\"}}`. " +
		"No other keys. No code fence commentary."

	// ReminderArgsGuidance — args-repair counter == 1.
	ReminderArgsGuidance = "reminder: your previous tool call had arguments that failed the tool's schema. " +
		"Match every argument name and type to the tool's `args_schema` exactly before calling it again."
	// WarningArgsGuidance — args-repair counter == 2.
	WarningArgsGuidance = "warning: your tool arguments have failed schema validation twice. " +
		"Re-read the chosen tool's `args_schema` in <available_tools>. Include every required field, " +
		"use the exact field names, and match the declared types — strings quoted, numbers unquoted."
	// CriticalArgsGuidance — args-repair counter >= 3.
	CriticalArgsGuidance = "critical: your tool arguments have failed schema validation three or more times. " +
		"Pick the simplest tool that can make progress, copy its `args_schema` field-for-field, " +
		"and supply only the fields that schema declares. If no tool fits, `_finish` with an explanation."

	// ReminderMultiActionGuidance — multi-action counter == 1.
	ReminderMultiActionGuidance = "reminder: your previous response contained more than one JSON action block. " +
		"Emit exactly ONE JSON object per turn, inside a single ```json code fence."
	// WarningMultiActionGuidance — multi-action counter == 2.
	WarningMultiActionGuidance = "warning: you have emitted multiple JSON action blocks twice. " +
		"One turn = one action. To run tools concurrently, use a single `parallel` action — " +
		"never several separate JSON objects."
	// CriticalMultiActionGuidance — multi-action counter >= 3.
	CriticalMultiActionGuidance = "critical: you have emitted multiple JSON action blocks three or more times. " +
		"Respond with ONE and only one JSON object. If you need concurrent tool calls, " +
		"wrap them in a single `{\"tool\": \"parallel\", \"args\": {\"steps\": [...]}}` action."
)

Repair-guidance hint copy — Phase 83c (D-145). The copy lives in exported constants so operators can grep it and so a copy change shows up as a reviewable diff (the nine golden fixtures under testdata/repair_guidance/ pin the rendered bodies). Each tier opens with its own tier name so a copy-paste typo that reuses the wrong tier's text is caught by the smoke script.

Copy-design note (brief 13 §2.2 risk): an over-aggressive `critical` hint can confuse the model. The copy escalates in firmness, not in volume — `critical` is direct and specific, not shouty.

View Source
const AwaitTaskToolName = "_await_task"

AwaitTaskToolName is the reserved tool name the LLM emits to block the foreground turn on a previously-spawned task. The planner intercepts this in mapDecision and translates it to a typed planner.AwaitTask Decision. D-056 — Phase 47.

View Source
const DefaultMaxSteps = 12

DefaultMaxSteps is the planner-side circuit-breaker default for the observed trajectory step count. Set small enough to surface bugs quickly; large enough to leave 3-step scenarios headroom. The runtime's hop / cost budget (Phase 47+) is the authoritative gate; the planner-side cap is defence in depth (§13 + D-051).

View Source
const DefaultSystemPrompt = "harbor.react.default-system-prompt"

DefaultSystemPrompt is the sentinel value the planner sends as the leading system-prompt argument when WithSystemPrompt is not set.

Phase 83a (RFC §6.2, brief 13 §2.1) replaced the former flat-string prompt with the twelve XML-tagged structured sections assembled by `defaultBuilder.buildSystemContent`. The structured sections ARE the default prompt content; this constant is the routing sentinel the builder compares against to decide whether to emit the structured twelve-section layout (sentinel matched → structured) or to honour an operator's verbatim WithSystemPrompt override (any other value → verbatim). The constant value is intentionally a stable non-empty string, never empty: `New` seeds `systemPrompt` with it, `WithSystemPrompt("")` falls back to it, and `buildSystemContent` branches on identity-equality with it.

The old single-string Phase 45/47 prompt constant is intentionally removed (not renamed to `legacyDefaultSystemPrompt`) — the golden fixture `testdata/golden_default_prompt.txt` is the normative spec for the rendered default prompt going forward, and a dangling legacy constant would be dead code (CLAUDE.md §13).

View Source
const DriverName = "react"

DriverName is the canonical name the react planner registers under. The `internal/config` validator's `allowedPlannerDrivers` allowlist mirrors this constant (D-103). `cmd/harbor/main.go` blank-imports this package so the registration fires at process boot (§4.4 seam pattern; D-095 OAuth-provider precedent).

View Source
const FinishToolName = "_finish"

FinishToolName is the reserved tool name the LLM emits to signal completion. The planner intercepts this BEFORE returning the Decision; `"_finish"` never reaches the runtime as a real tool call. The leading underscore is a documented convention; future runtime catalog registration MAY reject `_`-prefixed tool names. D-051.

View Source
const SpawnTaskToolName = "_spawn_task"

SpawnTaskToolName is the reserved tool name the LLM emits to spawn a background task. The planner intercepts this in mapDecision and translates it to a typed planner.SpawnTask Decision before returning — the runtime never sees `"_spawn_task"` as a real tool call. D-056 — Phase 47 adds the third reserved emission shape.

Variables

This section is empty.

Functions

This section is empty.

Types

type Option

type Option func(*ReActPlanner)

Option configures a ReActPlanner at construction time. Options are applied in order; later options override earlier ones.

func WithArgFillEnabled

func WithArgFillEnabled(b bool) Option

WithArgFillEnabled toggles Phase 44's schema-repair path. When false, the loop surfaces the parser's first action verbatim and lets the dispatcher reject misshaped args. Default true.

func WithMaxConsecutiveArgFailures

func WithMaxConsecutiveArgFailures(n int) Option

WithMaxConsecutiveArgFailures passes the repair.Config.MaxConsecutiveArgFailures storm-guard counter through to Phase 44's loop. Default repair.DefaultMaxConsecutiveArgFailures (2).

func WithMaxSteps

func WithMaxSteps(n int) Option

WithMaxSteps overrides the DefaultMaxSteps circuit-breaker cap. Values ≤ 0 fall back to DefaultMaxSteps. The breaker fires when `len(rc.Trajectory.Steps) >= MaxSteps`; the planner emits planner.EventTypePlannerMaxStepsExceeded AND returns `Finish{NoPath, Metadata["max_steps_exceeded"]=true}`.

func WithMaxToolExamplesPerTool

func WithMaxToolExamplesPerTool(n int) Option

WithMaxToolExamplesPerTool caps how many curated examples each tool renders in the `<available_tools>` section of the system prompt (Phase 83b — D-144). The runtime wires this from `config.PlannerConfig.MaxToolExamplesPerTool`. A value ≤ 0 (the default) resolves to [defaultMaxToolExamples] (3) at render time. Examples are ranked `minimal` > `common` > `edge-case` > untagged; the renderer keeps the top N.

The option applies only when the default prompt builder is in use; an operator-supplied WithPromptBuilder owns its own prompt assembly and ignores this value.

func WithPromptBuilder

func WithPromptBuilder(b PromptBuilder) Option

WithPromptBuilder injects a custom PromptBuilder. Default: the in-package builder. A nil builder is rejected (the option is a no-op).

func WithReasoningReplay

func WithReasoningReplay(mode planner.ReasoningReplayMode) Option

WithReasoningReplay sets the agent-configured reasoning-replay mode (Phase 83e — D-148). The runtime wires this from `config.PlannerConfig.ReasoningReplay`. The default — and the value for an empty / unset mode — is planner.ReasoningReplayNever: a prior step's captured reasoning is NEVER re-injected into the next prompt. planner.ReasoningReplayText opts the agent into prepending each prior step's captured `ReasoningTrace` as a text block above the action JSON. A per-run `RunContext.ReasoningReplay` override wins over this configured value at render time.

An invalid mode is rejected (the option is a no-op) — config validation already rejects bad values pre-boot.

func WithRepairAttempts

func WithRepairAttempts(n int) Option

WithRepairAttempts passes the repair.Config.RepairAttempts knob through to Phase 44's loop. Default repair.DefaultRepairAttempts (3).

func WithSystemPrompt

func WithSystemPrompt(s string) Option

WithSystemPrompt overrides the DefaultSystemPrompt. An empty string falls back to DefaultSystemPrompt.

A non-default, non-empty string is honoured verbatim by the default prompt builder: it REPLACES the twelve-section structured layout (the structured sections ARE the default prompt content). The optional injection sections (`<available_tools>`, `<additional_guidance>`, `<planning_constraints>`) still append, so tool rendering and WithSystemPromptExtra guidance survive a custom base prompt.

func WithSystemPromptExtra

func WithSystemPromptExtra(s string) Option

WithSystemPromptExtra injects operator-supplied guidance into the `<additional_guidance>` section of the rendered system prompt (Phase 83a, RFC §6.2, brief 13 §2.1 section 11). The string is rendered verbatim; the operator is responsible for content hygiene. An empty (or whitespace-only) string is a no-op — the `<additional_guidance>` section is then omitted from the prompt entirely rather than emitted as an empty tag pair.

The guidance applies only when the default prompt builder is in use; an operator-supplied WithPromptBuilder owns its own prompt assembly and ignores this option. `internal/config`'s `PlannerConfig.ExtraGuidance` key flows to this option at construction (see `internal/planner/react/init.go`).

type PromptBuilder

type PromptBuilder interface {
	// Build returns the LLM request to send for the current step.
	// The builder reads from rc; it MUST NOT mutate rc. The returned
	// request carries Model = "" (the LLM client / wrapper chain
	// resolves the configured model at registry edge); callers that
	// need to pin a model override can wrap a default builder.
	Build(rc planner.RunContext, systemPrompt string) llm.CompleteRequest
}

PromptBuilder constructs the llm.CompleteRequest from a planner.RunContext. Default implementation ships in-package as [defaultBuilder]; operators may inject their own via WithPromptBuilder per RFC §6.2 (the planner's small set of genuinely policy-shaped knobs).

Implementations MUST be safe for concurrent use (the planner is a reusable artifact per D-025; the prompt builder is read on every Next call).

type ReActPlanner

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

ReActPlanner is Harbor's reference LLM-driven planner. Reusable artifact (D-025): the receiver is read-only after construction; per-call state lives on the stack and in the planner.RunContext.

All fields are set at construction by New (with Option applied); none are mutated by [Next].

func New

func New(client llm.LLMClient, opts ...Option) *ReActPlanner

New constructs a ReActPlanner backed by the supplied llm.LLMClient with the given options applied. Nil client panics — composition error caught at boot.

func (*ReActPlanner) Next

Next implements planner.Planner. The flow is documented in the package godoc.

func (*ReActPlanner) StepsTaken

func (p *ReActPlanner) StepsTaken() int64

StepsTaken returns the process-wide count of ReActPlanner.Next invocations served. Used by tests; not part of the planner contract. Atomic load — safe across goroutines.

func (*ReActPlanner) WakeMode

func (p *ReActPlanner) WakeMode() planner.WakeMode

WakeMode declares the planner's wake-on-resolution strategy (D-032 + Phase 45 spec). ReAct ships the `push` mode: a non-retain-turn SpawnTask emission (deferred to a later phase) would return control to the runtime; the runtime would register the planner against tasks.TaskRegistry.WatchGroup; on `GroupCompletion` the runtime would re-invoke `Next` with the resolved `MemberOutcome` surfaced through `RunContext.Trajectory.Background`.

type RepairTier

type RepairTier string

RepairTier names an escalation level for repair guidance. The three tiers map to counter values: 1 → reminder, 2 → warning, >= 3 → critical. A zero counter has no tier (the empty string).

const (
	// RepairTierNone is the absence of a tier — the counter is 0, so
	// no guidance block is rendered.
	RepairTierNone RepairTier = ""
	// RepairTierReminder is the first escalation level (counter == 1):
	// a gentle nudge.
	RepairTierReminder RepairTier = "reminder"
	// RepairTierWarning is the second escalation level (counter == 2):
	// a firmer correction.
	RepairTierWarning RepairTier = "warning"
	// RepairTierCritical is the top escalation level (counter >= 3):
	// the strongest correction copy.
	RepairTierCritical RepairTier = "critical"
)

Repair-guidance escalation tiers (Phase 83c — D-145).

Jump to

Keyboard shortcuts

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