/** * Iterative-refinement plugin for Strands agents * Validates the agent's response after each invocation; if it doesn't * satisfy the goal, feeds validator feedback back as a user message and re-enters the * agent loop via `AfterInvocationEvent.resume`. Loops until validation passes, * `maxAttempts` is reached, or `timeout` elapses. * * @example * ```ts * import { Agent } from '@strands-agents/sdk' * import { GoalLoop } from '@strands-agents/sdk/vended-plugins/goal' * * // Natural-language goal — judged by an internal Agent built from the host's model. * const concise = new GoalLoop({ * goal: 'At most 3 sentences, accessible to a 10-year-old, no jargon.', * maxAttempts: 3, * }) * const agent = new Agent({ model, plugins: [concise] }) * await agent.invoke('Explain how rainbows form.') * console.log(concise.lastResult(agent)) * ``` * * @example * ```ts * // Programmatic validator — pass a function as `goal` to run your own check * // (here, a word-count cap). * const wordCount = new GoalLoop({ * goal: (response) => { * const text = response.content.flatMap((b) => (b.type === 'textBlock' ? [b.text] : [])).join(' ') * const words = text.trim().split(/\s+/).length * return words <= 50 || { passed: false, feedback: `Too long (${words} words). Cap at 50.` } * }, * maxAttempts: 5, * timeout: 30_000, * }) * ``` * * @example * ```ts * // Toolchain-driven validator — runs `npm test` after each attempt and gates * // on exit code (a Ralph-shaped use). Pair with file-editor / bash tools so the * // agent can actually fix what the test runner reports. * import { exec } from 'node:child_process' * import { promisify } from 'node:util' * const execAsync = promisify(exec) * * new GoalLoop({ * goal: async () => { * try { * await execAsync('npm test') * return true * } catch (err) { * const e = err as { stdout?: string; stderr?: string; code?: number } * const out = `${e.stdout ?? ''}${e.stderr ?? ''}`.slice(-4000) * return { passed: false, feedback: `npm test exited ${e.code}.\n\n${out}` } * } * }, * maxAttempts: 10, * }) * ``` */ import type { Model } from '../../models/model.js'; import type { Plugin } from '../../plugins/plugin.js'; import type { LocalAgent } from '../../types/agent.js'; import type { ContentBlock, Message } from '../../types/messages.js'; /** Outcome a validator returns. */ export interface ValidationOutcome { passed: boolean; /** Feedback fed to the agent as a user message before the next attempt. */ feedback?: string; } /** * Programmatic validator. Must return `true`, `false`, or a `ValidationOutcome`. * Booleans are shorthand: `true` → pass, `false` → fail with no feedback. Use * the object form when you have actionable feedback for the next attempt. * * The second argument is the host agent — read `agent.messages` for the full * transcript (the same view the built-in NL judge sees), or any other state * the validator needs. */ export type Validator = (response: Message, agent: LocalAgent) => boolean | ValidationOutcome | Promise; /** Why a goal run ended. */ export type GoalStopReason = 'satisfied' | 'maxAttempts' | 'timeout'; /** Single attempt summary preserved on `GoalResult`. */ export interface GoalAttempt { /** 1-indexed attempt number. */ attempt: number; passed: boolean; feedback?: string; } /** Aggregate result of a goal run, exposed via `GoalLoop.lastResult`. */ export interface GoalResult { passed: boolean; stopReason: GoalStopReason; attempts: readonly GoalAttempt[]; } /** * Tuning for the auto-built judge used when {@link GoalLoopOptions.goal} is a * natural-language string. Harmlessly ignored when `goal` is a validator * function — no judge is built in that case. */ export interface JudgeConfig { /** * Model the judge agent uses. Defaults to the host agent's model. Consider * passing a cheaper or faster model (e.g. Haiku) to keep judging cheap. */ model?: Model; /** * System prompt for the judge agent. Defaults to {@link JUDGE_SYSTEM_PROMPT}. * Override to retune the judging rubric, localize the instructions, or * tighten/relax the pass criteria. */ systemPrompt?: string; } /** * Configuration for {@link GoalLoop}. */ export interface GoalLoopOptions { /** * What "done" means for this loop. Either: * * - a **natural-language goal** (`string`) — an internal judge Agent grades * each attempt against it and returns feedback on failure; or * - a **programmatic validator** ({@link Validator}) — your own predicate that * inspects the response (and host agent) and returns pass/fail plus optional * feedback. */ goal: string | Validator; /** Tuning for the auto-built judge used when `goal` is a natural-language string. */ judge?: JudgeConfig; /** Max attempts. Defaults to `Infinity`. `warnOnce` when both this and `timeout` are unbounded. */ maxAttempts?: number; /** * Wall-clock budget for the whole run, in ms. Defaults to `Infinity`. * Checked between attempts (after each `AfterInvocationEvent`), so an * in-flight invocation isn't interrupted mid-stream — actual wall-clock * may exceed this by one attempt's duration. */ timeout?: number; /** * Plugin name. Defaults to `'strands:goal-loop'`. Only one goal-loop plugin * is supported per agent; if you need multiple constraints, compose them in * a single validator function rather than attaching multiple instances. */ name?: string; /** * Whether to preserve the agent's conversation history across retry attempts. * * When `true` (default), the agent sees its own prior responses and the * validator's feedback messages — use when prior attempts inform the next. * * When `false` (Ralph-Wiggum mode), each failed attempt restores the agent's * full model-visible session (messages, systemPrompt, modelState, interrupts) * to the state captured immediately before attempt 1's first model call, * then re-runs the goal with the latest validator feedback as a fresh user * message. The snapshot is taken *after* any conversation manager or other * pre-model hook has run, so retries restore to what the model actually saw, * not the raw user input. The agent never sees its own prior attempts — * including via server-side conversation chaining (e.g. OpenAI's * `previous_response_id`). Only `appState` accumulates across attempts, * since that's plugin scratch space invisible to the model. * * @defaultValue true */ preserveContext?: boolean; /** * Builds the user message fed to the agent before each retry. Receives the * trimmed validator feedback (or `undefined` if the validator gave none). * Override to localize the default English, retune the framing, or embed * feedback in a domain-specific structure. Return a string for plain text or * a `ContentBlock[]` to mix in non-text content (e.g. an image block). */ resumePromptTemplate?: (feedback: string | undefined) => string | ContentBlock[]; } /** * Iterative-refinement plugin. A single `GoalLoop` instance can be attached to * multiple `Agent`s; per-agent run state is keyed off the agent, so concurrent * runs on different agents don't interfere. Only one GoalLoop is supported per * individual agent — see {@link agentsWithGoalLoop}. */ export declare class GoalLoop implements Plugin { readonly name: string; /** Set when a programmatic validator was supplied as `goal`; mutually exclusive with `_goal`. */ private readonly _validator?; /** Set when a natural-language goal string was supplied; mutually exclusive with `_validator`. */ private readonly _goal?; private readonly _judgeModel?; private readonly _judgeSystemPrompt; private readonly _maxAttempts; private readonly _timeout; private readonly _preserveContext; private readonly _resumePromptTemplate; /** Per-agent run state. Keyed by agent so one plugin instance can serve many. */ private readonly _runs; constructor(opts: GoalLoopOptions); /** * Result of the most recent completed run on `agent`, or `undefined` if no * run has finished on that agent since this plugin was constructed. Reads * while a run is in-flight, or after a thrown invoke that left a run * half-finished, return `undefined` rather than stale data — the previous * run's snapshot is dropped on the next invoke. */ lastResult(agent: LocalAgent): GoalResult | undefined; initAgent(agent: LocalAgent): void; /** * Compiles the configured `goal` (a natural-language string or a `Validator` * function) into the canonical `(response) => Promise` * shape used by the After hook. The string path builds a fresh judge `Agent` * per call so prior judgements' prompts don't leak into the next judgement's * context. */ private _buildValidator; } //# sourceMappingURL=plugin.d.ts.map