# Sequential reading

A `@Knowledge` entry with `use = Knowledge.Use.SEQUENTIAL` gives an action
the source's next unit on each call. `SequentialConfig` and
`SequentialUnitReader` expose the same cursor to your own code.

Use ordered consumption when a workflow must visit every ingested unit once
in a reproducible order. It is not retrieval: there is no query, ranking,
reranking, cache, or context truncation. The runtime loads the selected source,
preserves the loader-provided ingest order, then advances one target-owned
cursor. The bundled FILE pipeline sorts by normalized path and unit position.
A custom loader must return a stable order when reproducibility matters.

## Replace a parser and cursor

A hand-written scripted workflow usually owns two pieces of infrastructure:
file parsing and an integer cursor. Keep only the application rule (for
example, returning `DONE` after four interview questions) and let the RAG
runtime own ingestion and position.

Add `tnsai-rag` as an optional application module when you need the
runtime implementation and the programmatic reader. `@Knowledge` and
`SequentialConfig` are declared in `tnsai-core`. The source loader, shared
cursor, and `SequentialUnitReader` live in `tnsai-rag`.

<!-- java-contract: src/main/java/com/example/tnsai/docs/SequentialConsumptionExample.java -->
```java
package com.example.tnsai.docs;

import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.Knowledge;
import com.tnsai.annotations.RoleSpec;
import com.tnsai.enums.ActionType;
import com.tnsai.intelligence.rag.binding.SequentialUnitReader;
import com.tnsai.models.role.RoleIdentity;
import com.tnsai.rag.DocumentFormat;
import com.tnsai.rag.KnowledgeUnit;
import com.tnsai.roles.Role;

import java.util.List;
import java.util.function.Function;

public final class SequentialConsumptionExample {
    private SequentialConsumptionExample() {}

    @RoleSpec(knowledge = @Knowledge(
        name = "interview-questions",
        path = "knowledge/interview",
        format = DocumentFormat.TEXT,
        unit = KnowledgeUnit.LINE,
        use = Knowledge.Use.SEQUENTIAL))
    public static final class InterviewRole extends Role {
        private static final int QUESTION_COUNT = 4;
        private int calls;

        @Override
        public RoleIdentity getIdentity() {
            return new RoleIdentity(
                "interviewer", "Runs a finite interview", "Interview");
        }

        @ActionSpec(
            type = ActionType.LOCAL,
            description = "Narrate the next interview question",
            knowledge = @Knowledge(name = "interview-questions"))
        public String nextQuestion() {
            return calls++ >= QUESTION_COUNT ? "DONE" : "";
        }

        public String nextProgrammatically() {
            return reader().next()
                .map(document -> {
                    calls++;
                    return document.content();
                })
                .orElse("DONE");
        }

        public void resetQuestions() {
            reader().reset();
            calls = 0;
        }

        private SequentialUnitReader reader() {
            return SequentialUnitReader.forTarget(
                this, "interview-questions");
        }
    }

    public static List<String> shareOneCursor(
            InterviewRole role,
            Function<InterviewRole, String> configuredActionDispatch
    ) {
        String first = configuredActionDispatch.apply(role);
        String second = role.nextProgrammatically();
        return List.of(first, second);
    }
}
```

`configuredActionDispatch` represents your application's already accountable
Agent/ActionExecutor dispatch of `nextQuestion`. That annotation-backed call
consumes the first line. The programmatic call then returns the **second** line:
`forTarget(role, "interview-questions")` resolves the same cursor by target
identity, runtime Role class, and source name.

The `calls` counter is deliberately not the source cursor. It encodes the
application-owned terminal rule. Parsing, ordering, and source position stay
inside the framework.

## SCOP path

With an accountable executor, SCOP uses the same ActionExecutor enforcement
path: a `SEQUENTIAL` entry writes one unit to `_rag_context`, and a blank LOCAL return
lets the existing narration fallback surface it. There is no second
SCOP-specific cursor or retrieval path.

When you call `SCOPBridge` directly, set `authorityScope(...)` on the bridge,
plus `liabilitySink(...)` if you want liability records, and pass the principal
on the invocation:
`executeAction(target, action, params, resolvedPaths, invocation)` with an
`InvocationContext` such as
`InvocationContext.autonomous(agent, principal.id(), scope, principal)`. A
dispatch whose invocation carries no principal is refused with
`principal=missing` before retrieval runs, so the cursor does not advance.

## Cursor contract

- **Ingest order:** exactly the order returned by the selected source loader.
  The bundled FILE loader uses normalized file path, then unit position within
  each file. Custom URL, database, or memory loaders own their stable ordering.
- **Target isolation:** a second `InterviewRole` instance starts at the first
  unit even when another instance has advanced.
- **Source isolation:** one target can advance different named sources
  independently.
- **Exhaustion:** `SequentialUnitReader.next()` returns `Optional.empty()` and
  remains empty. `isExhausted()` reports whether the last `next()` returned
  empty: it is `true` from the start when the source ingested nothing, and
  still `false` on the call that delivered the last unit. Return your terminal
  value from this flag rather than by counting units. Annotation dispatch
  injects no stale narration and reports a retrieved-document count of zero.
  Your action decides whether to return `DONE`, another terminal value, or
  continue.
- **Progress:** `size()` is the number of ingested units, and `remaining()` the
  number not yet consumed. Concurrent `next()` callers receive distinct units.
  `next(CancellationToken)` throws if the token is cancelled before a unit is
  consumed.
- **Reset:** `reset()` moves that shared target/source cursor to the first
  unit. The example also resets its separate application counter.
- **Lifetime:** cursor state is process-local. A restart begins at the first
  unit.

## Mixing it with retrieval

Each source an action names behaves by its own `use`. A `SEQUENTIAL` source
contributes its next unit, `SEARCH` sources run through one retrieval engine,
and `DIRECT` sources enter whole. The blocks join in declaration order.

Choose `SEARCH` when the input is a query and relevance should select the
context. Choose `SEQUENTIAL` when every unit must be consumed exactly in
ingest order. A `SEQUENTIAL` source has no equivalents of `topK`, `minScore`,
rerank, query expansion, cache TTL, or `contextWindow`. It also does not embed
or truncate a unit.

## Related

- [RAG](index.md): knowledge fields and other RAG pages
- [RAG diagnostics](diagnostics.md): inspecting a Role's resolved sources
