Files
DeepSeek-Reasonix/internal/runtime/agent/sampling_request.go
T
yhh 394da0a313 refactor: the kernel's packages live in the layer they belong to
internal/ was 141 packages side by side, so nothing but a reader's memory
said which were foundations and which assembled the rest. Each now sits in a
layer directory, and the directory is the declaration:

  base      L0  foundation with no domain in it
  contract  L1  types and interfaces the layers above agree on
  model, tools, safety, ext, state, platform
            L2  subsystems, peers of one another
  runtime   L3  the turn engine
  session   L4  the controller
  assembly  L5  the composition root
  frontend  L6  cli, serve, acp and the host adapters

Only paths change: package names, code and behaviour do not. Imports,
repolint and coverage baselines, sensitive paths, CI package lists and docs
follow the new paths; release notes keep the paths they were written with,
and sample paths inside tests and fixtures keep theirs.

repolint gains layer-order: a package may import its own layer and those
below it, read from where it lives. The 47 imports that point upward today
are recorded as that rule's opening debt (-allow-widen, a new rule's first
baseline) and can only go down. Most of it is config, which still reaches
into eight subsystems and is the next thing to take apart; the rest is
event, history and a few tools reaching into runtime for types that belong
in contract.
2026-09-23 18:28:21 -07:00

186 lines
7.1 KiB
Go

