Skip to main content

Development and architecture

Current version​

ASR uses the MC-independent speech/asr/AsrProvider and AliyunAsrProvider, and the client AssistantRecognition manages recording, cancellation and draft filling. TTS / ASR usage is stored separately by LocalSpeechUsage per client account UUID and is not reported to the server; the server SavedData records AI only. The current network protocol is 1. The optional SVC recording backend and F8 push-to-talk share the existing ASR entry point; see Speech recognition.

The NLS credentials are the client AppKey and AccessKey ID / Secret; AliyunNlsToken calls CreateToken with a JDK HMAC-SHA1 signature, reuses an in-memory cache across sessions and refreshes it on demand when less than two minutes of validity remain. Saving settings and validate do not go online, issuance is cancelled with the recognition worker thread, and the requestStarted callback of AsrProvider.recognize starts metering before the first recognition command is sent; authentication and handshake failures do not count as ASR attempts.

The speech provider interfaces and the client recording and playback live in speech.asr and speech.tts, the provider factories TtsProviders / AsrProviders live in speech, and the protocol implementations live in speech.aliyun; configuration is collected in config. Aliyun uses qwen3-tts-flash by default and additionally supports the Qwen-Audio-TTS HTTP protocol used by designed voice bindings; it reuses HttpTransport and adds no dependencies or model files. See Speech synthesis and voice design.

ComponentConfiguration
Mod and Agent Java toolchain / bytecode25 / Java 25
Gradle / ModDev9.2.1 / 2.0.148
Minecraft / NeoForge26.1.2 / 26.1.2.114
ApricityUI1.2.9; client minimum version 1.2.9
JupiterCurseMaven 8569832
Saturn1.2.6 / CurseMaven 9509793; required on both client and server, providing unified Cost, module DataStorage, Codec utilities, compressed NBT files, player information views and the HUD editor
Simple Voice Chat2.6.26+26.1.2 / CurseMaven 9307181; optional client recording, compileOnly + localRuntime, not bundled into the release JAR
Card JSON editor (standalone development tool)Vite 8.3.6; Node.js ^20.19.0 or >=22.12.0, located in html/card-editor; not included in the mod JAR
Gson2.13.2; the main project declares the dependency directly, Minecraft provides the runtime classes
FTB Quests / Library / Teams26.1.2.8 / 26.1.2.9 / 26.1.2.4, optional compileOnly
LangChain4JRemoved; the JDK HTTP gateway is used
Local intent routinglexical-tfidf-v1, JDK + existing Gson; no new third-party dependencies, native libraries or model files
Artifact size limitThe full System JAR must not exceed 10,000,000 bytes, enforced by the jar task

AUI 1.2.9 splits layout state out of RenderElement into LayoutState. Card collapse and page refresh invalidate the layout through Element.getLayout().invalidateLayoutSubtree() and then commit the update through AssistantPages.commit.

Player queries and cards use Saturn's data.information.PlayerViews, PlayerInformation and InformationText, and the display result type is data.information.model.InformationEntry. Queries still select the basic/status groups and the existing row allowlist and keep the limits of 32 rows and 256 characters per row; groups are resolved from the current world into Holders by ResourceKey, results are used directly through entry.value()/label()/tooltip(), and ordering and importance are read from the original row definitions. Saturn is a required dependency with a minimum version of 1.2.6; the notification position editor reuses its HUD framework and the world save uses its NBT_COMPRESSED entry point. The version and the CurseMaven file ID are managed by saturn_version and saturn_file_id in gradle.properties, and dependency metadata synchronization uses the same minimum version.

The Agent mechanism and interfaces remain in com.iafenvoy.system.agent: engine, executors, context, the Module/Tool/Store/Journal contracts, routing and model calls. Concrete workflows and feature assembly live in module/builtin, MC business logic, binding and trade live in assistant, parameter results, persistence and resource loading live in data, and speech behavior and quest narration live in speech. Business DataStorage stays in module/builtin/storage, and the execution ledger implementation lives in data/store. General utilities, configuration, game screens, networking and the optional compatibility layer keep their corresponding packages; all code still shares the main source set and the Java 25 toolchain. For the detailed boundaries see Package responsibilities and data flow and Functional modularity.

.\gradlew.bat compileJava testModClasses jar

All Agent classes go directly into build/libs/system-<mod_version>.jar. FTB queries, details and claiming directly implement the unified typed Tool, with no separate service interface and without going through stdio, HTTP MCP or a simulated client package. Optional transport adapters, if they need to be integrated, should also reuse the same Tool contract.

Model requests uniformly use the OpenAI Chat Completions format and keep DeepSeek by default; provider-specific thinking parameters are isolated per provider. For the configuration and message boundaries see Model interface and DeepSeek compatibility.

The administrator AI permission entry point is module/builtin/chat/AiManagementCommands, registered by the core module as /system ai <player>. Configuration is written by UUID into the AiAccessData SavedData of the current world save, using setDirty for automatic saving; the default level is also stored by that data, at <world save>/data/system/ai_access.dat. /system ai default [mode] reads or changes the default level. AssistantServer.refreshAiAccess cancels in-flight interactions, syncs state and shows the chat hint, and the execution context keeps checking the administrator and personal switches at the request boundary. The client receives only its own effective permissions and cannot write administrator configuration. For the commands and interface see Free chat.

Directories and responsibilities​

PathResponsibility
agentAgentResult, AgentException, AgentFailure and AgentLifecycle common contracts; for the detailed sub-packages see Agent runtime structure
agent/executionAgentEngine, interactive/event executors, execution context, budget, cancellation and session read/write
agent/module, agent/toolWorkflow contract and action dispatch, module registration; tool types, permission scopes and typed observations
agent/chatChatSupport prompt / ToolProvider extensions, ChatActions merged workflows and read-only tools, ChatObservation typed observations; the free chat workflow lives in module/builtin/chat
agent/command, agent/reactBrigadier command declaration and parsing; the generic bounded ReAct loop and its independent context
agent/storageAgentStore / AgentJournal storage contracts, ConversationMemory history projection; the persistence implementation is still in data/store
agent/routing, agent/modelHybrid routing, semantic matching and caching; model interfaces, unified requests and the metering entry point, OpenAI-compatible protocol
agent/request, agent/responseStandalone request/response and MapCodec; registration declarations are still provided by SystemRequestTypes / SystemResponseTypes
assistantServer assembly, binding business, player connections, request dispatch, game thread queue, MinecraftTools and the client session bridge
assistant/tool, assistant/itemMC single-behavior Tools for players, items, rewards and trade, plus the game item name index
assistant/commercePlayer catalog permissions, Saturn Cost evaluation, short-lived quotes and trade orchestration; not owned by AgentEngine
assistant/usage, data/usageUsage service, standalone SavedData, AI/web metering and immutable snapshots, not deleted by revoke
moduleFeature contracts, ModuleContext install dependencies, ModuleManager, player permissions, storage declarations and lifecycle
module/builtinBuilt-in feature assembly, platform commands and both-side entry points; sub-packages organize Agent workflows by feature, and BuiltinTools declares shared tool keys
module/builtin/storageBusiness DataStorage for sign-in, inbox, advancements, trade, module overrides and pinned card records
module/clientClient module contract, binding/command caches, dynamic navigation and the workbench
dataCommon state and business data; split into sub-packages by definition, storage, network or resource loading responsibility
data/input, data/output, data/result, data/searchTyped tool/workflow parameters, query results, card business results and search decision data
data/storeFileAgentStore, player records, conversation files, business receipt models and the AgentJournalDataStorage implementation
data/database, data/promptWorld-save shared/player databases and the knowledge index; prompt snapshots, placeholders and the txt reload listener
data/module, data/catalogModule definitions, the three catalog types and CommerceCatalog registry snapshots; SystemDefinition / LotteryPool live in the data root package
data/action, data/condition, data/speechSaturn extension types and speech network payloads
data/card, data/card/clientCommon CardData/Builder/View/Update, action and narration descriptions; client snapshots and the unified action controller
screen/card, screen/card/nodeCard templates, node dispatch Codec, rendering, placeholders and resource reload
screen, screen/guiWorkbench, settings, usage screens and the notification queue
speech/questQuest narration formatting, model organization and background classification cache, sharing budget and metering through the Agent context
speech/aliyun, speech/tts, speech/asrSpeech provider protocols, streaming playback sessions, audio cache and cancelable recording
searchSearch / Fetch protocol, the DeepSeek client and SearchGateway; the optional search module exports Tools
registryStatic and data pack registry declarations, key/sound/protocol registrations, client module cache and the speech factory Map
configAgent, model, semantic, client, speech and billing configuration and ConfigInstance
command, command/clientCommand assembly and completion bridging on both sides; concrete entry points are declared by modules
util, util/codecCodec/Schema, type dispatch, HTTP/SSE and other general utilities
compat/ftb, compat/voicechatFTB tools/events and the SVC capture implementation, loaded only after the dependency is detected
event, network, network/payloadExtension events, protocol 1, Handlers and bounded payloads

The paths in the table above are all relative to src/main/java/com/iafenvoy/system/. The Agent and the game integration code live in the same main project; for the detailed dependencies and lifecycle see Package responsibilities and data flow. Modules, tools and service interfaces keep their division of responsibility, and tools use stable logical IDs to make later business extensions easier.

