Deliver references to eligible runtime targets

This commit is contained in:
2026-07-05 16:38:17 +00:00
parent 4cafde2502
commit 22032dfd6d
7 changed files with 104 additions and 38 deletions

View File

@@ -60,10 +60,11 @@ Reference flags are resolved against selected chunk, extractor, and normalizer
targets before the run starts. Flat slot names are accepted only when exactly targets before the run starts. Flat slot names are accepted only when exactly
one selected target declares that slot. Bound reference files are read before one selected target declares that slot. Bound reference files are read before
pipeline work starts, validated as UTF-8 text, and recorded as provenance for pipeline work starts, validated as UTF-8 text, and recorded as provenance for
the target that declares the slot. Runtime reference content is currently passed the target that declares the slot. Runtime reference content is passed to the
only to lane extractors. Notarius infers reference media types from file chunker, extractor, or normalizer target that declares the slot. Notarius infers
extensions for provenance and for optional slot checks. Reference content is not reference media types from file extensions for provenance and for optional slot
written to diagnostics, logs, errors, or manifests. checks. Reference content is not written to diagnostics, logs, errors, or
manifests.
Reference binding precedence is: Reference binding precedence is:

View File

@@ -149,10 +149,10 @@ compatibility bindings, and run-time `--reference` or `--without-reference`
overrides are applied. Config-relative paths are resolved relative to the config overrides are applied. Config-relative paths are resolved relative to the config
file; CLI reference paths are resolved relative to the current working file; CLI reference paths are resolved relative to the current working
directory. Materialized bound files must be UTF-8 text. Materialized reference directory. Materialized bound files must be UTF-8 text. Materialized reference
provenance is recorded for chunk, extractor, and normalizer targets; runtime provenance is recorded for chunk, extractor, and normalizer targets, and runtime
reference content is currently passed only to lane extractors that declare the reference content is passed to the target that declares the slot. Reference
slot. Reference media types are inferred from file extensions, recorded as media types are inferred from file extensions, recorded as canonical base media
canonical base media types, and checked only when a module declares types, and checked only when a module declares
`AcceptedMediaTypes`; unknown extensions are recorded as `AcceptedMediaTypes`; unknown extensions are recorded as
`application/octet-stream`. Reference content is not written to diagnostics, `application/octet-stream`. Reference content is not written to diagnostics,
logs, errors, or manifests. logs, errors, or manifests.

View File

@@ -32,14 +32,15 @@ compares the canonical base media type inferred from the file extension,
case-insensitively and without parameters. case-insensitively and without parameters.
The resolver materializes reference content for chunk, extractor, and The resolver materializes reference content for chunk, extractor, and
normalizer targets. Runtime delivery is currently implemented only for lane normalizer targets. Runtime delivery uses `contracts.ChunkRequest.References`,
extractors through `contracts.ExtractionRequest.References`. Reference material `contracts.ExtractionRequest.References`, and
is not source evidence and must not be converted into `SourceRef` values. If a `contracts.NormalizeRequest.References`. Reference material is not source
module prompt uses references, load the prompt bundle with the same declared evidence and must not be converted into `SourceRef` values. If a module prompt
slots and render with `RenderUserSystemWithReferences`. Prompt templates may use uses references, load the prompt bundle with the same declared slots and render
the `reference` function for content and the `hasreference` function for with `RenderUserSystemWithReferences`. Prompt templates may use the `reference`
conditional sections. Prompt metadata hashes remain based on template source, function for content and the `hasreference` function for conditional sections.
not rendered reference bytes. Prompt metadata hashes remain based on template source, not rendered reference
bytes.
Chunk modules receive the structured LLM client through `contracts.ChunkRequest` Chunk modules receive the structured LLM client through `contracts.ChunkRequest`
when they need model-backed chunking. The pipeline runner validates generic when they need model-backed chunking. The pipeline runner validates generic

View File

