# Phase 1: Adoption Readiness — Core DX Fixes

> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Fix the 8 highest-impact gaps that cause new users to bounce within 30 minutes of trying the framework.

**Architecture:** All changes are additive — no refactors, no breaking changes. Error types gain `.context()` methods. Builder gains 5 new convenience methods + build-time validation. Docs get 3 new guides. TSDoc comments added to all public API surfaces. Memory tier naming gets deprecation warnings.

**Tech Stack:** TypeScript, Effect-TS, bun:test, Starlight/Astro docs

**Spec:** `docs/superpowers/specs/2026-03-11-adoption-readiness-design.md` (items 1.1–1.8)

**Key file references (from exploration):**
- Error types: `packages/runtime/src/errors.ts` (lines 1–148)
- Builder: `packages/runtime/src/builder.ts` (lines 663–1477, ReactiveAgent at 2288–2860)
- Config: `packages/runtime/src/types.ts` (lines 252–370)
- Provider defaults: `packages/llm-provider/src/provider-defaults.ts` (lines 7–22)
- Stream types: `packages/runtime/src/stream-types.ts` (lines 1–65)
- Docs sidebar: autogenerated from directory structure (no config changes needed)

---

## Chunk 1: Actionable Error Messages (Item 1.1)

### Task 1: Add `.context()` to runtime error types

**Files:**
- Modify: `packages/runtime/src/errors.ts` (lines 1–148)
- Test: `packages/runtime/tests/errors.test.ts` (new)

- [ ] **Step 1: Write failing tests for error `.context()` methods**

**IMPORTANT:** All error classes use Effect-TS `Data.TaggedError` pattern. They take a single object argument, NOT positional args:
```typescript
// CORRECT: new MaxIterationsError({ message: "...", taskId: "...", iterations: 10, maxIterations: 10 })
// WRONG:   new MaxIterationsError("...", "...", 10, 10)
```

Create `packages/runtime/tests/errors.test.ts`:

```typescript
import { describe, test, expect } from "bun:test";
import {
  ExecutionError,
  MaxIterationsError,
  GuardrailViolationError,
  BudgetExceededError,
  KillSwitchTriggeredError,
  BehavioralContractViolationError,
  HookError,
  errorContext,
} from "../src/errors";

describe("Error context and suggestions", () => {
  test("MaxIterationsError has context with suggestion", () => {
    const err = new MaxIterationsError({ message: "limit", taskId: "task-1", iterations: 10, maxIterations: 10 });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toContain("maxIterations");
    expect(ctx.suggestion).toContain("adaptive");
  });

  test("BudgetExceededError has context with budget details", () => {
    const err = new BudgetExceededError({ message: "over", taskId: "task-1", budgetType: "perRequest", limit: 1.0, current: 1.5 });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toContain("withCostTracking");
    expect(ctx.suggestion).toContain("perRequest");
  });

  test("GuardrailViolationError has context with guardrail type", () => {
    const err = new GuardrailViolationError({ message: "blocked", taskId: "task-1", violation: "injection" });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toContain("injection");
  });

  test("KillSwitchTriggeredError has context", () => {
    const err = new KillSwitchTriggeredError({ message: "stopped", taskId: "task-1", agentId: "agent-1", reason: "manual" });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toContain("kill switch");
  });

  test("BehavioralContractViolationError has context", () => {
    const err = new BehavioralContractViolationError({ message: "denied", taskId: "task-1", rule: "tool-deny", violation: "file-write" });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toContain("withBehavioralContracts");
  });

  test("HookError has context with phase", () => {
    const err = new HookError({ message: "failed", phase: "think", timing: "before", cause: new Error("boom") });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toContain("withHook");
    expect(ctx.suggestion).toContain("think");
  });

  test("ExecutionError has context", () => {
    const err = new ExecutionError({ message: "failed", taskId: "task-1", phase: "think", cause: new Error("LLM timeout") });
    const ctx = errorContext(err);
    expect(ctx.suggestion).toBeDefined();
  });

  test("errorContext returns generic message for unknown errors", () => {
    const ctx = errorContext(new Error("something broke"));
    expect(ctx.suggestion).toContain("withObservability");
  });
});
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `cd packages/runtime && bun test tests/errors.test.ts`
Expected: FAIL — `.context()` method does not exist on error classes

- [ ] **Step 3: Add `errorContext()` function and `ErrorContext` type**

**IMPORTANT:** Error classes use `Data.TaggedError` which creates frozen objects. We CANNOT add methods to them directly. Instead, create a standalone `errorContext()` function that pattern-matches on `_tag`.

Modify `packages/runtime/src/errors.ts`. Add after the `RuntimeErrors` type union (after line 67):

```typescript
/** Context and remediation suggestion for a runtime error. */
export interface ErrorContext {
  suggestion: string;
  taskId?: string;
  phase?: string;
  details?: Record<string, unknown>;
}

/**
 * Extract actionable context and remediation suggestion from a runtime error.
 * Works with all Data.TaggedError types in the framework.
 *
 * @param error - Any error (runtime errors get specific suggestions, others get generic)
 * @returns ErrorContext with a human-readable suggestion
 */
export function errorContext(error: unknown): ErrorContext {
  if (error && typeof error === "object" && "_tag" in error) {
    const e = error as Record<string, unknown>;
    switch (e._tag) {
      case "MaxIterationsError": {
        const maxIter = e.maxIterations as number;
        return {
          suggestion: `Agent reached ${maxIter} iterations without completing. Consider: (1) simpler prompt, (2) increase maxIterations via .withMaxIterations(${maxIter + 5}), (3) enable adaptive strategy via .withReasoning({ defaultStrategy: "adaptive" })`,
          taskId: e.taskId as string,
          details: { iterations: e.iterations, maxIterations: maxIter },
        };
      }
      case "BudgetExceededError": {
        const budgetType = e.budgetType as string;
        const limit = e.limit as number;
        return {
          suggestion: `Budget limit exceeded: ${budgetType}=${limit} (current: ${e.current}). Increase via .withCostTracking({ budget: { ${budgetType}: ${limit * 2} } })`,
          taskId: e.taskId as string,
          details: { budgetType, limit, current: e.current },
        };
      }
      case "GuardrailViolationError": {
        const violation = e.violation as string;
        return {
          suggestion: `Input blocked by ${violation} guardrail. Options: (1) rephrase input to avoid trigger, (2) adjust thresholds via .withGuardrailThresholds({ ${violation}: <score> })`,
          taskId: e.taskId as string,
          phase: "guardrail",
          details: { violation },
        };
      }
      case "KillSwitchTriggeredError": {
        const reason = e.reason as string;
        return {
          suggestion: `Agent stopped by kill switch (reason: ${reason}). The kill switch was triggered ${reason === "manual" ? "manually" : "by policy"}. Call agent.resume() to continue.`,
          taskId: e.taskId as string,
          details: { agentId: e.agentId, reason },
        };
      }
      case "BehavioralContractViolationError":
        return {
          suggestion: `Behavioral contract violation: ${e.rule} (${e.violation}). Adjust via .withBehavioralContracts() or remove the constraint.`,
          taskId: e.taskId as string,
          details: { rule: e.rule, violation: e.violation },
        };
      case "HookError": {
        const cause = e.cause instanceof Error ? e.cause.message : String(e.cause ?? "unknown");
        return {
          suggestion: `Hook failed at phase "${e.phase}" (timing: ${e.timing}). Check your .withHook() handler for the "${e.phase}" phase. Cause: ${cause}`,
          phase: e.phase as string,
          details: { timing: e.timing },
        };
      }
      case "ExecutionError": {
        const rootCause = e.cause instanceof Error ? e.cause.message : String(e.cause ?? "");
        return {
          suggestion: `Execution failed at phase "${e.phase}". ${rootCause ? `Root cause: ${rootCause}` : 'Enable .withObservability({ verbosity: "debug" }) for detailed logs.'}`,
          taskId: e.taskId as string,
          phase: e.phase as string,
          details: { cause: rootCause },
        };
      }
    }
  }

  // Generic fallback for unknown errors
  const msg = error instanceof Error ? error.message : String(error);
  return {
    suggestion: `Unexpected error: ${msg}. Enable .withObservability({ verbosity: "debug" }) for detailed diagnostics.`,
  };
}
```

Also export `ErrorContext` and `errorContext` from `packages/runtime/src/index.ts`.

- [ ] **Step 4: Run tests to verify they pass**

Run: `cd packages/runtime && bun test tests/errors.test.ts`
Expected: All 8 tests PASS

- [ ] **Step 5: Commit**

```bash
git add packages/runtime/src/errors.ts packages/runtime/tests/errors.test.ts
git commit -m "feat(runtime): add .context() with remediation suggestions to all error types"
```

### Task 2: Clean error unwrapping at the facade level

**Files:**
- Modify: `packages/runtime/src/errors.ts` (unwrapError function, line 79)
- Modify: `packages/runtime/src/builder.ts` (ReactiveAgent.run(), line 2404)
- Test: `packages/runtime/tests/errors.test.ts` (extend)

- [ ] **Step 6: Write failing test for clean facade errors**

**IMPORTANT:** FiberFailure is detected via `Symbol.for("effect/Runtime/FiberFailure")`, not a `_tag` property. We must test with real Effect errors to get proper wrapping, or check the enhanced unwrapError against known tagged error types directly.

Add to `packages/runtime/tests/errors.test.ts`:

```typescript
import { unwrapError, errorContext } from "../src/errors";
import { Effect } from "effect";