Registrations are exposed as public static final fields of the registry classes. The static SERVER_MODULE is the MapCodec type registry; dynamic SystemModule configuration comes from the world's system:system_module registry, and the server field is dispatched by type. ServerModule and ClientModule implementations are split into separate files, the client Map is initialized only by SystemClientMod, and the common registry no longer references ClientModule. The data pack client field stores only an Identifier; the client binding cache is grouped by client ID and carries the list of configuration IDs. ToolModule, WorkflowModule, SharedModule and CoreModule still split server responsibilities by interface, and SharedModule no longer extends ClientModule. For the full entry points and lifecycle see Functional modularity and Registry quick reference.

AssistantServer assembles the engine, installs modules, forwards game events and refreshes resources; AssistantPlayers manages connections and the binding flow, AssistantBindings encapsulates binding transactions and business deletion, and ModuleManager creates a PlayerModule from ServerModule.runtime and uniformly manages module sessions, events, ticks and shutdown; the advancement baseline and dispatch are managed by the advancement module, AssistantRequests manages typed request dispatch and replies, and AssistantUsage manages usage queries and the metering queue. Commands, networking and optional bridges call the corresponding services through players(), requests() and usage(); asynchronous replies still check the connection generation, and model metering is still written into SavedData by the server main thread.

The INSTANCE of AssistantServer is created and closed with the server load and cleared on shutdown, and get(server) keeps the object identity check; it uses no server-keyed Map and is not attached to MinecraftServer. FTB state is held by the shared FtbQuestRuntime, ModuleContext.sharedRuntime is reused by type within the current installation scope, and ModuleManager runs validate/tick/close only once for a shared instance; event listeners are unregistered with the instance, and optional classes are not loaded when FTB is not installed.

AssistantClientState uniformly stores the conversation, pending requests, server preferences and capabilities, and the current interaction result, and produces an immutable snapshot on every update. AssistantClient holds only one state reference and handles side effects such as navigation, networking and speech; on logout or a successful revoke it is replaced with AssistantClientState.EMPTY. The interface refresh version increments independently and does not reset to zero when the state is cleared.

Client caches are exposed uniformly by ClientModuleCaches: BINDINGS directly holds the page declarations and local card actions, and COMMAND directly holds the command declarations and the compiled tree; the former Bindings / Registry relay objects have been removed. The binding cache plugs into Saturn RegistryCache and coordinates the two data sets: the client entry point is registered ahead of time, assembly is lazy on first use, static label updates rebuild it, and logout clears it at the same time; each round iterates the modules only once, then freezes the page/action sets and releases the command Builder, and asynchronous completion excludes stale results by command cache version. A module register should be repeatable and declare contributions only. Player permissions are still read in real time, and the Document of an already opened page does not enter the cache; CardTemplates is responsible only for resource JSON reload and is decoupled from the binding cache. There is no timer or per-tick scan; see Client contributions for details.

Codec boundaries​

SystemDataPackRegistries registers both system:system and system:system_module through DataPackRegistryEvent, and both are synced with the same data/network Codec. SystemDefinition stores the name/description translation keys and the module references; the Either of ModuleDefinition points to a Holder<SystemModule> or a TagKey<SystemModule>, and condition uses Saturn EntityCondition. The module configuration file is data/<namespace>/system/system_module/<path>.json, server is dispatched through ServerModule.CODEC, and client is only a local implementation ID. The system is still read from RegistryAccess by the player's binding ID, without caching a cross-world snapshot. ModuleManager manages start/stop, runtime and permissions by configuration ID, and the current parameter configuration is read only when the world loads; it is not a /reload retained listener. Manual overrides are still saved by the player's ModuleAccessDataStorage, and a revoke deletes business data but keeps usage.

The same entry point also registers the three server-only native data pack registries system:signin_rewards, system:shop_offers and system:lottery_pools, which use the CODEC of SignInRewardEntry, ShopOfferEntry and LotteryPool respectively and provide no network Codec. A module's rewards / offers / pools use Saturn RegistryCodecs.keyOrTagList and pass only ResourceKey / TagKey in the synced configuration, so the client is not required to resolve server-side Cost or the whole catalog. Entries of the three catalog registries are read when the world loads, and tags are loaded and reloaded by the vanilla flow.

Each configuration class declares FILE_NAME, CODEC and ConfigInstance<T> INSTANCE = ConfigInstance.of(...) itself. When the container is initialized for the first time, it copies missing default files from /defaults/<file name> and then reads them through the Codec; existing files are not overwritten, and a load failure logs an error and uses the defaults in memory. Reads use INSTANCE.get(), and committing a new immutable value uses INSTANCE.set(next); actual changes are marked dirty by the container automatically. If mutable configuration is used and modified in place in the future, INSTANCE.markDirty() must be called explicitly and the change marker must not be bypassed.

ConfigReloader calls reloadAll() on a resource reload, and the client/server tick and shutdown events call saveAll(), which saves only marked configurations. Checking the marker, saving and loading use the same instance lock; a failed save restores the marker so that it can be retried, and pending changes are saved before a reload. All effective configuration is read directly from the config.file.XxxConfig.INSTANCE.get() of the corresponding class; the engine, routing, model clients and the advancement bridge do not cache configuration objects, and constructors no longer pass global configuration, so the settings() forwarding chain is removed.

The settings command of the assistant input box is /settings, and execution and completion share the local command table contributed by each ClientModule; the game chat bar registers /system settings through the client RegisterClientCommandsEvent, reuses the same settings entry point, sends no server command or model request, and merges completions with the other subcommands of the server-side /system. Opening settings from the chat command returns to the game, while opening it from the assistant keeps the original page and draft. Settings controls commit the configuration of the current page on input/change/toggle operations; local configuration first marks the change with INSTANCE.set(next) and then calls save() immediately; server preferences keep the request-and-confirm flow. TTS / ASR allow incomplete credentials to be saved, and the full parameters are validated on preview, synthesis or when recognition starts; an invalid volume keeps the original value and shows an error, while other settings are still saved. The speech page has no JSON import, reload or manual save button. The TTS queue stores only text and session and reads the configuration when narration starts; model and speech requests keep the payload, provider and metering information of that call after they are sent, and in-flight requests are not rewritten by configuration changes. The model connection address, authentication and request body are read together inside the configuration instance lock, and the network request runs outside the lock. Routing thresholds, cache capacity and new-entry TTL are read from the current configuration; generated lexical indexes and the model concurrency semaphore are still built at startup, so changes to index example sentences, index language or concurrency capacity require a restart.

ConfigInstance.getConfigFolder() provides the root folder shared by all configurations; configuration instances and the TTS cache join their file names and the fixed tts-cache subdirectory under it respectively, so no extra SystemConfig path utility is maintained.

AgentConfig keeps only the execution budget, concurrency, the daily model quota and the time zone; ModelConfig handles model connections and provider parameters, SemanticConfig handles local routing, AdvancementConfig handles advancement binding, and StorageConfig handles capacity budgets for player files. The corresponding files are system-agent.json, system-model.json, system-semantic.json, system-advancements.json and system-storage.json, all under config/system/. Rewards, offers and lottery pools are defined entirely by data packs, do not participate in configuration parsing, and automatic migration from the old aggregate configuration is not provided.

CodecIO unifies the JSON text boundary and Codec encoding/decoding. Both history and requests of Conversation use the fault-tolerant containers of Saturn CollectionCodecs: each entry is decoded individually, and a parse error or constructor exception only drops that entry and logs it, partial values are not accepted, and encoding remains strict; the outer lenientOptionalFieldOf handles the case where the whole field type is invalid; old payloads are not migrated. One round of history is a sealed Message, and role is the single discriminator field: user stores the original text, and assistant nests AgentResult directly (the same as requests), so readers take values per variant instead of parsing JSON strings themselves; adding a role only needs one record and one branch in the dispatch. Binding, rewards, orders, write steps and request input fingerprints still use strict reads. Configuration uses immutable records, RecordCodecBuilder and record getters, and is read per field with lenientOptionalFieldOf. Ranges and field semantics are validated in sub-Codecs, and enum and nested constructor exceptions are converted into parse errors by ConfigCodecs. In configuration, a missing field, null, a wrong type, an invalid enum, an out-of-range value or a nested Codec constructor exception only falls back that field and keeps the other valid configuration; configuration collections return immutable snapshots, and player files still use the original force/atomic-replace transaction of FileAgentStore.

AgentRequest expresses chat, module calls, card buttons, control requests and preferences, and registers a StreamCodec in SystemRegistries.AGENT_REQUEST_TYPE through SystemRequestTypes; the object's streamCodec() returns the registered instance, and the root STREAM_CODEC dispatches by the synchronized registry numeric ID. AgentResponse dispatches through the corresponding official registry, and the old DispatchRegistry has been removed; a quest query returns a single data object or a List according to the Tool declaration instead of using a polymorphic result registry. Payloads on both sides encode and decode typed objects directly with StreamCodec instead of going through a Codec first; only nested persisted structures (AgentResult, AgentAction) still reuse their Codec. JSON is used only at the text boundary. Model routing return values are dispatched by capabilityId from the Codec Map of the optional modules of this request, sign-in only accepts a SignInDecision, and the sent player projection does not contain other online players. ConversationMemory uses a bounded object projection made of allowed fields: each card describes its own referable entities through CardData.memoryFact(), which returns empty by default, and truncation and count limits are centralized in CardFact, so new card types do not need changes to a central registry.

