Redouble AI Nucleo documentation
Part 13 · Inside the runtime

Inside artifacts

ai.redouble.nucleo.harness.artifacts · nucleo-core

This page is for people working on the runtime itself: how artifact references are kept canonical, how the parent's artifacts are resolved, the registry's two tiers, how artifact types are recovered from references, and the text-rendering classes. The guide for using artifacts is Artifacts: data the model cannot alter.


Resolution for the parent

The parent doesn't parse the DOI from text. It gets the CitationArtifact object with exact data from the registry - the thinker (SingleObjectiveThinker.resolveArtifacts in tools/thinking) resolves the refs through normalizeToKey() and populates the artifact set for the parent.

The one gate against a model-written artifact

The readers in this package rebuild an artifact from a payload, which is right for a payload a tool or another process wrote and wrong for one a model wrote. The two are told apart by where the payload is read, and a model's reply is read in one place: ResponseCorrection, which every model-calling job goes through (LLMCall for a thinker's turn, AbstractModelDependentTool for a one-call tool). After the parse and before required-field validation it calls ResponseHandler.heldArtifacts, whose default hands the reply to ArtifactRegistry.held with the conversation's registry.

held walks the reply the way the custody walk does (composite objects, lists, other collections, maps, arrays; never a JSON tree, never into an artifact) and replaces every Artifact it meets with get(ref), checking the held object against the declared type of the place where generics state one. It returns violations as text for the model, so they join the required-field errors: a correction turn within ResponseCorrection's budget, a ResponseValidationException past it. ThinkingResponseHandler overrides the hook because a turn's tool calls are answered one by one: a violation in a call's input becomes that call's parse error, which executeTools returns as the call's result, and the tool never runs.

The schema side has two renderers and one rule. PojoDefinition.modelWrittenFields() reduces an artifact's definition to its reference in the @-notation form, which is only ever read by a model. The JSON Schema form stays canonical, content included, for a caller that sends artifacts across a process boundary; NucleoSchemaKeywords.forModel reduces every node marked x-nucleo-type-alias to its reference on the path to a model, in the same step that strips the keywords. ArtifactAuthorshipTest pins both sides and the walk.

Canonical reference format

The canonical format for all artifact references is «artifact:type~uuid» (with guillemet delimiters). This single format is used everywhere - HashMap keys, registry display, @ref placeholders, artifactRefs lists, and tool inputs.

ArtifactRegistry.normalizeToKey() is the single normalization function that maps any variant to canonical format. All entry points (registry operations, response handling, tool inputs) normalize through this function, and ArtifactRegistry.refsMentionedIn() is the one authority for recognizing a ref inside prose.

A parsed response's refs are canonical by construction: ArtifactResponse's list setter is package-private (Jackson's door only - no outside caller can plant a variant spelling), the response handler runs canonicalizeArtifactRefs right after every parse (normalizing the declared refs and unioning the text mentions, deduplicated, declared order first), and programmatic additions go through addArtifactRef, which normalizes each ref.

Key classes

ComponentPurpose
AbstractArtifactBase class with artifactRef field and type extraction
LinkArtifactParent for artifacts with title + url (WebPageArtifact, CitationArtifact, PatentArtifact, etc.)
ListArtifactOrdered homogeneous list of artifacts (its iterands) - the bulk-data carrier, with lineage fields (derivedFromRef, workerToolName, instruction). Prompt-form is a bounded digest (artifact_ref, iterand_type, count, head sample), never the recursive per-iterand rendering; full form on plain write() for persistence
ArtifactRegistryStorage, reference generation, normalizeToKey() for canonical format. Registers against the Artifact interface (no AbstractArtifact requirement). Two tiers: top-level entries render and persist; the reachable tier (everything discovered by walking a registered artifact's object graph, plus explicit indexReachable() conveyances) is resolvable via get()/contains() and searchable via getAllArtifactsIncludingReachable(), but excluded from getAllArtifacts() so prompt rendering and persistence stay independent of data size
ArtifactResponseResponse base with artifactRefs field (canonical format) for LLM to populate
ArtifactRegistry.REF_FIELDThe serialized name of an artifact's ref (artifact_ref) - the one literal every reader that rebuilds an artifact from its payload keys on
ArtifactListDeserializer / ArtifactRefDeserializer / ArtifactMapDeserializerType recovery from the ref: the alias inside «artifact:alias~uuid» names the concrete class (TypeAliasRegistry.resolveAliasWithFallback climbs to the nearest known ancestor). The list and interface-field readers FAIL on an unresolvable alias - a silently shortened list or a silently null field is worse than a refused read; the snapshot-map reader SKIPS one with a warning, because restoring a conversation must survive an artifact class removed since the snapshot was written

PatentArtifact lives in nucleo-ext-patent.

The serialization that turns artifacts into refs and summarizes their large fields is the schema package's (NucleoJsonSerializer, @LLMSummarizable, the Summarizer strategies - Answers as Java objects and Inside the serializer). An artifact's summaries are cached on it (Artifact.cacheSummary(), SummarizedField) so they are computed once and reused across serializations.

Text rendering

ClassRole
ArtifactTextFormatterPer-type formatter contract: pure function, artifact in, canonical text out
TextFormatterRegistryClass-keyed registration, resolution up the class hierarchy, canonical-JSON fallback for unregistered types; find(Class) answers the nearest registered formatter or null, for a caller that renders differently when none is registered (a decision thinker's digest)
ListArtifactTextFormatterBuilt-in: count + iterand-type header, then every iterand numbered in list order through its own formatter; nested lists recurse
^