Skip to content

feat: add Reasonix engine adapter (DeepSeek-native CLI) - #148

Open
Aubrey45 wants to merge 1 commit into
yetone:mainfrom
Aubrey45:feat/reasonix-engine
Open

feat: add Reasonix engine adapter (DeepSeek-native CLI)#148
Aubrey45 wants to merge 1 commit into
yetone:mainfrom
Aubrey45:feat/reasonix-engine

Conversation

@Aubrey45

@Aubrey45 Aubrey45 commented Sep 1, 2026

Copy link
Copy Markdown

Adds a BYOA engine adapter for Reasonix, the DeepSeek-native coding agent CLI (npm: reasonix). Drives 'reasonix run' headlessly with --permission-mode auto --output-format json, resumes sessions via --resume , seeds the agent home with AGENTS.md (which Reasonix loads as project memory), and reports token usage from the terminal JSON result document.

  • engine.ts: ReasonixAdapter (probe/classify/run/seedHome) + reasonixReplyFromJson
  • registry.ts: PAIRABLE + EngineId include reasonix
  • engines.ts: UI label/bin for Reasonix

Reasonix enforces a workspace-root file-write sandbox + permission gates rather than an OS-level boundary, so it runs under CUMORA_BYOA_ALLOW_UNSANDBOXED=1 like the other compatibility engines.

Tested end-to-end: paired a local computer and ran 8 agents on
reasonix via BYOA — wake → triage → engine turn → reply all worked
(DeepSeek deepseek-v4-flash, ~3s/turn, session resume across daemon
restarts verified).

Adds a BYOA engine adapter for Reasonix, the DeepSeek-native coding
agent CLI (npm: reasonix). Drives 'reasonix run' headlessly with
--permission-mode auto --output-format json, resumes sessions via
--resume <session-id>, seeds the agent home with AGENTS.md (which
Reasonix loads as project memory), and reports token usage from the
terminal JSON result document.

- engine.ts: ReasonixAdapter (probe/classify/run/seedHome) + reasonixReplyFromJson
- registry.ts: PAIRABLE + EngineId include reasonix
- engines.ts: UI label/bin for Reasonix

Reasonix enforces a workspace-root file-write sandbox + permission
gates rather than an OS-level boundary, so it runs under
CUMORA_BYOA_ALLOW_UNSANDBOXED=1 like the other compatibility engines.
@yetone

yetone commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Thanks — and thank you for actually running it end to end. "8 agents, wake → triage → engine turn → reply, session resume across daemon restarts" is more validation than most engine PRs arrive with, and it is the part I cannot check for you.

Two things before this can land.

1. It conflicts with main. Please rebase.

2. The engine registry guard will almost certainly fail. This touches three files — engine.ts, registry.ts, src/lib/engines.ts — but making an engine runnable means adding its id to about ten hand-kept lists. #114 added scripts/guard-engine-registry.mjs for exactly this, and its header names the failure mode:

Missing one does not error — BYOA_SOURCES in particular is read through normalizeByoaSource(), which maps anything unknown to 'byoa-claude', so a half-wired engine silently bills its runs to Claude.

So a Reasonix turn would be attributed to Claude in llm_ledger and the observability panels, quietly, with nothing failing. Run this after the rebase and it will enumerate exactly what is still missing:

node --import tsx --test server/src/__tests__/guard-engine-registry.test.ts

I would expect at least BYOA_SOURCES, llm-ledger.ts, observability.ts, src/types.ts, src/api/client.ts, src/admin/api.ts, and the ENGINE_VERSION_SPECS entry in cli-version.ts (so the Computers card can report the installed Reasonix version like every other engine).

One design note, not a blocker. Gating Reasonix behind CUMORA_BYOA_ALLOW_UNSANDBOXED=1 is the right call given it enforces a workspace-root sandbox rather than an OS-level one — but please make that explicit where an operator will see it, not only in the PR description. Right now an engine that is installed and detected but never selected is invisible from the app; #138 is landing precisely to surface "installed, but Cumora will not run it here, and here is why". If Reasonix carries a refusal reason in the same shape, an operator who installs it without the opt-in gets told so instead of watching it silently not run.

Rebase, let the guard run, and ping me.

yetone added a commit that referenced this pull request Sep 1, 2026
Wired into every list an engine has to appear in — BYOA_SOURCES, the ledger,
observability, the version specs, the renderer, the big-brain guard — so the
registry guard passes rather than a half-wired engine silently billing its runs
to Claude. That is the completeness #148 still needs.