Fixed business data, configuration, world saves, model and speech payloads use Codecs. JSON remains only at the dynamic JSON Schema and general interface formatting boundaries; tool write receipts use the vanilla Dynamic/PASSTHROUGH to store results that are not interpreted for now, and a replay decodes them with the Codec of the corresponding tool result instead of reading values by key in business code.

Configuration fault tolerance applies only to valid JSON that has already been parsed into objects; when the syntax is broken, keys are duplicated, the file size limit is exceeded or the file is unreadable, an error is logged and the container falls back to the defaults of that configuration in memory without overwriting the bad file. An error in a list, a map or a strict business parameter can make the whole configuration field it belongs to fall back, while other top-level settings are kept; IDs are not invented for rewards, events and payment references. Entries in the pricing table whose unit price cannot be determined are skipped and marked as unpriced, while the other valid entries remain in use; when the whole pricing table fails to load, an empty table is used, usage is still recorded but nothing is priced.

Network conventions​

The main registration class is responsible only for payload and handler registration. C2S is validated by the server Handler on the main thread, which checks the UUID, reads the player from the connection and dispatches to the server-side capability; S2C is decoded by the client Handler and dispatched to the client state or interface. Client handlers are bound through NeoForge's client registration event, and a dedicated server does not parse client interface classes. Connection generations, late-response protection, result length limits and usage permission checks keep their existing rules.

Payload classNetwork ID
AssistantRequestC2Ssystem:assistant_request_c2s
AssistantResponseS2Csystem:assistant_response_s2c
TokenUsageRequestC2Ssystem:token_usage_request_c2s
TokenUsageResponseS2Csystem:token_usage_response_s2c

The current protocol version is 1, and the client and the server must be updated together. A C2S request is a request UUID plus AgentRequest; S2C distinguishes replies, HUD notifications, interface navigation, binding changes, state and history through the type of AgentResponse, and operations no longer occupy request IDs. Navigation can carry an associated request UUID; the client first clears the waiting state of that request and then requests the interface state; the usage payload expresses opening the page with an independent open flag. Invalid or unknown types are rejected during the streaming decode phase of the payload. The current project is unreleased, so no compatibility is kept for old network fields. When adding a payload, define a type with the direction suffix in network/payload, add a handling method in the corresponding Handler, and then register it through NetworkManager.

Extension modules​

Implement AgentModule<I extends AgentAction>, provide the registered MapCodec<I> through inputCodec(), and also declare the description information and List<ToolRegistry.Tool<?, ?>> tools(); execute receives the action object directly. The action implementation's codec() returns its own MapCodec. A built-in module extends AbstractAgentModule<I>: the constructor only takes the SchemaMapCodec<I>, the description and the tool list, the registration ID and the input Schema come from the same Codec, and the tool allowlist is generated from the list, so nothing is declared twice. A module is trusted Java code; the Codec does not replace business authorization and is not a same-JVM sandbox.

Module IDs use the vanilla Identifier: Descriptor.id, AgentAction.id() and the keys of the module registry are all of that type. First register the action MapCodec in SystemResourceKeys.ACTION_TYPE through a DeferredRegister; built-in definitions are provided by SystemActionTypes. A module's inputCodec() and an action's codec() must reuse the same instance; the engine rejects unregistered Codecs and Codecs whose registration ID does not match. An action without arguments uses its own type and Codec, and the same EmptyInput Codec cannot be registered for several actions. Configuration and networking still encode it as a namespace:path string.

Each Tool is responsible for exactly one behavior; ToolRegistry.Type<I, O> stores the stable ID, the parameter MapCodec<I> and the result Codec<O>, and Tool<I, O> stores writes, idempotency, event permission and a single Handler. A module calls context.tool(type, stepId, typedInput) directly to obtain a typed result; when one flow needs several behaviors, combine them with the module's List.of(toolA, toolB). Tool instances are shared within the same module configuration; different configurations may have different handlers for a tool of the same type, and the handler is selected by configuration owner at call time. The same ID with a different tool type key is still rejected.

Feature types are registered to SystemResourceKeys.SERVER_MODULE through DeferredRegister<MapCodec<? extends ServerModule>>, codec() returns the registered MapCodec, and a data pack creates configurable instances in the system_module registry. ModuleContext is independent per configuration and keeps the current id() and the tool cache of the configuration; AgentEngine and the module registry are fixed constructor fields, read through engine() / modules(). Other services share a Map indexed by ContextType<T>; AssistantServer puts each of them in, and a module gets the typed object directly with get. Adding a service does not require changing ModuleContext; keys match by identity, so the same public constant must be used. ModuleContextEvent is published on the game bus after the built-in services are injected and before any module is assembled, for extensions to add services synchronously. The client calls SystemClientModules.register(id, instance) in its own entry point, and the data pack client references that ID. When different configurations provide the same action, ModuleRegistry and ToolRegistry select the implementation by the owner enabled for the player and keep the owner check of the current flow; command, model and semantic contracts are still declared once per action type. SharedModule provides server-side commands, and CoreModule represents required infrastructure. Extensions that only add workflows can still be added through AssistantRegistrationEvent; for the detailed contract see Functional modularity.

AgentAction.CODEC selects the MapCodec returned by the instance through the byNameCodec().dispatch(...) of the official SystemRegistries.ACTION_TYPE by moduleId; the parameters are flattened as {"moduleId":"system:item_query","query":"stone"}. ModuleRegistry.call locates the modules installed on the current server by the Codec registration ID and checks the instance identity. Commands, semantic extraction and button decoding all validate Java-created actions through AgentAction.validate; the model only accepts actions in the currently available catalog. Modules that are not installed, event-only modules and structuredOnly() modules never become model-selectable capabilities. Networking, configuration and event saves use Codecs at the boundary, and AgentAction is passed directly internally with no extra type/value wrapper.

AgentCodecs.strict additionally rejects unknown keys and explicit null on top of the vanilla Codec; the exact integer Codec rejects decimals and overflow values, so the truncation behavior of vanilla Codec.INT cannot affect indexes. Defaults for business parameters are provided by the strict optionalFieldOf, and a type error does not silently fall back; configuration fields uniformly use lenientOptionalFieldOf to fall back to defaults, and the two do not affect each other. A Codec parse failure does not accept partial results. Write tool replay requires an exact fingerprint match and then restores the object with the result Codec of the corresponding tool; on a mismatch it refuses to execute, does not normalize old formats and does not re-run completed write operations.

Dispatch, routing and execution responsibilities​

ModuleRouting uses Selection to distinguish NORMAL, EXPLICIT_ONLY and STRUCTURED_ONLY. explicitOnly() still enters the model capability catalog and return value decoding, and explicitOnly: true in the catalog requires the model to choose that capability only when it is very confident that the current request needs it and to clarify otherwise; it does not allow local semantics or the intent cache to execute directly. structuredOnly() is what excludes model selection, keeping the registered explicit commands, the current message parser and structured buttons. The execution entry points check model selectability and local semantic selectability separately, so similarity cannot bypass the high-confidence requirement. Payment confirmation, the actual lottery draw and manual quest submission keep structuredOnly and are not opened to automatic model execution by this semantic adjustment.

  • AgentEngine provides the unified entry point, owns the service and player lifecycle, and validates module permissions, selection policy, execution flow and partial-success reporting through the same execute.
  • InteractiveExecutor manages one interactive request per player, cancellation generations, request fingerprints and result replay; enqueueing, initialization and write-back all check the cancellation generation.
  • EventExecutor manages persistent events, rule authorization, deduplication and recovery; recovery uses the saved policy, budget and tool receipts.
  • ConversationService handles history reads, clearing and preference changes and does not duplicate execution or queue logic.
  • QuestNarration manages read-only quest narration, the short-term per-player concurrency limit, the model-organized fragment cache and merging within the same batch, without occupying the main chat or creating conversation records. QuestBackgrounds persists background classification and copy (including "no background") by quest unique ID, language and content fingerprint, using the system:quest_backgrounds table of the shared database and containing no player progress. FTB's FtbQuestSpeech only provides a main-thread definition snapshot; the card sync returns existing structured paragraphs, and when the cache is not ready, organization runs in the background for later cards without blocking the notification or replaying the current message; the read source re-checks the current permission, and for the model budget and client session boundaries see Speech synthesis.
  • TaskExecutor unifies bounded dispatch, queue-full errors, shutdown and Future completion; tasks that were not executed after shutdown also get CANCELLED.
  • HybridRouter only orchestrates structured calls → the command registry → local semantics/cache → one model fallback. Each module provides its own commands, example sentences, argument extraction and optional read-only shortcuts through ModuleRouting<I>.
  • ExecutionContext is the common entry point for execution permissions, budgets, model calls and tool calls. A Module composes the flow, and a Tool performs exactly one behavior per call.

