diff --git a/benchmarks/decision-engine/__pycache__/contract.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/contract.cpython-312.pyc new file mode 100644 index 0000000000..9aa1becbc0 Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/contract.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/demo.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/demo.cpython-312.pyc new file mode 100644 index 0000000000..f9022d531a Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/demo.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/engine.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/engine.cpython-312.pyc new file mode 100644 index 0000000000..3a8395133c Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/engine.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/evaluate.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/evaluate.cpython-312.pyc new file mode 100644 index 0000000000..af0ab77cd4 Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/evaluate.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/interpretation.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/interpretation.cpython-312.pyc new file mode 100644 index 0000000000..ebf2404102 Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/interpretation.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/local_chat.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/local_chat.cpython-312.pyc new file mode 100644 index 0000000000..53cd6f08fa Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/local_chat.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/native_model_eval.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/native_model_eval.cpython-312.pyc new file mode 100644 index 0000000000..1274d38fec Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/native_model_eval.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/pipeline.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/pipeline.cpython-312.pyc new file mode 100644 index 0000000000..f43f81cd7c Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/pipeline.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/projection.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/projection.cpython-312.pyc new file mode 100644 index 0000000000..5fe04a10ef Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/projection.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/run.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/run.cpython-312.pyc new file mode 100644 index 0000000000..4354c9d2b5 Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/run.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/state.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/state.cpython-312.pyc new file mode 100644 index 0000000000..b36e86ead5 Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/state.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/test_engine.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/test_engine.cpython-312.pyc new file mode 100644 index 0000000000..95fd5f340d Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/test_engine.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/test_native_model_eval.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/test_native_model_eval.cpython-312.pyc new file mode 100644 index 0000000000..2aff6b01d1 Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/test_native_model_eval.cpython-312.pyc differ diff --git a/benchmarks/decision-engine/__pycache__/test_pipeline.cpython-312.pyc b/benchmarks/decision-engine/__pycache__/test_pipeline.cpython-312.pyc new file mode 100644 index 0000000000..78bf79d8eb Binary files /dev/null and b/benchmarks/decision-engine/__pycache__/test_pipeline.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/compare_heads.cpython-312.pyc b/benchmarks/decision-model/__pycache__/compare_heads.cpython-312.pyc new file mode 100644 index 0000000000..434ac1c030 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/compare_heads.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/corpus.cpython-312.pyc b/benchmarks/decision-model/__pycache__/corpus.cpython-312.pyc new file mode 100644 index 0000000000..a77dc2d14f Binary files /dev/null and b/benchmarks/decision-model/__pycache__/corpus.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/encoder.cpython-312.pyc b/benchmarks/decision-model/__pycache__/encoder.cpython-312.pyc new file mode 100644 index 0000000000..1a0a9ea340 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/encoder.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/external_dataset.cpython-312.pyc b/benchmarks/decision-model/__pycache__/external_dataset.cpython-312.pyc new file mode 100644 index 0000000000..e9220f5d6e Binary files /dev/null and b/benchmarks/decision-model/__pycache__/external_dataset.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/github_bundle.cpython-312.pyc b/benchmarks/decision-model/__pycache__/github_bundle.cpython-312.pyc new file mode 100644 index 0000000000..69393b1cd2 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/github_bundle.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/github_collect.cpython-312.pyc b/benchmarks/decision-model/__pycache__/github_collect.cpython-312.pyc new file mode 100644 index 0000000000..49f43304a6 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/github_collect.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/head_model.cpython-312.pyc b/benchmarks/decision-model/__pycache__/head_model.cpython-312.pyc new file mode 100644 index 0000000000..9a30ee86ef Binary files /dev/null and b/benchmarks/decision-model/__pycache__/head_model.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/model_config.cpython-312.pyc b/benchmarks/decision-model/__pycache__/model_config.cpython-312.pyc new file mode 100644 index 0000000000..de09382ab2 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/model_config.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/run.cpython-312.pyc b/benchmarks/decision-model/__pycache__/run.cpython-312.pyc new file mode 100644 index 0000000000..b1e9f63cac Binary files /dev/null and b/benchmarks/decision-model/__pycache__/run.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/scenarios.cpython-312.pyc b/benchmarks/decision-model/__pycache__/scenarios.cpython-312.pyc new file mode 100644 index 0000000000..b004555e1d Binary files /dev/null and b/benchmarks/decision-model/__pycache__/scenarios.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/service.cpython-312.pyc b/benchmarks/decision-model/__pycache__/service.cpython-312.pyc new file mode 100644 index 0000000000..b6fe0fe948 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/service.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/smoke.cpython-312.pyc b/benchmarks/decision-model/__pycache__/smoke.cpython-312.pyc new file mode 100644 index 0000000000..b9229bbdf4 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/smoke.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/test_answer.cpython-312.pyc b/benchmarks/decision-model/__pycache__/test_answer.cpython-312.pyc new file mode 100644 index 0000000000..991622ce1d Binary files /dev/null and b/benchmarks/decision-model/__pycache__/test_answer.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/test_corpus.cpython-312.pyc b/benchmarks/decision-model/__pycache__/test_corpus.cpython-312.pyc new file mode 100644 index 0000000000..4adf8dfc07 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/test_corpus.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/test_external_dataset.cpython-312.pyc b/benchmarks/decision-model/__pycache__/test_external_dataset.cpython-312.pyc new file mode 100644 index 0000000000..23ad67f32c Binary files /dev/null and b/benchmarks/decision-model/__pycache__/test_external_dataset.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/test_head.cpython-312.pyc b/benchmarks/decision-model/__pycache__/test_head.cpython-312.pyc new file mode 100644 index 0000000000..199fab8146 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/test_head.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/test_service.cpython-312.pyc b/benchmarks/decision-model/__pycache__/test_service.cpython-312.pyc new file mode 100644 index 0000000000..be44456bb9 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/test_service.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/test_training_data.cpython-312.pyc b/benchmarks/decision-model/__pycache__/test_training_data.cpython-312.pyc new file mode 100644 index 0000000000..c223274b14 Binary files /dev/null and b/benchmarks/decision-model/__pycache__/test_training_data.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/train_head.cpython-312.pyc b/benchmarks/decision-model/__pycache__/train_head.cpython-312.pyc new file mode 100644 index 0000000000..1d74ecd9fa Binary files /dev/null and b/benchmarks/decision-model/__pycache__/train_head.cpython-312.pyc differ diff --git a/benchmarks/decision-model/__pycache__/training_data.cpython-312.pyc b/benchmarks/decision-model/__pycache__/training_data.cpython-312.pyc new file mode 100644 index 0000000000..c1f6b6cbca Binary files /dev/null and b/benchmarks/decision-model/__pycache__/training_data.cpython-312.pyc differ diff --git a/internal/assembly/boot/archive_effect_test.go b/internal/assembly/boot/archive_effect_test.go index 38ece56a30..0df1945416 100644 --- a/internal/assembly/boot/archive_effect_test.go +++ b/internal/assembly/boot/archive_effect_test.go @@ -44,16 +44,10 @@ func (p *bulkReplyProvider) lastRequestMessageWith(text string) string { return "" } -func (p *bulkReplyProvider) summaryRequests() int { +func (p *bulkReplyProvider) requestCount() int { p.mu.Lock() defer p.mu.Unlock() - n := 0 - for _, req := range p.reqs { - if req.Summary { - n++ - } - } - return n + return len(p.reqs) } // TestEffectCompactArchivesInsteadOfSummarizing pins the fold at the boundary a @@ -92,6 +86,7 @@ model = "x" } } + before := rec.requestCount() verdict, err := ctrl.Compact(context.Background(), agent.CompactRequest{}) if err != nil { t.Fatalf("Compact: %v", err) @@ -99,8 +94,8 @@ model = "x" if !verdict.Compacted() { t.Fatalf("verdict = %+v, want a fold", verdict) } - if n := rec.summaryRequests(); n != 0 { - t.Fatalf("a manual compaction made %d summarizer request(s)", n) + if n := rec.requestCount() - before; n != 0 { + t.Fatalf("a manual compaction made %d model request(s)", n) } if err := ctrl.Run(context.Background(), "what is left to do"); err != nil { @@ -169,6 +164,7 @@ model = "x" t.Fatalf("first fold = %+v, %v", v, err) } say(4) + before := rec.requestCount() again, err := ctrl.Compact(context.Background(), agent.CompactRequest{}) if err != nil { t.Fatalf("second Compact: %v", err) @@ -176,8 +172,8 @@ model = "x" if !again.Compacted() { t.Fatalf("a request by hand after a little more was said was declined: %+v (%s)", again, agent.CompactDeclineText(again.Reason)) } - if n := rec.summaryRequests(); n != 0 { - t.Fatalf("folding made %d summarizer request(s)", n) + if n := rec.requestCount() - before; n != 0 { + t.Fatalf("folding made %d model request(s)", n) } if _, detail := ctrl.ContextReport(); !strings.Contains(detail, "archived") || strings.Contains(detail, "summarized") { t.Fatalf("the report does not name the fold an archive:\n%s", detail) diff --git a/internal/assembly/boot/compaction_failed_batch_effect_test.go b/internal/assembly/boot/compaction_failed_batch_effect_test.go index 81ae034985..e97d2065ba 100644 --- a/internal/assembly/boot/compaction_failed_batch_effect_test.go +++ b/internal/assembly/boot/compaction_failed_batch_effect_test.go @@ -31,11 +31,8 @@ func (p *failedBatchProvider) Name() string { return "boot-failed-batch" } func (p *failedBatchProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) { p.mu.Lock() p.reqs = append(p.reqs, req) - summary := req.Summary round := p.rounds - if !summary { - p.rounds++ - } + p.rounds++ p.mu.Unlock() ch := make(chan provider.Chunk, failedBatchReads+3) call := func(id, name string, args any) { @@ -43,8 +40,6 @@ func (p *failedBatchProvider) Stream(_ context.Context, req provider.Request) (< ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: id, Name: name, Arguments: string(raw)}} } switch { - case summary: - ch <- provider.Chunk{Type: provider.ChunkText, Text: "## Goal\n- read the packages"} case round < 5: for i := range failedBatchReads { call(fmt.Sprintf("r%d-%d", round, i), "read_file", map[string]string{"path": fmt.Sprintf("src%d.txt", i)}) diff --git a/internal/assembly/boot/compaction_model_switch_effect_test.go b/internal/assembly/boot/compaction_model_switch_effect_test.go index 4f273533d0..51b6fecf16 100644 --- a/internal/assembly/boot/compaction_model_switch_effect_test.go +++ b/internal/assembly/boot/compaction_model_switch_effect_test.go @@ -10,12 +10,9 @@ import ( "reasonix/internal/contract/provider" ) -func digestRequests(reqs []provider.Request) (summarizer, carrying int) { +// carryingProjection counts the requests that already hold a fold. +func carryingProjection(reqs []provider.Request) (carrying int) { for _, req := range reqs { - if req.Summary { - summarizer++ - continue - } for _, m := range req.Messages { if strings.Contains(m.Content, "") { carrying++ @@ -23,7 +20,7 @@ func digestRequests(reqs []provider.Request) (summarizer, carrying int) { } } } - return summarizer, carrying + return carrying } // modelSwitchFixtureChars is the first-request size this fixture is padded to: @@ -91,7 +88,7 @@ context_window = 32500 t.Fatalf("Run(%q): %v", prompt, err) } } - if _, carrying := digestRequests(rec.requests()); carrying == 0 { + if carryingProjection(rec.requests()) == 0 { t.Fatal("fixture never compacted") } if err := from.Snapshot(); err != nil { @@ -110,16 +107,11 @@ context_window = 32500 t.Fatalf("the fixture's first request is %d characters, want %d: the padding did not reach it", got, modelSwitchFixtureChars) } before := len(rec.requests()) - summBefore, _ := digestRequests(rec.requests()) if err := to.Run(context.Background(), "after the switch"); err != nil { t.Fatalf("Run after switch: %v", err) } after := rec.requests()[before:] - summ, carrying := digestRequests(after) - if summ != 0 { - t.Fatalf("switch spent %d summarization call(s) (had %d before)", summ, summBefore) - } - if carrying == 0 { + if carryingProjection(after) == 0 { t.Fatalf("request after the switch dropped the projection; messages=%s", messageDigest(after[len(after)-1].Messages)) } } diff --git a/internal/assembly/boot/compaction_progress_effect_test.go b/internal/assembly/boot/compaction_progress_effect_test.go index ea0e875f0b..1b1a88a6bd 100644 --- a/internal/assembly/boot/compaction_progress_effect_test.go +++ b/internal/assembly/boot/compaction_progress_effect_test.go @@ -39,16 +39,11 @@ func (p *progressEffectProvider) Name() string { return "boot-progress-effect" } func (p *progressEffectProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) { p.mu.Lock() p.reqs = append(p.reqs, req) - digest := req.Summary round := p.rounds - if !digest { - p.rounds++ - } + p.rounds++ p.mu.Unlock() ch := make(chan provider.Chunk, 2) switch { - case digest: - ch <- provider.Chunk{Type: provider.ChunkText, Text: "## Files\n- notes.md was written"} case round < len(p.script): ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: p.script[round]} default: diff --git a/internal/assembly/boot/compaction_reported_usage_effect_test.go b/internal/assembly/boot/compaction_reported_usage_effect_test.go index 41f91a4e76..03f78cd5fb 100644 --- a/internal/assembly/boot/compaction_reported_usage_effect_test.go +++ b/internal/assembly/boot/compaction_reported_usage_effect_test.go @@ -19,7 +19,6 @@ import ( type promptSizeProvider struct { mu sync.Mutex reported int - digests int mains []provider.Request } @@ -29,13 +28,6 @@ func (p *promptSizeProvider) Stream(_ context.Context, req provider.Request) (<- p.mu.Lock() defer p.mu.Unlock() ch := make(chan provider.Chunk, 3) - if req.Summary { - p.digests++ - ch <- provider.Chunk{Type: provider.ChunkText, Text: "## Summary\nSUMMARY of the earlier work."} - ch <- provider.Chunk{Type: provider.ChunkDone} - close(ch) - return ch, nil - } p.mains = append(p.mains, req) ch <- provider.Chunk{Type: provider.ChunkText, Text: "ok"} ch <- provider.Chunk{Type: provider.ChunkUsage, Usage: &provider.Usage{PromptTokens: p.reported, TotalTokens: p.reported + 1}} @@ -44,10 +36,26 @@ func (p *promptSizeProvider) Stream(_ context.Context, req provider.Request) (<- return ch, nil } -func (p *promptSizeProvider) counts() (digests, mains int) { +func (p *promptSizeProvider) counts() (folded, mains int) { p.mu.Lock() defer p.mu.Unlock() - return p.digests, len(p.mains) + for _, req := range p.mains { + if carriesArchivePointer(req) { + folded++ + } + } + return folded, len(p.mains) +} + +// carriesArchivePointer is whether a request already holds a fold: the earlier +// conversation was moved to a file and a pointer stands in for it. +func carriesArchivePointer(req provider.Request) bool { + for _, m := range req.Messages { + if strings.Contains(m.Content, "archived, not summarized") { + return true + } + } + return false } // imageSession is about 70k estimated tokens that include one screenshot, @@ -123,7 +131,15 @@ context_window = 100000 } func TestEffectReportedUsageBelowTriggerLeavesContextAlone(t *testing.T) { - if digests, _ := foldsAfterTwoTurns(t, 60_000); digests != 0 { - t.Fatalf("digest requests = %d with the provider reporting 60k of 100k", digests) + if _, second := foldsAfterTwoTurns(t, 60_000); carriesArchivePointer(second) { + t.Fatal("a fold reached the request with the provider reporting 60k of 100k") + } +} + +// The control arm: the same run with the provider reporting more than the +// trigger does fold, so the assertion above can fail. +func TestEffectReportedUsageAboveTriggerFolds(t *testing.T) { + if _, second := foldsAfterTwoTurns(t, 95_000); !carriesArchivePointer(second) { + t.Fatal("a provider reporting 95k of 100k left the context unfolded") } } diff --git a/internal/assembly/boot/compaction_retention_effect_test.go b/internal/assembly/boot/compaction_retention_effect_test.go index 73ef353992..07420786d0 100644 --- a/internal/assembly/boot/compaction_retention_effect_test.go +++ b/internal/assembly/boot/compaction_retention_effect_test.go @@ -28,12 +28,8 @@ func (p *compactionEffectProvider) Stream(_ context.Context, req provider.Reques p.mu.Lock() p.reqs = append(p.reqs, req) p.mu.Unlock() - text := p.bulk - if req.Summary { - text = "## Standing facts\n- none recorded" - } ch := make(chan provider.Chunk, 2) - ch <- provider.Chunk{Type: provider.ChunkText, Text: text} + ch <- provider.Chunk{Type: provider.ChunkText, Text: p.bulk} ch <- provider.Chunk{Type: provider.ChunkDone} close(ch) return ch, nil diff --git a/internal/assembly/boot/compaction_system_change_effect_test.go b/internal/assembly/boot/compaction_system_change_effect_test.go index dc38361ad9..f563e74ada 100644 --- a/internal/assembly/boot/compaction_system_change_effect_test.go +++ b/internal/assembly/boot/compaction_system_change_effect_test.go @@ -47,7 +47,7 @@ func compactedSession(t *testing.T, rec *compactionEffectProvider, home, dir str t.Fatalf("Run(%q): %v", prompt, err) } } - if _, carrying := digestRequests(rec.requests()); carrying == 0 { + if carryingProjection(rec.requests()) == 0 { t.Fatal("fixture never compacted") } if err := from.Snapshot(); err != nil { diff --git a/internal/assembly/boot/context_budget_effect_test.go b/internal/assembly/boot/context_budget_effect_test.go index 758f17202b..0220cce6e4 100644 --- a/internal/assembly/boot/context_budget_effect_test.go +++ b/internal/assembly/boot/context_budget_effect_test.go @@ -35,11 +35,6 @@ func (p *budgetEffectProvider) Stream(_ context.Context, req provider.Request) ( p.mu.Unlock() ch := make(chan provider.Chunk, 2) defer close(ch) - if req.Summary { - ch <- provider.Chunk{Type: provider.ChunkText, Text: "## Standing facts\n- none recorded"} - ch <- provider.Chunk{Type: provider.ChunkDone} - return ch, nil - } asked, answered := "", "" for _, m := range req.Messages { switch { diff --git a/internal/assembly/boot/effect_test.go b/internal/assembly/boot/effect_test.go index 19cb492026..9e14ba3649 100644 --- a/internal/assembly/boot/effect_test.go +++ b/internal/assembly/boot/effect_test.go @@ -9,7 +9,6 @@ import ( "encoding/json" "os" "path/filepath" - "reasonix/internal/runtime/agent/testutil" "reflect" "strings" "sync" @@ -39,7 +38,7 @@ func (p *effectRecordingProvider) Stream(ctx context.Context, req provider.Reque p.rawInputs = append(p.rawInputs, agent.RawUserInput(ctx, "")) p.mu.Unlock() ch := make(chan provider.Chunk, 2) - ch <- provider.Chunk{Type: provider.ChunkText, Text: testutil.SummaryReply(req, "ok")} + ch <- provider.Chunk{Type: provider.ChunkText, Text: "ok"} ch <- provider.Chunk{Type: provider.ChunkDone} close(ch) return ch, nil diff --git a/internal/assembly/boot/goal_contract_effect_test.go b/internal/assembly/boot/goal_contract_effect_test.go index e2d1201912..54fe3a81c1 100644 --- a/internal/assembly/boot/goal_contract_effect_test.go +++ b/internal/assembly/boot/goal_contract_effect_test.go @@ -71,9 +71,6 @@ context_window = 32000 const contractMark = "Treat the user's goal as a task contract" for i, req := range rec.requests() { - if isSummarizerRequest(req) { - continue - } if n := strings.Count(userTextOf(req), contractMark); n > 2 { t.Fatalf("request %d carried the full contract %d times; copies are piling up", i, n) } diff --git a/internal/assembly/boot/superseded_effect_test.go b/internal/assembly/boot/superseded_effect_test.go deleted file mode 100644 index 8589c49b43..0000000000 --- a/internal/assembly/boot/superseded_effect_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package boot - -import ( - "reasonix/internal/contract/provider" -) - -// isSummarizerRequest tells the compaction summarizer's own call apart from the -// agent's. Only the agent's requests carry the projection under test. -func isSummarizerRequest(req provider.Request) bool { - return req.Summary -} diff --git a/internal/contract/provider/request.go b/internal/contract/provider/request.go index dd9d2b6c42..b071bcaef5 100644 --- a/internal/contract/provider/request.go +++ b/internal/contract/provider/request.go @@ -1,10 +1,5 @@ package provider -import "errors" - -// ErrSummaryToolsUnsupported refuses retained schemas before an unsafe broker POST. -var ErrSummaryToolsUnsupported = errors.New("provider cannot guarantee disabled summary tools") - // Request is a single completion request. type Request struct { Messages []Message @@ -19,9 +14,6 @@ type Request struct { // Mode is the session's model mode id. An adapter sends only a mode its // config declared for this endpoint, so an unknown one reaches no wire. Mode string `json:"Mode,omitempty"` - // Summary asks for a compaction briefing with tool use disabled. It crosses - // the provider broker so remote and local summaries have the same contract. - Summary bool `json:"summary,omitempty"` } // ResponseFormat asks a provider to constrain its output shape. diff --git a/internal/model/anthropic/anthropic.go b/internal/model/anthropic/anthropic.go index d4e6b97fd4..743d010cf0 100644 --- a/internal/model/anthropic/anthropic.go +++ b/internal/model/anthropic/anthropic.go @@ -478,7 +478,7 @@ func (c *client) buildRequest(_ context.Context, req provider.Request) anthReque r.Thinking = &thinkingConfig{Type: t} } } - return r.forSummary(req.Summary) + return r } // readStream parses the Messages API SSE stream into Chunks. Text deltas emit live; diff --git a/internal/model/anthropic/summary_request.go b/internal/model/anthropic/summary_request.go index bfa62af7fd..1977c1446f 100644 --- a/internal/model/anthropic/summary_request.go +++ b/internal/model/anthropic/summary_request.go @@ -3,10 +3,3 @@ package anthropic type toolChoice struct { Type string `json:"type"` } - -func (r anthRequest) forSummary(summary bool) anthRequest { - if summary && len(r.Tools) > 0 { - r.ToolChoice = &toolChoice{Type: "none"} - } - return r -} diff --git a/internal/model/anthropic/summary_tools_test.go b/internal/model/anthropic/summary_tools_test.go deleted file mode 100644 index 7af26aff88..0000000000 --- a/internal/model/anthropic/summary_tools_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package anthropic - -import ( - "context" - "reflect" - "testing" - - "reasonix/internal/contract/provider" -) - -func TestSummaryRetainsSchemasWithToolUseDisabled(t *testing.T) { - c := &client{model: "m", webSearch: true} - req := provider.Request{Tools: []provider.ToolSchema{{Name: "read_file"}}} - normal := c.buildRequest(context.Background(), req) - req.Summary = true - summary := c.buildRequest(context.Background(), req) - if !reflect.DeepEqual(normal.Tools, summary.Tools) || summary.ToolChoice == nil || summary.ToolChoice.Type != "none" || normal.ToolChoice != nil { - t.Fatalf("summary changed the catalog or retained tool use: %+v", summary) - } -} diff --git a/internal/model/openai/openai.go b/internal/model/openai/openai.go index 68f3100650..fdf9a9e909 100644 --- a/internal/model/openai/openai.go +++ b/internal/model/openai/openai.go @@ -758,7 +758,7 @@ func (c *client) buildRequest(req provider.Request) chatRequest { // is left untouched for backends that also honour it. out.Thinking = &thinkingMode{Type: c.thinkingType} } - return out.forSummary(req.Summary) + return out } func (c *client) buildPrefixRequest(req provider.Request, content, reasoning string) chatRequest { diff --git a/internal/model/openai/summary_request.go b/internal/model/openai/summary_request.go index 1af0c968f5..52f9563828 100644 --- a/internal/model/openai/summary_request.go +++ b/internal/model/openai/summary_request.go @@ -14,11 +14,3 @@ type chatRequest struct { Thinking *thinkingMode `json:"thinking,omitempty"` ExtraBody map[string]any `json:"-"` } - -func (r chatRequest) forSummary(summary bool) chatRequest { - if summary && len(r.Tools) > 0 { - r.ToolChoice = "none" - r.ExtraBody = omitExtraBodyFields(r.ExtraBody, "tool_choice") - } - return r -} diff --git a/internal/model/openai/summary_tools_test.go b/internal/model/openai/summary_tools_test.go deleted file mode 100644 index bec2cd29b4..0000000000 --- a/internal/model/openai/summary_tools_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package openai - -import ( - "encoding/json" - "testing" - - "reasonix/internal/contract/provider" -) - -func TestSummaryKeepsSchemasButCannotChooseATool(t *testing.T) { - c := &client{model: "m", extraBody: map[string]any{"tool_choice": "required"}} - req := provider.Request{Tools: []provider.ToolSchema{{Name: "read_file"}}, Summary: true} - raw, err := json.Marshal(c.buildRequest(req)) - if err != nil { - t.Fatal(err) - } - var body map[string]any - if err := json.Unmarshal(raw, &body); err != nil { - t.Fatal(err) - } - if body["tool_choice"] != "none" || len(body["tools"].([]any)) != 1 { - t.Fatalf("summary lost its schemas or allowed calls: %s", raw) - } - req.Summary = false - raw, err = json.Marshal(c.buildRequest(req)) - if err != nil { - t.Fatal(err) - } - if err := json.Unmarshal(raw, &body); err != nil || body["tool_choice"] != "required" { - t.Fatalf("normal request no longer honors extra_body: %s, %v", raw, err) - } -} diff --git a/internal/model/providerbroker/client.go b/internal/model/providerbroker/client.go index 55d49f76b1..6550e6af7a 100644 --- a/internal/model/providerbroker/client.go +++ b/internal/model/providerbroker/client.go @@ -165,16 +165,6 @@ func (b *brokered) Stream(ctx context.Context, req provider.Request) (<-chan pro if err != nil { return nil, err } - if req.Summary && len(req.Tools) > 0 { - // Capability and POST use one authenticated target, never the picker's stale cache. - catalog, probeErr := b.client.fetchCatalogAt(ctx, base, token) - if ctx.Err() != nil { - return nil, ctx.Err() - } - if probeErr != nil || !catalog.SummaryToolChoiceNone { - return nil, provider.ErrSummaryToolsUnsupported - } - } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, base+PathStream, bytes.NewReader(body)) if err != nil { return nil, err diff --git a/internal/model/providerbroker/server.go b/internal/model/providerbroker/server.go index b5f645d982..aae2364b08 100644 --- a/internal/model/providerbroker/server.go +++ b/internal/model/providerbroker/server.go @@ -61,7 +61,7 @@ func (s *Server) handleCatalog(w http.ResponseWriter, r *http.Request) { return } w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(catalogResponse{Providers: s.resolver.Catalog(), SummaryToolChoiceNone: true}) + _ = json.NewEncoder(w).Encode(catalogResponse{Providers: s.resolver.Catalog()}) } func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) { diff --git a/internal/model/providerbroker/summary_policy_test.go b/internal/model/providerbroker/summary_policy_test.go deleted file mode 100644 index 83c8f04a44..0000000000 --- a/internal/model/providerbroker/summary_policy_test.go +++ /dev/null @@ -1,233 +0,0 @@ -package providerbroker - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "sync/atomic" - "testing" - "time" - - "reasonix/internal/contract/provider" -) - -func TestSummaryCatalogRequiresFreshPolicySupport(t *testing.T) { - for _, mode := range []string{"old", "false", "invalid", "server error", "unauthorized", "timeout", "supported"} { - t.Run(mode, func(t *testing.T) { - var catalog, posts atomic.Int32 - var behavior atomic.Value - behavior.Store("supported") - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == PathCatalog { - catalog.Add(1) - switch behavior.Load().(string) { - case "old": - _, _ = w.Write([]byte(`{"providers":[]}`)) - case "false": - _, _ = w.Write([]byte(`{"providers":[],"summary_tool_choice_none":false}`)) - case "invalid": - _, _ = w.Write([]byte(`not json`)) - case "server error": - w.WriteHeader(http.StatusInternalServerError) - case "unauthorized": - w.WriteHeader(http.StatusUnauthorized) - case "timeout": - <-r.Context().Done() - default: - _, _ = w.Write([]byte(`{"providers":[],"summary_tool_choice_none":true}`)) - } - return - } - posts.Add(1) - _ = json.NewEncoder(w).Encode(encodeChunk(provider.Chunk{Type: provider.ChunkDone})) - })) - defer srv.Close() - c := NewClient(srv.URL, testToken, srv.Client()) - p, err := c.Resolve(provider.Selection{Ref: "p/m"}) - if err != nil { - t.Fatal(err) - } - behavior.Store(mode) - before := catalog.Load() - ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond) - defer cancel() - ch, err := p.Stream(ctx, provider.Request{Summary: true, Tools: []provider.ToolSchema{{Name: "read_file"}}}) - if mode == "supported" { - if err != nil { - t.Fatal(err) - } - drain(t, ch) - if posts.Load() != 1 { - t.Fatal("supported summary did not run") - } - } else if (mode != "timeout" && !errors.Is(err, provider.ErrSummaryToolsUnsupported)) || (mode == "timeout" && !errors.Is(err, context.DeadlineExceeded)) || posts.Load() != 0 { - t.Fatalf("unsupported summary sent: err=%v posts=%d", err, posts.Load()) - } - if catalog.Load() != before+1 { - t.Fatal("summary relied on cached catalog capability") - } - behavior.Store("old") - before = catalog.Load() - ch, err = p.Stream(t.Context(), provider.Request{Summary: true}) - if err != nil { - t.Fatal(err) - } - drain(t, ch) - ch, err = p.Stream(t.Context(), provider.Request{Tools: []provider.ToolSchema{{Name: "read_file"}}}) - if err != nil { - t.Fatal(err) - } - drain(t, ch) - if catalog.Load() != before { - t.Fatal("ordinary and independent requests gained a capability dependency") - } - }) - } -} - -func TestSummaryCapabilityIsPinnedToTheAuthenticatedTarget(t *testing.T) { - var aPosts, bPosts atomic.Int32 - var target atomic.Value - b := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get(HeaderToken) != "token-b" { - t.Error("wrong B token") - } - if r.URL.Path == PathCatalog { - _, _ = w.Write([]byte(`{"providers":[]}`)) - return - } - bPosts.Add(1) - })) - defer b.Close() - var switchDuringProbe atomic.Bool - a := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get(HeaderToken) != "token-a" { - t.Error("wrong A token") - } - if r.URL.Path == PathCatalog { - if switchDuringProbe.Load() { - target.Store(b.URL) - } - _, _ = w.Write([]byte(`{"providers":[],"summary_tool_choice_none":true}`)) - return - } - aPosts.Add(1) - _ = json.NewEncoder(w).Encode(encodeChunk(provider.Chunk{Type: provider.ChunkDone})) - })) - defer a.Close() - target.Store(a.URL) - c := NewEndpointClient(func() (string, string, error) { - url := target.Load().(string) - token := "token-a" - if url == b.URL { - token = "token-b" - } - return url, token, nil - }, a.Client()) - p, err := c.Resolve(provider.Selection{Ref: "p/m"}) - if err != nil { - t.Fatal(err) - } - req := provider.Request{Summary: true, Tools: []provider.ToolSchema{{Name: "read_file"}}} - switchDuringProbe.Store(true) - ch, err := p.Stream(t.Context(), req) - if err != nil { - t.Fatal(err) - } - drain(t, ch) - if aPosts.Load() != 1 || bPosts.Load() != 0 { - t.Fatalf("probe and POST did not share target: A=%d B=%d", aPosts.Load(), bPosts.Load()) - } - if _, err = p.Stream(t.Context(), req); err == nil || bPosts.Load() != 0 { - t.Fatal("positive A capability leaked to legacy B") - } -} - -func TestCurrentServerAdvertisesSummaryPolicy(t *testing.T) { - p := &fakeProvider{name: "p"} - s, err := NewServer(staticResolver(p), testToken) - if err != nil { - t.Fatal(err) - } - r := httptest.NewRequest(http.MethodGet, PathCatalog, nil) - r.Header.Set(HeaderToken, testToken) - w := httptest.NewRecorder() - s.Handler().ServeHTTP(w, r) - var body struct { - Summary bool `json:"summary_tool_choice_none"` - } - if json.Unmarshal(w.Body.Bytes(), &body) != nil || !body.Summary { - t.Fatal("new broker does not advertise policy") - } -} - -func TestSummaryCapabilityPreservesCancellation(t *testing.T) { - probing := make(chan struct{}) - var posts atomic.Int32 - var block atomic.Bool - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == PathCatalog { - if block.Load() { - close(probing) - <-r.Context().Done() - return - } - _, _ = w.Write([]byte(`{"providers":[]}`)) - return - } - posts.Add(1) - })) - defer srv.Close() - p, err := NewClient(srv.URL, testToken, srv.Client()).Resolve(provider.Selection{Ref: "p/m"}) - if err != nil { - t.Fatal(err) - } - block.Store(true) - ctx, cancel := context.WithCancel(t.Context()) - done := make(chan error, 1) - go func() { - _, err := p.Stream(ctx, provider.Request{Summary: true, Tools: []provider.ToolSchema{{Name: "read_file"}}}) - done <- err - }() - <-probing - cancel() - if err := <-done; !errors.Is(err, context.Canceled) || posts.Load() != 0 { - t.Fatalf("cancel err=%v posts=%d", err, posts.Load()) - } -} - -func TestSummaryCapabilityUsesRotatedTokenAtTheSameURL(t *testing.T) { - var token atomic.Value - token.Store("before") - var posts atomic.Int32 - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get(HeaderToken) != token.Load().(string) { - t.Error("stale token") - w.WriteHeader(http.StatusUnauthorized) - return - } - if r.URL.Path == PathCatalog { - _, _ = w.Write([]byte(`{"providers":[],"summary_tool_choice_none":true}`)) - return - } - posts.Add(1) - _ = json.NewEncoder(w).Encode(encodeChunk(provider.Chunk{Type: provider.ChunkDone})) - })) - defer srv.Close() - c := NewEndpointClient(func() (string, string, error) { return srv.URL, token.Load().(string), nil }, srv.Client()) - p, err := c.Resolve(provider.Selection{Ref: "p/m"}) - if err != nil { - t.Fatal(err) - } - token.Store("after") - ch, err := p.Stream(t.Context(), provider.Request{Summary: true, Tools: []provider.ToolSchema{{Name: "read_file"}}}) - if err != nil { - t.Fatal(err) - } - drain(t, ch) - if posts.Load() != 1 { - t.Fatal("rotated token summary did not reach provider") - } -} diff --git a/internal/model/providerbroker/tunnel_test.go b/internal/model/providerbroker/tunnel_test.go index d84df94b14..9427ddb871 100644 --- a/internal/model/providerbroker/tunnel_test.go +++ b/internal/model/providerbroker/tunnel_test.go @@ -83,7 +83,6 @@ func TestATurnCompletesOverTheReverseForward(t *testing.T) { } ch, err := p.Stream(context.Background(), provider.Request{ Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}, - Summary: true, }) if err != nil { t.Fatalf("Stream: %v", err) @@ -98,7 +97,7 @@ func TestATurnCompletesOverTheReverseForward(t *testing.T) { if got[2].Usage == nil || got[2].Usage.CacheHitTokens != 4 { t.Fatalf("usage did not survive the tunnel: %+v", got[2]) } - if sent := <-seen; len(sent.Messages) != 1 || sent.Messages[0].Content != "hi" || !sent.Summary { + if sent := <-seen; len(sent.Messages) != 1 || sent.Messages[0].Content != "hi" { t.Fatalf("the request did not reach the local provider intact: %+v", sent) } } diff --git a/internal/model/providerbroker/wire.go b/internal/model/providerbroker/wire.go index 3994a3457c..8247800b39 100644 --- a/internal/model/providerbroker/wire.go +++ b/internal/model/providerbroker/wire.go @@ -19,8 +19,7 @@ const ( ) type catalogResponse struct { - Providers []provider.Descriptor `json:"providers"` - SummaryToolChoiceNone bool `json:"summary_tool_choice_none"` + Providers []provider.Descriptor `json:"providers"` } type streamRequest struct { diff --git a/internal/model/responses/responses.go b/internal/model/responses/responses.go index f39e47f65e..eea32ac82f 100644 --- a/internal/model/responses/responses.go +++ b/internal/model/responses/responses.go @@ -256,7 +256,7 @@ func (c *client) Stream(ctx context.Context, req provider.Request) (<-chan provi } c.authed.Store(true) out := make(chan provider.Chunk, 64) - go c.readStream(requestCtx, resp, out, wireMessages, downgrade, !req.Summary) + go c.readStream(requestCtx, resp, out, wireMessages, downgrade, true) return out, nil } @@ -375,7 +375,6 @@ func (c *client) buildRequestBodyWithContinuation(req provider.Request, allowPre }) } body["tools"] = tools - applySummaryToolChoice(body, req.Summary) } instructions, rest := splitInstructions(messages) if instructions != "" { @@ -386,7 +385,7 @@ func (c *client) buildRequestBodyWithContinuation(req provider.Request, allowPre previousID, expectedDigest := c.lastResponseID, c.expectedPrefixDigest mode := c.effectiveModeLocked() c.mu.Unlock() - if allowPrevious && !req.Summary && mode == "stateful" && previousID != "" && len(messages) > 0 && + if allowPrevious && mode == "stateful" && previousID != "" && len(messages) > 0 && messages[len(messages)-1].Role == provider.RoleUser && c.conversationDigest(messages[:len(messages)-1]) == expectedDigest { body["input"] = messages[len(messages)-1].Content diff --git a/internal/model/responses/summary_request.go b/internal/model/responses/summary_request.go deleted file mode 100644 index 06fa46488d..0000000000 --- a/internal/model/responses/summary_request.go +++ /dev/null @@ -1,7 +0,0 @@ -package responses - -func applySummaryToolChoice(body map[string]any, summary bool) { - if summary { - body["tool_choice"] = "none" - } -} diff --git a/internal/model/responses/summary_tools_test.go b/internal/model/responses/summary_tools_test.go deleted file mode 100644 index 0ee5ab47e3..0000000000 --- a/internal/model/responses/summary_tools_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package responses - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "reflect" - "testing" - - "reasonix/internal/contract/provider" -) - -func TestSummaryKeepsSchemasWithoutAContinuationOrToolUse(t *testing.T) { - c := &client{model: "m", mode: "stateful", webSearch: true, lastResponseID: "main-response"} - prefix := []provider.Message{{Role: provider.RoleUser, Content: "task"}, {Role: provider.RoleAssistant, Content: "finding"}} - c.expectedPrefixDigest = c.conversationDigest(prefix) - req := provider.Request{Messages: append(prefix, provider.Message{Role: provider.RoleUser, Content: "summarize"}), Tools: []provider.ToolSchema{{Name: "read_file"}}} - normal, used, _ := c.buildRequestBody(req) - if !used { - t.Fatal("fixture did not exercise the normal continuation") - } - req.Summary = true - summary, used, _ := c.buildRequestBody(req) - if used || summary["previous_response_id"] != nil || summary["tool_choice"] != "none" || !reflect.DeepEqual(summary["tools"], normal["tools"]) { - t.Fatalf("summary changed tools or reused a live continuation: %+v", summary) - } -} - -func TestSummaryDoesNotReplaceTheMainResponseContinuation(t *testing.T) { - for _, invalid := range []bool{false, true} { - t.Run(fmt.Sprint("invalid=", invalid), func(t *testing.T) { - var bodies []map[string]any - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var body map[string]any - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Error(err) - } - bodies = append(bodies, body) - if invalid && len(bodies) == 2 { - writeEvents(w, `{"type":"response.completed","response":{"id":"poisoned","output":[{"type":"function_call","call_id":"c","name":"echo","arguments":{}}]}}`) - return - } - writeEvents(w, `{"type":"response.output_text.delta","item_id":"msg","delta":"answer"}`, - fmt.Sprintf(`{"type":"response.completed","response":{"id":"r%d"}}`, len(bodies))) - })) - defer srv.Close() - p := New(Config{Name: "relay", BaseURL: srv.URL, Model: "m"}) - req := provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "one"}}} - collect(t, p, req) - req.Messages = append(req.Messages, provider.Message{Role: provider.RoleAssistant, Content: "answer"}, provider.Message{Role: provider.RoleUser, Content: "summarize"}) - req.Summary = true - chunks := collect(t, p, req) - if invalid { - assertTerminalFailure(t, chunks, provider.ErrInvalidFunctionCall) - } - req.Summary = false - req.Messages[len(req.Messages)-1].Content = "two" - collect(t, p, req) - if bodies[1]["previous_response_id"] != nil || bodies[2]["previous_response_id"] != "r1" { - t.Fatalf("summary replaced the main continuation: %+v", bodies) - } - }) - } -} diff --git a/internal/runtime/agent/compact_cache_live_test.go b/internal/runtime/agent/compact_cache_live_test.go deleted file mode 100644 index 9b8c03e55e..0000000000 --- a/internal/runtime/agent/compact_cache_live_test.go +++ /dev/null @@ -1,145 +0,0 @@ -//go:build live_compaction - -package agent - -import ( - "context" - "encoding/json" - "net/http" - "os" - "strings" - "testing" - "time" - - "reasonix/internal/contract/event" - "reasonix/internal/contract/provider" - "reasonix/internal/contract/tool" - "reasonix/internal/model/openai" - "reasonix/internal/state/sessionstore" -) - -type liveSummaryRecorder struct { - provider.Provider - requests []provider.Request - results []liveSummaryResult -} -type liveSummaryResult struct { - Kind string `json:"kind"` - Milliseconds int64 `json:"milliseconds"` - Digest bool `json:"digest"` - Grounded bool `json:"grounded"` - ToolCalls int `json:"tool_calls"` - Usage *provider.Usage `json:"usage"` -} - -func (p *liveSummaryRecorder) Stream(ctx context.Context, req provider.Request) (<-chan provider.Chunk, error) { - p.requests = append(p.requests, req) - start := time.Now() - ch, err := p.Provider.Stream(ctx, req) - if err != nil { - return nil, err - } - out := make(chan provider.Chunk) - go func() { - defer close(out) - var text strings.Builder - var usage *provider.Usage - calls := 0 - for c := range ch { - if c.Type == provider.ChunkToolCall { - calls++ - } - if c.Type == provider.ChunkText { - text.WriteString(c.Text) - } - if c.Type == provider.ChunkUsage { - usage = c.Usage - } - select { - case out <- c: - case <-ctx.Done(): - return - } - } - kind := "warm" - if req.Summary { - kind = "reuse" - if len(req.Tools) == 0 { - kind = "independent" - } - } - p.results = append(p.results, liveSummaryResult{Kind: kind, Milliseconds: time.Since(start).Milliseconds(), Digest: hasDigestHeading(text.String()), Grounded: strings.Contains(text.String(), "ParseDuration") && strings.Contains(strings.ToLower(text.String()), "zero"), ToolCalls: calls, Usage: usage}) - }() - return out, nil -} - -func TestLiveCompactionPrefix(t *testing.T) { - key := os.Getenv("DEEPSEEK_API_KEY") - if key == "" { - t.Skip("DEEPSEEK_API_KEY is unset") - } - model := os.Getenv("REASONIX_LIVE_MODEL") - if model == "" { - model = "deepseek-flash" - } - savedTransport := http.DefaultTransport - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.DisableKeepAlives = true - http.DefaultTransport = transport - t.Cleanup(func() { transport.CloseIdleConnections(); http.DefaultTransport = savedTransport }) - p, err := openai.New(provider.Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: model, APIKey: key, Extra: map[string]any{"thinking": "disabled"}}) - if err != nil { - t.Fatal(err) - } - for _, rich := range []bool{false, true} { - for trial := range 3 { - rec := &liveSummaryRecorder{Provider: p} - messages := []provider.Message{ - {Role: provider.RoleSystem, Content: "You are a coding agent. Preserve the public API. " + strings.Repeat("Keep changes narrow and run relevant tests. ", 100)}, - {Role: provider.RoleUser, Content: "Investigate why ParseDuration accepts negative values. Preserve function names and return a typed error."}, - {Role: provider.RoleAssistant, Content: "Inspected duration.go: ParseDuration lacks a negative-value guard. Plan: reject values below zero and add boundary tests."}, - {Role: provider.RoleUser, Content: "Continue; ensure zero remains valid."}, - } - if rich { - messages = append(messages[:2], provider.Message{Role: provider.RoleAssistant, ReasoningContent: "Inspect the input guard.", ToolCalls: []provider.ToolCall{{ID: "call_read", Name: "read_file", Arguments: `{"path":"duration.go","private_payload":"synthetic-argument-marker"}`}}}, provider.Message{Role: provider.RoleTool, Name: "read_file", ToolCallID: "call_read", Content: "func ParseDuration(v int) (int,error) { return v,nil }"}, messages[2], messages[3]) - } - schemas := []provider.ToolSchema{{Name: "read_file", Description: "Read a file", Parameters: json.RawMessage(`{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}`)}} - a := New(rec, tool.NewRegistry(), &sessionstore.Session{Messages: messages}, Options{ContextWindow: 128000, MaxOutputTokens: 1024}, event.Discard) - ctx, cancel := context.WithTimeout(t.Context(), 180*time.Second) - ch, err := a.streamProviderRequest(ctx, freezeProviderRequest(provider.Request{Messages: messages, Tools: schemas, MaxTokens: 64})) - if err != nil { - cancel() - t.Fatal(err) - } - for c := range ch { - if c.Type == provider.ChunkError { - cancel() - t.Fatal(c.Err) - } - } - fold := messages[2 : len(messages)-1] - _, _, _, _, err = a.window().runCompactionSummary(ctx, fold, "") - if err != nil { - cancel() - t.Fatal(err) - } - _, _, err = a.window().summarizeAtMode(ctx, fold, "", 1024, 0, false) - cancel() - if err != nil { - t.Fatal(err) - } - for _, r := range rec.results { - if r.Kind != "warm" && (!r.Digest || !r.Grounded || r.ToolCalls != 0 || r.Usage == nil || r.Usage.FinishReason != "stop") { - t.Fatalf("summary contract failed: %+v", r) - } - } - data, _ := json.Marshal(struct { - Model string `json:"model"` - Rich bool `json:"rich"` - Trial int `json:"trial"` - Results []liveSummaryResult `json:"results"` - }{model, rich, trial + 1, rec.results}) - t.Log(string(data)) - } - } -} diff --git a/internal/runtime/agent/compact_fold_input_test.go b/internal/runtime/agent/compact_fold_input_test.go index 9be2df72cc..3d0e16e6d6 100644 --- a/internal/runtime/agent/compact_fold_input_test.go +++ b/internal/runtime/agent/compact_fold_input_test.go @@ -3,7 +3,6 @@ package agent import ( "context" "fmt" - "reasonix/internal/runtime/agent/testutil" "reasonix/internal/contract/provider" ) @@ -20,7 +19,7 @@ func (p *countingProvider) Name() string { return "counting" } func (p *countingProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) { p.got = append(p.got, req) ch := make(chan provider.Chunk, 2) - ch <- provider.Chunk{Type: provider.ChunkText, Text: testutil.SummaryReply(req, fmt.Sprintf("%s %d", p.reply, len(p.got)))} + ch <- provider.Chunk{Type: provider.ChunkText, Text: fmt.Sprintf("%s %d", p.reply, len(p.got))} ch <- provider.Chunk{Type: provider.ChunkDone} close(ch) return ch, nil diff --git a/internal/runtime/agent/compact_helpers_test.go b/internal/runtime/agent/compact_helpers_test.go index ae631bb9a9..30cfcf9552 100644 --- a/internal/runtime/agent/compact_helpers_test.go +++ b/internal/runtime/agent/compact_helpers_test.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "path/filepath" - "reasonix/internal/runtime/agent/testutil" "reasonix/internal/state/sessionstore" "slices" "strings" @@ -93,7 +92,7 @@ func (p *scriptedSummarizer) Stream(_ context.Context, req provider.Request) (<- r := p.replies[at] ch := make(chan provider.Chunk, 4) if r.text != "" { - ch <- provider.Chunk{Type: provider.ChunkText, Text: testutil.SummaryReply(req, r.text)} + ch <- provider.Chunk{Type: provider.ChunkText, Text: r.text} } if r.usage != nil { ch <- provider.Chunk{Type: provider.ChunkUsage, Usage: r.usage} diff --git a/internal/runtime/agent/compact_test.go b/internal/runtime/agent/compact_test.go index 5d74a84100..db3b218393 100644 --- a/internal/runtime/agent/compact_test.go +++ b/internal/runtime/agent/compact_test.go @@ -2,7 +2,6 @@ package agent import ( "context" - "reasonix/internal/runtime/agent/testutil" "reasonix/internal/state/sessionstore" "strings" "testing" @@ -44,7 +43,7 @@ func (f *fakeProvider) answer(req provider.Request) string { if f.verbatim { return f.reply } - return testutil.SummaryReply(req, f.reply) + return f.reply } func (f *fakeProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) { diff --git a/internal/runtime/agent/testutil/mock_provider.go b/internal/runtime/agent/testutil/mock_provider.go index 607e8a9ada..42662a98cd 100644 --- a/internal/runtime/agent/testutil/mock_provider.go +++ b/internal/runtime/agent/testutil/mock_provider.go @@ -95,7 +95,7 @@ func (p *MockProvider) Stream(ctx context.Context, req provider.Request) (<-chan chunks = append(chunks, provider.Chunk{Type: provider.ChunkReasoning, Text: t.Reasoning}) } if t.Text != "" { - chunks = append(chunks, provider.Chunk{Type: provider.ChunkText, Text: SummaryReply(req, t.Text)}) + chunks = append(chunks, provider.Chunk{Type: provider.ChunkText, Text: t.Text}) } for i := range t.ToolCalls { tc := t.ToolCalls[i] diff --git a/internal/runtime/agent/testutil/summary_contract.go b/internal/runtime/agent/testutil/summary_contract.go deleted file mode 100644 index 44959d87cf..0000000000 --- a/internal/runtime/agent/testutil/summary_contract.go +++ /dev/null @@ -1,23 +0,0 @@ -package testutil - -import ( - "strings" - - "reasonix/internal/contract/provider" -) - -// SummaryReply is what a double that follows the summary contract answers: the -// summarizer prompt requires headings, and the host refuses a reply with -// none. A plain reply to a summary request gets one, so a test that only cares -// about the digest's text keeps reading it. -func SummaryReply(req provider.Request, text string) string { - if !req.Summary { - return text - } - for line := range strings.SplitSeq(text, "\n") { - if hashes := len(line) - len(strings.TrimLeft(line, "#")); hashes >= 1 && hashes <= 6 && strings.HasPrefix(line[hashes:], " ") { - return text - } - } - return "## Summary\n" + text -} diff --git a/internal/runtime/guardian/guardian_test.go b/internal/runtime/guardian/guardian_test.go index 00d05c117f..4ce6d27225 100644 --- a/internal/runtime/guardian/guardian_test.go +++ b/internal/runtime/guardian/guardian_test.go @@ -7,7 +7,6 @@ import ( "fmt" "os" "path/filepath" - "reasonix/internal/runtime/agent/testutil" "reasonix/internal/state/sessionstore" "reflect" "slices" @@ -65,7 +64,7 @@ func (p *scriptedProvider) Stream(ctx context.Context, req provider.Request) (<- ch <- provider.Chunk{Type: provider.ChunkReasoning, Text: resp.reasoning} } if resp.text != "" { - ch <- provider.Chunk{Type: provider.ChunkText, Text: testutil.SummaryReply(req, resp.text)} + ch <- provider.Chunk{Type: provider.ChunkText, Text: resp.text} } if resp.usage != nil { ch <- provider.Chunk{Type: provider.ChunkUsage, Usage: resp.usage}