diff --git a/coordinate.go b/coordinate.go index 34e793a..e3a484f 100644 --- a/coordinate.go +++ b/coordinate.go @@ -43,11 +43,20 @@ type CoordinationResult struct { } // 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. // Callback and execution errors retain their identities. Cancellation observed // 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) { if e == nil || e.internal == nil { return CoordinationResult{}, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) diff --git a/doc.go b/doc.go index 0e1d8e4..8e82ffe 100644 --- a/doc.go +++ b/doc.go @@ -6,7 +6,10 @@ // call [Engine.InspectConfiguration], [Engine.InspectPrompt], // [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.InspectBackend], // [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 // implementation details. // @@ -14,7 +17,7 @@ // // An Engine supports concurrent InspectConfiguration, InspectPrompt, // 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 // configured, while different backend pools and unlimited backends continue // independently. An injected [LLMClient] or [ArtifactReader] can therefore @@ -43,7 +46,7 @@ // that enforces their filesystem, authorization, and size policies. // // 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 // injected collaborators are responsible for access control, retention, // logging, and secret handling appropriate to their data. diff --git a/docs/integrations/openai-compatible-chat.md b/docs/integrations/openai-compatible-chat.md index a514933..9ca2864 100644 --- a/docs/integrations/openai-compatible-chat.md +++ b/docs/integrations/openai-compatible-chat.md @@ -93,6 +93,8 @@ target's model must be available. Each ordinary message contains its `role` and string `content`. A cache-controlled message instead uses a text content block containing `type`, `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 does so without provider-specific translation. Tool and deprecated function diff --git a/docs/internal/observation.md b/docs/internal/observation.md index 9256625..0693c15 100644 --- a/docs/internal/observation.md +++ b/docs/internal/observation.md @@ -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 other content. Events are values with no mutable containers. Callback panics 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 the invocation, including namespace qualification when supplied by an engine 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 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 generation or HTTP collaborators. Their private attempt evidence is assembled into a `SmokeTestReport`; it does not create ordinary run events or enter an diff --git a/docs/internal/overview.md b/docs/internal/overview.md index bbca30e..aa81388 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -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/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/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/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) | @@ -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/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/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/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/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, 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 representations. Consumers depend only on the root facade. diff --git a/docs/internal/runner.md b/docs/internal/runner.md index 370532b..4b30f79 100644 --- a/docs/internal/runner.md +++ b/docs/internal/runner.md @@ -30,6 +30,23 @@ internal callers and tests. Each invocation carries its state in request, prepared-run, and result values. 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 The internal engine applies each namespace view to slashless caller selections @@ -124,8 +141,8 @@ profile, or backend: schema metadata from it when required; 2. load and hash input artifacts; 3. render messages and the prompt-defined session; -4. apply any direct session ID, then append already-normalized request messages - after the rendered definition messages; +4. apply any direct session ID, then compose captured history, rendered + definition messages, and already-normalized request messages in that order; 5. hash the effective rendered prompt; and 6. construct the prepared value and preparation timing. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 55c9516..8861bd7 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -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 -**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 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 +**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. 1. Review every new public declaration and GoDoc against this plan and the feature diff --git a/exchange.go b/exchange.go index fd427fa..09ac99d 100644 --- a/exchange.go +++ b/exchange.go @@ -4,7 +4,9 @@ import "go.maximumdirect.net/promptkit/internal/domain" 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 } // ExchangeDetails contains a detached copy of an exchange's logical history. @@ -14,7 +16,8 @@ type ExchangeDetails struct { 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 { if e == nil || e.internal == nil { return ExchangeDetails{} diff --git a/exchange_capture_contract_test.go b/exchange_capture_contract_test.go index 0a8d3ac..0170c05 100644 --- a/exchange_capture_contract_test.go +++ b/exchange_capture_contract_test.go @@ -89,3 +89,25 @@ func TestExchangeFailuresLeaveSourceReusable(t *testing.T) { 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") + } +} diff --git a/execution_group.go b/execution_group.go index 36720f2..b09b9c0 100644 --- a/execution_group.go +++ b/execution_group.go @@ -16,6 +16,12 @@ type ExecutionGroupMember struct { } // 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 { Members []ExecutionGroupMember `json:"members"` TerminalCoverageComplete bool `json:"terminal_coverage_complete"` diff --git a/generation_error_contract_test.go b/generation_error_contract_test.go index 8bb209f..88e69d2 100644 --- a/generation_error_contract_test.go +++ b/generation_error_contract_test.go @@ -78,6 +78,7 @@ func TestBuiltInRepairGenerationError(t *testing.T) { t.Fatalf("NewEngine: %v", err) } req := simpleGenerationRequest() + req.CaptureExchange = true req.Validation = &promptkit.OutputContract{ Format: promptkit.FormatJSON, ValidationMode: promptkit.ValidationJSON, diff --git a/internal/usecase/runner_test.go b/internal/usecase/runner_test.go index ac1ff42..c1e37f8 100644 --- a/internal/usecase/runner_test.go +++ b/internal/usecase/runner_test.go @@ -1726,12 +1726,13 @@ func TestRunnerReleasesAdmissionAcrossRunOutcomes(t *testing.T) { ) result, err := runner.Run(context.Background(), domain.RunRequest{ - PromptID: "p", - ProfileID: "exec", - Inputs: singleInputRef(), + PromptID: "p", + ProfileID: "exec", + Inputs: singleInputRef(), + CaptureExchange: true, }) 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) } } else { diff --git a/types.go b/types.go index b6159cb..ef6d99d 100644 --- a/types.go +++ b/types.go @@ -85,9 +85,12 @@ const ( // own private execution snapshot for RunPrepared. Excessively deep or large // JSON-shaped values are rejected for safety. 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 // 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:"-"` // PromptID is the required prompt identifier. A namespace-scoped engine view // qualifies a slashless caller value; otherwise it selects the exact