engine.start() validates module policies and event references, then freezes the module, tool, command and semantic registration information. Extensions must complete registration in AssistantRegistrationEvent; adding or removing modules at runtime is not currently supported. Administrator lifecycle commands and client interface operations stay on their respective sides, and Agent capabilities are called through the unified registry. Game chat's /system <instruction> is the generic forwarding entry point and reads the Agent registry in real time for completion; the assistant input box obtains the same set of commands through a state snapshot and needs no client command table changes for extensions. For a detailed extension example see Agent runtime structure.

Built-in inputs and outputs​

Each type in data.input and data.output lives in its own file and is imported directly under names such as QuestDetailInput and PlayerSnapshotOutput, no longer referenced through an aggregate outer class. Inputs use a MapCodec with validation and Schema metadata, and outputs use a Codec; the quest list and name search return List<QuestBriefOutput>, details return QuestOutput and the starter gift returns StarterGiftOutput. The reward-credit parameter RewardCreditInput belongs to the input package, items use the vanilla ItemStackTemplate.CODEC directly, and rewards use data.store.Reward.CODEC; Reward.ITEMS_CODEC uniformly limits the output to 1–36 entries. No item stack is created during reload; at runtime the template's create() is called on the game thread.

Generating input schemas from Codec definitions​

SchemaCodecs.record((i, f) -> ...) still uses the vanilla RecordCodecBuilder and records JSON Schema metadata while declaring fields, returning a SchemaMapCodec that can be used directly with vanilla dispatch. For example:

public record Search(String query, int offset) {
public static final SchemaMapCodec<Search> CODEC = SchemaCodecs.record((i, f) -> i.group(
f.required("query", SchemaCodecs.STRING).forGetter(Search::query),
f.optional("offset", SchemaCodecs.NON_NEGATIVE_INT, 0).forGetter(Search::offset)
).apply(i, Search::new));
}

Search.CODEC.schema() returns a schema containing type, properties, required, default, the non-negative constraint and additionalProperties: false. SchemaCodecs.STRING, NON_NEGATIVE_INT and BOOL provide common types; when there is a real business basis, string(min, max) and intRange(min, max) can specify a range. matching provides both regex validation and pattern, object and Value.listOf() support nested objects and arrays, and described adds a description to a field or object. Empty parameters use SchemaCodecs.unit. The schema is assembled only at definition time and adds no network call or dependency; the router reads the generated result through the existing module description entry point, and module constructors no longer receive a hand-written schema.

This is not reflective decompilation of arbitrary vanilla Codecs: vanilla has no general structure access interface, and xmap and validate may also contain arbitrary Java logic. xmap, flatXmap, validate and the lifecycle wrapper of SchemaMapCodec keep the existing structure metadata but do not automatically translate extra predicates. For example, the three-way choice of the quest selector is still validated in the Codec and explained to the model through described. When integrating an external Codec, you can explicitly construct SchemaCodecs.Value(codec, schema) or wrap the external object Codec with SchemaMapCodec(codec, schema); the accuracy of that explicit schema is the extension's responsibility. The generated result is used to prompt the model, while execution still follows the Codec and business authorization, and no new model-side structured-output protocol is enabled.

Result cards and actions​

AgentResult no longer extends Saturn's result type: Result<T> is a sealed interface in Saturn 1.2 that only allows its own SuccessResult / FailureResult implementations, so AgentResult holds the cards field itself and derives isSuccess() / isFailure() from Status. Status is SUCCEEDED/FAILED/PARTIALLY_SUCCEEDED, encoded as a stable lowercase string; partial success counts as a failure and must not be written to the success cache. CardUpdate identifies a message with CardRef(UUID, revision) and supports SHOW, REPLACE and REMOVE; update delivery semantics are independent of the business success state.

The common card package defines CardData, CardBuilder/CardView, CardButton/CardAction, CardContext and SpeechContent and loads no client, AUI, FTB or server runtime. CardData provides its own MapCodec and buildCard, and the client call yields the complete template, parameters, main text, buttons and speech. CardPresentation handles local translation, CardText lazy item resolution, the plain mode and controlled callbacks, and JSON controls only the layout. A card with exactly one plain text node and no buttons or extra content is shown as an ordinary message; text with buttons stays a card.

The server DTO contains the paged original query terms, the catalog version, the quote, the claim reference and the actionable state, and the client does not look up workflows or judge business itself. CardAction.Server sends an action Identifier and string parameters, which ModuleRouting.button converts back into the original typed input and validates, continuing into the original AgentModule/Tool. Local actions are executed by the client controller Map; labelKey is used only for display and speech matching. ClientCardBindings and notificationAction, which inferred behavior from tags, have been removed.

ClientCards keeps the latest message, revision and activity state within a connection, and ServerCards keeps only published revision metadata and reserves a revision when an asynchronous action starts. REPLACE modifies only the same ID and does not enqueue or speak anew; REMOVE stops the display/speech of that card, and history keeps a read-only snapshot. A notification does not switch automatically by duration or when speech ends; WAIT keeps the current notification and disables server operations, and a failure, a request timeout and a zero-card reply all complete the wait. State and history are responded to independently; history restore sends no packet and does not speak, and does not restore the interactive capability of old references.

CardNode implements codec/render/text/css in screen/card/node, and the root Codec dispatches by type from the official CARD_NODE_TYPE; style accepts a string or a list. The three Context maps are flattened and shared, and items binds List<ItemPreviewOutput> directly instead of passing items through JSON strings. Notification body actions show only BODY buttons, FOOTER is still generated by code; a quest list notification shows 10 entries per page with local paging inside the node, and after viewing details the player can return to the original page. A missing template falls back to the summary and the existing buttons and never shows the raw DTO. For the format see Client JSON cards; for the full flow and BuiltinTools responsibilities see Card and message data flow.

Results in networking, history and player saves uniformly use AgentResult.CODEC, and the text boundary uses CodecIO. The protocol stays at 1; the project is unreleased and provides no migration for old cards, networking or development saves. The Codec of CardData is always shared and references no optional mod class, and the real FTB tools and workflows are loaded only when the mod is installed. TTS/ASR private keys and usage are not uploaded to the server.

Game access still goes through the restricted main-thread queue, and waiting for model or tool Futures on the main thread is forbidden. An extension module declares ModuleRouting<I> in routing(), and the same module registration automatically builds the commands and the RoutingProfile; see Lightweight local intent routing.

A tool declares whether it writes, whether it is idempotent and whether the event subject may call it. Event-only modules do not enter the chat catalog. A resumable write step uses a stable stepId; when a module entry is re-executed during recovery, already completed write steps are skipped by receipt. Do not produce hidden side effects, call models or run infinite loops outside tool dispatch; Java interfaces cannot enforce isolation of malicious extensions.

Independent policies​

AgentConfig.INSTANCE.get().policies() overrides the whole ExecutionPolicy object by module ID. Routing uses routingPolicy separately, and the model calls and tokens already consumed are transferred to the selected module; the module's total deadline is counted from when the request was first received.

FieldMeaning / valid values
modelCallsActual model attempts, non-negative integer
toolCallsTool attempts, non-negative integer
stepsTotal model/tool step attempts, positive integer
writeStepsWrite tool attempts, non-negative integer
inputBytesUTF-8 byte limit per prompt + input, positive integer
outputTokensMaximum output per call, positive integer
totalTokensConservative whole-task input byte estimate + output reservation plus the difference of actual usage, positive integer
ttlMillisTask deadline counted from acceptance, positive integer
modelTimeoutMillisDeadline of a single model call, positive integer and no greater than the task deadline
reactIterationsModel decision rounds of a single ReAct loop, including the final summary, non-negative integer; 0 disables it, a missing / invalid value falls back to 3

The budget is decided by the module default policy or the administrator configuration, and arbitrary hard caps are no longer added on top of the budget values. Configuration validates effective values at the Codec boundary, and the engine and platform startup do not repeatedly walk the same fields; permissions, duplicate IDs, business references, payment state and the execution budget are still checked at their respective boundaries. Configuration fields still use lenientOptionalFieldOf, and wrong fields fall back to defaults.

First-batch fast modules usually make 0..2 model calls within 14.5 seconds, the advancement module defaults to 5 minutes and its deterministic path makes no model call. A custom module can make more bounded decisions in its own execution logic without changing the fast-module policy. For configuration examples see examples/agent.

The current implementation uses fixed bounded thread pools: 4 interactive workers/32 waiting slots, 2 event workers/64 slots and 2 maintenance workers/32 slots; one active interactive task per player. Model concurrency is additionally limited by a server-wide semaphore, and queue capacity and worker counts are not configurable yet. The platform main-thread queue attempts at most 16 items per tick with a 2 ms scheduling budget, and a single API call cannot be preempted; large queries still need monitoring and further slicing.

Persistence and recovery​

Self-managed files inside the world save are uniformly located in <world save>/system/: players/<UUID>.dat stores business state, chat/<UUID>.dat stores conversations and the request replay cache, and database/shared.dat and database/players/<UUID>.dat store the shared and per-player standalone databases. All data files are read and written as GZIP-compressed NBT through FileAccess.NBT_COMPRESSED and a Codec; store.lock is the process file lock. Vanilla SavedData is still managed by Minecraft and stays in data/system/.

