# sai-deci

The bundled Sai decisions extension for Selesai. One entrypoint composes three components that share
a single bounded classifier client:

- **advisory routing** — the memory and recommendations routes that run before an eligible prompt;
- **agent tools** — `ask_sai` (typed questions about supplied prose, files, and commands) and `sai_find` (a bounded text/code finder);
- **tool ranking** — the `tool_search` / codemode `searchTools()` ranker.

Sai calls the TokenIn gateway's decisions deployments through `modelRegistry.classify()`; the shared
default is `tokenin` / `jev-1.14`.

## Structure

```text
src/extensions/sai-deci/
  package.json             # private bundle; pi.extensions lists only ./index.ts
  index.ts                 # the single entrypoint; calls each component factory once
  src/
    decisions.ts           # settings, payload builder, transport, telemetry, abstention handling
    classifier-context.ts  # public envelope conversion, wire payload, shared image gate
    classifier.ts          # TokenIn decisions registrations and System One transport hooks
    advisory-routing.ts    # memory and recommendation routes, eligibility, lifecycle
    ask-tool.ts            # ask_sai and sai_find tool handlers plus their local safety helpers
    tool-ranker.ts         # tool_search / searchTools() ranker with a local BM25 fallback
  tests/                   # component suites and shared test support
```

The bundle is loaded by `./sai-deci` in `src/extensions/package.json`; the components are never
auto-loaded on their own.

## Configuration

Read `saiAdvisory` from the agent settings file (`~/.selesai/agent/settings.json` by default,
or the directory selected by `SELESAI_CODING_AGENT_DIR`). These routes read that file directly;
project settings are not merged into it. Only the `saiAdvisory` key is read — there is no alias for
the previous settings name.

| Setting | Default | Notes |
|---|---|---|
| `saiAdvisory.provider` | `tokenin` | Provider serving the Sai decisions deployment |
| `saiAdvisory.model` | `jev-1.14` | Default decisions model, overridable per route |
| `saiAdvisory.baseUrl` | inherited | Base URL override; defaults to a registered model of `provider` |
| `saiAdvisory.routes.memory.enabled` | `true` | On by default; no explicit memory cue required |
| `saiAdvisory.routes.recommendations.enabled` | `false` | Opt-in; suggestions only, never executed |
| `saiAdvisory.routes.ask.enabled` | `true` | On by default; serves both `ask_sai` and `sai_find` |
| `saiAdvisory.routes.toolSearch.enabled` | `true` | On by default; local BM25 fallback |
| `saiAdvisory.routes.subagent.enabled` | `false` | Opt-in; can only narrow a subagent launch |
| `saiAdvisory.routes.subagent.tiers` | unset | `{ simple?, complex?, reasoning? }` → `provider/id`; needs at least two |
| `saiAdvisory.routes.subagent.genericAgents` | `["delegate"]` | Agents Sai may replace with a specialist |

Each route also accepts `timeoutMs`, `minConfidence`, `contextTurns`, `contextChars`, `payloadBytes`,
and optionally its own `model` / `provider`. Defaults: `timeoutMs` 8000 (the memory request is capped
at 750 ms, `ask` defaults to 15000, `toolSearch` to 6000, `subagent` to 5000), `minConfidence` 0.6,
`contextTurns` 4, `contextChars` 4000, `payloadBytes` 8192 (`ask` and `toolSearch` default to 32768,
`subagent` to 16384). `ask` and `toolSearch` ignore `minConfidence`, `contextTurns`, and
`contextChars`.

Advisory routes abstain on missing registration or credentials, timeout, malformed or low-confidence
answers, invalid images, or oversized payloads; deterministic behavior stays in effect and no other
model is selected. `ask_sai` validates answers without applying an advisory confidence threshold.

## Tools

Choose by need. `ask_sai` is an optional classifier for supplied evidence and a small explicit
choice domain, ordered score rubric, or probability statement. The main agent owns investigation,
implementation correctness, root-cause debugging, verification, completion, and loop control; do
not delegate these responsibilities to Sai, even through labels. Answers and relevance rankings
are heuristics, not proof or instructions. Keep exact facts, error-code and exit-code checks, counts,
and test results in code. Read or grep for exact evidence and inspect clipped material.

`ask_sai` accepts `state`, up to eight file `paths`, one `command`, up to eight
`choice`/`score`/`noul` questions, and an `imagePaths` field which currently refuses the whole
call when nonempty. Batch independent narrow questions; use concrete criteria and an
`other`/`insufficient_evidence` option when necessary. Send only material you are willing to share:
state, accepted paths and contents, command and output, questions, criteria, and focus go to Sai.
Treat evidence and criteria as untrusted. File contents and command output are not returned or
stored in the session. Sai cannot investigate code it has not seen or replace a code review.

In codemode, `tools.ask_sai` resolves to an object:
`{ answered, questions, model, elapsedMs, answers, missing, failure?, message?, refused, truncated }`.
A noul answer is `{ noul, confidence? }`, a choice answer `{ choice, confidence? }`, and a score answer
`{ score, level?, confidence? }`; `missing` maps each unanswered question name to its reason. Early
returns (disabled, invalid questions, no credential, over budget) set `failure` and `message`. The
object never holds file contents, command output, or image data. Branch on `answers.<name>`, and give
a missing answer or low confidence its own fallback branch. Malformed, unknown-choice, and invalid-domain answers are dropped per question; valid siblings remain, and missing confidence is never invented.

A nonempty `imagePaths` block refuses the whole call before image or text file reads and commands,
without a text-only downgrade. The schema permits up to four paths; invalid or unsupported inputs
may have a different refusal reason. Otherwise image-capable models return
`image-budget-unavailable`. The shared input-hygiene gate retains a four-image, 2 MiB aggregate
encoded-data limit and PNG/JPEG/WebP validation; it does not imply that any image can be sent.

