# https://github.com/loopgain-ai/loopgain Project Manual

Generated at: 2026-07-19 13:12:21 UTC

## Table of Contents

- [Core LoopGain Engine and Trajectory Classifier](#page-1)
- [Funnel Telemetry: Lifecycle, Initialization, and Known Issues](#page-2)
- [Framework Adapters: Wiring LoopGain into Agent Stacks](#page-3)
- [Usage Examples, Error Signals, and Common Workflows](#page-4)

<a id='page-1'></a>

## Core LoopGain Engine and Trajectory Classifier

### Related Pages

Related topics: [Funnel Telemetry: Lifecycle, Initialization, and Known Issues](#page-2), [Usage Examples, Error Signals, and Common Workflows](#page-4)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [loopgain/core.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/core.py)
- [loopgain/classifier.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/classifier.py)
- [loopgain/__init__.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/__init__.py)
- [loopgain/funnel.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/funnel.py)
- [PROTOCOL_v2_classifier.md](https://github.com/loopgain-ai/loopgain/blob/main/PROTOCOL_v2_classifier.md)
- [RESULTS_v2_classifier.md](https://github.com/loopgain-ai/loopgain/blob/main/RESULTS_v2_classifier.md)
</details>

# Core LoopGain Engine and Trajectory Classifier

The Core LoopGain engine is the central runtime that records, classifies, and routes agent trajectories during a session, while the Trajectory Classifier is the component that decides which trajectories are worth keeping, replaying, or discarding. Together they implement the closed-loop "observe → classify → adapt" pattern that gives the project its name.

## Purpose and Scope

LoopGain provides a feedback loop for AI agent systems: each agent step is observed, scored by the trajectory classifier, and the resulting signal is fed back into adapter configuration so that subsequent steps converge faster on successful paths. The package exposes this through a small public surface defined in `loopgain/__init__.py`, where the engine and classifier are re-exported alongside funnel helpers. Source: [loopgain/__init__.py:1-40]().

The engine is intentionally engine-agnostic. It does not call LLMs directly; instead it accepts structured events (`on_init`, `on_first_observe`, `note_outcome`, `note_adapter`) and produces classification verdicts and adapter suggestions. This separation lets it sit in front of any agent framework without owning the agent's decision logic.

## Core Engine Architecture

The engine in `loopgain/core.py` maintains three pieces of state: a mode flag (`_mode`), a lazy-loaded classifier instance, and an in-memory trajectory buffer. The mode flag is what gates whether the engine is actively recording. Source: [loopgain/core.py:1-80]().

Initialization follows a lazy-load pattern. The first call to `on_init()` or `on_first_observe()` invokes `_ensure_loaded()`, which is responsible for hydrating the classifier, defaulting `_mode` to `_ENABLED`, and applying any pending configuration. Until that method runs, `_mode` remains `None`. Source: [loopgain/core.py:42-78]().

`note_outcome()` and `note_adapter()` are the two recording entry points. Each guards on `self._mode != _ENABLED` before doing any work. This guard is the subject of a known community bug: if these methods are invoked before `_ensure_loaded()` runs, `_mode` is `None` rather than `_DISABLED`, and the comparison `None != _ENABLED` is still `True` in Python — but downstream code that assumes `_mode` is either `_ENABLED` or `_DISABLED` can misclassify the pre-init window as enabled and silently no-op. Source: [loopgain/funnel.py:120-180](). The funnel module wraps these engine methods to provide the public `note_outcome` / `note_adapter` API and inherits the same lifecycle hazard.

| State | `_mode` value | Behavior of `note_outcome` |
|---|---|---|
| Pre-init | `None` | Guard evaluates, but state is ambiguous (bug) |
| Loaded, enabled | `_ENABLED` | Records outcome, forwards to classifier |
| Loaded, disabled | `_DISABLED` | Returns early, no record |

## Trajectory Classifier

The classifier lives in `loopgain/classifier.py` and is the only component that decides what to do with a recorded trajectory. It exposes a `classify(trajectory)` method that returns a verdict (`KEEP`, `REPLAY`, `DROP`) and, for `KEEP` verdicts, a confidence score and suggested adapter parameters. Source: [loopgain/classifier.py:1-60]().

The classifier ships in two protocol versions. Version 1 was the original heuristic-only scorer; version 2, documented in `PROTOCOL_v2_classifier.md`, adds structured feature extraction (step count, error rate, token efficiency, adapter delta) and a learned scoring head. The engine selects the version based on configuration at `_ensure_loaded()` time. Source: [PROTOCOL_v2_classifier.md:1-120]().

Reported evaluation results for v2 are summarized in `RESULTS_v2_classifier.md`, including agreement with human-labeled trajectories and rejection rates on adversarial inputs. Source: [RESULTS_v2_classifier.md:1-80](). These results are what the engine uses to calibrate confidence thresholds when emitting adapter suggestions back to the caller.

## Lifecycle and Known Pitfalls

The engine's lifecycle is: **construct → first observe → load → record → classify → adapt**. Each stage is observable, and the funnel layer (`loopgain/funnel.py`) is the recommended entry point because it enforces ordering on the caller's behalf. Source: [loopgain/funnel.py:1-100]().

The community-reported initialization bug is worth highlighting for any integrator: calling `note_outcome()` or `note_adapter()` before `on_init()` is currently a silent no-op rather than a raised error. A defensive workaround is to call `on_first_observe()` with a synthetic event before any recording call, which forces `_ensure_loaded()` to run and pins `_mode` to a known value. Source: [loopgain/funnel.py:140-180]().

For trajectory inspection, the engine writes recorded events to an in-memory ring buffer keyed by session id; the classifier consumes them lazily during `classify()`. This means recording is O(1) per event and classification cost scales with the configured trajectory window, not total session length. Source: [loopgain/core.py:90-140]().

## Putting It Together

```mermaid
flowchart LR
    Agent[Agent Loop] -->|on_first_observe| Core[loopgain/core.py]
    Core -->|ensure_loaded| Cls[loopgain/classifier.py]
    Agent -->|note_outcome| Funnel[loopgain/funnel.py]
    Agent -->|note_adapter| Funnel
    Funnel --> Core
    Core -->|trajectory| Cls
    Cls -->|verdict + adapter| Core
    Core -->|adapter suggestion| Agent
```

The data flow is unidirectional per cycle: the agent emits events into the funnel, the funnel forwards them to the core, the core buffers them, the classifier consumes the buffer and returns a verdict, and the verdict drives the next adapter suggestion. The loop closes when the agent applies that suggestion on a subsequent step.

---

<a id='page-2'></a>

## Funnel Telemetry: Lifecycle, Initialization, and Known Issues

### Related Pages

Related topics: [Core LoopGain Engine and Trajectory Classifier](#page-1)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [loopgain/funnel.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/funnel.py)
- [loopgain/telemetry.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/telemetry.py)
- [loopgain/cli.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/cli.py)
- [loopgain/__main__.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/__main__.py)
- [TELEMETRY.md](https://github.com/loopgain-ai/loopgain/blob/main/TELEMETRY.md)
</details>

# Funnel Telemetry: Lifecycle, Initialization, and Known Issues

Funnel telemetry is the subsystem inside `loopgain/funnel.py` that records user-visible outcomes and adapter signals emitted during an instrumented run, then forwards them through `loopgain/telemetry.py` according to an opt-in mode flag. This page documents how the funnel reaches an active state, which methods participate in that lifecycle, and the most prominent initialization bug filed by the community.

## Purpose and Scope

The `funnel` module owns the runtime record-keeping surface for the loopgain CLI. Its methods are called by the rest of the codebase whenever a meaningful event occurs (an outcome completed, an adapter reported a signal, etc.). The funnel does not decide transport policy — that responsibility is delegated to `loopgain/telemetry.py`, which the funnel consults via the `_mode` flag and the `_ENABLED` constant. Entry points such as `loopgain/__main__.py` and `loopgain/cli.py` are responsible for triggering the funnel's lifecycle hooks during process startup. Source: [loopgain/funnel.py:1-40]()

## Lifecycle and Initialization

The funnel transitions through three observable states, driven by two public hooks:

- **Uninitialized** — `self._mode` is `None` because `_ensure_loaded()` has not yet run.
- **Loaded** — `_ensure_loaded()` has resolved the effective mode against the user's opt-in settings and `_mode` is set.
- **Enabled / Disabled** — once loaded, `_mode` equals either `_ENABLED` or a sentinel that causes note methods to short-circuit.

The two public hooks that drive this transition are:

1. `on_init()` — typically invoked early in the CLI bootstrap path. It calls `_ensure_loaded()` so that subsequent note calls have a meaningful mode value.
2. `on_first_observe()` — a safety-net hook invoked the first time the funnel is asked to record a signal. If `on_init()` was skipped, this hook still triggers `_ensure_loaded()`.

Until either hook runs, `_mode` remains `None`, which is the root cause of the known issue described below. Source: [loopgain/funnel.py:40-120]()

```
CLI startup  ──►  on_init()        ──►  _ensure_loaded()  ──►  _mode = _ENABLED | disabled
                                                 ▲
First note call without on_init()  ──►  on_first_observe() ┘
```

The mode-resolution logic is shared with `loopgain/telemetry.py`, which centralizes the constants (`_ENABLED`) and the policy for resolving environment variables and CLI flags. Source: [loopgain/telemetry.py:1-60]()

## Telemetry API Surface

The funnel exposes two record-keeping methods, both gated on the mode flag:

- `note_outcome(...)` — records a terminal outcome for the current run.
- `note_adapter(...)` — records a signal from an adapter integration.

Both methods begin with the same guard:

```python
if self._mode != _ENABLED:
    return
```

This guard is intentional once `_mode` has been resolved: it suppresses work when telemetry is disabled or when the process is running in a non-telemetry context. The methods themselves perform no I/O when disabled — they simply return. Source: [loopgain/funnel.py:120-200]()

The CLI wires these methods into user-facing flows: `loopgain/cli.py` calls into the funnel at command boundaries, and `loopgain/__main__.py` ensures `on_init()` runs before any CLI handler executes so that the funnel is in the loaded state when user code first triggers a note call. Source: [loopgain/cli.py:1-80](), [loopgain/__main__.py:1-40]()

## Known Issue: Silent Pre-Initialization Failure

The community's top-engagement issue describes a quiet failure mode in the guard above. When `note_outcome()` or `note_adapter()` is invoked before either `on_init()` or `on_first_observe()` has been called, `self._mode` is still `None`. The comparison `None != _ENABLED` evaluates to `True`, so the method returns early without recording the event and without raising an error.

| Scenario | `_mode` value | Guard result | Behavior |
|---|---|---|---|
| After `on_init()` | `_ENABLED` or disabled sentinel | matches policy | Correct |
| After `on_first_observe()` only | `_ENABLED` or disabled sentinel | matches policy | Correct |
| Before either hook | `None` | `True` (returns early) | Silent data loss |

The practical impact is that callers who invoke the funnel before the CLI bootstrap completes — for example, from a library consumer, a test harness, or an embedded usage — see their telemetry events vanish without any diagnostic. Source: [loopgain/funnel.py:120-200]()

The project documentation in `TELEMETRY.md` describes the intended public contract but, per the issue report, does not currently warn that the funnel must be initialized through one of the lifecycle hooks before any note method is safe to call. Source: [TELEMETRY.md:1-80]()

## Mitigations and Recommended Usage

Until the guard is corrected, callers should observe the following contract:

1. Invoke `on_init()` explicitly when constructing a process that uses the funnel outside the CLI entry point.
2. Rely on `on_first_observe()` as a backstop only if `on_init()` is impractical; it will lazily load the mode on first use.
3. Never assume `note_outcome()` or `note_adapter()` has recorded anything until the funnel has been initialized, and consider adding a startup-time assertion in test code that constructs the funnel directly.

These practices align with the lifecycle state machine documented above and ensure that the mode comparison in the guard reflects a resolved policy rather than the uninitialized `None` sentinel. Source: [loopgain/funnel.py:40-200](), [loopgain/telemetry.py:1-60](), [TELEMETRY.md:1-80]()

---

<a id='page-3'></a>

## Framework Adapters: Wiring LoopGain into Agent Stacks

### Related Pages

Related topics: [Core LoopGain Engine and Trajectory Classifier](#page-1), [Usage Examples, Error Signals, and Common Workflows](#page-4)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [loopgain/integrations/__init__.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/integrations/__init__.py)
- [loopgain/integrations/langgraph.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/integrations/langgraph.py)
- [loopgain/integrations/crewai.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/integrations/crewai.py)
- [loopgain/integrations/autogen.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/integrations/autogen.py)
- [loopgain/integrations/langchain.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/integrations/langchain.py)
- [loopgain/integrations/openai_agents.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/integrations/openai_agents.py)
</details>

# Framework Adapters: Wiring LoopGain into Agent Stacks

## Purpose and Scope

The `loopgain/integrations/` package is the bridge between LoopGain's learning loop and the surrounding agent ecosystem. While the core funnel tracks observations, adapter references, and outcomes, the integrations package supplies thin, framework-specific shims that translate third-party orchestration events into funnel calls. Each module targets a single orchestration library and exposes a stable surface that downstream applications can import without depending on LoopGain internals directly. Source: [loopgain/integrations/__init__.py:1-40]()

The adapters cover the most common agent runtimes in the Python ecosystem: stateful graph runtimes (`langgraph.py`), role-based multi-agent crews (`crewai.py`), conversational multi-agent dialogues (`autogen.py`), the broader `langchain.py` Runnable/Chain surface, and the OpenAI Agents SDK via `openai_agents.py`. Keeping these in a dedicated subpackage prevents framework-specific imports from leaking into the core and lets users opt in to only the runtime they actually use. Source: [loopgain/integrations/langgraph.py:1-30](), [loopgain/integrations/crewai.py:1-30]()

## Adapter Architecture

Each adapter follows the same conceptual shape: it listens for lifecycle events emitted by its target framework and forwards them to a shared `Funnel` instance. The funnel exposes the canonical hooks — `on_init`, `on_first_observe`, `note_adapter`, and `note_outcome` — and the adapters are responsible for mapping framework callbacks onto them. This separation lets the core stay framework-agnostic while integrations remain narrow and easy to audit. Source: [loopgain/integrations/langgraph.py:30-80](), [loopgain/integrations/autogen.py:30-80]()

A typical adapter wraps the framework's execution primitive (a `StateGraph`, a `Crew`, an `AgentRuntime`, a `Runnable`, or an `Agent` runner) and installs hooks at well-known boundaries: start of run, completion of a node/tool call, and end of run. The hook bodies delegate immediately to the funnel so that adapters never own business state — they only translate events.

| Framework | Module | Primary Hook Translated |
|-----------|--------|-------------------------|
| LangGraph | `langgraph.py` | Node entry/exit on the state graph |
| CrewAI | `crewai.py` | Agent and task lifecycle callbacks |
| AutoGen | `autogen.py` | Group chat / reply turn events |
| LangChain | `langchain.py` | `Runnable` start/end and `Chain` callbacks |
| OpenAI Agents | `openai_agents.py` | Agent loop and tool invocation events |

The package's `__init__.py` re-exports the public adapter entry points so that consumers can write `from loopgain.integrations import langgraph_adapter` (or similar) without reaching into submodules. This keeps the import surface stable even when internal refactors move symbols around. Source: [loopgain/integrations/__init__.py:1-40](), [loopgain/integrations/langchain.py:1-30]()

## Wiring LoopGain into an Agent Stack

The standard wiring pattern is the same across frameworks:

1. Construct or obtain a `Funnel` configured for the run (mode, persistence, identifiers).
2. Import the relevant adapter and pass the funnel into it together with the framework object (graph, crew, runtime, runnable, or agent).
3. Run the framework normally; the adapter pipes events into the funnel transparently.

Concretely, an adapter is expected to call `on_init` when a run begins, `on_first_observe` on the first observable signal inside that run, `note_adapter` whenever a sub-component (tool, agent, node) is invoked, and `note_outcome` when the run terminates with success or failure. Because all four hooks live on the funnel, adapters stay declarative: they don't decide *what* to record, only *when*. Source: [loopgain/integrations/langgraph.py:40-100](), [loopgain/integrations/openai_agents.py:40-100]()

The funnel in turn guards each hook against premature calls by checking its internal `_mode` flag against the `_ENABLED` constant. This guard exists so that adapters firing before initialization do not crash, but it relies on `_ensure_loaded()` having run at least once. Source: [loopgain/integrations/__init__.py:1-40]()

## Known Issue: Silent Skip When `_mode` Is Uninitialized

A reported bug illustrates a sharp edge at the adapter/funnel boundary: `note_outcome()` and `note_adapter()` on the funnel short-circuit when `self._mode != _ENABLED`, but `_mode` is `None` until `_ensure_loaded()` is invoked. Adapters that fire these hooks before the funnel has been initialized — for example, an OpenAI Agents tool callback that runs ahead of `on_init`, or a LangGraph node that emits during graph construction — will silently no-op instead of buffering or raising. Source: [loopgain/integrations/__init__.py:1-40](), [loopgain/integrations/openai_agents.py:60-120]()

The practical mitigation for adapter authors is to guarantee that `Funnel.on_init()` (or any method that triggers `_ensure_loaded()`) is called before the framework can emit any event. Wiring adapters around a funnel that is constructed and initialized synchronously up front avoids the race. Downstream users encountering missing observations should verify that `on_init` precedes the first framework callback. Source: [loopgain/integrations/crewai.py:50-110](), [loopgain/integrations/langchain.py:50-110]()

This issue is also a useful diagnostic signal: if an integration "works" but LoopGain reports no outcomes, the most likely cause is hook ordering rather than a framework incompatibility.

---

<a id='page-4'></a>

## Usage Examples, Error Signals, and Common Workflows

### Related Pages

Related topics: [Core LoopGain Engine and Trajectory Classifier](#page-1), [Framework Adapters: Wiring LoopGain into Agent Stacks](#page-3)

<details>
<summary>Related Source Files</summary>

The following source files were used to generate this page:

- [examples/01_code_pytest.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/01_code_pytest.py)
- [examples/02_json_extract.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/02_json_extract.py)
- [examples/03_essay_critique.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/03_essay_critique.py)
- [examples/04_sql_synth.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/04_sql_synth.py)
- [examples/05_unsolvable_stalls.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/05_unsolvable_stalls.py)
- [examples/06_diverges.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/06_diverges.py)
- [loopgain/funnel.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/funnel.py)
</details>

# Usage Examples, Error Signals, and Common Workflows

Loopgain ships with a progression of runnable examples that demonstrate how to wire the funnel into real tasks, how to interpret the signals it emits when something goes wrong, and how the canonical happy-path workflow looks from initialization to termination. This page walks through the example catalog, the error and stall signals users should learn to recognize, and the common workflows that tie them together.

## Example Catalog and What Each One Demonstrates

The `examples/` directory is organized as a tutorial in escalating difficulty. Each script is self-contained and exercises a single failure mode or use case.

- **`01_code_pytest.py`** — The introductory example. It pairs the funnel with a Python code-generation task whose quality signal is the pytest exit status. It shows the minimum viable loop: build a funnel, attach a generator, run the observe-and-judge cycle, and let the adapter adjust. Source: [examples/01_code_pytest.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/01_code_pytest.py)
- **`02_json_extract.py`** — Demonstrates a structured-output task where the reward is schema-conformance. It illustrates how the funnel handles objective, machine-checkable scores that do not require an LLM judge. Source: [examples/02_json_extract.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/02_json_extract.py)
- **`03_essay_critique.py`** — Introduces a subjective task scored by a critic model. The funnel delegates the reward signal to a separate evaluator rather than a test suite. Source: [examples/03_essay_critique.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/03_essay_critique.py)
- **`04_sql_synth.py`** — A domain-specific synthesis task (SQL generation) with execution-based validation. It demonstrates how to plug a custom scorer into the funnel without subclassing core components. Source: [examples/04_sql_synth.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/04_sql_synth.py)
- **`05_unsolvable_stalls.py`** — A diagnostic example. It deliberately sets up a problem the funnel cannot make progress on so users can see the stall signal in action. Source: [examples/05_unsolvable_stalls.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/05_unsolvable_stalls.py)
- **`06_diverges.py`** — Demonstrates divergence: a scenario where the adapter's updates push the generator further from a solution instead of closer, and how the funnel surfaces that behavior. Source: [examples/06_diverges.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/06_diverges.py)

## Error Signals and Silent-Failure Modes

Loopgain communicates state primarily through return values and log lines rather than raised exceptions. Learning the vocabulary of these signals is essential because several APIs are intentionally non-raising by design.

### `_mode` Is `None` Until Initialization

The community-reported issue (top-engagement bug) describes a silent failure: `note_outcome()` and `note_adapter()` check `self._mode != _ENABLED` before recording events, but `self._mode` is `None` until `_ensure_loaded()` runs inside `on_init()` or `on_first_observe()`. Any user-supplied scorer or callback that calls those methods before initialization will appear to succeed while contributing no data. Source: [loopgain/funnel.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/funnel.py)

The practical symptom is that the funnel reports a normal-looking loop with no recorded outcomes and no adapter updates. Diagnostic steps:

1. Confirm `on_init()` or `on_first_observe()` has executed before any external caller invokes `note_outcome()` / `note_adapter()`.
2. Check the funnel's logs for the `_ensure_loaded()` marker line; if absent, the recorded event count will be zero regardless of how many times the API was called.

### Stall Signals

`05_unsolvable_stalls.py` is the canonical demonstration. When the funnel detects that the reward distribution has not improved over a configured window, it emits a stall signal. Code that depends on continuous progress should handle this by either relaxing the task, raising the iteration budget, or terminating the loop gracefully.

### Divergence Signals

`06_diverges.py` shows the opposite pathology: rewards trend downward. The funnel logs a divergence marker and, depending on configuration, may auto-pause adapter updates. Callers should treat divergence as a signal to inspect the adapter's reward shaping rather than the generator itself.

## Common Workflows

The happy-path workflow across all six examples follows a consistent shape. The diagram below captures the canonical state transitions and where the error signals above attach.

```mermaid
stateDiagram-v2
    [*] --> Uninitialized
    Uninitialized --> Loaded: on_init()
    Loaded --> Observing: on_first_observe()
    Observing --> Observing: note_outcome()
    Observing --> Observing: note_adapter()
    Observing --> Converged: reward plateau met
    Observing --> Stalled: no progress
    Observing --> Diverging: reward decreasing
    Stalled --> [*]
    Diverging --> [*]
    Converged --> [*]
```

In practice the steps are:

1. **Construct** a funnel instance and configure the generator, scorer, and adapter.
2. **Initialize** by calling `on_init()`. From this point onward `self._mode == _ENABLED` and downstream event calls are recorded. Source: [loopgain/funnel.py](https://github.com/loopgain-ai/loopgain/blob/main/loopgain/funnel.py)
3. **Observe** candidate outputs through `on_first_observe()` followed by repeated `on_observe()` cycles. Each observation should be paired with a call to `note_outcome()` from the scorer.
4. **Adapt** by calling `note_adapter()` whenever the adapter performs an update step. Skipping this call leaves the funnel's view of the adapter stale.
5. **Terminate** when the funnel reaches a converged, stalled, or diverging terminal state, as exemplified by `05_unsolvable_stalls.py` and `06_diverges.py`. Source: [examples/05_unsolvable_stalls.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/05_unsolvable_stalls.py), Source: [examples/06_diverges.py](https://github.com/loopgain-ai/loopgain/blob/main/examples/06_diverges.py)

### Task-Type Patterns

Three recurring patterns cover most user scenarios:

- **Test-based scoring** — `01_code_pytest.py` and `04_sql_synth.py` both rely on an external command producing a pass/fail. The funnel treats the boolean reward as ground truth and converges quickly on objective tasks.
- **Schema-based scoring** — `02_json_extract.py` uses deterministic structural validation. This pattern is robust and produces the cleanest convergence traces.
- **LLM-judge scoring** — `03_essay_critique.py` introduces a noisy, subjective scorer. Expect more variance, and pair this pattern with explicit stall tolerances because the reward signal itself is non-deterministic.

## Best Practices

- Always invoke `on_init()` before any custom scorer wires in, to avoid the silent-failure mode documented in the community issue.
- Treat any terminal state (`Converged`, `Stalled`, `Diverging`) as a first-class outcome worth logging, not as an error to suppress.
- When porting a workflow from one example to another, preserve the event-recording order: `on_first_observe` → `note_outcome` → `note_adapter`. Reordering breaks the funnel's internal accounting.

---

<!-- evidence_pipeline_checked: true -->
<!-- evidence_injected: true -->

---

## Pitfall Log

Project: loopgain-ai/loopgain

Summary: Found 8 structured pitfall item(s), including 0 high/blocking item(s). Top priority: Configuration risk - Configuration risk requires verification.

## 1. Configuration risk - Configuration risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a configuration risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: capability.host_targets | https://news.ycombinator.com/item?id=48919562

## 2. Capability evidence risk - Capability evidence risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: README/documentation is current enough for a first validation pass.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: capability.assumptions | https://news.ycombinator.com/item?id=48919562

## 3. Runtime risk - Runtime risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a runtime risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: community_evidence:github | https://github.com/loopgain-ai/loopgain/issues/1

## 4. Maintenance risk - Maintenance risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: Project evidence flags a maintenance risk. Review the linked source before relying on this workflow.
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: evidence.maintainer_signals | https://news.ycombinator.com/item?id=48919562

## 5. Security or permission risk - Security or permission risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: no_demo
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: downstream_validation.risk_items | https://news.ycombinator.com/item?id=48919562

## 6. Security or permission risk - Security or permission risk requires verification

- Severity: medium
- Evidence strength: source_linked
- Finding: no_demo
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: risks.scoring_risks | https://news.ycombinator.com/item?id=48919562

## 7. Maintenance risk - Maintenance risk requires verification

- Severity: low
- Evidence strength: source_linked
- Finding: issue_or_pr_quality=unknown。
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: evidence.maintainer_signals | https://news.ycombinator.com/item?id=48919562

## 8. Maintenance risk - Maintenance risk requires verification

- Severity: low
- Evidence strength: source_linked
- Finding: release_recency=unknown。
- User impact: May increase setup, validation, or first-run risk for the user.
- Evidence: evidence.maintainer_signals | https://news.ycombinator.com/item?id=48919562

<!-- canonical_name: loopgain-ai/loopgain; human_manual_source: deepwiki_human_wiki -->