Reading is recognized automatically by Saturn for GZIP / uncompressed and for the standard / legacy header without a root name; new writes are always GZIP with a standard NBT header. Existing uncompressed files inside system/ need no migration, they switch to the compressed format on the next normal save, and world saves are not rewritten in bulk.

Different worlds use their own directories, and the prototype only supports a single-process local file system: AtomicFiles uniformly writes <file name>.tmp in the same directory, calls force(true) and then replaces atomically. When the file system does not support an atomic replace, the write is refused instead of degrading to an unsafe overwrite. The byte limits of business files and the replay cache, the entry-count and byte limits of the two log tables, and the number of retained conversations and days all come from config/system/system-storage.json, with defaults of 4 MiB for business files, 32 MiB for the cache and 768 KiB (step receipts) / 256 KiB (event tasks) for the logs; the file limit is calculated from the actual compressed bytes, and the log budget is accumulated from the GZIP NBT encoded size of each record and clamped inside the business file budget so that the logs cannot overflow the business file themselves. Limits are read in real time on every read, write and reclamation, and changes take effect on resource reload (/reload). When the cache exceeds its limit, the earliest rounds are evicted; if it still does not fit, this cache write is skipped and logged without affecting business transactions. There is no SQLite or multi-server shared writing.

This round enables the new directories and format directly and does not migrate, read or delete the old system-agent/, system-database/. Bindings, business state, chats and the knowledge base in the old directories do not automatically appear in the new ones; the accumulated usage and the administrator AI permissions inside data/system/ keep the existing records. The client settings in config/system/, the private speech usage and the audio cache are not part of the world save, and this round does not change their location or format.

Player files are read as NBT binary, and the file handle is closed before decoding, so an unreleased read handle on Windows cannot block a later atomic replace. Read, write, delete and directory enumeration failures log the operation, the path and the exception reason on the server, and keep the error chain through an AgentException(AgentFailure.STORAGE_UNAVAILABLE, cause) that carries the cause; the exception saves a Saturn Failure, and the client receives only its translation key and displayable text, never an internal path or stack trace. For the full failure propagation convention see Agent runtime structure.

CardUpdate dispatches by mode; SHOW / REPLACE require data, and REMOVE reads only the reference, so no invalid partial object is constructed when the payload is missing. When a player file is read, replay results and assistant history cards that cannot be parsed are dropped automatically, while valid results and user messages are kept; the cleaned content is written with the next normal save, so there is no need to delete the player file by hand. The recovery log records only the number of dropped entries and the necessary file location, not the cache body. NBT format errors, business Codec constructor exceptions and encoding exceptions are all classified as storage errors; the original file is kept, no empty player file is created over it, and existing business error codes such as STORAGE_FULL are preserved.

Long-term AI usage uses server-global SavedData stored in <world save>/data/system/token_usage.dat; it does not enter the business files or player attachments above and is not cleared by revoke. All AI requests go through ModelGateway and are accumulated from the API's actual usage. TTS / ASR requests are made by the client with private credentials, usage is stored only on the local machine in config/system/speech-usage/<UUID>.json per client account UUID, and cost is read from the client's SpeechPricingConfig. The server has no speech reporting message, storage field or snapshot field; the administrator's server-wide view covers AI only, the personal speech panel accumulates across servers, and the protocol stays at 1. See Usage statistics for details.

Business files contain the daily sign-in, the inbox, business operation receipts, event tasks, advancement state and module overrides, each belonging to its own DataStorage; conversations and request replay are stored separately under chat/, so clearing a conversation does not affect business unique markers. Configuration does not enter player files; keys always come from server environment variables. Protect the world save, temporary files and backups with host permissions.

Persistent references to data pack entries uniformly use PersistHolder<T>(id, value) (util): the codec flattens the fields of value next to id, and neither the data pack JSON nor the old world save shape changes; orders, inbox deliveries, sign-in history, receipts and reward cards save the id and value as of writing, so a later change to or deletion of the data pack never rewrites what has already been persisted. Runtime registry reads use Holder<T> (CommerceCatalog), and only persistence and cross-thread passing convert to PersistHolder.

The top-level RecordCodecBuilder of PlayerData saves only 6 items: the version, bindings and preferences, the DataStorageHolder<Identifier> and ModelQuota; Conversation is written by a separate chat file in the same transaction, and copy() still copies both to keep the read snapshot consistent. Sign-in, inbox, event/operation receipts, shop, advancement state and module overrides each implement Saturn DataStorage and are declared by ServerModule.storages; built-in Codecs are registered by SystemDataStorages, extension Codecs are registered separately to Saturn DATA_STORAGE_TYPE and dispatched by DataStorageHolder.codec(Identifier.CODEC); an extension module can use its own DataStorage instead of growing the shared player record. The access entry point is ModuleData.get(data, SomeSystemModule.STORAGE), which calls get / set by the Holder key and type underneath, separate from the DataStorage Codec type ID; copying must isolate mutable collections and the mutable records inside them. Player files are still written transactionally as a whole by AgentStore.update and are not player attachments. Conversation history and request replay are managed by Conversation, Conversation.clear() clears them uniformly and advances the memory generation; internal deliveries, events, step receipts and orders use RecordCodecBuilder.

Advancement rules and event saves store the complete action in the action field; the action dispatches by moduleId internally and flattens its parameters, so it cannot clash with the rule's advancementId or the event task's ruleId; event authorization uses only the ruleId saved in the task itself. Results, speech statistics and ASR statistics are read with the current Codec structure, without guessing or falling back to fields from old records.

Binding and revocation​

PlayerData.systems defaults to SystemBindings.EMPTY, and bound() is derived from the set instead of storing a separate boolean. The set uses an immutable copy as a structure reserved for multiple systems later, and the current Codec and granting entry points allow at most one ID. An administrator grants with /system grant <system ID> [player] or /system grant <system ID> for themselves; the type is completed from the world save's data pack registry and checked in the service layer, then AssistantBindings.grant(UUID, systemId, initialize) is called. Whether a binding already exists is checked inside the transaction lock: the same ID returns already bound, a different ID is rejected; a binding HUD message is sent only after a new binding has been persisted, and no interface is opened automatically and no records are cleared. Unbound interactive tasks and event tasks are rejected before business state is read or created. The MC adapter establishes the completed advancement baseline at binding time and does not issue rewards earned before the binding.

ModuleAccess obtains the binding ID in the login transaction, confirms on the main thread that the definition still exists, and then evaluates the module conditions with the definition selected by that player; ModuleManager.system(uuid) exposes the current ready system, and the optional ID parameter of has_system matches against it as well. ClientState syncs the current system ID, and the client reads the corresponding translation key and description from the synced data pack registry; a server-wide fixed default definition is not used. A removed definition is not implicitly rebound, and the player can act again after the definition is restored or after an explicit revoke. The business ledger is still saved per player, and independent ledgers and simultaneous operation for multiple systems are not enabled yet.

AssistantBindings.clear(UUID, reset) switches the player runtime generation, cancels interactions and disables event rules through engine.invalidatePlayer first; the maintenance thread runs the AgentLifecycle.revoked hook, and the binding service then deletes the player file and the conversation cache. The trade service clears its own quotes, quest narration cancels in-flight requests, the database service deletes the player's standalone database, and the FTB adapter clears claim references. A successful reply closes the system interface and clears the client session; a failure reports the real error.

Business tasks and platform tools must use context.store(); platform asynchronous work may obtain engine.playerStore(UUID) before submission. This restricted view checks the runtime generation at submission time inside the real transaction lock; tool callbacks, event receipts and conversation results from before a revoke cannot rebuild deleted data. context.check() also rejects invalidated generations, so queued game callbacks after a revoke do not run either. engine.store() is the raw administrative interface of the trusted platform and should not be used to bypass the view in asynchronous business callbacks; third-party Java extensions are still not a sandbox.

The server also replaces that player's reply generation and discards old interactive replies; event notifications and login initialization check the runtime generation separately, and a repeated initialization uses an independent marker. When deletion fails, the original running tasks are still cancelled and the client stops waiting on old requests, but no claim of successful cleanup is made. A revoke does not roll back MC inventory changes, FTB progress or external charges that have already happened; an in-flight external payment may also finish after the local revoke, so an administrator should confirm before using this test reset operation.

grant/revoke/reset [player] is allowed only with COMMANDS_GAMEMASTER; omitting the target uses the executor, and an explicit target must be a single online player. Network packets and the model cannot call these administrative operations. reset deletes the player file directly like revoke and does not attempt to migrate an unparseable old format; it additionally shows an old-format hint, and a new grant is required afterwards. The player data format stays at 1, and new boolean fields are filled in with their default values.

An interactive task does not resume inference automatically after a crash; requesting it again validates the business receipts. An event task saves the input, module version, policy, accumulated budget and the stable receipt of every write step; after relogging it resumes QUEUED/RUNNING/WAITING without refreshing the original deadline. Deleting or disabling a rule does not authorize its old tasks. There is currently no generic dynamic step graph, lease-based multi-worker coordination, automatic compensation or independent notification outbox; recovery is implemented through module entry points and write step receipts.