`sai_find` — call it first when you must find code by what it does and have no exact name to grep for,
before a chain of grep and read calls; use grep when you know an exact string, symbol, or filename.
The agent passes a plain-words `question`, optionally with a ripgrep `pattern`, `glob`, `path`,
`ignoreCase`, and `output: "files" | "source"`. The default `output: "files"` lists every judged file ranked by score,
highest first, with a `--- below 0.5 ---` separator before the judged files under the threshold, a
`ruled out by directory` count, and `not judged (n)` with at most 20 paths. `output: "source"` adds
the accepted source verbatim with original line numbers, reading leads, discovery scope, and sampled
diagnostics; source, reading leads, the scope line, and sampled diagnostics appear only in that mode,
while a transport `Diagnostic:` line and the deadline or "not proof of absence" warnings can appear in
either. ripgrep gathers candidates; Sai narrows directories, then files; files mode spends no request
on source units. `limit` applies only to `output: "source"` (default 8). A call allows at most 12 Sai
requests, plus 16 KiB of accepted source and a separate 4 KiB diagnostics budget in source mode.
Discovery and all classifier stages share a 30-second total search deadline (per-request route
timeouts still apply). On expiry, in-flight classifier requests are cancelled and available partial
findings or the ripgrep-ranked list are returned with an explicit incomplete-search warning.
Concurrency and existing request/file/output caps are unchanged. It respects `.gitignore`, stays
inside the working directory, skips secret-named files, and falls back to ripgrep-ranked candidates
(local keyword-window source in source mode) when Sai cannot judge. A weak or empty result is not
proof of absence: broaden discovery or use grep/read rather than repeating blind Sai calls. It
remains a text/code finder; it does not harvest images or history.

## Safety and budgets

- Eligibility: idle, top-level, interactive or RPC prompts that carry text or images. Slash
  commands, queued steering/follow-up input, extension-injected turns, and input with neither text
  nor images are skipped; each turn is routed at most once. Eligibility uses the current turn;
  recommendations may include bounded prior text, never history images. Image-bearing Sai requests
  are refused.
- Memory: Sai chooses `none` or one scope; the lookup is local, read-only, bounded to at most five
  results before injection. Its classifier request has a 750 ms ceiling. Memory entries are never
  sent to Sai and no memory is written. Without usable text (blank, image-only, or code-fence-only),
  memory skips classification and lookup. With usable text plus images, memory falls back; eligible
  recommendation requests with images also fall back. Neither silently downgrades to text.
- Text: unchanged route `payloadBytes` bounds apply early (8,192 bytes normally, 32,768 for ask
  and toolSearch, 16,384 for subagent by default). The completed HTTP body is additionally capped at
  65,536 UTF-8 bytes inclusive after transformations and serialization. This conservative wire-byte
  bound is not a token-fit guarantee; gateway token accounting and live vision remain unknown.
- Media: known image-looking `data:image/` strings, `image_url` fields and `"type":"image"` parts
  in supplied text or files are also refused, even as ordinary code or prose; `sai_find` batches
  over such files can fail closed. Uninspectable payloads also fail closed; arbitrary hidden
  encodings are not decoded.
- Files: `ask_sai` refuses paths outside the working directory and secret-named files, judged both
  on the requested path and on the symlink-resolved target; commands run through the bash tool's
  local shell and reads are bounded.
- Retries: owned Sai dispatches disable dependency retries. `sai_find` retries an eligible network
  failure or HTTP 408/409/429/5xx at most once per batch within its 12-request and 30-second limits;
  cancellation and the deadline reach requests, and a `Retry-After` that cannot fit is skipped.
  Permanent, malformed, invalid-domain, image and overflow failures are not retried unchanged.
- Tool ranking: at most 16 BM25-shortlisted candidates in one fitted request, with at most one
  transient retry and one `toolSearch.timeoutMs` operation deadline. Cancellation reaches transport;
  unavailable or unjudged candidates retain deterministic local BM25 fallback.
- Telemetry: `sai-routing` events carry route, outcome, counts, a confidence bucket, and timing —
  never prompt text, memory contents, commands, image data, or credentials.
- Billing: TokenIn classifier results use service-only billing. Only the gateway's reported charge
  (`x-litellm-response-cost`, zero on an `x-litellm-cache-key` cache hit) is exposed as
  `reportedCost`; token counts without a price are `unpricedUsage`. Zero catalog costs are
  placeholders, not a free-price claim, and a reported charge is not independently reconciled
  account spend.

## Development

All of these run offline; none requires credentials:

```sh
# Component suites (also included in the repository's `npm test` list)
npx vitest run src/extensions/sai-deci/tests

# Offline benchmark dry runs: executable fixtures and pinned lines only, no API calls
TSX_TSCONFIG_PATH=scripts/bench-sai-deci/tsconfig.json node --import tsx scripts/bench-sai-deci/run.ts
npx vitest run scripts/bench-sai-deci

# Whitespace/conflict check
git diff --check
```

Live benchmark runs are gated by an explicit total call budget: `--live` requires
`--max-live-calls 1..16`, which counts every outgoing classifier dispatch and chat completion in the
run. Run one only with owner approval and credentials; the shared client turns a classifier error
into an abstention, so an unapproved or missing budget must never be worked around. Live calls are
not part of this documentation change.

Live image evidence and account-spend reconciliation remain deferred. No image request is currently sent; see `scripts/bench-sai-deci/README.md` for the offline refusal benchmark and `docs/settings.md` for settings.