describe("unwrapError facade", () => {
  test("unwrapError preserves known tagged error types", () => {
    // Data.TaggedError instances have _tag but are not instanceof Error
    const original = new MaxIterationsError({ message: "limit", taskId: "task-1", iterations: 10, maxIterations: 10 });
    const unwrapped = unwrapError(original);
    // Should preserve the tagged error (it has _tag = "MaxIterationsError")
    expect(unwrapped).toBeDefined();
    expect(errorContext(unwrapped).suggestion).toContain("maxIterations");
  });

  test("unwrapError extracts clean message from real FiberFailure", async () => {
    // Create a real FiberFailure by running an Effect that fails
    const failEffect = Effect.fail(
      new MaxIterationsError({ message: "hit limit", taskId: "t1", iterations: 10, maxIterations: 10 }),
    );
    try {
      await Effect.runPromise(failEffect);
      expect(true).toBe(false); // should not reach here
    } catch (e) {
      const unwrapped = unwrapError(e);
      // Should extract the root error, not show FiberFailure noise
      expect(unwrapped.message).not.toContain("FiberFailure");
    }
  });

  test("unwrapError with suggestion includes remediation for known errors", () => {
    const err = new BudgetExceededError({ message: "over", taskId: "task-1", budgetType: "perRequest", limit: 1.0, current: 1.5 });
    const unwrapped = unwrapErrorWithSuggestion(err);
    expect(unwrapped.message).toContain("withCostTracking");
  });
});
```

- [ ] **Step 7: Run test to verify it fails**

Run: `cd packages/runtime && bun test tests/errors.test.ts`
Expected: FAIL — unwrapError doesn't preserve original error instance

- [ ] **Step 8: Add `unwrapErrorWithSuggestion()` and update `unwrapError()` to preserve tagged errors**

**IMPORTANT:** FiberFailure detection uses `Symbol.for("effect/Runtime/FiberFailure")` and Cause extraction uses `Symbol.for("effect/Runtime/FiberFailure/Cause")`. The existing `extractMessage()` already handles this correctly. We add a new `unwrapErrorWithSuggestion()` that enriches the message with remediation context.

Modify `packages/runtime/src/errors.ts`:

1. Update `unwrapError()` to try preserving known tagged error types:

```typescript
const KNOWN_TAGS = new Set([
  "ExecutionError", "HookError", "MaxIterationsError",
  "GuardrailViolationError", "KillSwitchTriggeredError",
  "BehavioralContractViolationError", "BudgetExceededError",
]);

export function unwrapError(error: unknown): Error {
  // If it's already a known tagged error, wrap in Error with clean message
  if (isKnownTaggedError(error)) {
    const msg = (error as { message: string }).message;
    const err = new Error(cleanMessage(msg));
    // Attach the original tagged error for consumers who want to inspect it
    (err as any)._originalTaggedError = error;
    return err;
  }

  // Already a clean Error with no FiberFailure nesting
  if (error instanceof Error && !isFiberFailure(error)) {
    return error;
  }

  // Dig through FiberFailure — try to find a known tagged error inside
  const root = extractRootTaggedError(error);
  if (root && isKnownTaggedError(root)) {
    const msg = (root as { message: string }).message;
    const err = new Error(cleanMessage(msg));
    (err as any)._originalTaggedError = root;
    return err;
  }

  // Fallback: extract clean message string
  const message = extractMessage(error);
  return new Error(message);
}

/**
 * Like unwrapError but appends the remediation suggestion to the error message.
 * Use this at the facade level (agent.run()) for maximum user helpfulness.
 */
export function unwrapErrorWithSuggestion(error: unknown): Error {
  const base = unwrapError(error);
  const original = (base as any)._originalTaggedError ?? error;
  const ctx = errorContext(original);
  if (ctx.suggestion && !base.message.includes("Consider:") && !base.message.includes("Options:")) {
    base.message = `${base.message}\n  → ${ctx.suggestion}`;
  }
  return base;
}

function isKnownTaggedError(error: unknown): boolean {
  return (
    error != null &&
    typeof error === "object" &&
    "_tag" in error &&
    KNOWN_TAGS.has((error as { _tag: string })._tag)
  );
}

function extractRootTaggedError(error: unknown): unknown {
  // Dig through FiberFailure/Cause chain looking for a tagged error
  if (error == null || typeof error !== "object") return null;
  if (isKnownTaggedError(error)) return error;

  if (isFiberFailure(error)) {
    const cause = (error as Record<string | symbol, unknown>)[
      Symbol.for("effect/Runtime/FiberFailure/Cause")
    ];
    if (cause) return extractRootTaggedError(cause);
  }

  const tagged = error as { _tag?: string; error?: unknown; defect?: unknown };
  if (tagged._tag === "Fail" && tagged.error) return extractRootTaggedError(tagged.error);
  if (tagged._tag === "Die" && tagged.defect) return extractRootTaggedError(tagged.defect);

  return null;
}
```

2. Export `unwrapErrorWithSuggestion` from `packages/runtime/src/index.ts`.

- [ ] **Step 9: Run tests to verify they pass**

Run: `cd packages/runtime && bun test tests/errors.test.ts`
Expected: All tests PASS

- [ ] **Step 10: Update ReactiveAgent.run() to use enhanced unwrapError**

Modify `packages/runtime/src/builder.ts` around line 2404. The `.run()` method currently does:
```typescript
return Effect.runPromise(/* ... */).catch((e) => { throw unwrapError(e); });
```

Verify this already uses `unwrapError` — if so, the enhanced version flows through automatically. If not, add the catch:

```typescript
async run(input: string): Promise<AgentResult> {
  // ... existing code ...
  return this.runtime.runPromise(effect).catch((e) => {
    throw unwrapError(e);
  });
}
```

- [ ] **Step 11: Run full runtime test suite**

Run: `cd packages/runtime && bun test`
Expected: All existing tests PASS (no regressions)

- [ ] **Step 12: Commit**

```bash
git add packages/runtime/src/errors.ts packages/runtime/src/builder.ts packages/runtime/tests/errors.test.ts
git commit -m "feat(runtime): unwrapError preserves known error types with context and suggestions"
```

---

## Chunk 2: Build-Time Validation + Builder Convenience Methods (Items 1.2 + 1.3)

### Task 3: Build-time provider validation

**Files:**
- Modify: `packages/runtime/src/builder.ts` (build() method, line 1499)
- Create: `packages/runtime/src/build-validation.ts` (new)
- Test: `packages/runtime/tests/build-validation.test.ts` (new)

- [ ] **Step 13: Write failing tests for build-time validation**

Create `packages/runtime/tests/build-validation.test.ts`:

```typescript
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { ReactiveAgents } from "../src";

describe("Build-time validation", () => {
  const originalEnv = { ...process.env };

  afterEach(() => {
    process.env = { ...originalEnv };
  });

  test("warns on missing API key for anthropic provider", async () => {
    delete process.env.ANTHROPIC_API_KEY;
    const warnings: string[] = [];
    const origWarn = console.warn;
    console.warn = (msg: string) => warnings.push(msg);

    await ReactiveAgents.create()
      .withProvider("anthropic")
      .build();

    console.warn = origWarn;
    expect(warnings.some(w => w.includes("ANTHROPIC_API_KEY"))).toBe(true);
  });

  test("throws on missing API key with strict validation", async () => {
    delete process.env.ANTHROPIC_API_KEY;
    expect(
      ReactiveAgents.create()
        .withProvider("anthropic")
        .withStrictValidation()
        .build()
    ).rejects.toThrow("ANTHROPIC_API_KEY");
  });

  test("skips API key check for ollama provider", async () => {
    const warnings: string[] = [];
    const origWarn = console.warn;
    console.warn = (msg: string) => warnings.push(msg);

    await ReactiveAgents.create()
      .withProvider("ollama")
      .build();

    console.warn = origWarn;
    expect(warnings.some(w => w.includes("API_KEY"))).toBe(false);
  });

  test("skips validation for test provider", async () => {
    const warnings: string[] = [];
    const origWarn = console.warn;
    console.warn = (msg: string) => warnings.push(msg);

    await ReactiveAgents.create()
      .withProvider("test")
      .build();

    console.warn = origWarn;
    expect(warnings.some(w => w.includes("API_KEY"))).toBe(false);
  });

  test("warns on model/provider mismatch", async () => {
    process.env.ANTHROPIC_API_KEY = "sk-ant-test";
    const warnings: string[] = [];
    const origWarn = console.warn;
    console.warn = (msg: string) => warnings.push(msg);

    await ReactiveAgents.create()
      .withProvider("anthropic")
      .withModel("gpt-4o")
      .build();

    console.warn = origWarn;
    expect(warnings.some(w => w.includes("gpt-4o") && w.includes("anthropic"))).toBe(true);
  });

  test("logs resolved provider info on build", async () => {
    process.env.ANTHROPIC_API_KEY = "sk-ant-test123";
    const logs: string[] = [];
    const origLog = console.log;
    console.log = (msg: string) => { if (typeof msg === "string") logs.push(msg); };

    await ReactiveAgents.create()
      .withProvider("anthropic")
      .build();

    console.log = origLog;
    expect(logs.some(l => l.includes("Provider:") && l.includes("anthropic"))).toBe(true);
  });
});
```

- [ ] **Step 14: Run tests to verify they fail**

Run: `cd packages/runtime && bun test tests/build-validation.test.ts`
Expected: FAIL — `.withStrictValidation()` does not exist, no warnings emitted

- [ ] **Step 15: Create build-validation module**

Create `packages/runtime/src/build-validation.ts`:

```typescript
import type { ProviderName } from "@reactive-agents/llm-provider";

