Finalize conversation and execution group documentation

This commit is contained in:
2026-11-14 01:35:06 +00:00
parent 31cc0069fc
commit 5c135c5b28
13 changed files with 96 additions and 18 deletions

View File

@@ -43,11 +43,20 @@ type CoordinationResult struct {
} }
// Coordinate advances one branch from Start using ordinary runs. Next is called // Coordinate advances one branch from Start using ordinary runs. Next is called
// initially even for a zero budget and receives each completed result once. // initially even for a zero budget and receives each completed result once,
// including the final permitted response. The initial exchange is not itself a
// newly delivered result. Each started continuation consumes one turn; retries
// and structural repairs do not. Callback work runs after admission is released.
// A nil request stops; requesting a run without remaining budget exhausts it. // A nil request stops; requesting a run without remaining budget exhausts it.
// Callback and execution errors retain their identities. Cancellation observed // Callback and execution errors retain their identities. Cancellation observed
// before delivery returns the single completed response as Undelivered; // before delivery returns the single completed response as Undelivered;
// cancellation after callback invocation does not replay that response. // cancellation after callback invocation does not replay that response. Context
// cancellation takes precedence over a callback's returned action or error.
// Start must be a captured exchange, Next non-nil, and AdditionalTurns
// nonnegative; invalid values match ErrInvalidRequest. A nil engine returns
// ErrInvalidConfig. The selected request is copied and its ContinueFrom is
// replaced by the coordinator's current exchange. The request observer runs
// before the coordinator observer for each continuation.
func (e *Engine) Coordinate(ctx context.Context, req CoordinateRequest) (CoordinationResult, error) { func (e *Engine) Coordinate(ctx context.Context, req CoordinateRequest) (CoordinationResult, error) {
if e == nil || e.internal == nil { if e == nil || e.internal == nil {
return CoordinationResult{}, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) return CoordinationResult{}, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)

9
doc.go
View File

@@ -6,7 +6,10 @@
// call [Engine.InspectConfiguration], [Engine.InspectPrompt], // call [Engine.InspectConfiguration], [Engine.InspectPrompt],
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.InspectBackend], // [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.InspectBackend],
// [Engine.ListProfiles], [Engine.ListPrompts], [Engine.Prepare], // [Engine.ListProfiles], [Engine.ListPrompts], [Engine.Prepare],
// [Engine.PrepareExecution], [Engine.Run], or [Engine.RunPrepared]. Concrete registries, repositories, // [Engine.PrepareExecution], [Engine.Run], [Engine.RunPrepared], or
// [Engine.Coordinate]. Captured [Exchange] values support independent
// continuations, and [ExecutionGroup] collects explicitly observed runs.
// Concrete registries, repositories,
// validators, and the built-in OpenAI-compatible client remain internal // validators, and the built-in OpenAI-compatible client remain internal
// implementation details. // implementation details.
// //
@@ -14,7 +17,7 @@
// //
// An Engine supports concurrent InspectConfiguration, InspectPrompt, // An Engine supports concurrent InspectConfiguration, InspectPrompt,
// InspectRequirements, InspectProfile, InspectBackend, ListProfiles, ListPrompts, // InspectRequirements, InspectProfile, InspectBackend, ListProfiles, ListPrompts,
// Prepare, PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies // Prepare, PrepareExecution, Run, RunPrepared, and Coordinate calls. Engine-local backend policies
// bound admitted Run and RunPrepared calls and model generations where // bound admitted Run and RunPrepared calls and model generations where
// configured, while different backend pools and unlimited backends continue // configured, while different backend pools and unlimited backends continue
// independently. An injected [LLMClient] or [ArtifactReader] can therefore // independently. An injected [LLMClient] or [ArtifactReader] can therefore
@@ -43,7 +46,7 @@
// that enforces their filesystem, authorization, and size policies. // that enforces their filesystem, authorization, and size policies.
// //
// Rendered messages, input and output [Artifact] bodies, [RunResult.RawOutput], // Rendered messages, input and output [Artifact] bodies, [RunResult.RawOutput],
// and [ValidationResult.Errors] may contain sensitive data. Credential // [ValidationResult.Errors], and [ExchangeDetails.Messages] may contain sensitive data. Credential
// exclusion and redaction do not sanitize those values. Applications and // exclusion and redaction do not sanitize those values. Applications and
// injected collaborators are responsible for access control, retention, // injected collaborators are responsible for access control, retention,
// logging, and secret handling appropriate to their data. // logging, and secret handling appropriate to their data.

View File

@@ -93,6 +93,8 @@ target's model must be available.
Each ordinary message contains its `role` and string `content`. A Each ordinary message contains its `role` and string `content`. A
cache-controlled message instead uses a text content block containing `type`, cache-controlled message instead uses a text content block containing `type`,
`text`, and `cache_control`; an empty cache-control TTL is omitted. `text`, and `cache_control`; an empty cache-control TTL is omitted.
For a continuation, captured history precedes the new definition and appended
messages. Ancestry is local metadata and is not an upstream request field.
Promptkit sends only `developer`, `system`, `user`, and `assistant` roles and Promptkit sends only `developer`, `system`, `user`, and `assistant` roles and
does so without provider-specific translation. Tool and deprecated function does so without provider-specific translation. Tool and deprecated function

View File

@@ -27,6 +27,8 @@ callback. The caller supplies normalized failure metadata; the recorder never
parses an error string or receives provider bodies, credentials, headers, or parses an error string or receives provider bodies, credentials, headers, or
other content. Events are values with no mutable containers. Callback panics other content. Events are values with no mutable containers. Callback panics
propagate to the caller. propagate to the caller.
The immediate parent run ID is set before start delivery and is retained as
selection metadata changes. It describes ancestry, not implicit group membership.
Resolved prompt and profile IDs are the exact runtime identities selected for Resolved prompt and profile IDs are the exact runtime identities selected for
the invocation, including namespace qualification when supplied by an engine the invocation, including namespace qualification when supplied by an engine
view. view.
@@ -48,6 +50,13 @@ attempts above the first in a generation, and repair generations with at least
one dispatch. A terminal event is an aggregate and must not be added to its one dispatch. A terminal event is an aggregate and must not be added to its
attempt events. attempt events.
An execution group is a zero-ready synchronized collector of explicitly selected
events. It enrolls by run ID and aggregates each run's first terminal counters and
presence-aware usage exactly once. Snapshots copy members and totals under one
lock; terminal coverage and token completeness are distinct. Observer fan-out
copies the callback slice and invokes callbacks synchronously without holding a
group lock. No group history, registry, or automatic nested membership exists.
Smoke diagnostics mask inherited execution-recorder state while invoking Smoke diagnostics mask inherited execution-recorder state while invoking
generation or HTTP collaborators. Their private attempt evidence is assembled generation or HTTP collaborators. Their private attempt evidence is assembled
into a `SmokeTestReport`; it does not create ordinary run events or enter an into a `SmokeTestReport`; it does not create ordinary run events or enter an

View File

@@ -18,7 +18,7 @@ contributor workflow and validation.
| `internal/backend` | Holds a defensive, immutable lookup of final backend definitions, including diagnostic metadata, plus capacity policies validated by the configuration adapter. | [Backend registry](../../internal/backend/registry.go) | | `internal/backend` | Holds a defensive, immutable lookup of final backend definitions, including diagnostic metadata, plus capacity policies validated by the configuration adapter. | [Backend registry](../../internal/backend/registry.go) |
| `internal/configuration` | Loads root and namespaced resources through ConfigKit; validates final values, captures indexed resource views and source-relative prompt content, compiled templates, requirement metadata, identity inventories, backend and presence-aware execution profiles, and the read-only provenance inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) | | `internal/configuration` | Loads root and namespaced resources through ConfigKit; validates final values, captures indexed resource views and source-relative prompt content, compiled templates, requirement metadata, identity inventories, backend and presence-aware execution profiles, and the read-only provenance inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) |
| `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) | | `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) |
| `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, requirement inspection, profiles, execution targets, rendering, generation, validation, and source-neutral smoke diagnostics, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, output contracts, and resource-identity syntax and qualification. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [requirement values](../../internal/domain/requirements.go), [endpoint invariant](../../internal/domain/endpoint.go) | | `internal/domain` | Defines internal framework values for requests, immutable conversation exchanges, artifacts, prompt definitions, requirement inspection, profiles, execution targets, rendering, generation, validation, and source-neutral smoke diagnostics, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, output contracts, and resource-identity syntax and qualification. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [exchange storage](../../internal/domain/exchange.go), [requirement values](../../internal/domain/requirements.go), [endpoint invariant](../../internal/domain/endpoint.go) |
| `internal/engine` | Assembles caller-selected configuration into one snapshot, then constructs shared capacity, rendering, validation, generation, repair, the ordinary runner, and the smoke tester for each engine. Immutable namespace views share those collaborators and qualify caller-supplied execution, inspection, and diagnostic selections without rewriting captured references. Built-in engines provide the diagnostic HTTP collaborator; injected-client engines do not. It delegates resource inspection and identity listing directly to the captured configuration rather than the runner. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#typed-configuration-loading), [internal runner](runner.md) | | `internal/engine` | Assembles caller-selected configuration into one snapshot, then constructs shared capacity, rendering, validation, generation, repair, the ordinary runner, and the smoke tester for each engine. Immutable namespace views share those collaborators and qualify caller-supplied execution, inspection, and diagnostic selections without rewriting captured references. Built-in engines provide the diagnostic HTTP collaborator; injected-client engines do not. It delegates resource inspection and identity listing directly to the captured configuration rather than the runner. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#typed-configuration-loading), [internal runner](runner.md) |
| `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries, cancellable backoff, ordinary attempt observations, and invocation-local evidence for diagnostics. | [Internal generation coordination](generation.md) | | `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries, cancellable backoff, ordinary attempt observations, and invocation-local evidence for diagnostics. | [Internal generation coordination](generation.md) |
| `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) | | `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) |
@@ -30,8 +30,8 @@ contributor workflow and validation.
| `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) | | `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) |
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) | | `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
| `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded response decoding, authentication, deadline handling, per-invocation HTTP evidence and built-in diagnostic probes, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) | | `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded response decoding, authentication, deadline handling, per-invocation HTTP evidence and built-in diagnostic probes, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) |
| `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, and synchronous metadata event delivery for ordinary and claimed prepared runs and generation attempts; smoke diagnostics keep their private evidence outside ordinary observer events. | [Internal execution observation](observation.md) | | `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, synchronous metadata event delivery and composition, and explicit terminal-based group aggregation; smoke diagnostics keep their private evidence outside ordinary observer events. | [Internal execution observation](observation.md) |
| `internal/usecase` | Selects captured prompts and profiles, enumerates requirements from captured prompt syntax, applies request overrides and completion checks, coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair, and runs explicit smoke diagnostics with private cases, shared generation/capacity, and optional built-in HTTP probes. | [Internal runner](runner.md), [requirement inspection](../../internal/usecase/requirements_inspection.go), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) | | `internal/usecase` | Selects captured prompts and profiles, enumerates requirements from captured prompt syntax, applies request overrides and completion checks, coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair, advances a single conversation branch, and runs explicit smoke diagnostics with private cases, shared generation/capacity, and optional built-in HTTP probes. | [Internal runner](runner.md), [requirement inspection](../../internal/usecase/requirements_inspection.go), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
The internal engine assembles these internal components without exposing their The internal engine assembles these internal components without exposing their
representations. Consumers depend only on the root facade. representations. Consumers depend only on the root facade.

