From dc39562ff754cad60863717ff8bb32c35e1a00c9 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 29 Jul 2026 20:25:00 +0000 Subject: [PATCH] Keep session IDs on repair requests and retire completed plans --- docs/internal/runner.md | 11 +- docs/roadmap/implementation.md | 556 ------------------------------ docs/roadmap/profile-overrides.md | 176 ---------- internal/usecase/repairer.go | 36 +- internal/usecase/runner.go | 1 + internal/usecase/runner_test.go | 35 ++ 6 files changed, 62 insertions(+), 753 deletions(-) delete mode 100644 docs/roadmap/implementation.md delete mode 100644 docs/roadmap/profile-overrides.md diff --git a/docs/internal/runner.md b/docs/internal/runner.md index 840544f..8df1cb8 100644 --- a/docs/internal/runner.md +++ b/docs/internal/runner.md @@ -77,15 +77,16 @@ an inability to perform validation is an operational error. When an internal repairer is present, a JSON or JSON Schema content failure can trigger bounded repair attempts. Repair receives the effective execution -target, validation errors, prior output, and structured-output specification. -This capability remains internal and is not a public option. +target and session ID, validation errors, prior output, and structured-output +specification. This capability remains internal and is not a public option. A successful result includes the output artifact and raw output, validation state, effective session ID, prompt and rendered-prompt hashes, selected profile and backend, effective settings, input hashes, token usage, a generated -run identifier, and UTC timing. The same effective session reaches generation -through the rendered prompt. The same effective target, including backend -identity, reaches generation and any repair attempt. +run identifier, and UTC timing. The same effective session reaches initial +generation and any repair attempt through the rendered prompt. The same +effective target, including backend identity, reaches generation and any +repair attempt. ## Failure Categories diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md deleted file mode 100644 index 98a07f4..0000000 --- a/docs/roadmap/implementation.md +++ /dev/null @@ -1,556 +0,0 @@ -# Explicit Per-Run Profile Overrides Implementation Plan - -**Status:** Complete. - -## Purpose - -This document is the decision-complete implementation plan for the -[explicit per-run profile overrides](profile-overrides.md). It is written for -a coding agent that will implement each stage in order. - -The feature roadmap owns the intended capability, user intent, policy choices, -compatibility decision, and target end state. This document owns the concrete -API changes, internal representation, implementation sequence, test ownership, -documentation updates, and completion gates. - -## Implementation Rules - -- Complete the stages in order. Keep the repository compiling and the focused - tests passing at every stage boundary. -- Preserve unrelated working-tree changes. The selected feature roadmap and - the removal of its source idea from `future.md` may already be uncommitted - when implementation begins; retain both. -- Follow every policy under `docs/policy/`, the task-specific reading guide in - `docs/development.md`, and the target behavior in - `profile-overrides.md`. -- Keep the public API in the root `promptkit` package and implementation - details under `internal/`. Do not create another public package. -- Do not broaden the feature into conversation storage, automatic session - generation, provider capability discovery, backend-specific headers, - concurrency control, retries, queues, or file-format changes. -- Preserve synchronous `Prepare` and `Run`, engine-local invocation state, - endpoint-only profiles, backend-selected profiles, and injected - `LLMClient` behavior. -- Keep all tests deterministic and offline. Use existing fakes or local - `httptest` servers; never contact OpenRouter or another live provider. -- Update exact GoDoc with each exported declaration change. Update durable - current-state documents only after the corresponding behavior is - implemented. -- Add tests at the narrowest stable owner described below. Do not reproduce - the same precedence table at the domain, runner, facade, and transport - layers. -- Do not create a release, change a module version, or tag a commit. The final - implementation handoff must call out the planned pre-`v1` source - compatibility change so it can be included in the next minor release note. - -## Fixed Design - -### Public API - -Add a direct session field to `RunRequest`: - -```go -type RunRequest struct { - // Insert after ProfileID and before APIKey. - SessionID string -} -``` - -Its exact GoDoc must state that: - -- a nonblank value is trimmed and overrides the prompt definition's - `session_id` template; -- a blank value supplies no direct override; -- the maximum is 256 Unicode code points after trimming; -- a direct value is opaque consumer metadata, is not a credential, and may be - exposed in prepared values, results, collaborator requests, provider - requests, and provider observability; and -- callers should use stable, non-sensitive identifiers. - -Change the existing reasoning override field: - -```go -type ExecutionTargetOverride struct { - // Existing fields remain otherwise unchanged. - ReasoningEffort *string -} -``` - -The field has these exact states: - -| Public value | Effective behavior | -| --- | --- | -| `nil` | Inherit the profile's reasoning effort. | -| Pointer to a nonblank string | Trim and replace the profile value. | -| Pointer to an empty or whitespace-only string | Clear the inherited value and disable reasoning for this run. | - -Do not add `DisableReasoning`, a parallel reasoning override, a public optional -string wrapper, or a fixed reasoning enum. The pointer is the one supported -runtime reasoning surface. - -Add completed-run session metadata: - -```go -type RunResult struct { - // Insert after PromptHash and before RenderedPromptHash. - SessionID string `json:"session_id,omitempty"` -} -``` - -`PreparedRun.SessionID` remains the preparation metadata field. Update its -GoDoc from "rendered" to "effective direct or rendered" session identity. -Likewise, update `RenderedPrompt.SessionID` GoDoc because injected clients may -receive a direct request value rather than one produced by the prompt -renderer. - -Do not add session identity to `ExecutionTarget`, -`ExecutionTargetOverride`, `Profile`, or the profile file format. Do not add a -second result field that records whether the session came from the request or -prompt. - -### Internal Values And Copy Ownership - -Mirror the public request changes in `internal/domain`: - -```go -type RunRequest struct { - // Insert after ProfileID and before APIKey. - SessionID string -} - -type ExecutionTargetOverride struct { - // Existing fields... - ReasoningEffort *string -} - -type RunResult struct { - // Insert after PromptHash and before RenderedPromptHash. - SessionID string -} -``` - -`toDomainRunRequest` copies `RunRequest.SessionID`. The public-to-domain -execution override conversion must allocate a new string for a non-nil -reasoning pointer rather than retain the caller's pointer. Add a small -`copyStringPtr` helper beside the existing numeric pointer-copy helpers in -`convert.go`. - -The internal and public profile and effective-target fields remain ordinary -strings: - -- `domain.ExecutionProfile.ReasoningEffort` -- `Profile.ReasoningEffort` -- `domain.ExecutionTarget.ReasoningEffort` -- `ExecutionTarget.ReasoningEffort` - -Do not extend `ExecutionTargetPresence`. That value exists to preserve -explicit numeric zero serialization. After reasoning resolution, an empty -effective reasoning string is sufficient to instruct built-in and injected -clients to omit reasoning. - -### Shared Session Normalization - -Add `internal/domain/session.go` with one shared internal domain rule: - -```go -func NormalizeSessionID(raw string) (string, error) -``` - -The function: - -- trims surrounding Unicode whitespace with `strings.TrimSpace`; -- returns `""`, `nil` for a blank value; -- counts Unicode code points with `utf8.RuneCountInString`; -- rejects a normalized value longer than `SessionIDMaxLength`; and -- returns an ordinary internal error containing the actual and maximum - lengths, without attaching a prompt, request, or transport error sentinel. - -Keep `SessionIDMaxLength` in `internal/domain` at 256. The renderer, runner, -and model client wrap normalization failures into their own error boundaries: - -- prompt session template: prompt-render failure; -- direct request session: invalid request; and -- defensive model-client validation: invalid generate request. - -Refactor `internal/prompt.renderSessionID` and -`internal/llm.openAIChatRequestFromGenerateRequest` to use this helper. Remove -their duplicated trimming and rune-count logic and any now-unused imports. - -### Direct Session Resolution - -In `Runner.Prepare`, preserve required prompt-ID validation as the first -request check. Immediately afterward, normalize `req.SessionID`. A -normalization failure must return an error matching use-case -`ErrInvalidRequest` before prompt or profile loading begins. - -Continue loading and hashing the original prompt definition. When the -normalized direct session is nonempty: - -1. make a value copy of the loaded prompt definition; -2. clear only the copy's `SessionID` template; -3. pass the copied definition to the existing renderer; and -4. set the renderer's returned `RenderedPrompt.SessionID` to the normalized - direct value before hashing or constructing `PreparedRun`. - -Do not mutate the repository-owned prompt definition. Do not change the -renderer interface, inject the direct value into `Vars`, or expose it to -message templates. Message rendering continues to use the request's existing -variables and inputs. - -When the direct session is blank, pass the original definition to the renderer -and retain current prompt-template behavior. This includes current missing -variable, invalid template, blank result, and maximum-length behavior. - -The prompt-definition hash must always be computed from the original -definition. The rendered-prompt hash must use the effective session ID after -the direct override is applied. - -`Runner.Run` must copy `PreparedRun.SessionID` into the internal `RunResult`. -The existing generation request must continue receiving the same effective -value through `RenderedPrompt.SessionID`. - -### Reasoning Resolution - -Leave `mergeExecutionTarget`, which overlays framework/backend/profile -`ExecutionTarget` values, unchanged: blank profile-level reasoning strings -continue to mean "no replacement." - -Change only `mergeExecutionTargetOverride`, which applies the request's -pointer-based override: - -```go -if override.ReasoningEffort != nil { - out.ReasoningEffort = strings.TrimSpace(*override.ReasoningEffort) -} -``` - -Assign the trimmed value even when it is empty. This is the operation that -clears inherited reasoning. Do not validate against a fixed vocabulary and do -not translate an empty value to `none`, `null`, or any other provider value. - -The built-in client already omits an empty effective `ReasoningEffort` and -serializes a nonempty value. Preserve that transport behavior. A consumer may -still explicitly provide a provider-supported nonblank value such as `none`; -Promptkit treats it as an opaque override rather than its own disable -mechanism. - -### Stable JSON And Result Conversion - -Add `session_id,omitempty` to the private `runResultJSON` representation in -`json.go`. Carry it in both `RunResult.MarshalJSON` and -`RunResult.UnmarshalJSON`. - -The stable JSON contract is: - -- a nonempty `RunResult.SessionID` serializes as top-level `session_id`; -- an empty value is omitted; and -- JSON produced before this field existed remains valid and decodes with an - empty session ID. - -Carry the internal result value through `fromDomainRunResult`. No custom JSON -work is needed for `PreparedRun`, whose existing `SessionID` field already has -the required name and omission behavior. - -### OpenAI-Compatible Wire Contract - -Do not add `x-session-id`. The built-in client continues sending the effective -nonempty value only as the top-level `session_id` body field. Keep -`session_id` in the reserved extra-parameter field set. - -The existing transport tests that assert body serialization, blank omission, -length rejection, and absence of the session header remain the canonical wire -tests. Refactoring normalization must not weaken those assertions. - -### Error Identities - -Do not add public or internal error sentinels. - -| Failure | Required identity | -| --- | --- | -| Direct session exceeds the maximum | Public `ErrInvalidRequest` through use-case `ErrInvalidRequest` | -| Prompt session template is invalid or cannot render | Existing `ErrPromptRender` behavior | -| Prompt-rendered session exceeds the maximum | Existing `ErrPromptRender` behavior | -| Model client defensively receives an overlong session | Existing internal LLM `ErrInvalidRequest` | -| Blank reasoning pointer | No error; clears inherited reasoning | -| Nonblank unfamiliar reasoning value | No Promptkit validation error; pass through after trimming | - -Do not make exact diagnostic prose a public contract. Tests should use -`errors.Is` and inspect only a useful semantic fragment when necessary. - -## Test Ownership - -Use the following ownership split: - -- `internal/domain/session_test.go` owns shared trimming, blank handling, and - the 256/257 Unicode-code-point boundary. -- `internal/prompt/renderer_test.go` retains ownership of session-template - parsing, rendering, and prompt-render error mapping. Update existing tests - only as required by the shared normalizer refactor. -- `internal/usecase/runner_test.go` owns request/profile reasoning precedence, - direct-session bypass of the template, invalid direct-session - classification, and application of the effective session before hashing. -- Root external-package tests own public conversion, an assembled - direct-session workflow, completed-result propagation, and stable public - JSON. -- `internal/llm/openai_compatible_client_test.go` remains the sole owner of - body-versus-header delivery and wire omission. Do not duplicate those exact - HTTP assertions in a root workflow test. - -Prefer extending coherent existing table tests over creating parallel -end-to-end suites. Keep literal-limit testing centralized around -`domain.SessionIDMaxLength`; use relative boundary cases rather than copying -the number into multiple tests. - -## Stage 1 — Tri-State Reasoning Override - -**Status:** Complete. - -### Goal - -Replace the ambiguous string request override with an owned pointer and -implement inherit, replace, and clear semantics without changing profiles or -the provider payload model. - -### Work - -1. In `types.go`, change - `ExecutionTargetOverride.ReasoningEffort` to `*string` and write exact - tri-state GoDoc. Update the surrounding override summary so it no longer - says every empty string inherits. -2. In `internal/domain/domain.go`, make the corresponding override field a - `*string`. Leave profile and effective-target fields unchanged. -3. In `convert.go`, add `copyStringPtr` beside `copyFloat64Ptr` and - `copyIntPtr`, and use it when converting the override. Never retain the - caller's pointer. -4. In `internal/usecase/runner.go`, change only the request-override merge to - assign `strings.TrimSpace(*override.ReasoningEffort)` whenever the pointer - is non-nil. Preserve profile-level blank-string behavior. -5. Update all in-repository public and internal - `ExecutionTargetOverride` composite literals to use string pointers where - they intend an override. Do not change `Profile`, `ExecutionProfile`, or - effective `ExecutionTarget` literals. -6. Keep `ExecutionTargetPresence`, the model-client request type, and the - OpenAI-compatible wire structure unchanged. - -### Tests - -1. Update the existing root execution-precedence test so its ordinary - nonblank reasoning override uses a pointer. -2. Extend that coherent public precedence coverage with a profile that has a - nonempty reasoning default and a pointer to a blank request value; assert - that `PreparedRun.EffectiveModelParams.ReasoningEffort` is empty. -3. In the runner resolution tests, cover all three states: - - nil inherits the profile value; - - a pointer to `" high "` produces `"high"`; and - - a pointer to whitespace produces `""`. -4. Retain the existing model-client tests for serializing nonempty reasoning - and omitting empty reasoning. Do not add another HTTP test for the same - transport branches. - -### Focused Validation - -Run: - -```sh -gofmt -w types.go convert.go internal/domain/domain.go \ - internal/usecase/runner.go engine_test.go \ - internal/usecase/runner_test.go -go test . ./internal/usecase ./internal/llm -go vet . ./internal/usecase ./internal/llm -git diff --check -``` - -### Completion Gate - -This stage is complete when the repository builds with the pointer-based -public API, all three reasoning states resolve correctly, caller pointer -ownership is not retained, and the provider client still receives only the -resolved string. - -## Stage 2 — Direct Session Resolution And Result Metadata - -**Status:** Complete. - -### Goal - -Add direct per-run session identity, bypass the prompt session template when -selected, and carry the effective value consistently through hashes, -generation, results, and stable JSON. - -### Work - -1. Add `internal/domain/session.go` with the shared normalization function and - refactor the prompt renderer and model client to consume it. -2. Add `SessionID` to public and internal `RunRequest` and copy it in - `toDomainRunRequest`. -3. Implement direct normalization at the start of `Runner.Prepare` after - required prompt-ID validation. Wrap an overlong value with use-case - `ErrInvalidRequest`. -4. Preserve the original definition for its hash. For a nonempty direct - session, render with a copied definition whose session template is cleared, - then apply the direct value before computing the rendered-prompt hash. -5. Leave message variables and templates unchanged. Confirm that a direct - session succeeds even if the unused prompt session template would fail - because of a missing variable. -6. Add `SessionID` to internal and public `RunResult`, populate it from - `PreparedRun` in `Runner.Run`, and carry it through `fromDomainRunResult`. -7. Add the field to `runResultJSON`, `MarshalJSON`, and `UnmarshalJSON` with - `omitempty`. -8. Update GoDoc for `RunRequest.SessionID`, `PreparedRun.SessionID`, - `RunResult.SessionID`, and `RenderedPrompt.SessionID`. Ensure none describes - the value as necessarily renderer-produced. -9. Keep the built-in HTTP request body and reserved-field behavior unchanged; - no session header is added. - -### Tests - -1. Add focused domain normalization cases for: - - surrounding whitespace; - - blank input; - - exactly `SessionIDMaxLength` Unicode code points; and - - one code point over the limit. -2. Add runner tests showing: - - a direct value is trimmed and wins over a prompt session template; - - a direct value bypasses an otherwise failing session template while - message templates still render normally; - - blank direct input retains prompt-template behavior; - - changing only the direct session changes the rendered-prompt hash but not - the prompt-definition hash; and - - an overlong direct value matches `ErrInvalidRequest` and does not invoke - generation. -3. Add one assembled root-package workflow using an injected fake model - client. Assert that the same direct session appears in `PreparedRun`, - `GenerateRequest.Prompt`, and `RunResult`. This test owns the public adapter - path; it must not repeat body/header assertions. -4. Extend the public `RunResult` JSON round-trip test to cover a nonempty - session, and add or extend omission coverage for an empty session. -5. Keep the existing prompt-renderer and model-client boundary tests passing - after normalization is centralized. - -### Focused Validation - -Run: - -```sh -gofmt -w types.go convert.go json.go internal/domain/domain.go \ - internal/domain/session.go internal/domain/session_test.go \ - internal/prompt/go_renderer.go internal/prompt/renderer_test.go \ - internal/usecase/runner.go internal/usecase/runner_test.go \ - internal/llm/openai_compatible_client.go \ - internal/llm/openai_compatible_client_test.go \ - engine_test.go public_contract_test.go -go test . ./internal/domain ./internal/prompt ./internal/usecase ./internal/llm -go vet . ./internal/domain ./internal/prompt ./internal/usecase ./internal/llm -git diff --check -``` - -If an existing test file did not require an edit, omit it from the `gofmt` -arguments rather than touching it mechanically. - -### Completion Gate - -This stage is complete when one normalized effective session flows from the -public request through preparation, hashing, generation, completed results, -and stable JSON; direct values bypass the prompt session template; and the -existing body-only transport contract remains intact. - -## Stage 3 — Durable Documentation And Final Validation - -**Status:** Complete. - -### Goal - -Make current-state contracts accurately describe the implemented feature, -record the compatibility impact for release handoff, and validate the complete -repository. - -### Work - -1. Review all changed exported declarations in `types.go` and `doc.go`. - Ensure GoDoc is the canonical owner of exact field types, pointer states, - copying, normalization, privacy exposure, JSON, and error behavior. -2. Update `docs/consumers/pkg-promptkit.md` with concise task-oriented guidance - for: - - supplying a direct per-run session; - - inheriting, replacing, and disabling profile reasoning with a string - pointer; and - - treating session IDs as stable, non-secret correlation values. - Keep snippets illustrative and link to GoDoc for the exact contract. -3. Update `docs/formats.md` only where the file format interacts with runtime - precedence: - - a direct request session bypasses the prompt `session_id` template; and - - reasoning is the exception to the general nonempty request-string rule - because its pointer can explicitly clear the profile value. - Do not add or change YAML fields. -4. Update `docs/integrations/openai-compatible-chat.md` to describe the - effective direct-or-rendered session, body-only delivery, the 256-code-point - check, and omission of an explicitly disabled effective reasoning setting. - Do not duplicate public Go field declarations. -5. Update `docs/internal/runner.md` to describe direct-session normalization - and template bypass, effective hashing, tri-state reasoning precedence, and - propagation into completed results. -6. Do not update `docs/internal/llm.md` or the internal component inventory. - Shared normalization is an incidental mechanism and does not change the - model client's documented flow or any package responsibility. -7. Do not change architecture, documentation, or testing policy unless - implementation reveals a genuine policy change. This feature is expected - to fit the current policies. -8. After every implementation and documentation check passes, set the feature - roadmap and this implementation plan status to `Complete`. Do not remove - either roadmap in the implementation change; lifecycle retirement can - follow after review. -9. In the implementation handoff, explicitly identify - `ExecutionTargetOverride.ReasoningEffort` changing from `string` to - `*string` as a pre-`v1` minor-release source compatibility change. Do not - edit release procedure or create a tag. - -### Full Validation - -Run the complete sequence from `docs/development.md`: - -```sh -go test ./... -go test -race ./... -go vet ./... -go build ./... -go run ./examples/go-library/prepare -gofmt -l $(git ls-files '*.go') -git diff --check -``` - -The formatting command must produce no paths. Follow every added or changed -Markdown link and confirm its target and heading exist. Also inspect: - -```sh -git status --short -git diff --stat -git diff -``` - -Confirm that: - -- only intended feature, test, documentation, and roadmap files changed; -- no `go.work`, `go.work.sum`, local module replacement, credential, generated - binary, or unrelated change was introduced; -- all public and internal override literals use the correct pointer or string - type for their layer; -- `session_id` remains reserved in extra parameters; -- the built-in client does not set `x-session-id`; -- current-state documents describe implemented behavior rather than referring - readers to the roadmaps; and -- the feature and implementation roadmaps contain no unresolved work marked - complete. - -### Completion Gate - -The implementation is complete only when every target-end-state item in -`profile-overrides.md` is implemented, the full validation sequence passes, -durable contracts no longer depend on roadmap prose, and the compatibility -change is clearly reported for the next minor release. - -## Open Questions - -None. The feature roadmap and this plan fix the public representation, -precedence, normalization, error identities, body-versus-header choice, -metadata, hashing, test ownership, compatibility treatment, and non-goals -needed for implementation. diff --git a/docs/roadmap/profile-overrides.md b/docs/roadmap/profile-overrides.md deleted file mode 100644 index f4b8a4a..0000000 --- a/docs/roadmap/profile-overrides.md +++ /dev/null @@ -1,176 +0,0 @@ -# Explicit Per-Run Profile Overrides - -**Status:** Complete. - -## Purpose - -This roadmap defines the scope and target end state for explicit per-run -session and reasoning controls. It establishes the behavioral boundary and -policy choices for the work. - -This document is planning material, not a description of current behavior. -Current exported contracts remain owned by Go declarations and GoDoc, prompt -and profile files by the [format reference](../formats.md), and outbound HTTP -behavior by the -[OpenAI-compatible integration contract](../integrations/openai-compatible-chat.md). - -## Motivation - -Prompt definitions can currently render a session ID from request variables, -and execution profiles can provide a default reasoning effort. A nonblank -per-run reasoning value can replace that profile default, but consumers cannot -directly supply a session ID or explicitly clear inherited reasoning. - -Consumers should be able to reuse one prompt and profile across conversations, -agent workflows, and reasoning levels. Choosing a session or reasoning level -for one run should not require duplicate prompt definitions, duplicate -profiles, or provider-specific values that mean "disabled." - -## Scope - -The work will add two independent per-run controls: - -- a direct session ID associated with the run; and -- a tri-state reasoning override that can inherit, replace, or disable the - selected profile's reasoning effort. - -These controls belong to the existing request and execution-target surfaces. -They will not introduce another prompt or profile selection mechanism. - -## Session ID Behavior - -`RunRequest` will accept an optional direct session ID. Promptkit will treat -session IDs as opaque, consumer-supplied correlation values rather than -credentials or conversation storage keys managed by the library. - -The effective session ID will resolve as follows: - -1. a nonblank direct request session ID; -2. the rendered prompt-definition session ID template; or -3. no session ID. - -A direct value will be trimmed and will take complete precedence over the -prompt template. When a direct value is present, Promptkit will not parse or -render the prompt's session ID template. An unused template therefore cannot -fail the run because of a missing variable or another template error. A blank -direct value means that no direct override was supplied and retains the prompt -template behavior. - -The direct value will use the existing session validation rule: at most 256 -Unicode code points after trimming. An invalid direct value will fail at the -per-run request boundary. Prompt-rendered session IDs will retain their -existing prompt-render failure boundary. - -For the built-in OpenAI-compatible client, a nonempty effective session ID -will continue to be sent as the top-level `session_id` request-body field. It -will not also be sent through the `x-session-id` header. This matches -OpenRouter's documented request schema and avoids two competing wire values. -Promptkit will not claim that every OpenAI-compatible backend implements -OpenRouter's sticky-routing or observability semantics. - -Consumers are responsible for choosing stable identifiers for related calls. -Session IDs may appear in prepared values, results, provider requests, and -provider observability systems, so they must not contain credentials or -unnecessarily sensitive data. - -## Reasoning Override Behavior - -The profile's reasoning effort will remain a reusable default. The per-run -execution override will distinguish three states: - -- omitted: inherit the selected profile's reasoning effort; -- nonblank: replace the profile value with the trimmed request value; and -- explicitly blank: disable inherited reasoning for the run. - -The public `ExecutionTargetOverride.ReasoningEffort` field will become a string -pointer so `nil`, a pointer to a nonblank string, and a pointer to a blank -string represent those three states directly. `Profile.ReasoningEffort` and -the effective `ExecutionTarget.ReasoningEffort` will remain strings. - -Promptkit will not define a closed set of reasoning effort names because -supported values may vary across OpenAI-compatible backends. Explicit disable -will produce an empty effective reasoning setting, and the built-in client -will omit `reasoning_effort` from the provider request. Promptkit will not -translate disable into a provider-specific magic value such as `none`. - -## Metadata And Generation Boundary - -The effective direct or rendered session ID will: - -- remain visible in `PreparedRun`; -- be added to `RunResult`; -- be included in the rendered-prompt hash; and -- be supplied to injected model clients through the rendered prompt. - -The prompt-definition hash will continue to describe the selected definition, -including its configured session template, even when a direct value bypasses -that template. The rendered-prompt hash will describe the effective session ID -and rendered messages used for the run. - -The effective reasoning effort will remain visible through -`EffectiveModelParams` in prepared and completed run metadata and through the -execution target supplied to injected model clients. An empty effective value -means that the client should omit a reasoning setting. - -## Compatibility - -Adding a direct session field and completed-run session metadata is additive. -Changing `ExecutionTargetOverride.ReasoningEffort` from `string` to `*string` -is a source compatibility change for consumers that initialize that field. - -Promptkit's [pre-`v1` release policy](../release.md#release-model) permits -public API changes in a minor release. This work will use that release boundary -in favor of a single idiomatic tri-state field rather than permanently adding -a second disable flag or parallel override field. Release notes for the -version that publishes the change will identify the required consumer update. - -Existing behavior will otherwise remain compatible: - -- prompts without a direct session ID continue rendering their session - template; -- prompts without either session source continue without a session ID; -- profiles continue supplying reasoning defaults; -- omitted per-run reasoning continues inheriting the profile; -- endpoint-only and backend-selected profiles behave identically; -- injected model clients continue receiving the effective rendered prompt and - execution target; and -- synchronous `Prepare` and `Run` behavior remains unchanged. - -## Non-Goals - -This scope does not include: - -- conversation history, memory, or message persistence; -- durable session storage or session lifecycle management; -- automatic session ID generation; -- user identity, authorization, or tenancy policy; -- provider-specific reasoning vocabularies or capability discovery; -- translating reasoning settings between provider protocols; -- backend-specific header configuration or other transport capabilities; -- changes to prompt or profile file formats; -- concurrency limits, admission queues, asynchronous jobs, or retries; or -- changes to model, backend, prompt, or profile selection. - -## Target End State - -This roadmap reaches its target end state when: - -- consumers can provide a direct per-run session ID without modifying prompt - variables or definitions; -- a direct session ID bypasses and takes precedence over the prompt session - template; -- direct and rendered session IDs share the established normalization and - maximum-length contract; -- the effective session ID is carried consistently through preparation, - hashing, execution, completed-run metadata, and injected clients; -- the built-in client sends the effective session ID only as the documented - top-level request-body field; -- consumers can inherit, replace, or explicitly disable a profile's reasoning - effort through one tri-state override; -- explicit reasoning disable results in omission from the provider request; -- effective reasoning remains visible in preparation, results, and the model - client boundary; -- the compatibility impact is identified for the next release according to - the pre-`v1` minor-release policy; and -- current-state GoDoc, consumer, integration, and internal documentation - describe the implemented behavior without relying on this roadmap. diff --git a/internal/usecase/repairer.go b/internal/usecase/repairer.go index 9202fc6..a705c74 100644 --- a/internal/usecase/repairer.go +++ b/internal/usecase/repairer.go @@ -17,6 +17,7 @@ type OutputRepairer interface { type RepairRequest struct { PreviousOutput string ValidationErrors []string + SessionID string Target domain.ExecutionTarget StructuredOutput *domain.StructuredOutputSpec Attempt int @@ -42,23 +43,26 @@ func (r *defaultOutputRepairer) Repair(ctx context.Context, req RepairRequest) ( errs = strings.Join(req.ValidationErrors, "\n") } - prompt := domain.RenderedPrompt{Messages: []domain.RenderedMessage{ - { - Role: "system", - Content: "You repair invalid JSON output. Return only corrected JSON. Do not include explanations or markdown code fences.", + prompt := domain.RenderedPrompt{ + SessionID: req.SessionID, + Messages: []domain.RenderedMessage{ + { + Role: "system", + Content: "You repair invalid JSON output. Return only corrected JSON. Do not include explanations or markdown code fences.", + }, + { + Role: "user", + Content: fmt.Sprintf( + "Repair attempt %d of %d for validation mode %s.\n\nValidation errors:\n%s\n\nPrevious output:\n%s\n\nReturn only corrected JSON.", + req.Attempt, + req.MaxAttempts, + req.Mode, + errs, + req.PreviousOutput, + ), + }, }, - { - Role: "user", - Content: fmt.Sprintf( - "Repair attempt %d of %d for validation mode %s.\n\nValidation errors:\n%s\n\nPrevious output:\n%s\n\nReturn only corrected JSON.", - req.Attempt, - req.MaxAttempts, - req.Mode, - errs, - req.PreviousOutput, - ), - }, - }} + } resp, err := r.llm.Generate(ctx, domain.GenerateRequest{ Prompt: prompt, diff --git a/internal/usecase/runner.go b/internal/usecase/runner.go index b7850c7..607d9dd 100644 --- a/internal/usecase/runner.go +++ b/internal/usecase/runner.go @@ -127,6 +127,7 @@ func (r *Runner) Run(ctx context.Context, req domain.RunRequest) (*domain.RunRes repairResp, repairErr := r.repairer.Repair(ctx, RepairRequest{ PreviousOutput: genResp.Content, ValidationErrors: validationResult.Errors, + SessionID: prepared.SessionID, Target: prepared.EffectiveModelParams, StructuredOutput: prepared.StructuredOutput, Attempt: attemptsUsed, diff --git a/internal/usecase/runner_test.go b/internal/usecase/runner_test.go index db421ed..bf58335 100644 --- a/internal/usecase/runner_test.go +++ b/internal/usecase/runner_test.go @@ -1690,6 +1690,41 @@ func TestRunnerRunStructuredRepairRemainsBoundedAndUsesEffectiveModelSettings(t } } +func TestRunnerRunRepairCarriesEffectiveSessionID(t *testing.T) { + llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: `{"broken":`}} + runner := NewRunnerWithRepairer( + &fakePromptRepo{def: promptDef(domain.FormatJSON, domain.ValidationJSON, 1)}, + &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{ + "exec": {ID: "exec", Endpoint: "http://example.test/v1", Model: "model"}, + }}, + nil, + defaultArtifactReader(), + defaultRenderer(), + llmClient, + validate.NewStandardValidator("."), + NewDefaultOutputRepairer(llmClient), + ) + + result, err := runner.Run(context.Background(), domain.RunRequest{ + PromptID: "p", + ProfileID: "exec", + SessionID: " repair-session ", + Inputs: singleInputRef(), + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if llmClient.calls != 2 { + t.Fatalf("expected initial generation and one repair, got %d calls", llmClient.calls) + } + if llmClient.lastReq.Prompt.SessionID != "repair-session" { + t.Fatalf("expected repair generation to retain effective session, got %q", llmClient.lastReq.Prompt.SessionID) + } + if result.SessionID != "repair-session" { + t.Fatalf("expected result to retain effective session, got %q", result.SessionID) + } +} + func TestRunnerRunJSONSchemaRepairCarriesStructuredOutputSpec(t *testing.T) { def := promptDef(domain.FormatJSON, domain.ValidationJSONSchema, 1) def.Validation.SchemaPath = "events.schema.json"