Skip to content
tnsaijava agent framework

Ordered knowledge-unit consumption

@Sequential, SequentialConfig, and SequentialUnitReader are @since 0.15.0. They first landed on framework main at TnsAI@933756d1 (PR #222 / TAN-5880). They ship in 0.15.0 and later — see Installation for the coordinates.

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-intelligence as an optional application module when you need the runtime implementation and the programmatic reader. @Sequential and SequentialConfig are declared in tnsai-core; the source loader, shared cursor, and SequentialUnitReader live in tnsai-intelligence.

package com.example.tnsai.docs;

import com.tnsai.annotations.ActionSpec;
import com.tnsai.annotations.KnowledgeSource;
import com.tnsai.annotations.Sequential;
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() {}

    @KnowledgeSource(
        name = "interview-questions",
        path = "knowledge/interview",
        format = DocumentFormat.TEXT,
        unit = KnowledgeUnit.LINE)
    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")
        @Sequential(source = "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 and current limitation

With an accountable executor, SCOP uses the same ActionExecutor enforcement path: @Sequential 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.

Do not turn the callback above into a direct SCOPBridge.getInstance().executeAction(...) call at this framework SHA. SCOPBridge constructs a private bare ActionExecutor, while accountability requires a principal, liability sink, and authority scope; the bridge exposes no public wiring seam. The framework regression test wires those fields with reflection, which application code must not copy. This public-runtime blocker is tracked in TAN-5976.

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. 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.
  • 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.

Do not combine it with retrieval

An action cannot have both effective @Sequential and @Retrieval configuration. Discovery/dispatch rejects the combination because ordered consumption and ranked retrieval have incompatible semantics.

Choose @Retrieval when the input is a query and relevance should select the context. Choose @Sequential when every unit must be consumed exactly in ingest order. @Sequential has no equivalents of topK, minScore, rerank, query expansion, cache TTL, or contextWindow; it also does not embed or truncate a unit.

Source evidence