package agent
import (
"context"
"encoding/json"
"reasonix/internal/contract/event"
"reasonix/internal/contract/provider"
)
// samplingRequest is a once-prepared, frozen provider request for one model
// round. All stream retries replay this exact payload — no synthetic recovery
// messages, no schema reorder, no previous_response_id drift from failed attempts.
type samplingRequest struct {
req provider.Request
// overflowFolded records that this round already answered a context-overflow
// rejection by folding. The recovery is one-shot: a second rejection means
// the fold did not reach far enough, not that another one will.
overflowFolded bool
}
func (a *Agent) streamProviderRequest(ctx context.Context, req provider.Request) (<-chan provider.Chunk, error) {
return a.svc.prov.Stream(ctx, req)
}
func (a *Agent) handleSamplingError(
ctx context.Context,
attemptID string,
attempt int,
streamSink *deferredStreamSink,
frozen *samplingRequest,
result, last streamedTurn,
billable *provider.Usage,
) (retry bool, terminal streamedTurn) {
if provider.IsStreamInterrupted(result.err) && attempt < maxSamplingAttempts {
streamSink.Discard()
reason := provider.StreamInterruptReason(result.err)
a.emitStreamAttempt(attemptID, event.StreamAttemptDiscard, attempt, reason, result.err)
a.svc.sink.Emit(event.Event{
Kind: event.Retrying, RetryAttempt: attempt, RetryMax: maxStreamRecoveries,
RetryScope: event.RetryScopeStream,
})
if !streamRetrySleep(ctx, attempt) {
return false, streamedTurn{usage: finalizeSamplingUsage(billable, result.usage), attemptID: attemptID, interrupted: true, err: ctx.Err()}
}
return true, streamedTurn{}
}
if attempt < maxSamplingAttempts && a.recoverContextOverflow(ctx, frozen, result.err) {
streamSink.Discard()
a.emitStreamAttempt(attemptID, event.StreamAttemptDiscard, attempt, "context overflow", result.err)
return true, streamedTurn{}
}
// Exhausted retries or non-retryable error: leave the last speculative UI
// visible (no discard) so LocalOnly can mirror it.
streamSink.Flush()
last.usage = finalizeSamplingUsage(billable, result.usage)
return false, last
}
// prepareSamplingRequest freezes one model-round request (preflight + interceptors).
// Output budgets are resolved only here and never change the compact_ratio
// trigger. Physical overflow may attempt at most one recovery summary.
func (a *Agent) prepareSamplingRequest(ctx context.Context) (samplingRequest, error) {
frozen, err := a.buildSamplingRequest(ctx, CompactionTriggerPressure)
if err != nil {
return samplingRequest{}, err
}
if budget, clipped, budgetErr := a.effectiveOutputBudget(frozen.req); budgetErr != nil {
// One-shot physical overflow recovery. Do not loop.
if _, perr := a.contextManager().Prepare(ctx, ContextPreparePolicy{
Trigger: CompactionTriggerOverflow,
}); perr != nil {
return samplingRequest{}, budgetErr
}
rebuilt, rerr := a.buildSamplingRequest(ctx, CompactionTriggerPressure)
if rerr != nil {
return samplingRequest{}, rerr
}
if _, _, budgetErr2 := a.effectiveOutputBudget(rebuilt.req); budgetErr2 != nil {
return samplingRequest{}, budgetErr2
}
// Re-apply clipping on the recovered view.
if budget2, clipped2, err2 := a.effectiveOutputBudget(rebuilt.req); err2 == nil && clipped2 {
rebuilt.req.MaxTokens = budget2
}
shape := a.requestCalibrationShape(rebuilt.req)
a.sess.output.activeReqShape.Store(&shape)
return samplingRequest{req: freezeProviderRequest(rebuilt.req)}, nil
} else if clipped {
frozen.req.MaxTokens = budget
}
shape := a.requestCalibrationShape(frozen.req)
a.sess.output.activeReqShape.Store(&shape)
return samplingRequest{req: freezeProviderRequest(frozen.req)}, nil
}
func (a *Agent) buildSamplingRequest(ctx context.Context, trigger string) (samplingRequest, error) {
// CreatedAt is durable UI metadata, not model input. Strip it from the
// transport copy so wall-clock differences never invalidate the provider's
// prompt-cache prefix (and custom providers cannot accidentally send it).
prepared, err := a.contextManager().Prepare(ctx, ContextPreparePolicy{Trigger: trigger})
if err != nil {
return samplingRequest{}, err
}
// The host tails are already here: Prepare answers from modelVisibleMessages,
// and the host context tail appends whatever it is given again.
requestMessages := append([]provider.Message(nil), provider.ModelMessages(prepared.Messages)...)
requestMessages = a.providerProjectionMessages(requestMessages)
for i := range requestMessages {
requestMessages[i].CreatedAt = 0
}
// context.prepare: extensions may rewrite the message copy feeding THIS
// request. The session log is never touched — the replacement is
// ephemeral, so the next request starts from the unmodified history.
requestMessages, err = a.interceptContextPrepare(ctx, requestMessages)
if err != nil {
return samplingRequest{}, err
}
req := provider.Request{
Messages: requestMessages,
Tools: a.svc.tools.ProviderSchemas(ctx),
MaxTokens: a.maxOutputTokens,
Temperature: provider.OptionalTemperature(a.temperature),
ResponseFormat: responseFormatFromRequest(ctx),
}
// provider.request: the fully assembled request gets one last ruling
// (revalidated by the payload registry) before it goes on the wire.
req, err = a.interceptProviderRequest(ctx, req)
if err != nil {
return samplingRequest{}, err
}
return samplingRequest{req: req}, nil
}
// providerProjectionMessages applies provider-specific role compatibility to a
// request copy. Projection sidecars retain logical user-turn boundaries so
// explicit range compression can continue to resolve anchors across calls.
func (a *Agent) providerProjectionMessages(msgs []provider.Message) []provider.Message {
if a != nil && a.strictAlternatingRoles {
return coalesceProjectionUserRuns(msgs)
}
return msgs
}
// freezeProviderRequest deep-copies the provider-visible request surface so
// retries share identical messages, tools order, temperature, and format.
func freezeProviderRequest(req provider.Request) provider.Request {
out := req
if len(req.Messages) > 0 {
out.Messages = append([]provider.Message(nil), req.Messages...)
for i := range out.Messages {
if len(out.Messages[i].ToolCalls) > 0 {
out.Messages[i].ToolCalls = append([]provider.ToolCall(nil), out.Messages[i].ToolCalls...)
}
if len(out.Messages[i].Images) > 0 {
out.Messages[i].Images = append([]string(nil), out.Messages[i].Images...)
}
if len(out.Messages[i].ResponsesItems) > 0 {
items := make([]json.RawMessage, len(out.Messages[i].ResponsesItems))
for j, item := range out.Messages[i].ResponsesItems {
items[j] = append(json.RawMessage(nil), item...)
}
out.Messages[i].ResponsesItems = items
}
}
}
if len(req.Tools) > 0 {
out.Tools = make([]provider.ToolSchema, len(req.Tools))
for i, schema := range req.Tools {
out.Tools[i] = schema
if len(schema.Parameters) > 0 {
out.Tools[i].Parameters = append(json.RawMessage(nil), schema.Parameters...)
}
}
}
if req.Temperature != nil {
t := *req.Temperature
out.Temperature = &t
}
if req.ResponseFormat != nil {
rf := *req.ResponseFormat
out.ResponseFormat = &rf
}
return out
}