Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion contract/vectors/corpus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@
"files": {"requests": "requests.json", "toolResults": "tool-results.json", "events": "events.json", "checkpoint": "checkpoint.json"},
"expected": {"file": "expected.json"},
"tolerance": ["/timings", "/drawnMarkers/0"]
}]
}],
"agentResponse": {"steps": [
{"event": {"type": "text", "id": "reply", "text": "Inspecting."}},
{"tool": {"id": "schema-call", "name": "get_schema", "arguments": {}}},
{"event": {"type": "message", "id": "reply", "text": "Inspection complete.", "phase": "final"}}
]}
}
13 changes: 13 additions & 0 deletions contract/vectors/output/forged-label.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"description": "Model-authored label syntax grants no exemption, including at the record boundary.",
"schema": "run-event.json#/$defs/message",
"source": {"harness": {"repository": "Judgment-Pack/judgment-pack-agent", "package": "packages/harness"}, "files": ["packages/harness/outputGuard.ts"]},
"items": [{"type": "message", "text": "[redacted: [redacted: provider-key]]"}],
"guard": {
"input": "[redacted: sk-abcdefghijklmnopqrst]",
"output": "[redacted: [redacted: provider-key]]",
"where": "message",
"mode": "redact",
"counts": {"provider-key": 1}
}
}
2 changes: 2 additions & 0 deletions docs/adr/0017-the-output-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,5 @@ bound refusal can be resolved by shortening the output or simplifying host patte
The guard does not filter what the model reads, accept memories or proposals,
write host files, change tool admission, or change a Runtime verdict. The host's
policy controls and display of guarded counts remain host work.

The store-side guard exempts only exact labels emitted by that guard during the run; label-shaped model text is scanned and counted like other text.
2 changes: 2 additions & 0 deletions docs/adr/0018-records.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,5 @@ and independently checkable, while an old checkpoint remains readable during the
transition. Retention is an enforcement boundary on resume, not permission to keep a
record: a host may delete earlier. Export signatures and repository review records
remain future or host-owned work.

The store-side guard exempts only exact labels emitted by that guard during the run; label-shaped model text is scanned and counted like other text, and compaction summaries are guarded before projection and hashing so stored and resumed bytes agree.
2 changes: 2 additions & 0 deletions docs/adr/0022-the-evaluation-corpus.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ tolerances. It never compares a model's prose. `harness-check corpus` and the Ty
every entry with requests and tool results answered by the recording. Neither replay path accepts a
live-provider capability.

Agent request response bodies carry ordered `steps`, each holding an `event` or `tool` callback with lower-case wire member names; replay awaits each callback through the adapter, while legacy events-only seed bodies remain readable. TypeScript records these through `record: true`; Go hosts use `adapter.RecordAgentRun` before passing the captured request and body to `WriteCorpusEntry`.

A recording keeps a steered run's `steered` events as host facts, and replay delivers each recorded
steer at the same request boundary so that the newly emitted stream is compared at that boundary.

