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.
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
InterviewRoleinstance starts at the first unit even when another instance has advanced. - Source isolation: one target can advance different named sources independently.
- Exhaustion:
SequentialUnitReader.next()returnsOptional.empty()and remains empty.isExhausted()reports whether the lastnext()returned empty: it istruefrom the start when the source ingested nothing, and stillfalseon 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 returnDONE, another terminal value, or continue. - Progress:
size()is the number of ingested units, andremaining()the number not yet consumed. Concurrentnext()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: knowledge fields and other RAG pages
- RAG diagnostics: inspecting a Role's resolved sources