@@ -53,8 +53,8 @@ empty bound files. Media-type acceptance is checked only when a slot declares
`application/octet-stream`. Reference content is omitted from diagnostics and `application/octet-stream`. Reference content is omitted from diagnostics and
manifests. The CLI writes provenance-only resolved reference diagnostics, and manifests. The CLI writes provenance-only resolved reference diagnostics, and
the run manifest records target-stage reference provenance separately from the run manifest records target-stage reference provenance separately from
source digests. Runtime reference content is currently passed only to the source digests. Runtime reference content is passed to the matching chunker,
matching lane extractor through `ExtractionRequest`. extractor, or normalizer request.
Prompt bundles can declare reference slots and use `reference` and Prompt bundles can declare reference slots and use `reference` and
`hasreference` template functions. Bundle loading validates string-literal slot `hasreference` template functions. Bundle loading validates string-literal slot

View File

@@ -95,6 +95,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (RunOutput, error) {
attachModuleManifestMetadata(&output, "chunker", chunker) attachModuleManifestMetadata(&output, "chunker", chunker)
chunkResult, err := chunker.Chunk(ctx, contracts.ChunkRequest{ chunkResult, err := chunker.Chunk(ctx, contracts.ChunkRequest{
Source: doc, Source: doc,
References: CloneReferenceSet(input.Pipeline.ChunkReferences.ReferenceSet),
LLMClient: input.LLMClient, LLMClient: input.LLMClient,
LLMProfile: input.Pipeline.Chunk.LLMProfile, LLMProfile: input.Pipeline.Chunk.LLMProfile,
Options: cloneOptions(input.Pipeline.Chunk.Options), Options: cloneOptions(input.Pipeline.Chunk.Options),
@@ -224,6 +225,7 @@ func (r *Runner) runLane(ctx context.Context, input RunInput, doc *source.Source
Source: doc, Source: doc,
LaneID: lane.ID, LaneID: lane.ID,
Candidates: mergeResult.Candidates, Candidates: mergeResult.Candidates,
References: CloneReferenceSet(lane.NormalizeReferences.ReferenceSet),
LLMClient: input.LLMClient, LLMClient: input.LLMClient,
LLMProfile: lane.Normalize.LLMProfile, LLMProfile: lane.Normalize.LLMProfile,
Options: cloneOptions(lane.Normalize.Options), Options: cloneOptions(lane.Normalize.Options),

View File

@@ -566,24 +566,7 @@ func TestRunPassesModuleBindingConfigToStageRequests(t *testing.T) {
func TestRunPassesLaneReferencesToExtractorRequests(t *testing.T) { func TestRunPassesLaneReferencesToExtractorRequests(t *testing.T) {
modules := defaultRunnerModules() modules := defaultRunnerModules()
pipeline := resolvedPipeline() pipeline := resolvedPipeline()
pipeline.ArtifactLanes[0].ExtractReferences.ReferenceSet = contracts.ReferenceSet{ pipeline.ArtifactLanes[0].ExtractReferences.ReferenceSet = testReferenceSet("roster", "reference text")
Slots: map[string]contracts.ResolvedReferenceSlot{
"roster": {
Slot: contracts.ReferenceSlot{Name: "roster"},
Items: []contracts.ReferenceItem{
{
SlotName: "roster",
MediaType: "text/plain; charset=utf-8",
Content: []byte("reference text"),
Digest: "sha256:test",
Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///tmp/reference.txt"},
SizeBytes: int64(len("reference text")),
BindingSource: contracts.ReferenceBindingSourceConfig,
},
},
},
},
}
_, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{Pipeline: pipeline}) _, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{Pipeline: pipeline})
if err != nil { if err != nil {
@@ -601,6 +584,55 @@ func TestRunPassesLaneReferencesToExtractorRequests(t *testing.T) {
} }
} }
func TestRunPassesChunkReferencesToChunkerRequest(t *testing.T) {
modules := defaultRunnerModules()
pipeline := resolvedPipeline()
pipeline.ChunkReferences.ReferenceSet = testReferenceSet("scene_guide", "chunk reference text")
_, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{Pipeline: pipeline})
if err != nil {
t.Fatalf("Run() error = %v, want nil", err)
}
req := modules.chunker.requests[0]
item := req.References.Slots["scene_guide"].Items[0]
if string(item.Content) != "chunk reference text" {
t.Fatalf("chunk reference content = %q, want chunk reference text", item.Content)
}
item.Content[0] = 'C'
if got := string(pipeline.ChunkReferences.ReferenceSet.Slots["scene_guide"].Items[0].Content); got != "chunk reference text" {
t.Fatalf("runner mutated chunk reference set content = %q", got)
}
}
func TestRunPassesNormalizeReferencesToNormalizerRequest(t *testing.T) {
modules := defaultRunnerModules()
pipeline := resolvedPipeline()
pipeline.ArtifactLanes[0].NormalizeReferences.ReferenceSet = testReferenceSet("normalization_notes", "normalize reference text")
_, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{Pipeline: pipeline})
if err != nil {
t.Fatalf("Run() error = %v, want nil", err)
}
req := modules.normalizers["normalize"].requests[0]
item := req.References.Slots["normalization_notes"].Items[0]
if string(item.Content) != "normalize reference text" {
t.Fatalf("normalize reference content = %q, want normalize reference text", item.Content)
}
item.Content[0] = 'N'
if got := string(pipeline.ArtifactLanes[0].NormalizeReferences.ReferenceSet.Slots["normalization_notes"].Items[0].Content); got != "normalize reference text" {
t.Fatalf("runner mutated normalize reference set content = %q", got)
}
}
func TestRunAllowsNilLLMClientWhenModulesDoNotUseIt(t *testing.T) {
_, err := New(newRunnerRegistries(t, defaultRunnerModules())).Run(context.Background(), RunInput{Pipeline: resolvedPipeline()})
if err != nil {
t.Fatalf("Run() error = %v, want nil with nil LLM client when modules do not use it", err)
}
}
func TestRunIncludesInputWarnings(t *testing.T) { func TestRunIncludesInputWarnings(t *testing.T) {
modules := defaultRunnerModules() modules := defaultRunnerModules()
warning := contracts.Warning{Scope: "reference", ReasonCode: "empty_reference", Message: "empty reference"} warning := contracts.Warning{Scope: "reference", ReasonCode: "empty_reference", Message: "empty reference"}
@@ -1235,6 +1267,27 @@ func resolvedPipelineWithValidators(validators ...string) ResolvedPipeline {
return pipeline return pipeline
} }
func testReferenceSet(slotName string, content string) contracts.ReferenceSet {
return contracts.ReferenceSet{
Slots: map[string]contracts.ResolvedReferenceSlot{
slotName: {
Slot: contracts.ReferenceSlot{Name: slotName},
Items: []contracts.ReferenceItem{
{
SlotName: slotName,
MediaType: "text/plain; charset=utf-8",
Content: []byte(content),
Digest: "sha256:test",
Origin: contracts.ReferenceOrigin{Type: "file", URI: "file:///tmp/reference.txt"},
SizeBytes: int64(len(content)),
BindingSource: contracts.ReferenceBindingSourceConfig,
},
},
},
},
}
}
type runnerModules struct { type runnerModules struct {
input *runnerInputAdapter input *runnerInputAdapter
chunker *runnerChunker chunker *runnerChunker

View File

@@ -43,8 +43,8 @@ func TestWalkingSkeletonFixture(t *testing.T) {
t.Fatalf("ContentType = %q, want application/json", output.OutputFiles[0].ContentType) t.Fatalf("ContentType = %q, want application/json", output.OutputFiles[0].ContentType)
} }
assertStructuralJSONEqual(t, output.OutputFiles[0].Bytes, expectedBytes) assertStructuralJSONEqual(t, output.OutputFiles[0].Bytes, expectedBytes)
if llmClient.calls != 2 { if llmClient.calls != 3 {
t.Fatalf("LLM calls = %d, want chunk count 2", llmClient.calls) t.Fatalf("LLM calls = %d, want extractor calls plus normalizer call", llmClient.calls)
} }
} }
@@ -345,6 +345,15 @@ func (normalizer walkingSkeletonNormalizer) ReferenceSlots() []contracts.Referen
} }
func (normalizer walkingSkeletonNormalizer) Normalize(ctx context.Context, req contracts.NormalizeRequest) (contracts.NormalizeResult, error) { func (normalizer walkingSkeletonNormalizer) Normalize(ctx context.Context, req contracts.NormalizeRequest) (contracts.NormalizeResult, error) {
var response struct {
Call int `json:"call"`
}
if _, err := req.LLMClient.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
StageName: "fake/normalize",
ResponseSchemaName: "fake_normalize",
}, &response); err != nil {
return contracts.NormalizeResult{}, err
}
return contracts.NormalizeResult{Candidates: req.Candidates}, nil return contracts.NormalizeResult{Candidates: req.Candidates}, nil
} }