The advancement listener uses AdvancementEvent.AdvancementEarnEvent, confirmed by reading the current dependency source. It fires only once per player/rule/advancement; when the baseline is first established, already earned advancements are not rewarded retroactively. Later logins check for gaps against the current completion state and cannot reconstruct revoked or repeated history. Events can still be missed between the game event and file persistence, so strict exactly-once event capture is not promised.

The default advancement module can set one persistent featureId marker and record it as a fixed reward entitlement; featureId is currently only an unlock marker that extension modules can query, with no old page or data pack unlock mechanism. When real game behavior is needed, register the corresponding business tool instead of exposing arbitrary console commands.

Shop and catalog extension​

data/catalog/CommerceCatalog builds an immutable business snapshot from the three catalog registries of the current server RegistryAccess; the default content is also placed inside the JAR under data/system/system/signin_rewards/, shop_offers/ and lottery_pools/, with unchanged paths. CatalogReloadListener, catalog file scanning and per-tick publishing logic have been removed. Vanilla handles resource priority, entry IDs, Codec loading and tags, and changing an entry requires reopening the world / restarting the server; the catalog version is generated per server instance and no cross-world static data is used. Prompts still use a retained reload listener to read txt files.

Sign-in, shop and lottery implement CatalogModule and declare the corresponding catalog registries and optional ResourceKey / TagKey lists. ModuleManager registers the configuration into CommerceCatalogs, and catalogs.available(player) merges only the entries allowed by the definitions enabled for that player; configurations of the same kind are unioned, the catalog order is stable and has no duplicates; omitting a field allows the whole table, and an empty array means empty. The three built-in modules explicitly reference their own #system:default. All lists, sign-in candidates and submission, quotes and charging read this restricted view, and the inbox state uses the same entry point; advancement events explicitly read the raw catalogs. A native tag reload needs no rebuild of the business content snapshot, and later queries read the current members; before confirmation and payment the quote source is also checked for availability. Configurations of the same kind share the original corresponding trade ledger, and the same static ModuleStorage may be assembled repeatedly without registering the storage type twice. For the format see Extended configuration.

Reward and offer IDs use Identifier and are parsed with Identifier.CODEC; sign-in selection, quote requests and advancement reward references keep that type and convert only at text boundaries such as search or conversation summaries. Offers and lottery pools store List<Cost> directly and dispatch by registered type through Saturn Cost.LIST_CODEC; the catalog obtains RegistryOps from the native registry loading flow, which supports dynamic registry references for resource Cost. CommerceCatalog.Price stores only the evaluated item icon and text component for the client and the order snapshot, and contains no executable cost.

system:shop browses by page, system:shop_quote generates a preview and system:shop_purchase handles the separate confirmation, and the latter refuses to be entered directly from model routing. Payment content comes only from the server-issued CommerceQuotes, which bind the player, the operation type and the catalog version; orders are persisted by quote ID, and clearing a conversation does not delete orders. ShopPurchaseTool and LotteryDrawTool each handle exactly one confirmation behavior, reserve a CommerceOrder and the locked inbox entitlement through the same CommerceTransactions, then enter the restricted main-thread queue to validate the template and deduct materials, and finally complete the order and unlock the entitlement in the same player transaction. A failed charge and an unknown result are recorded separately; an unknown order is not retried automatically and the entitlement stays locked. Purchase.order saves the operation type, the source, the price and a complete reward snapshot; there is no compatibility logic to migrate the old Purchase.offer, and if a development save cannot be parsed, use /system reset [player] and grant again.

system:lottery browses pools, system:lottery_preview pages through the prize weights and issues a confirmation, and system:lottery_draw accepts only a structured confirmation. Browsing and preview declare commands and a semantic profile; the actual draw does not enter the model capability catalog. LotteryPool selects one reward with the JDK ThreadLocalRandom.nextLong(totalWeight), and the result is stored in the order before payment; the item templates of the whole pool are created and validated before charging, so a wrong prize cannot change the probability of a successful draw. Entitlements that are pending payment or have an unknown payment are not shown as inbox rewards and only the order state is returned; repeating the confirmation after completion returns the original receipt.

Pool browsing sends only one LotteryCatalogOutput card: cells carry the pool name and the public templates of at most the 2 rarest prizes, and clicking a cell expands that pool's system:lottery_preview (reusing the server button pool.<index>, whose parameters include the catalog version), while quotes are still evaluated only when the pool is actually opened. The detail card LotteryPoolOutput.prizes carries at most 64 public reward templates, their weights and the complete total weight through the Codec, and the shelf and tooltip are generated from the same data; the lottery_prize card type is kept for old history and is no longer sent per prize. A preview does not generate a draw result, charge or call the model. Single-item costs are rendered uniformly by AssistantItems, and free, resource and other Costs use text; item cards of sign-in candidates keep the name of the reward pack they belong to.

The shop / lottery modules share the read-only CommercePreviewTool, and CommerceTransactions.preview obtains the trusted cost by catalog ID on the game thread and generates CommerceCosts.Prepared. A Cost is priced only once, and the short-lived quote keeps a Saturn CostPayment bound to the player entity, the payment target and the amount; the confirmation reuses that bill, and a bill is rejected when the entity changes because of a disconnect / respawn. CostContext includes origin=system:shop / system:lottery and system:source, which extension Costs can read. System no longer keeps a separate system currency interface or a custom item charger; currency can be expressed by a Saturn resource or an extension Cost.

Prepared expresses a pricing rejection with Optional<Failure> (empty means success), and the display price is held as a separate field — when pricing fails the preview still has to show the price and the failure explanation, so the price cannot be packed into "success or failure". Externally, getData() reads the display price, isSuccess() tells whether a quote can be generated, and failure() reads the rejection reason; the resource name is provided by Failure.resource() (the constructor attaches the failed resource with withResource). The failure explanation is added before the immutable price snapshot is created and formatted by Failure.message(failedResource); the bill, the paying player and the charge tracking stay on the server and do not enter the client snapshot. available in the client DTO is only a projection of that success state.

All payments are executed through Saturn's test() / pay(). The current dependency continues with other costs after a real charge error and may return success, so CommerceCosts wraps the binding transaction and additionally tracks the failure and exception of every real charge; once a partial / uncertain charge occurs, the order and the entitlement stay PAYMENT_UNKNOWN, no reward is issued and no automatic retry happens. Saturn does not uniformly reserve duplicate / overlapping costs and does not guarantee rollback across costs, so data packs should merge the same requirement. An extension Cost must follow the read-only contract of prepare and execute(true).

Orders and inbox entries are each limited to 4096 entries and the files to 4 MiB in total; there is no archiving tool for historical orders. Material matching and deduction are executed by Saturn Cost, and a plain item ID may consume a similar item with custom components, within the player's Inventory. An order saves only the display price and the complete reward snapshot, and does not serialize the bound CostPayment or charge automatically after recovery. On a crash the MC world save and external files cannot be committed together, so a manual reconciliation window remains; old price saves are not migrated, and /system reset [player] should be used to clean development data if necessary.

FTB adapter and boundaries​

The FTB safety module definition is always registered, and QuestSystemModule.available() checks ModList.get().isLoaded("ftbquests") internally; ModuleManager, command assembly and the storage enumeration filter unavailable definitions first, and FtbQuestAdapter is loaded reflectively only when the dependency exists. The registration of the common DTOs and storage Codecs does not change with the dependency and keeps the types needed to parse data packs and world saves. The adapter calls the Java API directly, reflection is used only to isolate loading, and business never goes through MCP transport. It checks ServerQuestFile.exists() first and then accesses the non-nullable getInstance().

Read tools each return a fixed type, and the native FTB Quest and TeamData are used only on the game thread; externally they return serializable data snapshots:

ToolParametersFixed return type
ftb.queryQuestQueryInput: keyword, whether to show only claimableList<QuestBriefOutput>
ftb.findQuestFindInput: quest name or ID, exact match preferredList<QuestBriefOutput>
ftb.detailQuestDetailInput: a known ID or the number from the last list, detail page offsetQuestOutput
ftb.starter_giftsEmptyInputStarterGiftOutput

QuestDetailModule passes a name input to ftb.find first; no match or several matches show the candidate list, and a single match reads the detail by ID. A unique match opens the detail directly without replacing the numbered list the player was looking at; an ID/number query that does not exist or is not viewable returns a business error without switching the result type. The starterGift marker of a detail is classified by the compatibility layer, and when the Module encounters a gift it calls the shared ftb.starter_gifts tool to show all claimable starter gifts, instead of having the detail Tool return gift objects. These read-only combinations call local tools at most 3 times and add no model call; the starter gift tool instance is shared with the standalone starter gift query module.