The security posture is the part worth keeping: it deliberately does NOT add
itself to SANDBOXED_ENGINE_IDS, even though the adapter passes `agy --sandbox`,
on the grounds that Cumora should not claim a verified fail-closed boundary
before the file/tool/credential/network surface has been independently proven.
After the week this repo just had — a sandbox profile that aborted every codex
turn on 103 workspaces — declining to assert an unverified boundary is the right
instinct, and I would rather have it stated than assumed.

Also normalizes Antigravity's cumulative session usage into per-turn deltas, so
resuming does not rebill prior turns and cache reads.

Conflict was the agent-cli version line against v0.12.1; kept the released
version, took their description.
@WhichPaths

Copy link
Copy Markdown
Collaborator

I've written two of these adapters (#99 gemini, #113 qwen) and the registry guard (#114), so I ran this branch through both. CI has not reported on this PR at all, and there are two things it would have caught — one mechanical, one not.

The guard fails on this branch

node scripts/guard-engine-registry.mjs on pr/148 as it stands:

🚨 engine registry is half-wired:
  src/types.ts → the renderer EngineId union
  server/src/agents/computer/cli-version.ts → ENGINE_VERSION_SPECS
  server/src/agents/runtime/byoa-source.ts → BYOA_SOURCES
  server/src/agents/llm-ledger.ts → LlmCallSource
  server/src/agents/observability.ts → TriageSource
  src/admin/api.ts → the admin LlmCallSource union
  src/api/client.ts → ApiTriageSource
  scripts/guard-big-brain.mjs → R4

Most are unions to widen, but two bite at runtime: without 'byoa-reasonix' in BYOA_SOURCES every reasonix run is attributed to Claude in the ledger, and without the R4 alternation a direct spawn of the reasonix binary goes unguarded by the big-brain rule.

input_tokens does not mean what usageFromClaude thinks

The usage field names already match EngineUsage

The names do. The semantics are the other convention, and that is the trap — it is the same one gemini set for me in #99.

From reasonix's own internal/cli/run_output.go, identical at the v1.31.1 you verified against and at today's v1.36.0:

s.usage.InputTokens              += e.Usage.PromptTokens      // → "input_tokens"
s.usage.CacheReadInputTokens     += e.Usage.CacheHitTokens    // → "cache_read_input_tokens"
s.usage.CacheCreationInputTokens += e.Usage.CacheMissTokens   // → "cache_creation_input_tokens"

and internal/provider/provider.go defines those as parts of a whole:

PromptTokens     int  // the complete input
CacheHitTokens   int  // prompt tokens served from cache
CacheMissTokens  int  // prompt tokens not cached, including CacheWriteTokens

So input_tokens == cache_read_input_tokens + cache_creation_input_tokens. normaliseUsage says the same thing from the other side (miss = prompt - hit).

Cumora's contract is the opposite, and states it explicitly in cost.ts:

All counts are the RAW (uncached) counts as the provider reports them: inputTokens excludes the cached portion

usageFromClaude therefore files the whole prompt under inputTokens and files its two halves again under the cache buckets. Second-order: reasonix's cache_creation_input_tokens is CacheMissTokens — ordinary uncached input, not a cache write — so it lands in cacheCreationTokens, which effectiveCostUsd bills at the cache-write premium (1.25× input on the Anthropic rows).

With total = hit + miss, the billed cost becomes

(hit + miss)·in  +  hit·cachedIn  +  miss·cacheWrite

where it should be miss·in + hit·cachedIn. Every cached token is charged an extra full input rate, and every fresh token is charged the write premium instead of the input rate. cacheHitRate is understated by the same construction, since its denominator is inputTokens + cachedInputTokens and inputTokens already contains the cached ones.

The reasonix fields map onto EngineUsage cleanly once read the other way — no subtraction needed, because the miss bucket is the fresh input:

// reasonix reports Anthropic's field NAMES with OpenAI's semantics:
// input_tokens is the whole prompt; the two cache fields are its parts, and
// cache_creation_input_tokens is CacheMissTokens (uncached input), not a
// cache WRITE. Cumora's cache-write bucket carries a price premium, so the
// raw document must not be passed through.
const hit = num(obj.usage?.cache_read_input_tokens)
const miss = num(obj.usage?.cache_creation_input_tokens)
const usage: EngineUsage = {
  input_tokens: miss,                   // fresh input
  cache_read_input_tokens: hit,
  cache_creation_input_tokens: 0,       // reasonix does not report writes separately
  output_tokens: num(obj.usage?.output_tokens),
}

usageFromOpenAI in cost.ts exists for exactly this class of provider, and gemini needed the same treatment in #99 — worth a line of comment either way, because the field names will keep inviting the pass-through.

Everything else read well to me: leaving reasonix out of SANDBOXED_ENGINE_IDS is right given the workspace-root sandbox, the --resume handling matches the one-shot pattern, and reasonixReplyFromJson fails loudly on a non-result document instead of guessing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants