Files
promptkit/observations.go

201 lines
8.8 KiB
Go

package promptkit
import (
"context"
"errors"
"time"
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
)
// ExecutionObserver receives immutable metadata events synchronously. Callers
// must synchronize shared state if separate runs use the same observer. A
// callback must return for its run to progress; cancellation cannot interrupt
// consumer code already in the callback. Do not block on work requiring the
// same run's admission slot. Panics propagate without a promise of a terminal
// event, and Promptkit keeps no asynchronous queue or durable history.
type ExecutionObserver func(ExecutionEvent)
// ExecutionEventKind identifies one point in an execution's lifecycle.
type ExecutionEventKind string
const (
ExecutionStarted ExecutionEventKind = "started"
ExecutionAttempt ExecutionEventKind = "attempt"
ExecutionRetryDelay ExecutionEventKind = "retry_delay"
ExecutionFinished ExecutionEventKind = "finished"
)
// ExecutionOutcome reports whether the operation or attempt succeeded.
type ExecutionOutcome string
const (
ExecutionOutcomeSuccess ExecutionOutcome = "success"
ExecutionOutcomeInvalidOutput ExecutionOutcome = "invalid_output"
ExecutionOutcomeError ExecutionOutcome = "error"
)
// ExecutionFailureKind is a content-free classification of an error.
type ExecutionFailureKind string
const (
ExecutionFailureNone ExecutionFailureKind = ""
ExecutionFailureCanceled ExecutionFailureKind = "canceled"
ExecutionFailureDeadline ExecutionFailureKind = "deadline"
ExecutionFailureTransport ExecutionFailureKind = "transport"
ExecutionFailureProviderStatus ExecutionFailureKind = "provider_status"
ExecutionFailureMalformedResponse ExecutionFailureKind = "malformed_response"
ExecutionFailureCapacity ExecutionFailureKind = "capacity"
ExecutionFailureInvalidConfig ExecutionFailureKind = "invalid_config"
ExecutionFailureInvalidRequest ExecutionFailureKind = "invalid_request"
ExecutionFailurePromptNotFound ExecutionFailureKind = "prompt_not_found"
ExecutionFailureProfileNotFound ExecutionFailureKind = "profile_not_found"
ExecutionFailurePromptLoad ExecutionFailureKind = "prompt_load"
ExecutionFailureProfileLoad ExecutionFailureKind = "profile_load"
ExecutionFailureArtifactLoad ExecutionFailureKind = "artifact_load"
ExecutionFailurePromptRender ExecutionFailureKind = "prompt_render"
ExecutionFailureValidation ExecutionFailureKind = "validation"
ExecutionFailureGeneration ExecutionFailureKind = "generation"
ExecutionFailureOther ExecutionFailureKind = "other"
)
// ObservedTokenCount distinguishes an omitted count from a reported zero.
// On an attempt, Known and Complete are true together only for a reported
// non-null integer. An aggregate Value sums known contributions; Known means
// at least one attempt reported the field, and Complete means every dispatched
// attempt did. With no dispatched attempts, aggregate zero is known and
// complete. Unknown fields have Value zero.
type ObservedTokenCount struct {
Value int
Known bool
Complete bool
}
// ObservedTokenUsage contains presence-aware usage for one attempt or an
// aggregate. It does not prove complete provider billing.
type ObservedTokenUsage struct {
PromptTokens ObservedTokenCount
CompletionTokens ObservedTokenCount
TotalTokens ObservedTokenCount
CachedTokens ObservedTokenCount
CacheWriteTokens ObservedTokenCount
}
// ExecutionEvent contains metadata only, not provider text, request content,
// credentials, URLs, headers, or error objects. It has no stable JSON contract.
// Identifiers themselves may be sensitive. Nonterminal aggregate counters are
// zero; a terminal usage total must not be added to attempt usage.
//
// Generation is one for the initial generation and increases for repairs;
// Attempt is one-based within each generation. Dispatched means Promptkit
// invoked an HTTP attempt or injected Generate, not that a network packet was
// sent or billable work occurred. A canceled capacity acquisition emits a
// non-dispatched attempt with no usage. A retry-delay event describes the
// completed or interrupted wait after the indexed failed attempt.
//
// GenerationDuration excludes capacity waiting, retry delay, and callback
// work. Terminal phase durations sum measured phases; terminal Duration is
// wall time and may also include preparation, validation, and callbacks.
// Attempt success means generation succeeded, even if later validation rejects
// its output. Failure is classified metadata; RetriesExhausted is a separate
// flag and does not replace the underlying failure kind.
type ExecutionEvent struct {
Kind ExecutionEventKind
RunID string
Time time.Time
PromptID string
// PromptVersion is the selected definition's descriptive label. Each event
// owns its pointer; nil means absent while a non-nil empty string is explicit.
PromptVersion *string
ProfileID string
BackendID string
Model string
Generation uint64
Attempt uint64
Dispatched bool
CapacityWait time.Duration
GenerationDuration time.Duration
RetryDelay time.Duration
Duration time.Duration
Outcome ExecutionOutcome
Failure ExecutionFailureKind
StatusCode int
RetriesExhausted bool
Usage ObservedTokenUsage
Attempts uint64
TransientRetries uint64
RepairCalls uint64
}
func fromDomainExecutionEvent(value domain.ExecutionEvent) ExecutionEvent {
count := func(c domain.ObservedTokenCount) ObservedTokenCount {
return ObservedTokenCount{Value: c.Value, Known: c.Known, Complete: c.Complete}
}
return ExecutionEvent{
Kind: ExecutionEventKind(value.Kind), RunID: value.RunID, Time: value.Time,
PromptID: value.PromptID, PromptVersion: copyStringPtr(value.PromptVersion), ProfileID: value.ProfileID,
BackendID: value.BackendID, Model: value.Model, Generation: value.Generation,
Attempt: value.Attempt, Dispatched: value.Dispatched, CapacityWait: value.CapacityWait,
GenerationDuration: value.GenerationDuration, RetryDelay: value.RetryDelay, Duration: value.Duration,
Outcome: ExecutionOutcome(value.Outcome), Failure: ExecutionFailureKind(value.Failure),
StatusCode: value.StatusCode, RetriesExhausted: value.RetriesExhausted,
Usage: ObservedTokenUsage{PromptTokens: count(value.Usage.PromptTokens), CompletionTokens: count(value.Usage.CompletionTokens),
TotalTokens: count(value.Usage.TotalTokens), CachedTokens: count(value.Usage.CachedTokens), CacheWriteTokens: count(value.Usage.CacheWriteTokens)},
Attempts: value.Attempts, TransientRetries: value.TransientRetries, RepairCalls: value.RepairCalls,
}
}
func observedFinish(result *RunResult, err error) domain.ExecutionEvent {
if err == nil {
outcome := domain.ExecutionOutcomeSuccess
if result.Validation.Status == ValidationFailed {
outcome = domain.ExecutionOutcomeInvalidOutput
}
return domain.ExecutionEvent{Outcome: outcome}
}
value := domain.ExecutionEvent{Outcome: domain.ExecutionOutcomeError}
var exhausted *RetryExhaustedError
value.RetriesExhausted = errors.As(err, &exhausted)
var provider *GenerationError
if errors.As(err, &provider) {
value.StatusCode = provider.StatusCode()
}
switch {
case errors.Is(err, context.Canceled):
value.Failure = domain.ExecutionFailureCanceled
case errors.Is(err, context.DeadlineExceeded):
value.Failure = domain.ExecutionFailureDeadline
case errors.Is(err, ErrTransport):
value.Failure = domain.ExecutionFailureTransport
case value.StatusCode != 0:
value.Failure = domain.ExecutionFailureProviderStatus
case errors.Is(err, ErrMalformedResponse):
value.Failure = domain.ExecutionFailureMalformedResponse
case errors.Is(err, ErrCapacityExceeded):
value.Failure = domain.ExecutionFailureCapacity
case errors.Is(err, ErrInvalidConfig):
value.Failure = domain.ExecutionFailureInvalidConfig
case errors.Is(err, ErrPromptNotFound):
value.Failure = domain.ExecutionFailurePromptNotFound
case errors.Is(err, ErrProfileNotFound):
value.Failure = domain.ExecutionFailureProfileNotFound
case errors.Is(err, ErrPromptLoad):
value.Failure = domain.ExecutionFailurePromptLoad
case errors.Is(err, ErrProfileLoad):
value.Failure = domain.ExecutionFailureProfileLoad
case errors.Is(err, ErrInvalidRequest):
value.Failure = domain.ExecutionFailureInvalidRequest
case errors.Is(err, ErrArtifactLoad):
value.Failure = domain.ExecutionFailureArtifactLoad
case errors.Is(err, ErrPromptRender):
value.Failure = domain.ExecutionFailurePromptRender
case errors.Is(err, ErrValidation):
value.Failure = domain.ExecutionFailureValidation
case errors.Is(err, ErrLLMGenerate):
value.Failure = domain.ExecutionFailureGeneration
default:
value.Failure = domain.ExecutionFailureOther
}
return value
}