Queries include quests that are completed but still have claimable rewards. A quest list returns all matching briefs at once and numbers them consecutively; queries and name candidates no longer carry a list page offset; the detail keeps 4 rewards per page and 8 options per choice reward; more rewards and options are read on through separate buttons. The claim reference is a random short-lived token binding the player, team, quest/reward object, completion time and choice table version, valid for 2 minutes; a restart, a team change or a change invalidates it and requires a new query. A read receipt is not considered successful on the strength of model text. In 26.1.2 a normal reward uses the native TeamData.claimReward; the choice path validates the index and, following the target version's ClaimChoiceRewardMessage, first successfully marks the outer reward and then calls claim on the candidate reward. In this version Reward.claim returns void and there is no tryClaim, so the boolean success check of 1.21.1 cannot be reused; the native claimed state is checked at the end, and no independent guarantee is made for the actual delivery of a third-party reward.

The quest list uses a height-limited scrolling area in the full interface, and notifications page through local controls with 10 entries per page, keeping the original fixed width and height limits; no server-side list paging action is created. S2C replies use the trusted Codec and NeoForge's large packet splitting, so they are no longer limited by the old 60000-character reply interception; C2S still uses restricted decoding. When the player file is low on capacity, whole rounds of conversation and replay cache are evicted first, and business records do not take part in eviction.

FtbQuestAdapter.install also installs the process-level listener of FtbQuestNotifications, still loaded only after the optional dependency is detected; its server snapshot is isolated by MinecraftServer and removed on shutdown. 26.1.2 uses FTBQuestsEvent.QuestProgress / TaskProgress / ChapterProgress / FileProgress on the NeoForge bus and identifies completion with ProgressType.COMPLETED; FTBQuestsEvent.ClearFileCache rebuilds the baseline and invalidates the guide snapshot, isolated per server by the ServerQuestFile in the event.

FTB has no dedicated unlock event. The set of visible quest IDs for which canStartTasks holds is saved per team; on a progress event the data is marked dirty and compared in a merged way at the end of the server tick; completion notices are sent first in event order; when there is exactly one new, unfinished normal quest, a QuestOutput detail is sent directly and the narration uses the unlocked event; with several quests they are merged into one QuestUnlockedOutput list notification in chapter/quest order, at most one per recipient per batch, and starter gifts still use their own summary first. The unlock list reuses QuestList's view-by-quest-ID button and the quest_list template, and hides the numbering and the active query hint with unlock_notice, without touching the recent query cache; the summary and the narration only state the count. Notifications use the capacity of the generic grid's columns × max_row, and local paging sends no request; after clicking a detail the original entry point is queried, PUSH keeps the source view and CardUiState, returning restores the original page, and the confirm button at the bottom ends the whole window. Completion notifications are still sent separately for each quest. IDs that are completed but still advanceable are kept as the baseline, so an ordinary reset cannot directly pose as a new unlock; a scan every 20 ticks covers lock, reset and repeatable-quest cooldown changes that produce no progress event. Lock switches, team changes and quest file cache clear reloads only rebuild the baseline and do not narrate existing quests in bulk. Only a successful new binding marks a pending guidance once in AssistantPlayers; the binding message is sent first, and once the login baseline is ready and the team is available the FTB tick consumes the marker and sends the claimable starter gift and the normal quest overview (the total count and the first three) in order; when there is no content it is skipped. The overview generates no list numbers, and only clicking "View quests" runs the full list query again. An ordinary login and a repeated grant create no marker; logout, revoke or reset clears an unsent marker, and a file reload does not repeat the guidance; a new binding after a revoke can trigger it again. Progress batches for which FTB explicitly disables notifications update the baseline silently, and hidden chapters and invisible quests are not sent.

FtbQuestText uniformly reads FTB translations by ServerPlayer.clientInformation().language() and uses the native missing-translation fallback; it does not change the global file locale or cache. The list, name search, detail, submission, notifications and narration all share this entry point, and a quest without a title falls back to the localized title of its first objective. JSON Components are decoded with the vanilla server-safe ComponentSerialization.CODEC and the registry JsonOps, a parse failure keeps the original literal, § / & are left to LegacyText, and the background organization input has format codes removed; hidden descriptions are still handled according to the FTB completion conditions. For the root cause, the real modpack samples and the 26.1.2 sync record see Cross-version fix record.

FtbQuestReader is the standalone read-only quest entry point; it is created from the current ServerQuestFile and TeamData and used on the server thread without caching progress. stream(Predicate<Quest>) unifies chapter traversal, visibility and intro-quest filtering, and other types can extend it with composed predicates; queries, details, narration, starter gifts and notifications share the read decision here. A quest with no tasks, or of which all tasks are native CheckmarkTask with no rewards, counts as an intro quest: the system does not read it, does not modify its progress and does not bypass it when it is a dependency. When there is at least one native checkmark objective, all objectives are checkmarks, none is repeatable and at least one has a non-empty native item reward, native experience / level rewards may be attached, and all rewards must be non-hidden for the quest to be recognized as a starter gift. Exact class matching is used; only experience, mixed objectives, command / choice / random / third-party rewards and repeatable quests keep normal handling, and nothing is guessed from quest names.

The read views are active(player) (visible, unlocked, advanceable, unfinished, excluding team locks and player pauses), unfinishedStarterGifts() (all visible but unfinished gifts, including those not yet unlocked), availableStarterGifts(player) (gifts claimable now, including completed but not yet claimed ones), claimable(player) (quests with claimable rewards), manualSubmission(player) (quests currently executable and containing manual objectives) and submittable(player) (with an unfinished, supported direct-submit objective whose order allows it). QuestQueryInput.view uses the ACTIVE / CLAIMABLE / SUBMITTABLE enum and connects the three public query views respectively, and the schema and the model capability description include that option; the /submittable [query] command and the quest page button query directly without calling AI. find(player, name) prefers an exact match of the title in the player's language or the hexadecimal ID, and the full list snapshot and the player numbering are still managed by FtbQuestAccess. Notifications use notificationBaseline() separately to keep completed and advanceable quests, so a reset is not treated as a new unlock.

FTB has no generic server-side "requires manual submission" field; requiresManualSubmission(quest) runs anyMatch over all objectives, and any native manual objective puts the whole quest into manual submission, including optional objectives or manual objectives that are already completed. The actual configuration distinguishes item consumption, quest-interface-only items, checkmarks, experience and custom quests that only enable a button and do not check automatically; unsupported resource objectives are conservatively left to FTB-specific input. Type detection does not load TaskClient, and objectives such as kills or observation that simply have no automatic tick check are not mistaken for manual tasks. An explicit submission only walks the native objectives in submittableTasks(quest) that support a direct call: item consumption, checkmarks, experience and manual custom buttons; quest-interface-only, fluid, energy and unknown resource objectives are still operated in FTB, and queries and notifications never trigger submission.

canComplete(player, team, quest) checks whether all remaining objectives are native CheckmarkTask; canSubmit checks the executable state and at least one unfinished, order-allowed objective that supports a direct call. submissionAction returns the common enum QuestSubmission.NONE / CHECKMARK / SUBMIT, and both the detail and the notification save that snapshot; the enum uniformly builds the "Complete this quest" or "Submit quest" button. A query does not simulate submission, does not pre-deduct resources and does not promise that materials are sufficient or that a custom check succeeds; the native interface decides partial submission and the actual progress. The checkmark action carries QuestSubmitInput.checkmarkOnly=true, and the real execution re-checks for only checkmark objectives, so an old button cannot submit a newly added resource objective; a normal submission re-checks the completion and order condition of every objective in the loop. Dependencies, pauses, repeatable quest generations and completion notification merging still follow the original chain. The button does not execute automatically and does not change the intro quest and starter gift classification. The current network protocol is 1, the quest list has no page offset, and the Tool returns the complete brief List directly, which the Module then wraps as a QuestList card.

The starter gift detail uses StarterGiftOutput.gifts to summarize all currently claimable matching quests; each group syncs only the internal quest ID, the claim reference and an optional error, and the overall items / icons save the summed items and experience / level preview, and the interface and narration show neither quest names nor chapters. StarterGiftQueryModule and the read-only Tool refresh the complete list, and the claim module accepts only the StarterGiftInput.claimRefs carried by trusted buttons and does not take part in free model routing. A starter gift claim reference binds the player, team, original quest/reward object and completion time, and is kept within the same connection for the chat history; it is cleared on logout, revoke or when the quest module stops. Normal quest rewards still use a two-minute reference. An explicit batch claim first validates the ownership of the references, then checks unlock, pause, visibility and reward blocking state group by group, executes the native checkmark submission inside withPlayerContext, and claims the original rewards through TeamData.claimReward; no items or experience are consumed. Successful items are removed from the list, and failed items keep the error and the available actions; the reply only updates the state and the original starter gift list silently and creates no success notification, conversation record or narration, and after everything is claimed the next notification continues. Quest order, automatic claiming and team reward semantics follow FTB; no sign-in/inbox entitlement is created and the normal completion message of a starter gift quest is not pushed again.

Normal quests finished by an explicit submission and the native FTB completion event are merged into the same batch completion items for the same team and quest ID; the submission reply only ends the request and no longer appends a "submitted" or "progress updated" notification. Only when the quest is unfinished does it return whether the actual progress changed. AgentResult.silent() marks both kinds of silent replies uniformly; the client updates the request and the starter gift state and history gains no blank result; write steps and request replay are still kept. With the new starter gift list input and the richer quest notification payload, the network protocol of the unreleased version is uniformly 1, and client and server must be updated together.