View File

@@ -30,6 +30,23 @@ internal callers and tests.
Each invocation carries its state in request, prepared-run, and result values. Each invocation carries its state in request, prepared-run, and result values.
The runner has no durable run or session store. The runner has no durable run or session store.
Captured exchanges are immutable parent-linked domain values. Each holds only
its new rendered suffix, final assistant message, and run identities; flattening
iteratively makes a detached history for preparation or inspection. Preparation
prepends this history to newly rendered definition messages and appended caller
messages before computing the rendered hash. Follow-up selection and credentials
remain those of the new request. Prepared execution retains capture and parent
references in its single-claim payload, not its durable details; discard clears
the payload. Completed runs capture the exact final output, including exhausted
structural repair, while operational errors produce no exchange.
The sequential coordinator uses ordinary facade runs and retains the current
immutable exchange independently of callback-owned values. Each run releases its
logical admission lease and finishes observation before application callback
delivery. Context checks preserve a completed but undelivered result only when
cancellation is observed before invoking the callback. Coordinator budgets count
started runs, not transport attempts or structural repairs.
## Shared Prompt Selection ## Shared Prompt Selection
The internal engine applies each namespace view to slashless caller selections The internal engine applies each namespace view to slashless caller selections
@@ -124,8 +141,8 @@ profile, or backend:
schema metadata from it when required; schema metadata from it when required;
2. load and hash input artifacts; 2. load and hash input artifacts;
3. render messages and the prompt-defined session; 3. render messages and the prompt-defined session;
4. apply any direct session ID, then append already-normalized request messages 4. apply any direct session ID, then compose captured history, rendered
after the rendered definition messages; definition messages, and already-normalized request messages in that order;
5. hash the effective rendered prompt; and 5. hash the effective rendered prompt; and
6. construct the prepared value and preparation timing. 6. construct the prepared value and preparation timing.

