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
36 changes: 36 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,42 @@ command hooks.

Oracle mode is **opt-in and gated**: it only activates when `fusion.enabled = true`, `fusion.oracle_mode = true`, and the gate is in `oracle` mode. Unlike PoC mode, oracle mode does **not** use first-pass-wins; all candidates run to completion, a single judge evaluates all outputs in randomized order, and the highest-scoring candidate wins. Default cost cap is tighter ($2.00) and `fusion__oracle_tournament` is `ask` policy (M4).

### Context Compaction Modes (first PR — compaction-modes)

`cmd/sin-code/internal/agentloop/compaction_types.go` adds a mode-based
compaction layer on top of the legacy `CompactionStrategy` switch
(issue #278). The new API (`Compect2(ctx, CompactInput) CompactResult`)
honours mandate **M3**: verification evidence is always preserved
(`agentloop.compaction_preserve_evidence=true` by default).

| Config key | Type | Default | Used by |
|---|---|---|---|
| `agentloop.context_compaction` | enum | `"off"` | loopbuilder — selects the new compaction mode (off\|deterministic\|llm\|hybrid) |
| `agentloop.compaction_trigger` | enum | `"tokens"` | agentloop — when the compactor fires (turns\|tokens\|both) |
| `agentloop.context_window` | int | `0` (auto) | agentloop — effective token cap; 0 = `CompactionMaxTokens * 4` |
| `agentloop.compaction_preserve_evidence` | bool | `true` | agentloop — keep system prompt, first user goal, last N turns, role:tool messages, and any message matching `VERIFICATION PASSED \| VERIFICATION FAILED \| NOT DONE \| Open acceptance criteria` |
| `agentloop.compaction_recent_turns` | int | `4` | agentloop — number of recent human turns the retain rule keeps |
| `agentloop.compaction_max_tokens` | int | `8000` | agentloop — token budget for compacted messages |

**Request-only compaction** (mandate M3): when a non-off Mode is
selected, `Loop.Run` keeps the persisted `msgs` slice full in the
session DB and produces a deterministically compacted view for the
model via `Compact2`. The legacy `Compact()` API (used by
`agentloop.compaction_strategy=…` callers) remains unchanged for
backward compatibility.

**Sidecar snapshots**: lossy modes (`llm`, `hybrid`) write a
content-addressed JSON snapshot to
`~/.local/share/sin-code/context-snapshots/<session-id-hash>/turn-NNNNN.json`
(atomic temp+rename). The loop's persisted history stays complete; the
sidecar is the audit/rollback artifact and the surface that downstream
`compressor_test.go`-style golden harnesses consume (issue #172).

**Byte-stability**: every `Compact2` output is byte-stable per
`(input, Config)` pair so the eval harness (issue #171), the four-arm
comparator, and the compress unit tests can pin deterministic
snapshots.


### Model Performance Registry (issue #395)

Expand Down
197 changes: 197 additions & 0 deletions cmd/sin-code/analyse_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// SPDX-License-Identifier: MIT
// Purpose: `sin-code analyse` — passive bridge shell for the
// sin-analyse-suite Python MCP server (OpenSIN-Code/sin-analyse-suite).
// The actual tool surface lives in the upstream Python MCP; this CLI is
// read-only introspection only (status / tools / doctor). No tool calls
// are dispatched, no files are modified from this subcommand.
//
// The upstream binary is `sin-analyse` (invoked as `sin-analyse serve`
// per the registry entry in cmd/sin-code/internal/<bridge>/registry.go).
//
// Subcommands:
//
// analyse status # is sin-analyse on PATH + version banner
// analyse tools # curated catalogue of analyse__* tools
// analyse doctor # comprehensive readiness check (PATH, API, version)
//
// This file is deliberately NOT registered in cmd/sin-code/main.go.
// The Python upstream is still landing; the shell stays self-test-only
// until the MCP tool surface stabilises.
package main

import (
"fmt"
"os"
"os/exec"
"text/tabwriter"

"github.com/spf13/cobra"
)

// analyseBinary is the upstream Python MCP entry point.
const analyseBinary = "sin-analyse"

// analyseSuiteVersion is the expected upstream version. Override at
// runtime with SIN_ANALYSE_EXPECTED_VERSION.
const analyseSuiteVersion = "v0.1.0"

// analyseRegistry is the curated list of analyse__* tools shipped by
// the upstream suite. Kept here (not imported from Python) so the CLI
// shell is byte-stable and self-test-only.
var analyseRegistry = []struct {
Name string
Description string
}{
{"analyse__image_extract", "Extract text + metadata from images (OCR, EXIF, scene)."},
{"analyse__pdf_parse", "Parse PDF documents to structured text + tables."},
{"analyse__log_analyze", "Analyze log files: error clusters, tail histograms, anomaly hints."},
{"analyse__data_detect", "Detect data-file schema (CSV/Parquet/JSON/Arrow) and infer types."},
{"analyse__audio_transcribe", "Transcribe audio files via Whisper-1 / local whisper.cpp."},
{"analyse__video_extract", "Extract keyframes + audio track from video files."},
}

// effectiveVersion returns the env-overridable expected version. Empty
// env means fall back to the compile-time constant.
func effectiveVersion() string {
if v := os.Getenv("SIN_ANALYSE_EXPECTED_VERSION"); v != "" {
return v
}
return analyseSuiteVersion
}

// NewAnalyseCmd returns the `sin-code analyse` subcommand shell.
// Three read-only subcommands; no mutating ops by design.
func NewAnalyseCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "analyse",
Short: "Passive bridge shell for the sin-analyse-suite Python MCP (read-only)",
Long: `sin-code analyse is a passive bridge to the sin-analyse-suite Python
MCP server (OpenSIN-Code/sin-analyse-suite). It performs read-only
introspection only — no tool calls are dispatched, no files are
modified. Use ` + "`sin-code serve`" + ` to consume the upstream MCP
tools over JSON-RPC.

Subcommands:
status PATH lookup + version banner
tools curated catalogue of analyse__* tools (byte-stable)
doctor comprehensive readiness check (PATH, API, version)`,
SilenceUsage: true,
}
cmd.AddCommand(newAnalyseStatusCmd())
cmd.AddCommand(newAnalyseToolsCmd())
cmd.AddCommand(newAnalyseDoctorCmd())
return cmd
}

func newAnalyseStatusCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Check if `sin-analyse` is on PATH and print version banner",
Long: `Resolves the upstream Python MCP entry point (sin-analyse) on the
host PATH. If found, prints the resolved path and the expected suite
version. If missing, exits non-zero with install guidance.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
out := cmd.OutOrStdout()
path, err := exec.LookPath(analyseBinary)
if err != nil {
fmt.Fprintf(out, "✗ %s: not found on PATH\n", analyseBinary)
fmt.Fprintf(out, " expected suite version: %s\n", effectiveVersion())
fmt.Fprintf(out, " install: pipx install sin-analyse-suite\n")
return fmt.Errorf("%s not on PATH", analyseBinary)
}
tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "FIELD\tVALUE")
fmt.Fprintf(tw, "binary\t%s\n", analyseBinary)
fmt.Fprintf(tw, "path\t%s\n", path)
fmt.Fprintf(tw, "expected_version\t%s\n", effectiveVersion())
fmt.Fprintf(tw, "invoke\t%s serve\n", path)
fmt.Fprintf(tw, "shell_mode\tpassive (read-only introspection)\n")
return tw.Flush()
},
}
return cmd
}

func newAnalyseToolsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tools",
Short: "List the curated catalogue of analyse__* tools",
Long: `Prints the byte-stable table of analyse__* tools exposed by the
upstream sin-analyse-suite MCP. This is the local registry view — the
authoritative live list comes from ` + "`sin-code serve`" + ` over
stdio JSON-RPC.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
out := cmd.OutOrStdout()
tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, "TOOL\tDESCRIPTION")
fmt.Fprintf(tw, "(count)\t%d\n", len(analyseRegistry))
for _, t := range analyseRegistry {
fmt.Fprintf(tw, "%s\t%s\n", t.Name, t.Description)
}
return tw.Flush()
},
}
return cmd
}

func newAnalyseDoctorCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "doctor",
Short: "Comprehensive readiness check for sin-analyse-suite",
Long: `Runs all readiness checks:
- PATH lookup for sin-analyse
- API reachability placeholder (skipped until upstream ships)
- Version compatibility against expected suite version

The doctor is advisory only — it never modifies state. Each check
prints PASS / FAIL / SKIP; overall exit is non-zero only when a hard
FAIL (binary missing) is reported.`,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
out := cmd.OutOrStdout()
var hardFail bool

// Check 1 — binary on PATH.
path, lookErr := exec.LookPath(analyseBinary)
if lookErr != nil {
fmt.Fprintf(out, "[FAIL] binary %s not on PATH\n", analyseBinary)
fmt.Fprintf(out, " install: pipx install sin-analyse-suite\n")
hardFail = true
} else {
fmt.Fprintf(out, "[ OK ] binary %s -> %s\n", analyseBinary, path)
}

// Check 2 — API reachability. Passive shell — no live probe.
fmt.Fprintf(out, "[SKIP] api reachability probe not wired (passive shell, waiting on upstream)\n")

// Check 3 — version compatibility. Cannot probe without invoking
// `sin-analyse --version`, which would exceed the passive contract.
fmt.Fprintf(out, "[SKIP] version expected %s; cannot probe without invoking %s --version\n",
effectiveVersion(), analyseBinary)

tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw)
fmt.Fprintln(tw, "FIELD\tVALUE")
fmt.Fprintf(tw, "binary\t%s\n", analyseBinary)
fmt.Fprintf(tw, "expected_version\t%s\n", effectiveVersion())
fmt.Fprintf(tw, "tool_count\t%d\n", len(analyseRegistry))
fmt.Fprintf(tw, "shell_mode\tpassive\n")
fmt.Fprintf(tw, "override_env\tSIN_ANALYSE_EXPECTED_VERSION\n")
if hardFail {
fmt.Fprintf(tw, "verdict\tFAIL\n")
} else {
fmt.Fprintf(tw, "verdict\tOK\n")
}
if err := tw.Flush(); err != nil {
return err
}
if hardFail {
return fmt.Errorf("doctor: %s missing", analyseBinary)
}
return nil
},
}
return cmd
}
94 changes: 94 additions & 0 deletions cmd/sin-code/internal/mcpclient/analyse_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: MIT
// Purpose: integration tests for the sin-analyse-suite entry in
// mcpclient.DefaultServers(). Verifies the v3.22.0 multimodal preprocessing
// MCP is wired into the registry, short-name-mapped correctly, and reachable
// as a goNative-style stdio bridge (read-only analyse__* tools, allow policy
// per cmd/sin-code/internal/permission_defaults.go:66).
package mcpclient

import "testing"

// findAnalyse returns the ServerConfig for the sin-analyse-suite entry,
// or nil if missing. Inspection helper shared by every positive test below.
func findAnalyse(t *testing.T) *ServerConfig {
t.Helper()
for i := range DefaultServers() {
s := DefaultServers()[i]
if s.Name == "analyse" {
return &s
}
}
t.Fatal("sin-analyse-suite server not found in DefaultServers()")
return nil
}

func TestSinAnalyseRegistered(t *testing.T) {
s := findAnalyse(t)
if s.Transport != "stdio" {
t.Fatalf("sin-analyse-suite transport should be %q, got %q", "stdio", s.Transport)
}
if len(s.Args) != 1 || s.Args[0] != "serve" {
t.Fatalf("sin-analyse-suite args should be [serve], got %v", s.Args)
}
}

func TestSinAnalyseShortName(t *testing.T) {
if got := shortName("sin-analyse-suite"); got != "analyse" {
t.Fatalf("shortName(\"sin-analyse-suite\") should be %q, got %q", "analyse", got)
}
}

func TestSinAnalyseUniqueShortName(t *testing.T) {
count := 0
for _, s := range DefaultServers() {
if s.Name == "analyse" {
count++
}
}
if count != 1 {
t.Fatalf("exactly one server should map to short name %q, found %d", "analyse", count)
}
}

func TestSinAnalyseHasNonEmptyCommand(t *testing.T) {
// Override testSkillsDir so skillsDirOrDefault returns "" and goNative
// falls back to the bare binary name "sin-analyse". The Command field
// must be non-empty in that path so the bridge can actually invoke it.
orig := testSkillsDir
testSkillsDir = stringPtr("")
t.Cleanup(func() { testSkillsDir = orig })
t.Setenv("SIN_SKILLS_DIR", "")

s := findAnalyse(t)
if s.Command == "" {
t.Fatalf("sin-analyse-suite command must not be empty so the bridge can spawn it, got %q", s.Command)
}
}

func TestSinAnalyseIsGoNative(t *testing.T) {
// goNative-style entry: Name + Transport + Command all set, URL empty
// (the bridge is stdio, not HTTP/websocket).
orig := testSkillsDir
testSkillsDir = stringPtr("")
t.Cleanup(func() { testSkillsDir = orig })
t.Setenv("SIN_SKILLS_DIR", "")

s := findAnalyse(t)
if s.Name == "" {
t.Fatalf("sin-analyse-suite Name must be set, got empty")
}
if s.Transport != "stdio" {
t.Fatalf("sin-analyse-suite Transport should be %q, got %q", "stdio", s.Transport)
}
if s.Command == "" {
t.Fatalf("sin-analyse-suite Command must be set, got empty")
}
if s.URL != "" {
t.Fatalf("sin-analyse-suite URL must be empty (stdio bridge), got %q", s.URL)
}
if len(s.Args) != 1 || s.Args[0] != "serve" {
t.Fatalf("sin-analyse-suite Args should be [serve], got %v", s.Args)
}
}

func stringPtr(s string) *string { return &s }
Loading
Loading