Clarify artifact reader ownership transfer

This commit is contained in:
2026-11-05 13:51:29 +00:00
parent 7be677c80b
commit 7d500ee62f
4 changed files with 57 additions and 3 deletions

View File

@@ -0,0 +1,43 @@
package promptkit_test
import (
"context"
"fmt"
"sync"
"testing"
"go.maximumdirect.net/configkit"
"go.maximumdirect.net/promptkit"
)
type independentArtifactReader struct{}
func (independentArtifactReader) Read(_ context.Context, ref promptkit.ArtifactRef) (*promptkit.Artifact, error) {
return &promptkit.Artifact{Body: []byte(ref.Body), Hash: ref.Body}, nil
}
func TestConcurrentPreparationWithTransferredArtifacts(t *testing.T) {
input := configkit.Input{Source: configkit.Mapping("ownership", map[string]any{
"profiles": []any{map[string]any{"id": "profile", "endpoint": "https://example.invalid/v1", "model": "model"}},
"prompts": []any{map[string]any{"id": "prompt", "default_profile": "profile", "inputs": []any{map[string]any{"name": "document", "required": true}}, "messages": []any{map[string]any{"role": "user", "content": `{{input "document"}}`}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}},
})}
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}}, promptkit.WithArtifactReader(independentArtifactReader{}))
if err != nil {
t.Fatal(err)
}
var group sync.WaitGroup
for i := range 24 {
group.Go(func() {
value := fmt.Sprintf("body-%d", i)
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt", Inputs: map[string]promptkit.ArtifactRef{"document": promptkit.Inline(value)}})
if err != nil {
t.Errorf("prepare: %v", err)
return
}
if len(prepared.Messages) != 1 || prepared.Messages[0].Content != value || prepared.InputHashes["document"] != value {
t.Errorf("prepared = %+v", prepared)
}
})
}
group.Wait()
}

View File

@@ -891,6 +891,11 @@ of collaborator errors. Implementations must honor cancellation, safely manage
copies they retain, avoid unsafe logging of content or credentials, and enforce
the application policy that motivated the injection.
A successful `ArtifactReader.Read` transfers the returned artifact and mutable
body storage to Promptkit. Readers that previously reused a returned buffer
must instead return independent storage and stop mutating it after return;
private scratch buffers may still be reused when the returned bytes are copied.
## Handle Errors
Use `errors.Is` with the

View File

@@ -244,6 +244,10 @@ Focused authentication and public smoke tests pass on Go 1.27.3.
the accepted handoff, without promising safety for contract-violating readers.
Explain migration in consumer guidance; no borrowing/release protocol is added.
Completion: ArtifactReader ownership and migration guidance require independent
transferred storage. Focused concurrent preparation, adapter isolation, and
cause-propagation tests pass with the race detector on Go 1.27.3.
## Stage 9: Detach Prepared Ownership And Session Storage
**Catalog:** 16, 18. **Files:** runner/prepared state and domain session normalization.

View File

@@ -295,15 +295,17 @@ type Artifact struct {
//
// Read may be called concurrently. It must honor ctx cancellation to make
// Prepare, PrepareExecution, and Run responsive to cancellation. The engine
// passes a copied ref and immediately copies the returned Artifact.Body; it
// does not retain either value. Readers supply artifact metadata, and the
// passes a copied ref and defensively copies the returned Artifact.Body.
// A successful Read transfers the returned artifact and its mutable body
// storage to Promptkit. The reader must not mutate them through retained
// aliases after return. Private buffers may be reused only when the returned
// storage is independent. Readers supply artifact metadata, and the
// engine assigns an input-map name only when the returned artifact name is
// empty.
//
// An injected reader owns any application-specific path containment,
// authorization, content-size, and content-type policy. It must protect
// sensitive references and bodies in its logging and in any copies it retains.
// It may reuse or mutate the returned artifact and body after Read returns.
//
// Returning a non-nil error makes the engine return an error matching
// ErrArtifactLoad while preserving the reader error through errors.Is.