const PROVIDER_API_KEY_MAP: Record<string, string> = {
  anthropic: "ANTHROPIC_API_KEY",
  openai: "OPENAI_API_KEY",
  gemini: "GOOGLE_API_KEY",
  litellm: "OPENAI_API_KEY",
};

// Simple prefix-based heuristic for model/provider matching
const PROVIDER_MODEL_PREFIXES: Record<string, string[]> = {
  anthropic: ["claude"],
  openai: ["gpt", "o1", "o3", "chatgpt"],
  gemini: ["gemini"],
  ollama: [], // any model is valid for ollama
  litellm: [], // any model is valid for litellm proxy
  test: ["test"],
};

const NO_KEY_PROVIDERS = new Set(["ollama", "test"]);

export interface BuildValidationResult {
  warnings: string[];
  errors: string[];
  resolvedModel: string;
}

export function validateBuild(
  provider: ProviderName,
  model: string | undefined,
  defaultModel: string,
  strict: boolean,
): BuildValidationResult {
  const warnings: string[] = [];
  const errors: string[] = [];
  const resolvedModel = model ?? defaultModel;

  // 1. API key check
  if (!NO_KEY_PROVIDERS.has(provider)) {
    const keyName = PROVIDER_API_KEY_MAP[provider];
    if (keyName && !process.env[keyName]) {
      const msg = `Missing ${keyName} for provider "${provider}". Set it in your environment or .env file.`;
      if (strict) {
        errors.push(msg);
      } else {
        warnings.push(msg);
      }
    }
  }

  // 2. Model/provider mismatch check
  if (model && provider !== "ollama" && provider !== "litellm" && provider !== "test") {
    const prefixes = PROVIDER_MODEL_PREFIXES[provider] ?? [];
    if (prefixes.length > 0) {
      const modelLower = model.toLowerCase();
      const matches = prefixes.some(p => modelLower.startsWith(p));
      if (!matches) {
        warnings.push(
          `Model "${model}" may not be compatible with provider "${provider}". ` +
          `Expected model prefix: ${prefixes.join(", ")}. ` +
          `The provider's default model will be used if this model is unavailable.`
        );
      }
    }
  }

  return { warnings, errors, resolvedModel };
}

export function logBuildInfo(
  provider: ProviderName,
  resolvedModel: string,
): void {
  const keyName = PROVIDER_API_KEY_MAP[provider];
  const hasKey = keyName ? !!process.env[keyName] : false;
  const keyDisplay = hasKey && keyName
    ? `${process.env[keyName]!.slice(0, 8)}...***`
    : NO_KEY_PROVIDERS.has(provider) ? "(not required)" : "(missing)";

  console.log(`✓ Provider: ${provider} | Model: ${resolvedModel} | API key: ${keyDisplay}`);
}
```

- [ ] **Step 16: Wire validation into builder**

Modify `packages/runtime/src/builder.ts`:

1. Add private field (around line 710):
```typescript
private _strictValidation: boolean = false;
```

2. Add builder method (after `withTestResponses`, around line 1470):
```typescript
/**
 * Enable strict build-time validation. Missing API keys and model mismatches
 * become hard errors instead of warnings.
 */
withStrictValidation(): this {
  this._strictValidation = true;
  return this;
}
```

3. In `build()` method (line 1499), add validation before the Effect.runPromise:
```typescript
async build(): Promise<ReactiveAgent> {
  // Build-time validation
  const { validateBuild, logBuildInfo } = await import("./build-validation");
  const { getProviderDefaultModel } = await import("@reactive-agents/llm-provider");
  const defaultModel = getProviderDefaultModel(this._provider) ?? "unknown";
  const validation = validateBuild(
    this._provider,
    this._model,
    defaultModel,
    this._strictValidation,
  );

  for (const warning of validation.warnings) {
    console.warn(`⚠ ${warning}`);
  }
  if (validation.errors.length > 0) {
    throw new Error(`Build validation failed:\n${validation.errors.map(e => `  • ${e}`).join("\n")}`);
  }

  logBuildInfo(this._provider, validation.resolvedModel);

  // ... existing build logic ...
}
```

- [ ] **Step 17: Run tests to verify they pass**

Run: `cd packages/runtime && bun test tests/build-validation.test.ts`
Expected: All 6 tests PASS

- [ ] **Step 18: Run full runtime test suite**

Run: `cd packages/runtime && bun test`
Expected: All existing tests PASS. Some tests may now emit validation warnings — that's expected and harmless (most use "test" provider which skips validation).

- [ ] **Step 19: Commit**

```bash
git add packages/runtime/src/build-validation.ts packages/runtime/src/builder.ts packages/runtime/tests/build-validation.test.ts
git commit -m "feat(runtime): build-time provider validation with API key checks and model/provider mismatch warnings"
```

### Task 4: Builder convenience methods

**Files:**
- Modify: `packages/runtime/src/builder.ts` (add new methods)
- Modify: `packages/runtime/src/types.ts` (add config fields)
- Test: `packages/runtime/tests/builder-convenience.test.ts` (new)

- [ ] **Step 20: Write failing tests for new builder methods**

Create `packages/runtime/tests/builder-convenience.test.ts`:

```typescript
import { describe, test, expect } from "bun:test";
import { ReactiveAgents } from "../src";