View File

@@ -337,7 +337,7 @@ These tests own focused history behavior; the final suite verifies its assembly.
## Stage 2 — Add Public Handles And History-Aware Preparation ## Stage 2 — Add Public Handles And History-Aware Preparation
**Status:** Complete. Capture state still needs to be carried through execution. **Status:** Complete; capture state is carried through execution.
**Scope:** facade request conversion and the shared preparation path; generation **Scope:** facade request conversion and the shared preparation path; generation
capture is the next stage. capture is the next stage.
@@ -614,6 +614,8 @@ the maintained example rather than duplicated in prose.
## Stage 13 — Finalize Contracts, Documentation, And Sprint Validation ## Stage 13 — Finalize Contracts, Documentation, And Sprint Validation
**Status:** Complete. Full offline race suite, vet, and diff checks passed with Go 1.27.3.
**Scope:** complete the target and remove sprint-only rough edges. **Scope:** complete the target and remove sprint-only rough edges.
1. Review every new public declaration and GoDoc against this plan and the feature 1. Review every new public declaration and GoDoc against this plan and the feature

View File

@@ -4,7 +4,9 @@ import "go.maximumdirect.net/promptkit/internal/domain"
const exchangeString = "promptkit.Exchange{opaque}" const exchangeString = "promptkit.Exchange{opaque}"
// Exchange is an immutable, in-process snapshot of a completed conversation. // Exchange is an immutable, reusable in-process snapshot of a completed
// conversation. It is not a persistence or restart format. Different engines
// can continue it, and independent branches do not alter one another.
type Exchange struct{ internal *domain.Exchange } type Exchange struct{ internal *domain.Exchange }
// ExchangeDetails contains a detached copy of an exchange's logical history. // ExchangeDetails contains a detached copy of an exchange's logical history.
@@ -14,7 +16,8 @@ type ExchangeDetails struct {
Messages []RenderedMessage `json:"messages"` Messages []RenderedMessage `json:"messages"`
} }
// Details returns detached history, or zero details for an empty handle. // Details returns detached history, or zero details for a nil or empty handle.
// Messages include caller and model content and require appropriate handling.
func (e *Exchange) Details() ExchangeDetails { func (e *Exchange) Details() ExchangeDetails {
if e == nil || e.internal == nil { if e == nil || e.internal == nil {
return ExchangeDetails{} return ExchangeDetails{}

View File

@@ -89,3 +89,25 @@ func TestExchangeFailuresLeaveSourceReusable(t *testing.T) {
t.Fatalf("reused source = %#v, %v", result, err) t.Fatalf("reused source = %#v, %v", result, err)
} }
} }
func TestEmptyExchangeRejectedBeforePreparation(t *testing.T) {
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "answer"}}
engine := newSimpleEngine(t, "question", promptkit.WithLLMClient(client))
request := promptkit.RunRequest{PromptID: "missing", ContinueFrom: &promptkit.Exchange{}}
ctx := context.Background()
if _, err := engine.Prepare(ctx, request); !errors.Is(err, promptkit.ErrInvalidRequest) {
t.Fatalf("prepare error = %v", err)
}
if err := engine.ValidateRequest(ctx, request); !errors.Is(err, promptkit.ErrInvalidRequest) {
t.Fatalf("validate error = %v", err)
}
if _, err := engine.PrepareExecution(ctx, request); !errors.Is(err, promptkit.ErrInvalidRequest) {
t.Fatalf("prepare execution error = %v", err)
}
if _, err := engine.Run(ctx, request); !errors.Is(err, promptkit.ErrInvalidRequest) {
t.Fatalf("run error = %v", err)
}
if len(client.requests) != 0 {
t.Fatal("invalid exchange reached the model")
}
}