Notification recipients are online team members who have completed system binding initialization. AssistantPlayers.notificationsEnabled reuses the completed server-side login baseline as the binding cache and does not read player files during a tick; before sending, it re-checks the original player instance, the connection generation, the binding runtime generation and the current team. Members who just joined or were just bound do not receive unlock differences from before they joined, and offline teams keep no backlog. A single team keeps at most 256 different completion items in the same tick, and the client still uses the length-limited HUD queue of 32 waiting slots; bulk administrative operations do not promise unlimited notification retention. Messages enter the queue directly as an event reply through notifyEvent, with no model request, Agent write task or new reward side effect; the detail button still goes through the existing authorized query. Notifications have no independent persistent outbox and do not replay history from downtime or offline periods. FTB native toasts are not intercepted.

A completion notification builds its actions from the claimable rewards of the receiving player at send time and shares FtbQuestAccess.reference with the detail. A single normal reward can reuse quest_claim directly, several rewards or a choice reward go through quest_detail first for selection; with no claimable reward only the detail is kept. The arrival of the event itself does not issue a reward, and claiming still validates the reference deadline, the player/team and the object version. When the optional module is installed, the shared access object is passed to the notification bridge per server and cleaned up on shutdown.

Non-item objectives and rewards share FtbQuestPreviews, which syncs only the type ID, the text, the amount / progress and the hint lines of QuestIconOutput; quest detail / submission results, notifications, starter gifts and choices use the same system:icons node. The client obtains the native icon provider from the FTB type table and does not call the Icon API on the server; the adapter loads it reflectively only when the dependency is installed and on first render. Native tooltips are shown in the existing item tooltip pass, and drawing follows AUI pose, clip and alpha compositing. See Card icons and the 26.1.2 sync record.

Item parameters are filled into the common CardContext.Builder by CardData.buildCard(), the JSON items node selects the source, group title and description, and the client AssistantItems then draws them; cards, the current reply and notifications use the same AUI Item component and shared CSS. Item data comes from decoded business objects, without scanning JSON or model text; new types extend it in their own buildCard(). ItemPreviewOutput uses RecordCodecBuilder to save the item template and the amount text, and QuestChoiceOutput saves the candidate name and preview; only the isolated FTB adapter accesses ItemReward, and the core payload and the client do not reference optional mod classes. FTB uses a template with amount 1 to keep components and then shows the real configured amount or random range with getButtonText(), without constructing items outside the template range or modifying the original FTB stack. The client creates the actual stack at render time; the vanilla tooltip receives that stack, keeping item tooltip components and mod events. See Interface and localization for details.

The client AssistantRecognition.Session receives the validity check of its owning UI through a BooleanSupplier, so the input interface and HUD notifications share recording, networking and metering; the final callback also checks the original connection. AssistantRecognitionKeys gives push-to-talk to the currently visible notification first and then to the main input interface. A notification separates the button localization key from the callback and caches fixed example sentence vectors with the shared LexicalIndex; when nothing matches it enters the main chat routing. For the resource pack definitions, matching thresholds and lifecycle see Speech recognition and Lightweight routing.

FTB local guide index​

QuestGuideModule provides the system:quest_guide action, the /guide <query> command and one read-only ftb.guide Tool. FtbQuestKnowledge caches the static text of each language on the server thread for quests that use checkmarks / have no objectives and are not starter gifts, and FtbQuestGuideTool computes only the authorized IDs each time and re-checks the source text before returning. AgentDatabase stores the source text in the system:ftb_guides table of the shared database, and KnowledgeIndex reuses LexicalIndex to build an in-memory sparse TF-IDF; permissions are not written into the database. The native FTB cache clear event or /system ftb reload invalidates the snapshot, and the manual command rebuilds the index in the background.

AssistantServer uniformly owns AgentDatabases: <world save>/system/database/shared.dat is the shared database, and players/<UUID>.dat is a standalone database created on demand, both using GZIP-compressed NBT. Both use the same namespaced DatabaseTable and Codec API, and quest backgrounds no longer write separate files. Reads, writes and index building run in the background, queries sort with an immutable index; revoke / reset deletes the private database, and shutting down the server releases all handles. Player transactions and accumulated usage keep their own ownership; for the detailed boundaries see Layered local database.

Before returning, the connection / execution context, quest file, team, language, visibility and current text are checked again; intro is read only in the knowledge view and does not change the active / gift / completed behavior. QuestGuideOutput shows the passages and the source text directly, and the quest page and the guide command do not call embedding / LLM. FtbQuestAdapter exports the same ftb.guide Tool through ModuleContext for the core QuestionModule to reuse after authorization; ordinary knowledge Q&A may use the model to organize the passages and then query once more to verify. Neither entry point submits quests automatically. For the methods, limits and file description see Local guide retrieval.

Current semantic enhancements and remaining limitations​

Local routing has been changed to pure Java lexical matching, and the default for a new configuration is ENFORCE; OFF in an existing configuration is kept. It uses a small set of domain synonyms, word and adjacency features, TF-IDF cosine similarity and coverage, with no model runtime, external dictionary or vector service. Low scores, a small score gap, missing arguments, unfamiliar topics, complex phrasing or unavailability call the configured model service. The explicit profile declaration, the module tool declarations and the execution layer together restrict read-only direct routing; RouteSource distinguishes structured, command, local match, exact cache, LLM and event, and automatic routing does not bypass the shop confirmation. See Lightweight local intent routing and Rationale and future plans for details.

For successful executions, some read-only LLM intents can be cached exactly per player for a short time; only the module ID is stored, the current arguments are still parsed again and dynamic data is queried every time. A JDK bounded LRU/TTL replaces Caffeine, and shutdown saves without reading or writing this cache. The language is read from Minecraft's player.clientInformation().language(), and requestContext.playerLanguage is injected for every model call once the execution context is frozen. Clearing memory, preference changes, revoke and disconnect clean the corresponding cache scope, and old tasks must not backfill it. The old ONNX fields and thresholds are no longer read, and a model directory or JVM native arguments are no longer needed.

The local index is built only at startup from trusted profiles and the example sentences added by the server, and queries run on the existing interactive worker threads without adding a native thread pool or network request. Restart normally after changing modules, vocabularies, example sentences or the index language; the mode, matching thresholds, cache capacity and new-entry TTL read the configuration instance directly. The full size check covers System's embedded resources and libraries; Minecraft, NeoForge and the existing UI dependencies are counted separately.

The original design draft is a goal, and not every chapter should be treated as already implemented. The following are not included yet: complete recipe / modpack processing chain planning, querying arbitrary private material of others, multiple assistants / multiple sessions, model summaries, per-module configurable fair weights / retries, model provider circuit breaking, event hot reload, database migration, strict exactly-once inventory delivery, a complete observability backend and MCP endpoints. The generic bounded read-only tool loop is implemented; see ReAct loop.

The verification for this round is limited to compilation, packaging and static checks; no model service was actually called, no client/server was started and the in-game layout / claiming was not verified.

Knowledge Q&A and web search entry points​

  • module/builtin/CoreSystemModule: assembles the core QuestionModule and the cross-capability read-only ReasoningModule; action IDs system:question, system:reasoning.
  • module/builtin/knowledge: DatabaseQueryTool aggregates the current player / shared knowledge and the authorized guides; QuestionModule always runs the database query, organizes the hits and judges authorization on an empty result, and does not let the model skip the database query.
  • module/builtin/SearchSystemModule: optional ToolModule, exports only two tools; type ID system:web_search. The adjacent SearchSystemClientModule contributes the search page and submits QuestionInput.online.
  • agent/react, agent/tool/ToolContext: the generic bounded loop, independent observation state and tool permission snapshot; shared by ordinary multi-step analysis and search, without resetting the total request budget.
  • module/builtin/search: WebResearch provides the search tools and the citation policy and reuses the generic ReActLoop; SearchObservation lives in data/search, the old SearchStep has been removed; WebSearchTool / WebFetchTool check the network gate of this request every time.
  • search: SearchProvider, DeepSeekSearchClient and the unified player-metered SearchGateway; reuses HttpTransport.
  • config/file/SearchConfig, SearchPricingConfig: share SYSTEM_DEEPSEEK_API_KEY by default and manage the connection and the per-thousand-call price respectively.
  • data/usage/SearchUsage: immutable counters, merged into the existing world SavedData and the StreamCodec snapshot; the client speech ledger is not uploaded.

system:question defaults to 6 model calls, 9 tool calls, 20 steps and 90 seconds, with at most 20 seconds per model call; database queries / internal FTB queries and web steps share the budget. Descriptor.optionalTools declares optional read-only dependencies, and not enabling the search module does not disable local Q&A. WebResearch is entered only when the database is empty, the current QuestionInput.allowWeb explicitly allows it and the search tool is enabled; with results or a failed database query it does not search. Q&A semantic samples are only hints and do not provide local direct routing or cached authorization; when the web already has evidence, the final summary is reserved and paid tools are not retried automatically. See Search configuration and flow for details.

Administrator AI permissions are stored by the AiAccessData SavedData of the current world save, overriding the default level of that world save per UUID, and do not rewrite the player's external AI / free chat / history preferences. For the new flow, data and interfaces see Free chat.