describe("Builder convenience methods", () => {
  test("withTimeout sets execution timeout", async () => {
    const builder = ReactiveAgents.create()
      .withProvider("test")
      .withTimeout(5000);
    // Verify the config was set by building and checking agent exists
    const agent = await builder.build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("withRetryPolicy sets retry configuration", async () => {
    const builder = ReactiveAgents.create()
      .withProvider("test")
      .withRetryPolicy({ maxRetries: 3, backoffMs: 1000 });
    const agent = await builder.build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("withCacheTimeout sets semantic cache TTL", async () => {
    const builder = ReactiveAgents.create()
      .withProvider("test")
      .withCacheTimeout(300_000);
    const agent = await builder.build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("withGuardrailThresholds sets detection thresholds", async () => {
    const builder = ReactiveAgents.create()
      .withProvider("test")
      .withGuardrails()
      .withGuardrailThresholds({ injection: 0.9, pii: 0.85 });
    const agent = await builder.build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("methods are chainable", async () => {
    const agent = await ReactiveAgents.create()
      .withProvider("test")
      .withTimeout(5000)
      .withRetryPolicy({ maxRetries: 2, backoffMs: 500 })
      .withCacheTimeout(600_000)
      .withGuardrails()
      .withGuardrailThresholds({ injection: 0.95 })
      .withStrictValidation()
      .build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("withTimeout causes execution to fail after timeout", async () => {
    const agent = await ReactiveAgents.create()
      .withProvider("test")
      .withTestResponses({ ".*": "thinking..." })
      .withTimeout(100)  // 100ms — will timeout
      .withReasoning()
      .build();

    // This should timeout since test provider doesn't produce final answer
    const result = await agent.run("do something complex").catch(e => e);
    expect(result).toBeInstanceOf(Error);
    await agent.dispose();
  });
});
```

- [ ] **Step 21: Run tests to verify they fail**

Run: `cd packages/runtime && bun test tests/builder-convenience.test.ts`
Expected: FAIL — `.withTimeout()`, `.withRetryPolicy()`, etc. do not exist

- [ ] **Step 22: Add config fields to types.ts**

Modify `packages/runtime/src/types.ts` (inside the ReactiveAgentsConfig schema, around line 350):

```typescript
// Add these fields to the Schema.Struct:
executionTimeoutMs: Schema.optional(Schema.Number),
retryPolicy: Schema.optional(Schema.Struct({
  maxRetries: Schema.Number,
  backoffMs: Schema.Number,
})),
cacheTimeoutMs: Schema.optional(Schema.Number),
guardrailThresholds: Schema.optional(Schema.Struct({
  injection: Schema.optional(Schema.Number),
  pii: Schema.optional(Schema.Number),
  toxicity: Schema.optional(Schema.Number),
})),
```

Also update `defaultReactiveAgentsConfig` to include these fields with undefined defaults.

- [ ] **Step 23: Add builder methods**

Modify `packages/runtime/src/builder.ts`:

1. Add private fields (around line 710):
```typescript
private _executionTimeoutMs?: number;
private _retryPolicy?: { maxRetries: number; backoffMs: number };
private _cacheTimeoutMs?: number;
private _guardrailThresholds?: { injection?: number; pii?: number; toxicity?: number };
```

2. Add methods (after `withStrictValidation`):
```typescript
/**
 * Set a timeout for agent execution. If the agent doesn't complete within
 * this duration, execution is aborted and an error is thrown.
 * @param ms - Timeout in milliseconds
 * @example .withTimeout(30_000) // 30 seconds
 */
withTimeout(ms: number): this {
  this._executionTimeoutMs = ms;
  return this;
}

/**
 * Configure retry policy for LLM calls. When an LLM call fails with a
 * transient error (rate limit, network), it will be retried with exponential backoff.
 * @param policy - Retry configuration
 * @param policy.maxRetries - Maximum number of retries (default: 0 = no retries)
 * @param policy.backoffMs - Base backoff duration in milliseconds (doubled each retry)
 * @example .withRetryPolicy({ maxRetries: 3, backoffMs: 1000 })
 */
withRetryPolicy(policy: { maxRetries: number; backoffMs: number }): this {
  this._retryPolicy = policy;
  return this;
}

/**
 * Set the TTL for semantic cache entries. Cached LLM responses older than
 * this duration will be evicted.
 * @param ms - Cache TTL in milliseconds (default: 3,600,000 = 1 hour)
 * @example .withCacheTimeout(600_000) // 10 minutes
 */
withCacheTimeout(ms: number): this {
  this._cacheTimeoutMs = ms;
  return this;
}

/**
 * Set detection score thresholds for guardrail checks. Lower values are
 * more sensitive (more false positives). Higher values are more permissive.
 * Requires .withGuardrails() to be enabled.
 * @param thresholds - Score thresholds (0.0–1.0) per guardrail type
 * @example .withGuardrailThresholds({ injection: 0.9, pii: 0.85 })
 */
withGuardrailThresholds(thresholds: { injection?: number; pii?: number; toxicity?: number }): this {
  this._guardrailThresholds = thresholds;
  return this;
}
```

3. Wire these into the config object in `buildEffect()` (around line 1560 where config is assembled):
```typescript
executionTimeoutMs: this._executionTimeoutMs,
retryPolicy: this._retryPolicy,
cacheTimeoutMs: this._cacheTimeoutMs,
guardrailThresholds: this._guardrailThresholds,
```

4. In `buildEffect()`, if `_executionTimeoutMs` is set, wrap the execution Effect with `Effect.timeout`:
```typescript
// In the execute function that's passed to ReactiveAgent constructor:
let executeEffect = engine.execute(task);
if (config.executionTimeoutMs) {
  executeEffect = executeEffect.pipe(
    Effect.timeout(Duration.millis(config.executionTimeoutMs)),
    Effect.catchTag("TimeoutException", () =>
      Effect.fail(new ExecutionError(
        `Execution timed out after ${config.executionTimeoutMs}ms`,
        task.id, "think"
      ))
    ),
  );
}
```

- [ ] **Step 24: Run tests to verify they pass**

Run: `cd packages/runtime && bun test tests/builder-convenience.test.ts`
Expected: All 6 tests PASS

- [ ] **Step 25: Run full runtime test suite**

Run: `cd packages/runtime && bun test`
Expected: All existing tests PASS

- [ ] **Step 26: Commit**

```bash
git add packages/runtime/src/builder.ts packages/runtime/src/types.ts packages/runtime/tests/builder-convenience.test.ts
git commit -m "feat(runtime): add builder convenience methods — withTimeout, withRetryPolicy, withCacheTimeout, withGuardrailThresholds"
```

---

## Chunk 3: Documentation — Configuration Reference, Local Models Guide, Hooks Guide (Items 1.4 + 1.5 + 1.6)

### Task 5: Configuration reference docs page

**Files:**
- Create: `apps/docs/src/content/docs/reference/configuration.md`

- [ ] **Step 27: Create configuration reference page**

Create `apps/docs/src/content/docs/reference/configuration.md`:

```markdown
---
title: Configuration Reference
description: Complete reference of all builder methods, defaults, and environment variables
---

# Configuration Reference

Every aspect of Reactive Agents is configurable through the builder API. This page documents all available options, their defaults, and how they affect agent behavior.

## Builder Methods

### Core

| Method | Default | Description |
|--------|---------|-------------|
| `.withName(name)` | `"agent"` | Agent identifier used in logs and metrics |
| `.withProvider(provider)` | `"test"` | LLM provider: `"anthropic"` \| `"openai"` \| `"gemini"` \| `"ollama"` \| `"litellm"` \| `"test"` |
| `.withModel(model)` | Provider default | Model name or preset (e.g., `"claude-sonnet"`, `"gpt-4o"`) |
| `.withSystemPrompt(prompt)` | none | Custom system prompt prepended to all LLM calls |
| `.withPersona(persona)` | none | Structured persona: `{ name?, role?, background?, instructions?, tone? }` |
| `.withMaxIterations(n)` | `10` | Maximum reasoning loop iterations before stopping |
| `.withTimeout(ms)` | none | Per-execution timeout in milliseconds |

### Reasoning

| Method | Default | Description |
|--------|---------|-------------|
| `.withReasoning(options?)` | disabled | Enable reasoning loop. Options: `{ defaultStrategy?: string }` |
| `.withCircuitBreaker(config?)` | auto | Loop detection: `{ maxSameToolCalls?, maxConsecutiveThoughts? }` |

### Tools

| Method | Default | Description |
|--------|---------|-------------|
| `.withTools(options?)` | disabled | Enable tool registry. Options: `{ include?, exclude?, custom?, allowedTools?, adaptiveToolFiltering?, resultCompression? }` |
| `.withRequiredTools(config)` | none | Tools that must be called: `{ tools?, adaptive?, maxRetries? }` |
| `.withMCP(config)` | none | MCP server connections: `{ name, transport, command?, args?, url? }` |
| `.withGuardrailThresholds(t)` | provider defaults | Detection score thresholds: `{ injection?, pii?, toxicity? }` (0.0–1.0) |

### Memory

| Method | Default | Description |
|--------|---------|-------------|
| `.withMemory(options?)` | disabled | Enable memory. No args = standard tier. Options: `{ tier: "standard" \| "enhanced" }` |
| `.withMemoryConsolidation(config?)` | disabled | Background memory intelligence: `{ threshold?, decayFactor?, pruneThreshold? }` |
| `.withExperienceLearning()` | disabled | Cross-agent tool-use pattern learning |

### Safety & Control

| Method | Default | Description |
|--------|---------|-------------|
| `.withGuardrails(options?)` | disabled | Enable injection/PII/toxicity detection |
| `.withVerification(options?)` | disabled | Post-LLM fact-checking (semantic entropy, NLI) |
| `.withKillSwitch()` | disabled | Enable pause/resume/stop/terminate controls |
| `.withBehavioralContracts(config?)` | none | Rule enforcement: `{ deniedTools?, maxIterations?, allowedActions? }` |

### Cost & Efficiency

| Method | Default | Description |
|--------|---------|-------------|
| `.withCostTracking(options?)` | disabled | Budget enforcement. Options: `{ budget?: { perRequest?, perSession?, daily?, monthly? } }` |
| `.withCacheTimeout(ms)` | `3,600,000` (1hr) | Semantic cache TTL in milliseconds |
| `.withRetryPolicy(policy)` | `{ maxRetries: 0 }` | LLM call retry: `{ maxRetries, backoffMs }` |
| `.withContextProfile(profile)` | auto-detected | Model-adaptive context: `{ tier: "local" \| "mid" \| "large" \| "frontier" }` |

### Observability

| Method | Default | Description |
|--------|---------|-------------|
| `.withObservability(options?)` | disabled | Metrics dashboard + tracing. Options: `{ verbosity?, live? }` |
| `.withStreaming(options?)` | disabled | Token streaming. Options: `{ density?: "tokens" \| "full" }` |
| `.withTelemetry(config?)` | disabled | OpenTelemetry export configuration |
| `.withAudit()` | disabled | Compliance audit logging |

### Identity & Orchestration

| Method | Default | Description |
|--------|---------|-------------|
| `.withIdentity()` | disabled | Ed25519 agent certificates + RBAC |
| `.withOrchestration()` | disabled | Multi-agent workflow engine |
| `.withInteraction()` | disabled | 5 autonomy modes + checkpoints |
| `.withSelfImprovement()` | disabled | Cross-task strategy outcome learning |

### Sub-Agents

| Method | Default | Description |
|--------|---------|-------------|
| `.withAgentTool(name, config)` | none | Register a static sub-agent as a tool |
| `.withDynamicSubAgents(options?)` | disabled | Allow LLM to spawn sub-agents at runtime |
| `.withRemoteAgent(name, url)` | none | Connect to a remote agent via A2A protocol |

### Gateway

| Method | Default | Description |
|--------|---------|-------------|
| `.withGateway(options?)` | disabled | Persistent autonomous harness: `{ heartbeat?, crons?, webhooks?, policies?, channels? }` |

### Build Options

| Method | Default | Description |
|--------|---------|-------------|
| `.withStrictValidation()` | `false` | Make build-time warnings into hard errors |
| `.withTestResponses(map)` | none | Deterministic test responses: `Record<string, string>` |
| `.withLayers(layers)` | none | Inject custom Effect-TS layers |

## Environment Variables

| Variable | Required For | Default | Description |
|----------|-------------|---------|-------------|
| `ANTHROPIC_API_KEY` | Anthropic provider | — | Anthropic API key |
| `OPENAI_API_KEY` | OpenAI/LiteLLM provider | — | OpenAI API key |
| `GOOGLE_API_KEY` | Gemini provider | — | Google AI API key |
| `TAVILY_API_KEY` | Web search tool | — | Tavily search API key |
| `EMBEDDING_PROVIDER` | Enhanced memory tier | `"openai"` | Embedding provider |
| `EMBEDDING_MODEL` | Enhanced memory tier | `"text-embedding-3-small"` | Embedding model name |
| `LLM_DEFAULT_MODEL` | All providers | Provider default | Override default model |

## Hardcoded Defaults

These values have sensible defaults but are not currently configurable via the builder:

| Value | Default | Where | Notes |
|-------|---------|-------|-------|
| Max sub-agent iterations | 4 | `packages/tools/src/` | Sub-agents capped at 4 iterations |
| Max recursion depth | 3 | `packages/tools/src/` | Nested sub-agent limit |
| Parent context forwarding | 2000 chars | `packages/tools/src/` | Max parent context sent to sub-agents |
| Memory decay half-life | 7 days | `packages/memory/src/` | Episodic memory decay rate |
| Compaction trigger | 6 iterations | `packages/reasoning/src/` | Steps before context compaction (local tier) |
```

- [ ] **Step 28: Verify docs build**

Run: `cd apps/docs && npx astro build 2>&1 | tail -5`
Expected: Build succeeds, page count includes new reference page

- [ ] **Step 29: Commit**

```bash
git add apps/docs/src/content/docs/reference/configuration.md
git commit -m "docs: add comprehensive configuration reference with all builder methods, env vars, and defaults"
```

### Task 6: Local models guide

**Files:**
- Create: `apps/docs/src/content/docs/guides/local-models.md`

- [ ] **Step 30: Create local models guide**

Create `apps/docs/src/content/docs/guides/local-models.md`:

```markdown
---
title: Local Models Guide
description: Choose the right local model for your task and configure Reactive Agents for optimal performance
---

# Local Models Guide

Reactive Agents is designed to work with local models via Ollama. The model-adaptive context system automatically tunes prompts, compaction, and truncation for smaller models — but choosing the right model for your task matters.

## Quick Setup

```bash
# Install Ollama (macOS/Linux)
curl -fsSL https://ollama.com/install.sh | sh

# Pull a recommended model
ollama pull qwen3:14b

# Use it with Reactive Agents
const agent = await ReactiveAgents.create()
  .withProvider("ollama")
  .withModel("qwen3:14b")
  .withReasoning()
  .withTools()
  .withContextProfile({ tier: "local" })
  .build();
```

## Model Recommendations

### By Task Type

| Task | Recommended Model | Tier | Why |
|------|-------------------|------|-----|
| Simple Q&A (no tools) | `qwen3:4b` | local | Fast, low memory, good for chat |
| Tool-calling tasks | `qwen3:14b` | local | Best tool-call accuracy at this size |
| Research with web search | `qwen3:14b` or `llama3.1:8b` | local | Reliable ReAct format adherence |
| Code generation | `qwen2.5-coder:14b` | local | Specialized for code tasks |
| Complex reasoning | `cogito:14b` | local | Extended thinking mode support |
| Multi-step planning | `qwen3:14b` with Plan-Execute | local | Structured plan generation |

### Model Comparison

| Model | Params | Context | Tool Calling | ReAct Format | Speed | Memory |
|-------|--------|---------|:------------:|:------------:|-------|--------|
| `qwen3:4b` | 4B | 32K | Fair | Fair | Fast | ~3GB |
| `llama3.1:8b` | 8B | 128K | Good | Good | Medium | ~5GB |
| `qwen3:8b` | 8B | 32K | Good | Good | Medium | ~5GB |
| `phi-4:14b` | 14B | 16K | Good | Fair | Medium | ~9GB |
| `qwen3:14b` | 14B | 32K | Best | Best | Slower | ~9GB |
| `cogito:14b` | 14B | 32K | Good | Good | Slower | ~9GB |
| `llama3.1:70b` | 70B | 128K | Excellent | Excellent | Slow | ~40GB |

### Legend
- **Tool Calling**: How reliably the model generates valid tool call JSON
- **ReAct Format**: How well the model follows Think/Action/Observation format
- **Speed**: Tokens per second on typical hardware (relative)
- **Memory**: Approximate VRAM/RAM required

## Context Profile Tiers

Always set the context profile to match your model:

```typescript
// Small models (<=8B params)
.withContextProfile({ tier: "local" })
// → Lean prompts, aggressive compaction after 6 steps, 800-char truncation

// Medium models (8B-30B params)
.withContextProfile({ tier: "mid" })
// → Balanced prompts, moderate compaction

// Large cloud models
.withContextProfile({ tier: "large" })
// → Full context, standard compaction

// Frontier models (Claude Opus, GPT-4, Gemini Pro)
.withContextProfile({ tier: "frontier" })
// → Maximum context, minimal compaction
```

**Important:** If you skip `.withContextProfile()`, the framework uses `"large"` tier defaults — which wastes tokens and confuses smaller models with verbose prompts.

## Strategy Recommendations for Local Models

Not all reasoning strategies work well on small models:

| Strategy | <=8B | 14B | 70B | Notes |
|----------|:----:|:---:|:---:|-------|
| **ReAct** | Good | Best | Best | Most reliable for local models |
| **Reflexion** | Poor | Fair | Good | Self-critique requires model quality |
| **Plan-Execute** | Poor | Fair | Good | Structured plan generation is fragile on small models |
| **Tree-of-Thought** | Poor | Poor | Fair | BFS scoring unreliable below 70B |
| **Adaptive** | Fair | Good | Best | Falls back to ReAct on small models (good) |

**Recommendation:** Use `"reactive"` (ReAct) as default strategy for all local models. Only use `"adaptive"` if you're running 14B+ and want automatic strategy selection.

## Common Pitfalls

### 1. Model hallucinates tool calls
**Symptom:** Agent calls tools that don't exist or uses wrong parameter names.
**Fix:** Use `.withContextProfile({ tier: "local" })` and keep tool count low (3-5 tools max). Use `.withTools({ include: [...] })` to limit visible tools.

### 2. Agent loops without making progress
**Symptom:** Agent repeats the same action or thought.
**Fix:** The circuit breaker will catch this, but you can reduce iterations with `.withMaxIterations(5)`. Consider simpler prompts.

### 3. ReAct format not followed
**Symptom:** Agent outputs free-form text instead of `Thought:` / `ACTION:` format.
**Fix:** Switch to a model with better instruction following (`qwen3:14b` > `llama3.1:8b` for this). The `local` context profile uses more explicit format instructions.

### 4. Out of memory
**Symptom:** Ollama crashes or becomes unresponsive.
**Fix:** Use a smaller model or enable quantization: `ollama pull qwen3:14b-q4_K_M`. The q4 quantization uses ~60% less memory with minimal quality loss.

### 5. Sub-agents perform poorly
**Symptom:** Spawned sub-agents hallucinate or loop.
**Fix:** Known limitation — small models struggle with sub-agent tasks. Disable dynamic sub-agents (`.withDynamicSubAgents()`) for local models. Use static sub-agents with explicit task descriptions instead.

## Cost Comparison

Local models are free to run but consume hardware resources:

| Setup | Monthly Cost | Latency | Quality |
|-------|-------------|---------|---------|
| Ollama + qwen3:14b (local) | $0 (electricity only) | 1-5s/response | Good for most tasks |
| Anthropic claude-haiku | ~$5-15/month | 0.5-2s | Better quality |
| Anthropic claude-sonnet | ~$15-50/month | 1-3s | Best quality |
| Ollama + llama3.1:70b (beefy local) | $0 | 3-10s | Near cloud quality |

## Full Example

```typescript
import { ReactiveAgents } from "reactive-agents";

const agent = await ReactiveAgents.create()
  .withName("local-researcher")
  .withProvider("ollama")
  .withModel("qwen3:14b")
  .withReasoning({ defaultStrategy: "reactive" })
  .withTools({ include: ["web-search", "file-read", "file-write"] })
  .withContextProfile({ tier: "local" })
  .withMaxIterations(8)
  .withMemory()
  .withObservability({ verbosity: "normal" })
  .build();

const result = await agent.run("Research TypeScript testing frameworks and write a summary");
console.log(result.output);
console.log(result.metadata); // { duration, cost: 0, tokensUsed, stepsCount }
```
```

- [ ] **Step 31: Verify docs build**

Run: `cd apps/docs && npx astro build 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 32: Commit**

```bash
git add apps/docs/src/content/docs/guides/local-models.md
git commit -m "docs: add local models guide with model recommendations, pitfalls, and strategy guidance"
```

### Task 7: Hook system visibility (README + example + guide)

**Files:**
- Modify: `README.md` (add hooks section)
- Create: `apps/examples/src/advanced/19-lifecycle-hooks.ts`
- Create: `apps/docs/src/content/docs/guides/hooks.md`

- [ ] **Step 33: Add hooks section to README**

Modify `README.md`. Find the "### Streaming" section (around line 134) and add after it:

```markdown
### Lifecycle Hooks

Intercept any of the 10 execution phases with before, after, or error hooks:

```typescript
const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withReasoning()
  .withTools()
  .withHook({
    phase: "think",
    timing: "after",
    handler: (ctx) => {
      console.log(`Step ${ctx.metadata.stepsCount}: ${ctx.metadata.strategyUsed}`);
      return Effect.succeed(ctx);
    },
  })
  .withHook({
    phase: "act",
    timing: "after",
    handler: (ctx) => {
      // Log every tool call
      const lastTool = ctx.scratchpad.get("_last_tool_name");
      if (lastTool) console.log(`Tool called: ${lastTool}`);
      return Effect.succeed(ctx);
    },
  })
  .build();
```

Available phases: `bootstrap`, `guardrail`, `cost-route`, `strategy`, `think`, `act`, `observe`, `verify`, `memory-flush`, `complete`. Each supports `before`, `after`, and `on-error` timing.
```

- [ ] **Step 34: Create hooks example**

Create `apps/examples/src/advanced/19-lifecycle-hooks.ts`:

```typescript
/**
 * Example 19: Lifecycle Hooks
 *
 * Demonstrates how to use the 10-phase execution engine hooks to:
 * - Log execution progress
 * - Track tool usage
 * - Add custom metrics
 * - Handle errors per phase
 *
 * Run: bun run apps/examples/src/advanced/19-lifecycle-hooks.ts
 */

import { ReactiveAgents } from "reactive-agents";
import { Effect } from "effect";

async function main() {
  const toolCalls: { name: string; duration: number }[] = [];

  const agent = await ReactiveAgents.create()
    .withName("hooks-demo")
    .withProvider("test")
    .withTestResponses({
      ".*": "The answer is 42.",
    })
    .withReasoning()
    .withTools()
    // Hook 1: Log when reasoning starts
    .withHook({
      phase: "think",
      timing: "before",
      handler: (ctx) => {
        console.log(`🧠 Think phase starting (iteration ${ctx.metadata.stepsCount + 1})`);
        return Effect.succeed(ctx);
      },
    })
    // Hook 2: Track tool calls
    .withHook({
      phase: "act",
      timing: "after",
      handler: (ctx) => {
        const toolName = ctx.scratchpad.get("_last_tool_name") as string | undefined;
        if (toolName) {
          toolCalls.push({ name: toolName, duration: 0 });
          console.log(`🔧 Tool called: ${toolName}`);
        }
        return Effect.succeed(ctx);
      },
    })
    // Hook 3: Log completion
    .withHook({
      phase: "complete",
      timing: "after",
      handler: (ctx) => {
        console.log(`✅ Execution complete. Tools used: ${toolCalls.length}`);
        return Effect.succeed(ctx);
      },
    })
    // Hook 4: Handle errors
    .withHook({
      phase: "think",
      timing: "on-error",
      handler: (ctx) => {
        console.error(`❌ Think phase failed. Check your prompt or model.`);
        return Effect.succeed(ctx);
      },
    })
    .build();

  const result = await agent.run("What is the meaning of life?");
  console.log(`\nOutput: ${result.output}`);
  console.log(`Steps: ${result.metadata.stepsCount}`);

  await agent.dispose();
}

main().catch(console.error);
```

- [ ] **Step 35: Create hooks guide**

Create `apps/docs/src/content/docs/guides/hooks.md`:

```markdown
---
title: Lifecycle Hooks
description: Intercept and extend the 10-phase execution engine with custom hooks
---

# Lifecycle Hooks

Every agent execution flows through a deterministic 10-phase lifecycle. Hooks let you intercept any phase to add logging, metrics, validation, or custom behavior.

## Quick Example

```typescript
const agent = await ReactiveAgents.create()
  .withProvider("anthropic")
  .withReasoning()
  .withHook({
    phase: "think",
    timing: "after",
    handler: (ctx) => {
      console.log(`Iteration ${ctx.metadata.stepsCount}`);
      return Effect.succeed(ctx);
    },
  })
  .build();
```

## Available Phases

| Phase | When It Runs | Common Hook Use Cases |
|-------|-------------|----------------------|
| `bootstrap` | Before anything else | Load external config, validate preconditions |
| `guardrail` | Input safety check | Log blocked inputs, custom filtering |
| `cost-route` | Model tier selection | Override routing decisions |
| `strategy` | Strategy selection | Log which strategy was chosen |
| `think` | Each reasoning iteration | Progress logging, custom metrics |
| `act` | Tool execution | Tool call tracking, audit logging |
| `observe` | Process tool results | Result validation, caching |
| `verify` | Output fact-checking | Custom verification logic |
| `memory-flush` | Persist memories | Custom memory operations |
| `complete` | Final result assembly | Post-processing, cleanup |

## Hook Timing

Each phase supports three timing points:

- **`before`** — Runs before the phase executes. Can modify the `ExecutionContext`.
- **`after`** — Runs after the phase completes successfully. Receives the updated context.
- **`on-error`** — Runs when the phase throws an error. Can log or clean up, but cannot prevent the error from propagating.

## Hook Handler Signature

```typescript
handler: (ctx: ExecutionContext) => Effect.Effect<ExecutionContext, HookError>
```

The handler receives the current `ExecutionContext` and must return it (possibly modified). The context includes:
- `metadata` — step count, strategy, tokens used
- `scratchpad` — key/value store shared across phases
- `taskId` — current task identifier

## Ordering

Hooks registered for the same phase and timing run **sequentially in registration order**. If a hook fails:
- `before` hook failure: the phase is skipped and the `on-error` hook runs
- `after` hook failure: logged but does not affect the phase result
- `on-error` hook failure: logged but does not mask the original error

## Practical Patterns

### Progress Logging

```typescript
.withHook({
  phase: "think",
  timing: "before",
  handler: (ctx) => {
    const step = ctx.metadata.stepsCount + 1;
    const max = ctx.metadata.maxIterations ?? 10;
    console.log(`Step ${step}/${max}`);
    return Effect.succeed(ctx);
  },
})
```

### Cost Alert

```typescript
.withHook({
  phase: "complete",
  timing: "after",
  handler: (ctx) => {
    if (ctx.metadata.cost > 0.10) {
      console.warn(`⚠ Execution cost $${ctx.metadata.cost.toFixed(3)} exceeded $0.10 threshold`);
    }
    return Effect.succeed(ctx);
  },
})
```

### Audit Trail

```typescript
.withHook({
  phase: "act",
  timing: "after",
  handler: (ctx) => {
    const toolName = ctx.scratchpad.get("_last_tool_name");
    auditLog.append({ event: "tool_call", tool: toolName, taskId: ctx.taskId, timestamp: Date.now() });
    return Effect.succeed(ctx);
  },
})
```
```

- [ ] **Step 36: Verify docs build**

Run: `cd apps/docs && npx astro build 2>&1 | tail -5`
Expected: Build succeeds

- [ ] **Step 37: Commit**

```bash
git add README.md apps/examples/src/advanced/19-lifecycle-hooks.ts apps/docs/src/content/docs/guides/hooks.md
git commit -m "docs: add lifecycle hooks visibility — README section, example, and dedicated guide"
```

---

## Chunk 4: TSDoc on Public APIs + Memory Tier Cleanup (Items 1.7 + 1.8)

### Task 8: TSDoc comments on builder API

**Files:**
- Modify: `packages/runtime/src/builder.ts` (all `.with*()` methods, lines 734–1477)

- [ ] **Step 38: Add TSDoc to all builder methods**

Modify `packages/runtime/src/builder.ts`. Add JSDoc comments to every `.with*()` method. The builder is the highest-contact API surface — every user interacts with it.

Pattern for each method:
```typescript
/**
 * Short description of what this enables.
 *
 * @param paramName - What this parameter controls
 * @default defaultValue (if applicable)
 * @example
 * ```typescript
 * .withMethodName(value)
 * ```
 * @see {@link RelatedType} for full options
 */
```

Key methods to document (highest priority — these are the ones users discover first):

```typescript
/** Builder method examples — apply this pattern to ALL .with*() methods: */

/**
 * Set the LLM provider for this agent.
 *
 * @param provider - Provider name. Each provider requires its own API key
 *   environment variable (except "ollama" and "test").
 *
 *   | Provider | API Key Variable | Default Model |
 *   |----------|-----------------|---------------|
 *   | `"anthropic"` | `ANTHROPIC_API_KEY` | `claude-sonnet-4-20250514` |
 *   | `"openai"` | `OPENAI_API_KEY` | `gpt-4o` |
 *   | `"gemini"` | `GOOGLE_API_KEY` | `gemini-2.0-flash` |
 *   | `"ollama"` | none | `cogito:14b` |
 *   | `"litellm"` | `OPENAI_API_KEY` | `gpt-4o` |
 *   | `"test"` | none | `test-model` |
 *
 * @example
 * ```typescript
 * .withProvider("anthropic")
 * .withProvider("ollama") // Local models, no API key needed
 * ```
 */
withProvider(provider: ProviderName): this { ... }

/**
 * Enable the reasoning loop with a specific strategy.
 *
 * Without this, the agent makes a single LLM call (no tool use, no iteration).
 * With reasoning enabled, the agent enters a Think → Act → Observe loop
 * that continues until the task is complete or maxIterations is reached.
 *
 * @param options - Reasoning configuration
 * @param options.defaultStrategy - Strategy to use. Default: `"reactive"` (ReAct)
 *   - `"reactive"` — Think → Act → Observe loop (best for tool use)
 *   - `"reflexion"` — Generate → Critique → Improve (quality-critical output)
 *   - `"plan-execute-reflect"` — Plan → Execute → Reflect (structured multi-step)
 *   - `"tree-of-thought"` — Branch → Score → Prune (creative/open-ended)
 *   - `"adaptive"` — Auto-select best strategy per task
 *
 * @example
 * ```typescript
 * .withReasoning() // Uses ReAct by default
 * .withReasoning({ defaultStrategy: "adaptive" }) // Auto-select
 * ```
 */
withReasoning(options?: ReasoningOptions): this { ... }

/**
 * Enable persistent memory backed by SQLite.
 *
 * Without arguments, enables standard memory (working memory only).
 * Pass `{ tier: "enhanced" }` for full 4-layer memory (working + episodic +
 * semantic + procedural) — requires `EMBEDDING_PROVIDER` and `EMBEDDING_MODEL`
 * environment variables.
 *
 * @param tierOrOptions - Memory tier or configuration object
 * @default Standard tier (working memory only)
 *
 * @example
 * ```typescript
 * .withMemory() // Standard: working memory only
 * .withMemory({ tier: "enhanced" }) // Full 4-layer memory
 * ```
 *
 * @deprecated String form `"1"` / `"2"` is deprecated.
 * Use `.withMemory()` or `.withMemory({ tier: "enhanced" })` instead.
 */
withMemory(tierOrOptions?: "1" | "2" | MemoryOptions): this { ... }

/**
 * Enable built-in tools and optional MCP server connections.
 *
 * Built-in tools include: file-read, file-write, web-search, http-get,
 * code-execute, scratchpad, signal, context-status, task-complete.
 *
 * @param options - Tool configuration
 * @param options.include - Whitelist of tool names to enable (default: all)
 * @param options.exclude - Blacklist of tool names to disable
 * @param options.custom - Array of custom ToolDefinition objects
 * @param options.allowedTools - Strict whitelist (filters ALL tool sources)
 * @param options.adaptiveToolFiltering - LLM-based tool relevance filtering
 * @param options.resultCompression - Tool result compression config
 *
 * @example
 * ```typescript
 * .withTools() // All built-in tools
 * .withTools({ include: ["web-search", "file-read"] }) // Specific tools only
 * .withTools({ custom: [myCustomTool] }) // Add custom tools
 * ```
 */
withTools(options?: ToolsOptions): this { ... }
```

Apply this pattern to ALL remaining `.with*()` methods. Each needs at minimum:
- One-sentence description
- `@param` for each parameter
- `@example` with realistic usage
- `@default` where applicable

- [ ] **Step 39: Verify no TypeScript errors introduced**

Run: `cd packages/runtime && bunx tsc --noEmit`
Expected: No errors (TSDoc comments don't affect compilation)

- [ ] **Step 40: Commit**

```bash
git add packages/runtime/src/builder.ts
git commit -m "docs(runtime): add comprehensive TSDoc to all builder API methods"
```

### Task 9: TSDoc on config types, error types, and result types

**Files:**
- Modify: `packages/runtime/src/types.ts`
- Modify: `packages/runtime/src/errors.ts`
- Modify: `packages/runtime/src/stream-types.ts`
- Modify: `packages/runtime/src/chat.ts`
- Modify: `packages/runtime/src/debrief.ts`

- [ ] **Step 41: Add TSDoc to config type fields**

Modify `packages/runtime/src/types.ts`. Add JSDoc to each field in the ReactiveAgentsConfig schema (around lines 252–370):

```typescript
// Example pattern for each field:
/** Maximum number of reasoning loop iterations before the agent stops.
 * @default 10 */
maxIterations: Schema.Number.pipe(Schema.withDefault(10)),

/** LLM provider name (e.g., "anthropic", "ollama"). */
provider: Schema.optional(Schema.String),

/** Memory tier: "1" = working only, "2" = full 4-layer (requires embeddings).
 * @default "1"
 * @deprecated Use "standard" / "enhanced" via builder instead. */
memoryTier: Schema.optional(Schema.String),
```

Apply to ALL fields in the config schema.

- [ ] **Step 42: Add TSDoc to result types**

Modify `packages/runtime/src/builder.ts` (AgentResult, AgentResultMetadata around lines 555–573):

```typescript
/** Result of an agent execution. Returned by `agent.run()`. */
export interface AgentResult {
  /** The agent's final output text. */
  readonly output: string;
  /** Whether the execution completed successfully. */
  readonly success: boolean;
  /** Unique identifier for this execution task. */
  readonly taskId: string;
  /** Agent identifier. */
  readonly agentId: string;
  /** Execution metrics (duration, cost, tokens, steps). */
  readonly metadata: AgentResultMetadata;
  /** Output format detected from the agent's response (e.g., "json", "markdown"). */
  readonly format?: OutputFormat;
  /** How the agent's execution terminated.
   * - `"final_answer_tool"` — Agent used the final-answer tool
   * - `"final_answer"` — Agent produced a FINAL ANSWER text marker
   * - `"max_iterations"` — Reached maxIterations limit
   * - `"end_turn"` — LLM signaled end of turn */
  readonly terminatedBy?: TerminatedBy;
  /** Post-run debrief synthesis (if memory is enabled). */
  readonly debrief?: AgentDebrief;
}

/** Execution metrics included in every AgentResult. */
export interface AgentResultMetadata {
  /** Total execution duration in milliseconds. */
  readonly duration: number;
  /** Estimated cost in USD. */
  readonly cost: number;
  /** Total tokens consumed (input + output). */
  readonly tokensUsed: number;
  /** Name of the reasoning strategy used (e.g., "reactive", "plan-execute-reflect"). */
  readonly strategyUsed?: string;
  /** Number of reasoning loop iterations completed. */
  readonly stepsCount: number;
  /** Agent's self-assessed confidence in the result. */
  readonly confidence?: "high" | "medium" | "low";
}
```

- [ ] **Step 43: Add TSDoc to stream event types**

Modify `packages/runtime/src/stream-types.ts`:

```typescript
/** Density of stream events.
 * - `"tokens"` — Only TextDelta and StreamCompleted events
 * - `"full"` — All events including phase and tool call details */
export type StreamDensity = "tokens" | "full";

/** Events emitted during `agent.runStream()`. Use `event._tag` to discriminate. */
export type AgentStreamEvent =
  /** A chunk of generated text. Emitted at both density levels. */
  | { readonly _tag: "TextDelta"; readonly text: string }
  /** Stream completed successfully. Contains the full output and execution metrics. */
  | { readonly _tag: "StreamCompleted"; readonly output: string; readonly metadata: AgentResultMetadata; readonly taskId?: string; readonly agentId?: string }
  /** An error occurred during streaming. */
  | { readonly _tag: "StreamError"; readonly cause: string }
  // ... (document remaining full-density events similarly)
```

- [ ] **Step 44: Add TSDoc to chat types**

Modify `packages/runtime/src/chat.ts`:

```typescript
/** A message in a chat session history. */
export interface ChatMessage {
  /** Who sent this message. */
  role: "user" | "assistant";
  /** Message content. */
  content: string;
  /** Unix timestamp in milliseconds. */
  timestamp: number;
}

/** Response from `agent.chat()` or `session.chat()`. */
export interface ChatReply {
  /** The agent's response text. */
  message: string;
  /** Names of tools called during response generation (if reasoning was used). */
  toolsUsed?: string[];
  /** Whether the response was served from memory/cache. */
  fromMemory?: boolean;
  /** Total tokens consumed. */
  tokens?: number;
  /** Number of reasoning iterations. */
  steps?: number;
  /** Estimated cost in USD. */
  cost?: number;
}

/** Options for `agent.chat()`. */
export interface ChatOptions {
  /** Force tool use on or off. Default: auto-detected from message content. */
  useTools?: boolean;
  /** Max reasoning iterations for tool-capable path. @default 5 */
  maxIterations?: number;
}
```

- [ ] **Step 45: Add TSDoc to debrief types**

Modify `packages/runtime/src/debrief.ts`:

```typescript
/** Structured post-run analysis produced by the DebriefSynthesizer.
 * Generated via a single small LLM call after execution completes.
 * Persisted to SQLite via DebriefStore when memory is enabled. */
export interface AgentDebrief {
  /** Overall execution outcome. */
  outcome: "success" | "partial" | "failed";
  /** One-paragraph summary of what the agent did. */
  summary: string;
  /** Key facts or results discovered during execution. */
  keyFindings: string[];
  /** Errors encountered during execution (tool failures, guardrail blocks, etc.). */
  errorsEncountered: string[];
  /** Lessons learned that may improve future executions. */
  lessonsLearned: string[];
  /** Agent's self-assessed confidence in the result. */
  confidence: "high" | "medium" | "low";
  /** Caveats or limitations of the result. */
  caveats?: string;
  /** Summary of tools used with call counts and success rates. */
  toolsUsed: { name: string; calls: number; successRate: number }[];
  /** Quantitative execution metrics. */
  metrics: { tokens: number; duration: number; iterations: number; cost: number };
  /** Pre-rendered markdown version of the debrief. */
  markdown: string;
}
```

- [ ] **Step 46: Verify no TypeScript errors**

Run: `cd packages/runtime && bunx tsc --noEmit`
Expected: No errors

- [ ] **Step 47: Commit**

```bash
git add packages/runtime/src/types.ts packages/runtime/src/errors.ts packages/runtime/src/stream-types.ts packages/runtime/src/chat.ts packages/runtime/src/debrief.ts packages/runtime/src/builder.ts
git commit -m "docs(runtime): add TSDoc to all public types — config, errors, results, stream events, chat, debrief"
```

### Task 10: TSDoc on service interfaces (core + LLM + tools + memory)

**Files:**
- Modify: `packages/llm-provider/src/types.ts`
- Modify: `packages/tools/src/types.ts`
- Modify: `packages/memory/src/types.ts`
- Modify: `packages/core/src/types.ts`

- [ ] **Step 48: Add TSDoc to LLM provider types**

Modify `packages/llm-provider/src/types.ts`. Focus on:
- `CompletionRequest` interface — document all fields
- `CompletionResponse` interface — document all fields
- `LLMService` tag — document `complete()`, `stream()`, `embed()` methods
- `ModelPresets` — document each preset with cost and quality info
- `ProviderName` type — document what each provider supports

- [ ] **Step 49: Add TSDoc to tool types**

Modify `packages/tools/src/types.ts`. Focus on:
- `ToolDefinition` interface — document name, description, parameters, handler
- `ToolParameter` interface — document each field
- `ToolResult` interface — document result shape
- `ToolService` tag — document `listTools()`, `getTool()`, `execute()`, `toFunctionCallingFormat()`

- [ ] **Step 50: Add TSDoc to memory types**

Modify `packages/memory/src/types.ts`. Focus on:
- `MemoryService` tag — document `store*()`, `retrieve*()`, `search()` methods
- Memory entry types (working, episodic, semantic, procedural)

- [ ] **Step 51: Add TSDoc to core types**

**NOTE:** Core types are in `packages/core/src/types/` directory (NOT a single file). Modify these files:
- `packages/core/src/types/agent.ts` — AgentEvent union, document when each event fires
- `packages/core/src/types/task.ts` — Task interface, document all fields
- `packages/core/src/types/message.ts` — Message types
- `packages/core/src/types/config.ts` — Config types
- `packages/core/src/types/result.ts` — Result types

Also check `packages/core/src/services/` for EventBus service tag — document `publish()`, `on()`, `subscribe()`.

- [ ] **Step 52: Verify no TypeScript errors across all packages**

Run: `bun run build`
Expected: All packages build successfully

- [ ] **Step 53: Commit**

```bash
git add packages/llm-provider/src/types.ts packages/tools/src/types.ts packages/memory/src/types.ts packages/core/src/types/ packages/core/src/services/
git commit -m "docs: add TSDoc to service interfaces across core, llm-provider, tools, and memory packages"
```

### Task 11: Memory tier naming cleanup

**Files:**
- Modify: `packages/runtime/src/builder.ts` (withMemory method, line 966)
- Test: `packages/runtime/tests/memory-tier.test.ts` (new)

- [ ] **Step 54: Write failing tests for memory tier deprecation**

Create `packages/runtime/tests/memory-tier.test.ts`:

```typescript
import { describe, test, expect } from "bun:test";
import { ReactiveAgents } from "../src";

describe("Memory tier naming", () => {
  test("withMemory() with no args defaults to standard tier", async () => {
    const agent = await ReactiveAgents.create()
      .withProvider("test")
      .withMemory()
      .build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("withMemory({ tier: 'standard' }) works", async () => {
    const agent = await ReactiveAgents.create()
      .withProvider("test")
      .withMemory({ tier: "standard" })
      .build();
    expect(agent).toBeDefined();
    await agent.dispose();
  });

  test("withMemory('1') emits deprecation warning", async () => {
    const warnings: string[] = [];
    const origWarn = console.warn;
    console.warn = (msg: string) => { if (typeof msg === "string") warnings.push(msg); };

    await ReactiveAgents.create()
      .withProvider("test")
      .withMemory("1")
      .build()
      .then(a => a.dispose());

    console.warn = origWarn;
    expect(warnings.some(w => w.includes("deprecated"))).toBe(true);
  });

  test("withMemory('2') emits deprecation warning", async () => {
    const warnings: string[] = [];
    const origWarn = console.warn;
    console.warn = (msg: string) => { if (typeof msg === "string") warnings.push(msg); };

    await ReactiveAgents.create()
      .withProvider("test")
      .withMemory("2")
      .build()
      .then(a => a.dispose());

    console.warn = origWarn;
    expect(warnings.some(w => w.includes("deprecated") && w.includes("enhanced"))).toBe(true);
  });
});
```

- [ ] **Step 55: Run tests to verify they fail**

Run: `cd packages/runtime && bun test tests/memory-tier.test.ts`
Expected: FAIL — no deprecation warnings emitted, no object-form support

- [ ] **Step 56: Update withMemory to support new naming**

Modify `packages/runtime/src/builder.ts` at the `withMemory` method (line 966):

```typescript
/**
 * Enable persistent memory backed by SQLite.
 *
 * Without arguments, enables standard memory (working memory only).
 * Pass `{ tier: "enhanced" }` for full 4-layer memory (working + episodic +
 * semantic + procedural) — requires `EMBEDDING_PROVIDER` and `EMBEDDING_MODEL`
 * environment variables.
 *
 * @param tierOrOptions - Memory configuration. Accepts:
 *   - No arguments: standard tier (working memory only)
 *   - `{ tier: "standard" }`: same as no arguments
 *   - `{ tier: "enhanced" }`: full 4-layer memory with embeddings
 * @default Standard tier (working memory only)
 *
 * @example
 * ```typescript
 * .withMemory() // Standard: working memory only
 * .withMemory({ tier: "enhanced" }) // Full 4-layer memory
 * ```
 *
 * @deprecated String form `"1"` / `"2"` is deprecated.
 * Use `.withMemory()` or `.withMemory({ tier: "enhanced" })` instead.
 */
withMemory(tierOrOptions?: "1" | "2" | MemoryOptions): this {
  this._enableMemory = true;

  if (typeof tierOrOptions === "string") {
    // Deprecated string form
    const newForm = tierOrOptions === "2"
      ? '.withMemory({ tier: "enhanced" })'
      : ".withMemory()";
    console.warn(
      `⚠ withMemory("${tierOrOptions}") is deprecated. Use ${newForm} instead.`
    );
    this._memoryTier = tierOrOptions;
  } else if (tierOrOptions && typeof tierOrOptions === "object") {
    // New object form
    const tier = tierOrOptions.tier ?? "standard";
    this._memoryTier = tier === "enhanced" ? "2" : "1";
  } else {
    // No args = standard
    this._memoryTier = "1";
  }

  return this;
}
```

Also add the `MemoryOptions` type if it doesn't exist (check existing types first — it may already be defined):

```typescript
export interface MemoryOptions {
  /** Memory tier.
   * - `"standard"` — Working memory only (fast, no embeddings needed)
   * - `"enhanced"` — Full 4-layer memory (working + episodic + semantic + procedural).
   *   Requires `EMBEDDING_PROVIDER` and `EMBEDDING_MODEL` env vars. */
  tier?: "standard" | "enhanced";
}
```

- [ ] **Step 57: Run tests to verify they pass**

Run: `cd packages/runtime && bun test tests/memory-tier.test.ts`
Expected: All 4 tests PASS

- [ ] **Step 58: Run full runtime test suite**

Run: `cd packages/runtime && bun test`
Expected: All existing tests PASS (string form still works, just with warnings)

- [ ] **Step 59: Commit**

```bash
git add packages/runtime/src/builder.ts packages/runtime/tests/memory-tier.test.ts
git commit -m "feat(runtime): deprecate string memory tiers, add withMemory({ tier: 'standard' | 'enhanced' }) object form"
```

---

## Final: Phase 1 Verification

### Task 12: Full build + test verification

- [ ] **Step 60: Run full test suite**

Run: `bun test`
Expected: All 1,773+ existing tests PASS plus ~30 new tests from this phase

- [ ] **Step 61: Build all packages**

Run: `bun run build`
Expected: All 20 packages build successfully

- [ ] **Step 62: Build docs**

Run: `cd apps/docs && npx astro build`
Expected: Build succeeds with 3 new pages (configuration reference, local models guide, hooks guide)

- [ ] **Step 63: Update CLAUDE.md with new test count and features**

Update the test count, builder methods, and any other CLAUDE.md sections affected by this phase.

- [ ] **Step 64: Final commit**

```bash
git add CLAUDE.md
git commit -m "docs: update CLAUDE.md with Phase 1 adoption readiness changes"
```
