ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
198 lines
5.6 KiB
Go
198 lines
5.6 KiB
Go
// Package sidequestion captures immutable main-agent checkpoints. It never owns
|
|
// an agent session or a tool executor.
|
|
package sidequestion
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"iter"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Autumn-27/norma/harness"
|
|
"github.com/Autumn-27/norma/llm"
|
|
)
|
|
|
|
type Parent struct {
|
|
ConversationID int64 `json:"conversation_id,omitempty"`
|
|
TaskID int64 `json:"task_id,omitempty"`
|
|
ExplorationID int64 `json:"exploration_id,omitempty"`
|
|
IntentID int64 `json:"intent_id,omitempty"`
|
|
}
|
|
|
|
func (p Parent) Key() string {
|
|
if p.ConversationID > 0 {
|
|
return fmt.Sprintf("conv-%d", p.ConversationID)
|
|
}
|
|
if p.IntentID > 0 {
|
|
return fmt.Sprintf("task-%d-exp-%d-worker-i%d", p.TaskID, p.ExplorationID, p.IntentID)
|
|
}
|
|
return fmt.Sprintf("task-%d-exp-%d-main", p.TaskID, p.ExplorationID)
|
|
}
|
|
|
|
// Model contains only configuration identity, never credentials or proxy URLs.
|
|
type Model struct {
|
|
ProfileID int64 `json:"profile_id"`
|
|
Name string `json:"name"`
|
|
Format string `json:"format"`
|
|
Model string `json:"model"`
|
|
Identity string `json:"identity"`
|
|
Streaming bool `json:"streaming"`
|
|
WindowTokens int `json:"window_tokens"`
|
|
}
|
|
|
|
type Snapshot struct {
|
|
Parent Parent `json:"parent"`
|
|
RunID int64 `json:"run_id"`
|
|
Version int64 `json:"version"`
|
|
CapturedAt time.Time `json:"captured_at"`
|
|
Model Model `json:"model"`
|
|
Request llm.CompletionRequest `json:"request"`
|
|
}
|
|
|
|
func CloneRequest(req llm.CompletionRequest) (llm.CompletionRequest, error) {
|
|
b, err := json.Marshal(req)
|
|
if err != nil {
|
|
return llm.CompletionRequest{}, err
|
|
}
|
|
var out llm.CompletionRequest
|
|
err = json.Unmarshal(b, &out)
|
|
return out, err
|
|
}
|
|
|
|
type Publisher func(Snapshot)
|
|
type publisherKey struct{}
|
|
type captureKey struct{}
|
|
type attemptKey struct{}
|
|
|
|
func WithPublisher(ctx context.Context, publish Publisher) context.Context {
|
|
return context.WithValue(ctx, publisherKey{}, publish)
|
|
}
|
|
|
|
type Capture struct {
|
|
mu sync.Mutex
|
|
parent Parent
|
|
runID int64
|
|
version int64
|
|
publish Publisher
|
|
last *Snapshot
|
|
}
|
|
|
|
// Attach uses QueryDeps rather than a global provider hook so compaction and
|
|
// other auxiliary completions cannot replace the main conversation checkpoint.
|
|
func Attach(ctx context.Context, parent Parent, deps harness.QueryDeps, provider llm.Provider) (context.Context, harness.QueryDeps) {
|
|
publish, _ := ctx.Value(publisherKey{}).(Publisher)
|
|
if publish == nil {
|
|
return ctx, deps
|
|
}
|
|
c := &Capture{parent: parent, runID: time.Now().UnixNano(), publish: publish}
|
|
stream, complete := deps.CallModel, deps.CallModelSync
|
|
if stream == nil {
|
|
stream = provider.Stream
|
|
}
|
|
if complete == nil {
|
|
complete = provider.Complete
|
|
}
|
|
deps.CallModel = func(ctx context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
|
|
return stream(context.WithValue(ctx, attemptKey{}, c), req)
|
|
}
|
|
deps.CallModelSync = func(ctx context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error) {
|
|
return complete(context.WithValue(ctx, attemptKey{}, c), req)
|
|
}
|
|
return context.WithValue(ctx, captureKey{}, c), deps
|
|
}
|
|
|
|
func (c *Capture) save(req llm.CompletionRequest, model Model) {
|
|
copy, err := CloneRequest(req)
|
|
if err != nil {
|
|
return
|
|
} // Observability must not break the main run.
|
|
c.mu.Lock()
|
|
c.version++
|
|
s := Snapshot{Parent: c.parent, RunID: c.runID, Version: c.version, CapturedAt: time.Now().UTC(), Model: model, Request: copy}
|
|
c.last = &s
|
|
c.mu.Unlock()
|
|
c.publish(s)
|
|
}
|
|
|
|
// Finish adds tool results that landed after the final model request. Norma's
|
|
// first API message is its host reminder, absent from Terminal.Messages.
|
|
func Finish(ctx context.Context, messages []llm.Message) {
|
|
c, _ := ctx.Value(captureKey{}).(*Capture)
|
|
if c == nil {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
s := c.last
|
|
c.mu.Unlock()
|
|
if s == nil {
|
|
return
|
|
}
|
|
req := s.Request
|
|
var prefix []llm.Message
|
|
if len(req.Messages) > 0 && strings.HasPrefix(req.Messages[0].Text(), "<system-reminder>") {
|
|
prefix = append(prefix, req.Messages[0])
|
|
}
|
|
req.Messages = append(prefix, llm.MessagesForAPI(messages)...)
|
|
c.save(req, s.Model)
|
|
}
|
|
|
|
type boundProvider struct {
|
|
inner llm.Provider
|
|
model Model
|
|
}
|
|
|
|
// Bind belongs immediately around a concrete provider, inside any routing pool.
|
|
func Bind(inner llm.Provider, model Model) llm.Provider { return &boundProvider{inner, model} }
|
|
|
|
func (p *boundProvider) Stream(ctx context.Context, req llm.CompletionRequest) iter.Seq2[llm.StreamEvent, error] {
|
|
return func(yield func(llm.StreamEvent, error) bool) {
|
|
c, _ := ctx.Value(attemptKey{}).(*Capture)
|
|
if c == nil {
|
|
for ev, err := range p.inner.Stream(ctx, req) {
|
|
if !yield(ev, err) {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
c.save(req, p.model)
|
|
acc := llm.NewAccumulator()
|
|
failed := false
|
|
complete := false
|
|
for ev, err := range p.inner.Stream(ctx, req) {
|
|
if err != nil {
|
|
failed = true
|
|
} else {
|
|
acc.Add(ev)
|
|
if ev.Type == llm.SEMessageStop {
|
|
complete = true
|
|
}
|
|
}
|
|
if !yield(ev, err) {
|
|
return
|
|
}
|
|
}
|
|
if !failed && complete && ctx.Err() == nil {
|
|
msg := acc.Message()
|
|
req.Messages = llm.MessagesForAPI(append(append([]llm.Message{}, req.Messages...), msg))
|
|
c.save(req, p.model)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *boundProvider) Complete(ctx context.Context, req llm.CompletionRequest) (llm.Message, string, llm.Usage, error) {
|
|
c, _ := ctx.Value(attemptKey{}).(*Capture)
|
|
if c != nil {
|
|
c.save(req, p.model)
|
|
}
|
|
msg, stop, usage, err := p.inner.Complete(ctx, req)
|
|
if c != nil && err == nil && ctx.Err() == nil {
|
|
req.Messages = llm.MessagesForAPI(append(append([]llm.Message{}, req.Messages...), msg))
|
|
c.save(req, p.model)
|
|
}
|
|
return msg, stop, usage, err
|
|
}
|