View File

@@ -16,6 +16,12 @@ type ExecutionGroupMember struct {
} }
// ExecutionGroupSummary is a detached snapshot of terminal-only totals. // ExecutionGroupSummary is a detached snapshot of terminal-only totals.
// Usage sums known terminal values, with Known as their logical OR and
// Complete as their logical AND. Coverage describes whether every observed
// member has a terminal, independently of usage completeness. An empty group
// has complete coverage and known complete zero usage; a group with members
// but no terminals has unknown, incomplete usage. Overlapping groups are not
// generally additive.
type ExecutionGroupSummary struct { type ExecutionGroupSummary struct {
Members []ExecutionGroupMember `json:"members"` Members []ExecutionGroupMember `json:"members"`
TerminalCoverageComplete bool `json:"terminal_coverage_complete"` TerminalCoverageComplete bool `json:"terminal_coverage_complete"`

View File

@@ -78,6 +78,7 @@ func TestBuiltInRepairGenerationError(t *testing.T) {
t.Fatalf("NewEngine: %v", err) t.Fatalf("NewEngine: %v", err)
} }
req := simpleGenerationRequest() req := simpleGenerationRequest()
req.CaptureExchange = true
req.Validation = &promptkit.OutputContract{ req.Validation = &promptkit.OutputContract{
Format: promptkit.FormatJSON, Format: promptkit.FormatJSON,
ValidationMode: promptkit.ValidationJSON, ValidationMode: promptkit.ValidationJSON,

View File

@@ -1726,12 +1726,13 @@ func TestRunnerReleasesAdmissionAcrossRunOutcomes(t *testing.T) {
) )
result, err := runner.Run(context.Background(), domain.RunRequest{ result, err := runner.Run(context.Background(), domain.RunRequest{
PromptID: "p", PromptID: "p",
ProfileID: "exec", ProfileID: "exec",
Inputs: singleInputRef(), Inputs: singleInputRef(),
CaptureExchange: true,
}) })
if tc.wantError == nil { if tc.wantError == nil {
if err != nil || result == nil { if err != nil || result == nil || result.Exchange == nil {
t.Fatalf("successful run=(%+v, %v)", result, err) t.Fatalf("successful run=(%+v, %v)", result, err)
} }
} else { } else {

View File

@@ -85,9 +85,12 @@ const (
// own private execution snapshot for RunPrepared. Excessively deep or large // own private execution snapshot for RunPrepared. Excessively deep or large
// JSON-shaped values are rejected for safety. // JSON-shaped values are rejected for safety.
type RunRequest struct { type RunRequest struct {
// CaptureExchange requests an immutable snapshot of a completed run. // CaptureExchange requests an immutable snapshot of a completed run, even
// when its structural validation fails. Operational errors return no result.
CaptureExchange bool CaptureExchange bool
// ContinueFrom supplies captured history and implies capture of the new run. // ContinueFrom supplies captured history and implies capture of the new run.
// A non-nil zero-value exchange is invalid. Follow-up settings are selected
// normally and are not inherited from the exchange.
ContinueFrom *Exchange `json:"-" yaml:"-"` ContinueFrom *Exchange `json:"-" yaml:"-"`
// PromptID is the required prompt identifier. A namespace-scoped engine view // PromptID is the required prompt identifier. A namespace-scoped engine view
// qualifies a slashless caller value; otherwise it selects the exact // qualifies a slashless caller value; otherwise it selects the exact