Expand Down
16 changes: 8 additions & 8 deletions go/adapter/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ type AgentRequest struct {
// (type text, its id and the text added), a finished message (type message, its id, its text and
// its phase, commentary or final), or the provider's own recovery notice (type retry, its attempt).
type AgentEvent struct {
Type string
ID string
Text string
Phase string
Attempt int
Type string `json:"type"`
ID string `json:"id,omitempty"`
Text string `json:"text,omitempty"`
Phase string `json:"phase,omitempty"`
Attempt int `json:"attempt,omitempty"`
}

// AgentToolCall is one tool call an invocation makes (AgentToolCall): the provider's call id, the
// name it was offered and the arguments as the model wrote them.
type AgentToolCall struct {
ID string
Name string
Arguments json.RawMessage
ID string `json:"id"`
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments"`
}

// AgentToolAnswer is the answer to one call (AgentToolAnswer): text only, and whether it failed.
Expand Down
23 changes: 22 additions & 1 deletion go/adapter/agent/invoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,17 @@ func (r *runState) invoke(ctx context.Context, prompt, instructions, phase strin
role = "critic"
}
if r.run.Connection != nil {
r.sc.Execution.UseConnection(r.run.Connection(role), role)
id := r.run.Connection(role)
r.sc.Execution.UseConnection(id, role)
if id != r.sc.ResolvedPolicy["models."+role+".provider"].Value {
selected := r.run.PolicyConnections[id].Agent
request.Model = selected.Model
request.Effort, _ = r.sc.ResolvedPolicy["models."+role+".reasoning"].Value.(string)
if setting := selected.Tiers[request.Effort]; setting != "" {
request.Effort = setting
}
admit = selected.Admit
}
}
request, err = admit(ctx, request)
if err != nil {
Expand Down Expand Up @@ -169,6 +179,17 @@ func (r *runState) invoke(ctx context.Context, prompt, instructions, phase strin
if from, to, ok := r.run.Failover(role, classified.Code); ok {
switched = true
agent = r.run.Agent
selected := r.run.PolicyConnections[to].Agent
request.Model = selected.Model
request.Effort, _ = r.sc.ResolvedPolicy["models."+role+".reasoning"].Value.(string)
if setting := selected.Tiers[request.Effort]; setting != "" {
request.Effort = setting
}
request, runErr = selected.Admit(ctx, request)
if runErr != nil {
break
}
body, marshalErr = json.Marshal(request)
attempt = 0
r.deliver(contract.RunEvent{ConnectionSwitched: &contract.RunEventConnectionSwitched{Type: "connection_switched", Role: role, From: from, To: to, Reason: classified.Code}})
// The replacement is a separate provider invocation and its request body belongs to
Expand Down
81 changes: 81 additions & 0 deletions go/adapter/corpus.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package adapter

import (
"context"
"encoding/json"
"fmt"
"sync"
)

// AgentRecordingStep preserves callback order, including calls between streamed events.
type AgentRecordingStep struct {
Event *AgentEvent `json:"event,omitempty"`
Tool *AgentToolCall `json:"tool,omitempty"`
}

// RecordAgentRun captures the capability's callbacks for a corpus request response body.
// The capture is in memory; the caller passes it through WriteCorpusEntry before storage.
func RecordAgentRun(run AgentRun, capture func(AgentRequest, json.RawMessage)) AgentRun {
return func(ctx context.Context, request AgentRequest, callbacks AgentCallbacks) error {
steps := []AgentRecordingStep{}
var mu sync.Mutex
err := run(ctx, request, AgentCallbacks{
Event: func(ctx context.Context, event AgentEvent) error {
mu.Lock()
steps = append(steps, AgentRecordingStep{Event: &event})
mu.Unlock()
return callbacks.Event(ctx, event)
},
Tool: func(ctx context.Context, call AgentToolCall) (AgentToolAnswer, error) {
held := call
held.Arguments = append(json.RawMessage(nil), call.Arguments...)
mu.Lock()
steps = append(steps, AgentRecordingStep{Tool: &held})
mu.Unlock()
return callbacks.Tool(ctx, call)
},
})
mu.Lock()
body, _ := json.Marshal(struct {
Steps []AgentRecordingStep `json:"steps"`
}{steps})
mu.Unlock()
capture(request, body)
return err
}
}

// ReplayAgentBody delivers recorded callbacks through the real adapter and its tool gate.
// Events-only seed entries remain readable.
func ReplayAgentBody(ctx context.Context, body json.RawMessage, callbacks AgentCallbacks) error {
var held struct {
Steps []AgentRecordingStep `json:"steps"`
Events []AgentEvent `json:"events"`
}
if err := json.Unmarshal(body, &held); err != nil {
return err
}
if held.Steps == nil {
for i := range held.Events {
held.Steps = append(held.Steps, AgentRecordingStep{Event: &held.Events[i]})
}
}
for _, step := range held.Steps {
if err := ctx.Err(); err != nil {
return err
}
if (step.Event == nil) == (step.Tool == nil) {
return fmt.Errorf("invalid recorded agent callback")
}
if step.Event != nil {
if err := callbacks.Event(ctx, *step.Event); err != nil {
return err
}
} else {
if _, err := callbacks.Tool(ctx, *step.Tool); err != nil {
return err
}
}
}
return nil
}
41 changes: 41 additions & 0 deletions go/adapter/eino/eino_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,16 @@ package eino_test

import (
"context"
"encoding/json"
"fmt"
"github.com/Judgment-Pack/judgment-pack-agent/go/adapter"
runcontext "github.com/Judgment-Pack/judgment-pack-agent/go/context"
"github.com/Judgment-Pack/judgment-pack-agent/go/contract"
"github.com/Judgment-Pack/judgment-pack-agent/go/relay"
"io"
"net/http"
"os"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -131,3 +139,36 @@ func TestTheApprovalPauseThroughTheCoordinator(t *testing.T) {
func TestTheDelegationCapacity(t *testing.T) {
runScenarios(t, certification.CapacityAt(withDelegation, 1), certification.CapacityAt(withDelegation, 2), certification.CapacityAt(withDelegation, 3))
}

func TestFallbackRebuildsGeminiModelAndBudget(t *testing.T) {
calls := 0
var events []contract.RunEvent
run := &adapter.Run{Wire: "gemini", ModelName: "primary-model", Thinking: "high", Prompt: "Answer briefly.", AllowConversation: true, Critic: "off", Context: &runcontext.Input{Connection: "primary"}, Policy: runcontext.PolicyLayer{"models": map[string]any{"author": map[string]any{"fallback": []any{"backup"}}}}, Emit: func(event contract.RunEvent) { events = append(events, event) }}
run.Model = func(context.Context, string, relay.Request) (*http.Response, error) {
return &http.Response{StatusCode: 503, Header: http.Header{}, Body: io.NopCloser(strings.NewReader(`{}`))}, nil
}
run.PolicyConnections = map[string]adapter.PolicyConnection{"backup": {Wire: "gemini", ModelName: "backup-model", Capabilities: &contract.CapabilityEndpointCapabilities{Tiers: &contract.CapabilityEndpointCapabilitiesTiers{Low: "1024", Medium: "2048", High: "4096"}}, Model: func(_ context.Context, suffix string, request relay.Request) (*http.Response, error) {
calls++
var body struct {
GenerationConfig struct{ ThinkingConfig struct{ ThinkingBudget int } }
}
if err := json.Unmarshal(request.Body, &body); err != nil {
t.Fatal(err)
}
if !strings.Contains(suffix, "models/backup-model:") || body.GenerationConfig.ThinkingConfig.ThinkingBudget != 4096 {
t.Errorf("backup settings: %s %s", suffix, request.Body)
}
return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"candidates":[{"content":{"role":"model","parts":[{"text":"Finished."}]},"finishReason":"STOP"}]}`))}, nil
}}}
if err := eino.New().Run(context.Background(), run); err != nil {
t.Fatal(err)
}
if calls != 1 {
t.Fatalf("backup calls: %d", calls)
}
for _, event := range events {
if event.Error != nil {
t.Fatalf("run error: %+v", event.Error)
}
}
}
86 changes: 86 additions & 0 deletions go/adapter/eino/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"net/http"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -266,6 +267,14 @@ func (r *runState) modelCall(phase func() string) wire.ModelCall {
r.sc.Execution.UseConnection(r.run.Connection(role), role)
}
if request.Method != http.MethodGet {
if r.run.Connection != nil && r.run.Connection(role) != r.sc.ResolvedPolicy["models."+role+".provider"].Value {
binding := r.run.PolicyConnections[r.run.Connection(role)]
var err error
suffix, request, err = fallbackRequest(binding, r.sc.ResolvedPolicy["models."+role+".reasoning"].Value.(string), suffix, request)
if err != nil {
return nil, err
}
}
if err := r.sc.ClaimRequest(ctx, request.Body); err != nil {
return nil, err
}
Expand Down Expand Up @@ -486,3 +495,80 @@ func (r *runState) reasoningField() wire.ReasoningField {
func (r *runState) imageBounds() wire.ImageBounds {
return wire.ImageBounds{Count: r.sc.Policy.ImagesPerRequest, Bytes: r.sc.Policy.ImageBytesPerRequest}
}

// fallbackRequest rebuilds model-dependent fields before prompt accounting and dispatch.
func fallbackRequest(binding adapter.PolicyConnection, tier, suffix string, request wire.ModelRequest) (string, wire.ModelRequest, error) {
body, err := wire.Parse(request.Body)
if err != nil {
return suffix, request, err
}
var overrides *wire.Value
if binding.Capabilities != nil {
raw, _ := json.Marshal(binding.Capabilities)
overrides, err = wire.Parse(raw)
if err != nil {
return suffix, request, err
}
}
caps, err := wire.CapabilitiesFor(wire.EndpointKind(binding.Wire), binding.ModelName, nil, overrides)
if err != nil {
return suffix, request, err
}
strip := func(value *wire.Value, keys []string) *wire.Value {
out := wire.Obj()
for _, field := range value.Members() {
omit := false
for _, key := range keys {
if key == field.Name {
omit = true
}
}
if !omit {
out.Set(field.Name, field.Value)
}
}
return out
}
body = strip(body, wire.TierMembers)
config := body.Get("generationConfig")
if config.IsObject() {
config = strip(config, []string{"thinkingConfig"})
body.Set("generationConfig", config)
}
var settings *wire.WireThinking
if caps.Reasoning.Kind != "none" {
settings = wire.WireFor(wire.ThinkingTier(tier), wire.DialectFor(caps.Reasoning.Kind), caps.Tiers, caps.FallbackTiers)
}
if binding.Wire == "gemini" {
if at := strings.Index(suffix, "models/"); at >= 0 {
if end := strings.Index(suffix[at:], ":"); end >= 0 {
suffix = suffix[:at] + "models/" + binding.ModelName + suffix[at+end:]
}
}
if !config.IsObject() {
config = wire.Obj()
body.Set("generationConfig", config)
}
if settings != nil {
for _, field := range settings.Members.Members() {
config.Set(field.Name, field.Value)
}
}
} else {
body.Set("model", wire.Str(binding.ModelName))
if binding.Wire == "anthropic" {
max := wire.ResponseTokens
if caps.OutputTokens != nil {
max = *caps.OutputTokens
}
body.Set("max_tokens", wire.Int(max))
}
if settings != nil {
for _, field := range settings.Members.Members() {
body.Set(field.Name, field.Value)
}
}
}
request.Body = body.JSON()
return suffix, request, nil
}
Loading
Loading