reasonix 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,11 +1,264 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- DeepSeekClient
4
- } from "./chunk-T2ODXAJP.js";
5
2
 
6
3
  // src/cli/index.ts
7
4
  import { Command } from "commander";
8
5
 
6
+ // src/client.ts
7
+ import { createParser } from "eventsource-parser";
8
+
9
+ // src/retry.ts
10
+ var DEFAULT_RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504];
11
+ async function fetchWithRetry(fetchFn, url, init, opts = {}) {
12
+ const maxAttempts = opts.maxAttempts ?? 4;
13
+ const initial = opts.initialBackoffMs ?? 500;
14
+ const cap = opts.maxBackoffMs ?? 1e4;
15
+ const retryable = new Set(opts.retryableStatuses ?? DEFAULT_RETRYABLE_STATUSES);
16
+ let lastError;
17
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
18
+ if (opts.signal?.aborted) throw new Error("aborted");
19
+ try {
20
+ const resp = await fetchFn(url, init);
21
+ if (resp.ok || !retryable.has(resp.status)) return resp;
22
+ if (attempt === maxAttempts - 1) return resp;
23
+ await resp.text().catch(() => void 0);
24
+ const waitMs = computeWait(attempt, initial, cap, resp.headers.get("Retry-After"));
25
+ opts.onRetry?.({ attempt: attempt + 1, reason: `http ${resp.status}`, waitMs });
26
+ await sleep(waitMs, opts.signal);
27
+ } catch (err) {
28
+ lastError = err;
29
+ if (isAbortError(err) || opts.signal?.aborted) throw err;
30
+ if (attempt === maxAttempts - 1) throw err;
31
+ const waitMs = computeWait(attempt, initial, cap, null);
32
+ opts.onRetry?.({
33
+ attempt: attempt + 1,
34
+ reason: `network: ${messageOf(err)}`,
35
+ waitMs
36
+ });
37
+ await sleep(waitMs, opts.signal);
38
+ }
39
+ }
40
+ throw lastError ?? new Error("fetchWithRetry: loop exited unexpectedly");
41
+ }
42
+ function computeWait(attempt, initial, cap, retryAfter) {
43
+ if (retryAfter) {
44
+ const seconds = Number.parseFloat(retryAfter);
45
+ if (Number.isFinite(seconds) && seconds > 0) {
46
+ return Math.min(seconds * 1e3, cap);
47
+ }
48
+ }
49
+ const exp = initial * 2 ** attempt;
50
+ const jitter = exp * (0.75 + Math.random() * 0.5);
51
+ return Math.min(Math.max(jitter, 0), cap);
52
+ }
53
+ function sleep(ms, signal) {
54
+ if (ms <= 0) return Promise.resolve();
55
+ return new Promise((resolve2, reject) => {
56
+ const timer = setTimeout(resolve2, ms);
57
+ if (signal) {
58
+ const onAbort = () => {
59
+ clearTimeout(timer);
60
+ reject(new Error("aborted"));
61
+ };
62
+ if (signal.aborted) onAbort();
63
+ else signal.addEventListener("abort", onAbort, { once: true });
64
+ }
65
+ });
66
+ }
67
+ function isAbortError(err) {
68
+ if (!err || typeof err !== "object") return false;
69
+ const name = err.name;
70
+ return name === "AbortError";
71
+ }
72
+ function messageOf(err) {
73
+ if (err instanceof Error) return err.message;
74
+ try {
75
+ return String(err);
76
+ } catch {
77
+ return "unknown error";
78
+ }
79
+ }
80
+
81
+ // src/client.ts
82
+ var Usage = class _Usage {
83
+ constructor(promptTokens = 0, completionTokens = 0, totalTokens = 0, promptCacheHitTokens = 0, promptCacheMissTokens = 0) {
84
+ this.promptTokens = promptTokens;
85
+ this.completionTokens = completionTokens;
86
+ this.totalTokens = totalTokens;
87
+ this.promptCacheHitTokens = promptCacheHitTokens;
88
+ this.promptCacheMissTokens = promptCacheMissTokens;
89
+ }
90
+ promptTokens;
91
+ completionTokens;
92
+ totalTokens;
93
+ promptCacheHitTokens;
94
+ promptCacheMissTokens;
95
+ get cacheHitRatio() {
96
+ const denom = this.promptCacheHitTokens + this.promptCacheMissTokens;
97
+ return denom > 0 ? this.promptCacheHitTokens / denom : 0;
98
+ }
99
+ static fromApi(raw) {
100
+ const u = raw ?? {};
101
+ return new _Usage(
102
+ u.prompt_tokens ?? 0,
103
+ u.completion_tokens ?? 0,
104
+ u.total_tokens ?? 0,
105
+ u.prompt_cache_hit_tokens ?? 0,
106
+ u.prompt_cache_miss_tokens ?? 0
107
+ );
108
+ }
109
+ };
110
+ var DeepSeekClient = class {
111
+ apiKey;
112
+ baseUrl;
113
+ timeoutMs;
114
+ retry;
115
+ _fetch;
116
+ constructor(opts = {}) {
117
+ const apiKey = opts.apiKey ?? process.env.DEEPSEEK_API_KEY;
118
+ if (!apiKey) {
119
+ throw new Error(
120
+ "DEEPSEEK_API_KEY is not set. Put it in .env or pass apiKey to DeepSeekClient."
121
+ );
122
+ }
123
+ this.apiKey = apiKey;
124
+ this.baseUrl = (opts.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com").replace(/\/+$/, "");
125
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
126
+ this._fetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
127
+ this.retry = opts.retry ?? {};
128
+ }
129
+ buildPayload(opts, stream) {
130
+ const payload = {
131
+ model: opts.model,
132
+ messages: opts.messages,
133
+ stream
134
+ };
135
+ if (opts.tools?.length) payload.tools = opts.tools;
136
+ if (opts.temperature !== void 0) payload.temperature = opts.temperature;
137
+ if (opts.maxTokens !== void 0) payload.max_tokens = opts.maxTokens;
138
+ if (opts.responseFormat) payload.response_format = opts.responseFormat;
139
+ return payload;
140
+ }
141
+ async chat(opts) {
142
+ const ctrl = new AbortController();
143
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
144
+ const signal = opts.signal ?? ctrl.signal;
145
+ try {
146
+ const resp = await fetchWithRetry(
147
+ this._fetch,
148
+ `${this.baseUrl}/chat/completions`,
149
+ {
150
+ method: "POST",
151
+ headers: {
152
+ Authorization: `Bearer ${this.apiKey}`,
153
+ "Content-Type": "application/json"
154
+ },
155
+ body: JSON.stringify(this.buildPayload(opts, false)),
156
+ signal
157
+ },
158
+ { ...this.retry, signal }
159
+ );
160
+ if (!resp.ok) {
161
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text()}`);
162
+ }
163
+ const data = await resp.json();
164
+ const choice = data.choices?.[0]?.message ?? {};
165
+ return {
166
+ content: choice.content ?? "",
167
+ reasoningContent: choice.reasoning_content ?? null,
168
+ toolCalls: choice.tool_calls ?? [],
169
+ usage: Usage.fromApi(data.usage),
170
+ raw: data
171
+ };
172
+ } finally {
173
+ clearTimeout(timer);
174
+ }
175
+ }
176
+ async *stream(opts) {
177
+ const ctrl = new AbortController();
178
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
179
+ const signal = opts.signal ?? ctrl.signal;
180
+ let resp;
181
+ try {
182
+ resp = await fetchWithRetry(
183
+ this._fetch,
184
+ `${this.baseUrl}/chat/completions`,
185
+ {
186
+ method: "POST",
187
+ headers: {
188
+ Authorization: `Bearer ${this.apiKey}`,
189
+ "Content-Type": "application/json",
190
+ Accept: "text/event-stream"
191
+ },
192
+ body: JSON.stringify(this.buildPayload(opts, true)),
193
+ signal
194
+ },
195
+ { ...this.retry, signal }
196
+ );
197
+ } catch (err) {
198
+ clearTimeout(timer);
199
+ throw err;
200
+ }
201
+ if (!resp.ok || !resp.body) {
202
+ clearTimeout(timer);
203
+ throw new Error(`DeepSeek ${resp.status}: ${await resp.text().catch(() => "")}`);
204
+ }
205
+ const queue = [];
206
+ let done = false;
207
+ const parser = createParser({
208
+ onEvent: (ev) => {
209
+ if (!ev.data || ev.data === "[DONE]") {
210
+ done = true;
211
+ return;
212
+ }
213
+ try {
214
+ const json = JSON.parse(ev.data);
215
+ const delta = json.choices?.[0]?.delta ?? {};
216
+ const finishReason = json.choices?.[0]?.finish_reason ?? void 0;
217
+ const chunk = { raw: json, finishReason };
218
+ if (typeof delta.content === "string" && delta.content.length > 0) {
219
+ chunk.contentDelta = delta.content;
220
+ }
221
+ if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
222
+ chunk.reasoningDelta = delta.reasoning_content;
223
+ }
224
+ if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
225
+ const tc = delta.tool_calls[0];
226
+ chunk.toolCallDelta = {
227
+ index: tc.index ?? 0,
228
+ id: tc.id,
229
+ name: tc.function?.name,
230
+ argumentsDelta: tc.function?.arguments
231
+ };
232
+ }
233
+ if (json.usage) {
234
+ chunk.usage = Usage.fromApi(json.usage);
235
+ }
236
+ queue.push(chunk);
237
+ } catch {
238
+ }
239
+ }
240
+ });
241
+ const reader = resp.body.getReader();
242
+ const decoder = new TextDecoder();
243
+ try {
244
+ while (true) {
245
+ if (queue.length > 0) {
246
+ yield queue.shift();
247
+ continue;
248
+ }
249
+ if (done) break;
250
+ const { value, done: streamDone } = await reader.read();
251
+ if (streamDone) break;
252
+ parser.feed(decoder.decode(value, { stream: true }));
253
+ }
254
+ while (queue.length > 0) yield queue.shift();
255
+ } finally {
256
+ clearTimeout(timer);
257
+ reader.releaseLock();
258
+ }
259
+ }
260
+ };
261
+
9
262
  // src/harvest.ts
10
263
  function emptyPlanState() {
11
264
  return { subgoals: [], hypotheses: [], uncertainties: [], rejectedPaths: [] };
@@ -96,6 +349,66 @@ function sanitizeArray(raw, maxItems, maxItemLen) {
96
349
  return out;
97
350
  }
98
351
 
352
+ // src/consistency.ts
353
+ var defaultSelector = (samples) => {
354
+ if (samples.length === 0) throw new Error("defaultSelector: samples is empty");
355
+ return samples.slice().sort((a, b) => {
356
+ const uDiff = a.planState.uncertainties.length - b.planState.uncertainties.length;
357
+ if (uDiff !== 0) return uDiff;
358
+ const aLen = a.response.content?.length ?? 0;
359
+ const bLen = b.response.content?.length ?? 0;
360
+ return aLen - bLen;
361
+ })[0];
362
+ };
363
+ async function runBranches(client, request, opts = {}) {
364
+ const budget = Math.max(1, opts.budget ?? 1);
365
+ const temperatures = resolveTemperatures(budget, opts.temperatures);
366
+ const selector = opts.selector ?? defaultSelector;
367
+ const samples = await Promise.all(
368
+ temperatures.map(async (temperature, index) => {
369
+ const response = await client.chat({ ...request, temperature });
370
+ const planState = await harvest(response.reasoningContent, client, opts.harvestOptions);
371
+ const sample = { index, temperature, response, planState };
372
+ try {
373
+ opts.onSampleDone?.(sample);
374
+ } catch {
375
+ }
376
+ return sample;
377
+ })
378
+ );
379
+ return { chosen: selector(samples), samples };
380
+ }
381
+ function aggregateBranchUsage(samples) {
382
+ let promptTokens = 0;
383
+ let completionTokens = 0;
384
+ let totalTokens = 0;
385
+ let promptCacheHitTokens = 0;
386
+ let promptCacheMissTokens = 0;
387
+ for (const s of samples) {
388
+ promptTokens += s.response.usage.promptTokens;
389
+ completionTokens += s.response.usage.completionTokens;
390
+ totalTokens += s.response.usage.totalTokens;
391
+ promptCacheHitTokens += s.response.usage.promptCacheHitTokens;
392
+ promptCacheMissTokens += s.response.usage.promptCacheMissTokens;
393
+ }
394
+ return {
395
+ promptTokens,
396
+ completionTokens,
397
+ totalTokens,
398
+ promptCacheHitTokens,
399
+ promptCacheMissTokens
400
+ };
401
+ }
402
+ function resolveTemperatures(budget, custom) {
403
+ if (custom && custom.length >= budget) return [...custom.slice(0, budget)];
404
+ if (budget === 1) return [0];
405
+ const out = [];
406
+ for (let i = 0; i < budget; i++) {
407
+ out.push(Number((i / (budget - 1)).toFixed(2)));
408
+ }
409
+ return out;
410
+ }
411
+
99
412
  // src/memory.ts
100
413
  import { createHash } from "crypto";
101
414
  var ImmutablePrefix = class {
@@ -613,28 +926,73 @@ var CacheFirstLoop = class {
613
926
  client;
614
927
  prefix;
615
928
  tools;
616
- model;
617
929
  maxToolIters;
618
- stream;
619
- harvestEnabled;
620
- harvestOptions;
621
930
  log = new AppendOnlyLog();
622
931
  scratch = new VolatileScratch();
623
932
  stats = new SessionStats();
624
933
  repair;
934
+ // Mutable via configure() — slash commands in the TUI / library callers tweak
935
+ // these mid-session so users don't have to restart to try harvest or branch.
936
+ model;
937
+ stream;
938
+ harvestEnabled;
939
+ harvestOptions;
940
+ branchEnabled;
941
+ branchOptions;
625
942
  _turn = 0;
943
+ _streamPreference;
626
944
  constructor(opts) {
627
945
  this.client = opts.client;
628
946
  this.prefix = opts.prefix;
629
947
  this.tools = opts.tools ?? new ToolRegistry();
630
948
  this.model = opts.model ?? "deepseek-chat";
631
949
  this.maxToolIters = opts.maxToolIters ?? 8;
632
- this.stream = opts.stream ?? true;
633
- this.harvestEnabled = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
634
- this.harvestOptions = typeof opts.harvest === "object" && opts.harvest !== null ? opts.harvest : {};
950
+ if (typeof opts.branch === "number") {
951
+ this.branchOptions = { budget: opts.branch };
952
+ } else if (opts.branch && typeof opts.branch === "object") {
953
+ this.branchOptions = opts.branch;
954
+ } else {
955
+ this.branchOptions = {};
956
+ }
957
+ this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
958
+ const harvestForced = this.branchEnabled;
959
+ this.harvestEnabled = harvestForced || opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
960
+ this.harvestOptions = typeof opts.harvest === "object" && opts.harvest !== null ? opts.harvest : this.branchOptions.harvestOptions ?? {};
961
+ this._streamPreference = opts.stream ?? true;
962
+ this.stream = this.branchEnabled ? false : this._streamPreference;
635
963
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
636
964
  this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
637
965
  }
966
+ /**
967
+ * Reconfigure model/harvest/branch/stream mid-session. The loop's log,
968
+ * scratch, and stats are preserved — only the per-turn behavior changes.
969
+ * Used by the TUI's slash commands and by library callers who want to
970
+ * flip a knob between turns.
971
+ */
972
+ configure(opts) {
973
+ if (opts.model !== void 0) this.model = opts.model;
974
+ if (opts.stream !== void 0) this._streamPreference = opts.stream;
975
+ if (opts.branch !== void 0) {
976
+ if (typeof opts.branch === "number") {
977
+ this.branchOptions = { budget: opts.branch };
978
+ } else if (opts.branch && typeof opts.branch === "object") {
979
+ this.branchOptions = opts.branch;
980
+ } else {
981
+ this.branchOptions = {};
982
+ }
983
+ this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
984
+ }
985
+ if (opts.harvest !== void 0) {
986
+ const want = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
987
+ this.harvestEnabled = want || this.branchEnabled;
988
+ if (typeof opts.harvest === "object" && opts.harvest !== null) {
989
+ this.harvestOptions = opts.harvest;
990
+ }
991
+ } else if (this.branchEnabled) {
992
+ this.harvestEnabled = true;
993
+ }
994
+ this.stream = this.branchEnabled ? false : this._streamPreference;
995
+ }
638
996
  buildMessages(pendingUser) {
639
997
  const msgs = [...this.prefix.toMessages(), ...this.log.toMessages()];
640
998
  if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
@@ -651,8 +1009,85 @@ var CacheFirstLoop = class {
651
1009
  let reasoningContent = "";
652
1010
  let toolCalls = [];
653
1011
  let usage = null;
1012
+ let branchSummary;
1013
+ let preHarvestedPlanState;
654
1014
  try {
655
- if (this.stream) {
1015
+ if (this.branchEnabled) {
1016
+ const budget = this.branchOptions.budget ?? 1;
1017
+ yield {
1018
+ turn: this._turn,
1019
+ role: "branch_start",
1020
+ content: "",
1021
+ branchProgress: {
1022
+ completed: 0,
1023
+ total: budget,
1024
+ latestIndex: -1,
1025
+ latestTemperature: -1,
1026
+ latestUncertainties: -1
1027
+ }
1028
+ };
1029
+ const queue = [];
1030
+ let waiter = null;
1031
+ const onSampleDone = (sample) => {
1032
+ if (waiter) {
1033
+ const w = waiter;
1034
+ waiter = null;
1035
+ w(sample);
1036
+ } else {
1037
+ queue.push(sample);
1038
+ }
1039
+ };
1040
+ const branchPromise = runBranches(
1041
+ this.client,
1042
+ {
1043
+ model: this.model,
1044
+ messages,
1045
+ tools: toolSpecs.length ? toolSpecs : void 0
1046
+ },
1047
+ {
1048
+ ...this.branchOptions,
1049
+ harvestOptions: this.harvestOptions,
1050
+ onSampleDone
1051
+ }
1052
+ );
1053
+ for (let k = 0; k < budget; k++) {
1054
+ const sample = queue.shift() ?? await new Promise((resolve2) => {
1055
+ waiter = resolve2;
1056
+ });
1057
+ yield {
1058
+ turn: this._turn,
1059
+ role: "branch_progress",
1060
+ content: "",
1061
+ branchProgress: {
1062
+ completed: k + 1,
1063
+ total: budget,
1064
+ latestIndex: sample.index,
1065
+ latestTemperature: sample.temperature,
1066
+ latestUncertainties: sample.planState.uncertainties.length
1067
+ }
1068
+ };
1069
+ }
1070
+ const result = await branchPromise;
1071
+ assistantContent = result.chosen.response.content;
1072
+ reasoningContent = result.chosen.response.reasoningContent ?? "";
1073
+ toolCalls = result.chosen.response.toolCalls;
1074
+ const agg = aggregateBranchUsage(result.samples);
1075
+ usage = new Usage(
1076
+ agg.promptTokens,
1077
+ agg.completionTokens,
1078
+ agg.totalTokens,
1079
+ agg.promptCacheHitTokens,
1080
+ agg.promptCacheMissTokens
1081
+ );
1082
+ preHarvestedPlanState = result.chosen.planState;
1083
+ branchSummary = summarizeBranch(result.chosen, result.samples);
1084
+ yield {
1085
+ turn: this._turn,
1086
+ role: "branch_done",
1087
+ content: "",
1088
+ branch: branchSummary
1089
+ };
1090
+ } else if (this.stream) {
656
1091
  const callBuf = /* @__PURE__ */ new Map();
657
1092
  for await (const chunk of this.client.stream({
658
1093
  model: this.model,
@@ -712,17 +1147,13 @@ var CacheFirstLoop = class {
712
1147
  };
713
1148
  return;
714
1149
  }
715
- const turnStats = this.stats.record(
716
- this._turn,
717
- this.model,
718
- usage ?? new (await import("./client-RIVGDOJP.js")).Usage()
719
- );
1150
+ const turnStats = this.stats.record(this._turn, this.model, usage ?? new Usage());
720
1151
  if (pendingUser !== null) {
721
1152
  this.log.append({ role: "user", content: pendingUser });
722
1153
  pendingUser = null;
723
1154
  }
724
1155
  this.scratch.reasoning = reasoningContent || null;
725
- const planState = this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
1156
+ const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
726
1157
  const { calls: repairedCalls, report } = this.repair.process(
727
1158
  toolCalls,
728
1159
  reasoningContent || null
@@ -734,7 +1165,8 @@ var CacheFirstLoop = class {
734
1165
  content: assistantContent,
735
1166
  stats: turnStats,
736
1167
  planState,
737
- repair: report
1168
+ repair: report,
1169
+ branch: branchSummary
738
1170
  };
739
1171
  if (repairedCalls.length === 0) {
740
1172
  yield { turn: this._turn, role: "done", content: assistantContent };
@@ -770,6 +1202,14 @@ var CacheFirstLoop = class {
770
1202
  return msg;
771
1203
  }
772
1204
  };
1205
+ function summarizeBranch(chosen, samples) {
1206
+ return {
1207
+ budget: samples.length,
1208
+ chosenIndex: chosen.index,
1209
+ uncertainties: samples.map((s) => s.planState.uncertainties.length),
1210
+ temperatures: samples.map((s) => s.temperature)
1211
+ };
1212
+ }
773
1213
 
774
1214
  // src/env.ts
775
1215
  import { readFileSync } from "fs";
@@ -843,22 +1283,67 @@ var VERSION = "0.0.1";
843
1283
 
844
1284
  // src/cli/commands/chat.tsx
845
1285
  import { render } from "ink";
846
- import React7, { useState as useState3 } from "react";
1286
+ import React7, { useState as useState4 } from "react";
847
1287
 
848
1288
  // src/cli/ui/App.tsx
849
1289
  import { createWriteStream } from "fs";
850
- import { Box as Box5, Static, useApp } from "ink";
851
- import React5, { useCallback, useEffect, useMemo, useRef, useState } from "react";
1290
+ import { Box as Box5, Static, Text as Text5, useApp } from "ink";
1291
+ import React5, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
852
1292
 
853
1293
  // src/cli/ui/EventLog.tsx
854
1294
  import { Box as Box2, Text as Text2 } from "ink";
855
- import React2 from "react";
1295
+ import React2, { useEffect, useState } from "react";
856
1296
 
857
1297
  // src/cli/ui/markdown.tsx
858
1298
  import { Box, Text } from "ink";
859
1299
  import React from "react";
1300
+ var SUPERSCRIPT = {
1301
+ "0": "\u2070",
1302
+ "1": "\xB9",
1303
+ "2": "\xB2",
1304
+ "3": "\xB3",
1305
+ "4": "\u2074",
1306
+ "5": "\u2075",
1307
+ "6": "\u2076",
1308
+ "7": "\u2077",
1309
+ "8": "\u2078",
1310
+ "9": "\u2079",
1311
+ "+": "\u207A",
1312
+ "-": "\u207B",
1313
+ n: "\u207F"
1314
+ };
1315
+ var SUBSCRIPT = {
1316
+ "0": "\u2080",
1317
+ "1": "\u2081",
1318
+ "2": "\u2082",
1319
+ "3": "\u2083",
1320
+ "4": "\u2084",
1321
+ "5": "\u2085",
1322
+ "6": "\u2086",
1323
+ "7": "\u2087",
1324
+ "8": "\u2088",
1325
+ "9": "\u2089",
1326
+ "+": "\u208A",
1327
+ "-": "\u208B"
1328
+ };
1329
+ function toSuperscript(s) {
1330
+ let out = "";
1331
+ for (const c of s) out += SUPERSCRIPT[c] ?? c;
1332
+ return out;
1333
+ }
1334
+ function toSubscript(s) {
1335
+ let out = "";
1336
+ for (const c of s) out += SUBSCRIPT[c] ?? c;
1337
+ return out;
1338
+ }
860
1339
  function stripMath(s) {
861
- return s.replace(/\\\(\s*/g, "").replace(/\s*\\\)/g, "").replace(/\\\[\s*/g, "\n").replace(/\s*\\\]/g, "\n").replace(/\\boxed\{([^}]+)\}/g, "\u3010$1\u3011").replace(/\\sqrt\{([^}]+)\}/g, "\u221A($1)").replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, "($1)/($2)").replace(/\\text\{([^}]+)\}/g, "$1").replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/[ \t]{2,}/g, " ");
1340
+ return s.replace(/\\\(\s*/g, "").replace(/\s*\\\)/g, "").replace(/\\\[\s*/g, "\n").replace(/\s*\\\]/g, "\n").replace(
1341
+ /\\[dt]?frac\s*\{((?:[^{}]|\{[^{}]*\})+)\}\s*\{((?:[^{}]|\{[^{}]*\})+)\}/g,
1342
+ (_m, num, den) => `(${num.trim()})/(${den.trim()})`
1343
+ ).replace(
1344
+ /\\binom\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g,
1345
+ (_m, n, k) => `C(${n.trim()},${k.trim()})`
1346
+ ).replace(/\\sqrt\s*\{([^{}]+)\}/g, (_m, g) => `\u221A(${g.trim()})`).replace(/\\boxed\s*\{([^{}]+)\}/g, (_m, g) => `\u3010${g.trim()}\u3011`).replace(/\\text\s*\{([^{}]+)\}/g, (_m, g) => g.trim()).replace(/\\overline\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0304`).replace(/\\hat\s*\{([^{}]+)\}/g, (_m, g) => `${g.trim()}\u0302`).replace(/\\vec\s*\{([^{}]+)\}/g, (_m, g) => `\u2192${g.trim()}`).replace(/\\cdot/g, "\xB7").replace(/\\times/g, "\xD7").replace(/\\div/g, "\xF7").replace(/\\pm/g, "\xB1").replace(/\\mp/g, "\u2213").replace(/\\leq/g, "\u2264").replace(/\\geq/g, "\u2265").replace(/\\neq/g, "\u2260").replace(/\\approx/g, "\u2248").replace(/\\in\b/g, "\u2208").replace(/\\notin\b/g, "\u2209").replace(/\\infty/g, "\u221E").replace(/\\sum\b/g, "\u03A3").replace(/\\prod\b/g, "\u03A0").replace(/\\int\b/g, "\u222B").replace(/\\alpha/g, "\u03B1").replace(/\\beta/g, "\u03B2").replace(/\\gamma/g, "\u03B3").replace(/\\delta/g, "\u03B4").replace(/\\theta/g, "\u03B8").replace(/\\lambda/g, "\u03BB").replace(/\\mu/g, "\u03BC").replace(/\\pi/g, "\u03C0").replace(/\\sigma/g, "\u03C3").replace(/\\phi/g, "\u03C6").replace(/\\omega/g, "\u03C9").replace(/\\implies\b/g, "\u21D2").replace(/\\iff\b/g, "\u21D4").replace(/\\to\b/g, "\u2192").replace(/\\rightarrow/g, "\u2192").replace(/\\Rightarrow/g, "\u21D2").replace(/\\leftarrow/g, "\u2190").replace(/\\Leftarrow/g, "\u21D0").replace(/\\ldots/g, "\u2026").replace(/\\cdots/g, "\u22EF").replace(/\\quad/g, " ").replace(/\\qquad/g, " ").replace(/\\,/g, " ").replace(/\\;/g, " ").replace(/\\!/g, "").replace(/\\\\/g, "\n").replace(/\^\{([\w+-]+)\}/g, (_m, g) => toSuperscript(g)).replace(/\^([0-9+\-n])/g, (_m, g) => toSuperscript(g)).replace(/_\{([\w+-]+)\}/g, (_m, g) => toSubscript(g)).replace(/_([0-9+\-])/g, (_m, g) => toSubscript(g)).replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g, "($1)/($2)").replace(/\\[a-zA-Z]+\s*\{([^{}]+)\}/g, "$1").replace(/\\[a-zA-Z]+/g, "").replace(/[ \t]{2,}/g, " ");
862
1347
  }
863
1348
  var INLINE_RE = /(\*\*([^*\n]+?)\*\*|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
864
1349
  function InlineMd({ text }) {
@@ -1006,7 +1491,7 @@ var EventRow = React2.memo(function EventRow2({ event }) {
1006
1491
  }
1007
1492
  if (event.role === "assistant") {
1008
1493
  if (event.streaming) return /* @__PURE__ */ React2.createElement(StreamingAssistant, { event });
1009
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant")), event.reasoning ? /* @__PURE__ */ React2.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React2.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React2.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React2.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React2.createElement(Text2, { color: "magenta" }, event.repair) : null);
1494
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React2.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React2.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React2.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React2.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React2.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React2.createElement(Text2, { color: "magenta" }, event.repair) : null);
1010
1495
  }
1011
1496
  if (event.role === "tool") {
1012
1497
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", truncate(event.text, 400)));
@@ -1019,6 +1504,14 @@ var EventRow = React2.memo(function EventRow2({ event }) {
1019
1504
  }
1020
1505
  return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
1021
1506
  });
1507
+ function BranchBlock({ branch }) {
1508
+ const per = branch.uncertainties.map((u, i) => {
1509
+ const marker = i === branch.chosenIndex ? "\u25B8" : " ";
1510
+ const t = (branch.temperatures[i] ?? 0).toFixed(1);
1511
+ return `${marker} #${i} T=${t} u=${u}`;
1512
+ }).join(" ");
1513
+ return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, per)));
1514
+ }
1022
1515
  function PlanStateBlock({ planState }) {
1023
1516
  const lines = [];
1024
1517
  if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
@@ -1033,10 +1526,29 @@ function ReasoningBlock({ reasoning }) {
1033
1526
  const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
1034
1527
  return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
1035
1528
  }
1529
+ function Elapsed() {
1530
+ const [s, setS] = useState(0);
1531
+ useEffect(() => {
1532
+ const start = Date.now();
1533
+ const id = setInterval(() => setS(Math.floor((Date.now() - start) / 1e3)), 1e3);
1534
+ return () => clearInterval(id);
1535
+ }, []);
1536
+ const mm = String(Math.floor(s / 60)).padStart(2, "0");
1537
+ const ss = String(s % 60).padStart(2, "0");
1538
+ return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, `${mm}:${ss}`);
1539
+ }
1036
1540
  function StreamingAssistant({ event }) {
1541
+ if (event.branchProgress) {
1542
+ const p = event.branchProgress;
1543
+ if (p.completed === 0) {
1544
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
1545
+ }
1546
+ const pct = Math.round(p.completed / p.total * 100);
1547
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct, "%)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
1548
+ }
1037
1549
  const tail = lastLine(event.text, 140);
1038
1550
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
1039
- return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)")), reasoningTail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
1551
+ return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
1040
1552
  }
1041
1553
  function lastLine(s, maxChars) {
1042
1554
  const flat = s.replace(/\s+/g, " ").trim();
@@ -1062,7 +1574,7 @@ function PromptInput({
1062
1574
  disabled,
1063
1575
  placeholder
1064
1576
  }) {
1065
- const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? 'type a message, or "/exit"';
1577
+ const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
1066
1578
  return /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: disabled ? "gray" : "cyan", paddingX: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: disabled ? "gray" : "cyan" }, "you \u203A", " "), /* @__PURE__ */ React3.createElement(
1067
1579
  TextInput,
1068
1580
  {
@@ -1078,21 +1590,120 @@ function PromptInput({
1078
1590
  // src/cli/ui/StatsPanel.tsx
1079
1591
  import { Box as Box4, Text as Text4 } from "ink";
1080
1592
  import React4 from "react";
1081
- function StatsPanel({ summary, model, prefixHash }) {
1593
+ function StatsPanel({
1594
+ summary,
1595
+ model,
1596
+ prefixHash,
1597
+ harvestOn,
1598
+ branchBudget
1599
+ }) {
1082
1600
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
1083
1601
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
1084
- return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, { justifyContent: "space-between" }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, model), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, prefixHash)), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "turns ", summary.turns)), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cache hit "), /* @__PURE__ */ React4.createElement(Text4, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cost "), /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React4.createElement(Text4, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "saving "), /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
1602
+ const branchOn = (branchBudget ?? 1) > 1;
1603
+ return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Box4, { justifyContent: "space-between" }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, model), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React4.createElement(Text4, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React4.createElement(Text4, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cache hit "), /* @__PURE__ */ React4.createElement(Text4, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "cost "), /* @__PURE__ */ React4.createElement(Text4, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React4.createElement(Text4, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React4.createElement(Text4, null, /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "saving "), /* @__PURE__ */ React4.createElement(Text4, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
1604
+ }
1605
+
1606
+ // src/cli/ui/slash.ts
1607
+ function parseSlash(text) {
1608
+ if (!text.startsWith("/")) return null;
1609
+ const parts = text.slice(1).trim().split(/\s+/);
1610
+ const cmd = parts[0]?.toLowerCase() ?? "";
1611
+ if (!cmd) return null;
1612
+ return { cmd, args: parts.slice(1) };
1613
+ }
1614
+ function handleSlash(cmd, args, loop) {
1615
+ switch (cmd) {
1616
+ case "exit":
1617
+ case "quit":
1618
+ return { exit: true };
1619
+ case "clear":
1620
+ return { clear: true };
1621
+ case "help":
1622
+ case "?":
1623
+ return {
1624
+ info: [
1625
+ "Commands:",
1626
+ " /help this message",
1627
+ " /status show current settings",
1628
+ " /preset <fast|smart|max> one-tap presets \u2014 see below",
1629
+ " /model <id> deepseek-chat or deepseek-reasoner",
1630
+ " /harvest [on|off] Pillar 2: structured plan-state extraction",
1631
+ " /branch <N|off> run N parallel samples (N>=2), pick most confident",
1632
+ " /clear clear displayed history (log is kept)",
1633
+ " /exit quit",
1634
+ "",
1635
+ "Presets:",
1636
+ " fast deepseek-chat no harvest no branch ~1\xA2/100turns \u2190 default",
1637
+ " smart reasoner harvest ~10x cost, slower",
1638
+ " max reasoner harvest branch 3 ~30x cost, slowest"
1639
+ ].join("\n")
1640
+ };
1641
+ case "status": {
1642
+ const branchBudget = loop.branchOptions.budget ?? 1;
1643
+ return {
1644
+ info: `model=${loop.model} harvest=${loop.harvestEnabled ? "on" : "off"} branch=${branchBudget > 1 ? branchBudget : "off"} stream=${loop.stream ? "on" : "off"}`
1645
+ };
1646
+ }
1647
+ case "model": {
1648
+ const id = args[0];
1649
+ if (!id) return { info: "usage: /model <id> (try deepseek-chat or deepseek-reasoner)" };
1650
+ loop.configure({ model: id });
1651
+ return { info: `model \u2192 ${id}` };
1652
+ }
1653
+ case "harvest": {
1654
+ const arg = (args[0] ?? "").toLowerCase();
1655
+ const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
1656
+ loop.configure({ harvest: on });
1657
+ return { info: `harvest \u2192 ${loop.harvestEnabled ? "on" : "off"}` };
1658
+ }
1659
+ case "preset": {
1660
+ const name = (args[0] ?? "").toLowerCase();
1661
+ if (name === "fast" || name === "default") {
1662
+ loop.configure({ model: "deepseek-chat", harvest: false, branch: 1 });
1663
+ return { info: "preset \u2192 fast (deepseek-chat, no harvest, no branch)" };
1664
+ }
1665
+ if (name === "smart") {
1666
+ loop.configure({ model: "deepseek-reasoner", harvest: true, branch: 1 });
1667
+ return { info: "preset \u2192 smart (reasoner + harvest, ~10x cost vs fast)" };
1668
+ }
1669
+ if (name === "max" || name === "best") {
1670
+ loop.configure({ model: "deepseek-reasoner", harvest: true, branch: 3 });
1671
+ return {
1672
+ info: "preset \u2192 max (reasoner + harvest + branch3, ~30x cost vs fast, slowest)"
1673
+ };
1674
+ }
1675
+ return { info: "usage: /preset <fast|smart|max>" };
1676
+ }
1677
+ case "branch": {
1678
+ const raw = (args[0] ?? "").toLowerCase();
1679
+ if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
1680
+ loop.configure({ branch: 1 });
1681
+ return { info: "branch \u2192 off" };
1682
+ }
1683
+ const n = Number.parseInt(raw, 10);
1684
+ if (!Number.isFinite(n) || n < 2) {
1685
+ return { info: "usage: /branch <N> (N>=2, or 'off')" };
1686
+ }
1687
+ if (n > 8) {
1688
+ return { info: "branch budget capped at 8 to prevent runaway cost" };
1689
+ }
1690
+ loop.configure({ branch: n });
1691
+ return { info: `branch \u2192 ${n} (harvest auto-enabled; streaming disabled)` };
1692
+ }
1693
+ default:
1694
+ return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
1695
+ }
1085
1696
  }
1086
1697
 
1087
1698
  // src/cli/ui/App.tsx
1088
1699
  var FLUSH_INTERVAL_MS = 60;
1089
- function App({ model, system, transcript, harvest: harvest2 }) {
1700
+ function App({ model, system, transcript, harvest: harvest2, branch }) {
1090
1701
  const { exit } = useApp();
1091
- const [historical, setHistorical] = useState([]);
1092
- const [streaming, setStreaming] = useState(null);
1093
- const [input, setInput] = useState("");
1094
- const [busy, setBusy] = useState(false);
1095
- const [summary, setSummary] = useState({
1702
+ const [historical, setHistorical] = useState2([]);
1703
+ const [streaming, setStreaming] = useState2(null);
1704
+ const [input, setInput] = useState2("");
1705
+ const [busy, setBusy] = useState2(false);
1706
+ const [summary, setSummary] = useState2({
1096
1707
  turns: 0,
1097
1708
  totalCostUsd: 0,
1098
1709
  claudeEquivalentUsd: 0,
@@ -1103,7 +1714,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1103
1714
  if (transcript && !transcriptRef.current) {
1104
1715
  transcriptRef.current = createWriteStream(transcript, { flags: "a" });
1105
1716
  }
1106
- useEffect(() => {
1717
+ useEffect2(() => {
1107
1718
  return () => {
1108
1719
  transcriptRef.current?.end();
1109
1720
  };
@@ -1113,10 +1724,10 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1113
1724
  if (loopRef.current) return loopRef.current;
1114
1725
  const client = new DeepSeekClient();
1115
1726
  const prefix = new ImmutablePrefix({ system });
1116
- const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2 });
1727
+ const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2, branch });
1117
1728
  loopRef.current = l;
1118
1729
  return l;
1119
- }, [model, system, harvest2]);
1730
+ }, [model, system, harvest2, branch]);
1120
1731
  const prefixHash = loop.prefix.fingerprint;
1121
1732
  const writeTranscript = useCallback((ev) => {
1122
1733
  transcriptRef.current?.write(
@@ -1135,13 +1746,28 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1135
1746
  const text = raw.trim();
1136
1747
  if (!text || busy) return;
1137
1748
  setInput("");
1138
- if (text === "/exit" || text === "/quit") {
1139
- transcriptRef.current?.end();
1140
- exit();
1141
- return;
1142
- }
1143
- if (text === "/clear") {
1144
- setHistorical([]);
1749
+ const slash = parseSlash(text);
1750
+ if (slash) {
1751
+ const result = handleSlash(slash.cmd, slash.args, loop);
1752
+ if (result.exit) {
1753
+ transcriptRef.current?.end();
1754
+ exit();
1755
+ return;
1756
+ }
1757
+ if (result.clear) {
1758
+ setHistorical([]);
1759
+ return;
1760
+ }
1761
+ if (result.info) {
1762
+ setHistorical((prev) => [
1763
+ ...prev,
1764
+ {
1765
+ id: `sys-${Date.now()}`,
1766
+ role: "info",
1767
+ text: result.info
1768
+ }
1769
+ ]);
1770
+ }
1145
1771
  return;
1146
1772
  }
1147
1773
  setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
@@ -1172,6 +1798,23 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1172
1798
  if (ev.role === "assistant_delta") {
1173
1799
  if (ev.content) contentBuf.current += ev.content;
1174
1800
  if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
1801
+ } else if (ev.role === "branch_start") {
1802
+ setStreaming({
1803
+ id: assistantId,
1804
+ role: "assistant",
1805
+ text: "",
1806
+ streaming: true,
1807
+ branchProgress: ev.branchProgress
1808
+ });
1809
+ } else if (ev.role === "branch_progress") {
1810
+ setStreaming({
1811
+ id: assistantId,
1812
+ role: "assistant",
1813
+ text: "",
1814
+ streaming: true,
1815
+ branchProgress: ev.branchProgress
1816
+ });
1817
+ } else if (ev.role === "branch_done") {
1175
1818
  } else if (ev.role === "assistant_final") {
1176
1819
  flush();
1177
1820
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
@@ -1184,6 +1827,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1184
1827
  text: ev.content || streamRef.text,
1185
1828
  reasoning: streamRef.reasoning || void 0,
1186
1829
  planState: ev.planState,
1830
+ branch: ev.branch,
1187
1831
  stats: ev.stats,
1188
1832
  repair: repairNote || void 0,
1189
1833
  streaming: false
@@ -1217,7 +1861,19 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1217
1861
  },
1218
1862
  [busy, exit, loop, writeTranscript]
1219
1863
  );
1220
- return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(StatsPanel, { summary, model, prefixHash }), /* @__PURE__ */ React5.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React5.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React5.createElement(Box5, { marginY: 1 }, /* @__PURE__ */ React5.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React5.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }));
1864
+ return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(
1865
+ StatsPanel,
1866
+ {
1867
+ summary,
1868
+ model: loop.model,
1869
+ prefixHash,
1870
+ harvestOn: loop.harvestEnabled,
1871
+ branchBudget: loop.branchOptions.budget
1872
+ }
1873
+ ), /* @__PURE__ */ React5.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React5.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React5.createElement(Box5, { marginY: 1 }, /* @__PURE__ */ React5.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React5.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React5.createElement(CommandStrip, null));
1874
+ }
1875
+ function CommandStrip() {
1876
+ return /* @__PURE__ */ React5.createElement(Box5, { paddingX: 2 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
1221
1877
  }
1222
1878
  function describeRepair(repair) {
1223
1879
  const parts = [];
@@ -1228,12 +1884,12 @@ function describeRepair(repair) {
1228
1884
  }
1229
1885
 
1230
1886
  // src/cli/ui/Setup.tsx
1231
- import { Box as Box6, Text as Text5, useApp as useApp2 } from "ink";
1887
+ import { Box as Box6, Text as Text6, useApp as useApp2 } from "ink";
1232
1888
  import TextInput2 from "ink-text-input";
1233
- import React6, { useState as useState2 } from "react";
1889
+ import React6, { useState as useState3 } from "react";
1234
1890
  function Setup({ onReady }) {
1235
- const [value, setValue] = useState2("");
1236
- const [error, setError] = useState2(null);
1891
+ const [value, setValue] = useState3("");
1892
+ const [error, setError] = useState3(null);
1237
1893
  const { exit } = useApp2();
1238
1894
  const handleSubmit = (raw) => {
1239
1895
  const trimmed = raw.trim();
@@ -1254,7 +1910,7 @@ function Setup({ onReady }) {
1254
1910
  }
1255
1911
  onReady(trimmed);
1256
1912
  };
1257
- return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React6.createElement(
1913
+ return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React6.createElement(
1258
1914
  TextInput2,
1259
1915
  {
1260
1916
  value,
@@ -1263,12 +1919,12 @@ function Setup({ onReady }) {
1263
1919
  mask: "\u2022",
1264
1920
  placeholder: "sk-..."
1265
1921
  }
1266
- )), error ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { color: "red" }, error)) : value ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text5, { dimColor: true }, "(Type /exit to abort.)")));
1922
+ )), error ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, error)) : value ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "(Type /exit to abort.)")));
1267
1923
  }
1268
1924
 
1269
1925
  // src/cli/commands/chat.tsx
1270
1926
  function Root({ initialKey, ...appProps }) {
1271
- const [key, setKey] = useState3(initialKey);
1927
+ const [key, setKey] = useState4(initialKey);
1272
1928
  if (!key) {
1273
1929
  return /* @__PURE__ */ React7.createElement(
1274
1930
  Setup,
@@ -1287,7 +1943,8 @@ function Root({ initialKey, ...appProps }) {
1287
1943
  model: appProps.model,
1288
1944
  system: appProps.system,
1289
1945
  transcript: appProps.transcript,
1290
- harvest: appProps.harvest
1946
+ harvest: appProps.harvest,
1947
+ branch: appProps.branch
1291
1948
  }
1292
1949
  );
1293
1950
  }
@@ -1397,12 +2054,17 @@ program.name("reasonix").description("DeepSeek-native agent framework \u2014 bui
1397
2054
  program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
1398
2055
  "--harvest",
1399
2056
  "Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
2057
+ ).option(
2058
+ "--branch <n>",
2059
+ "Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
2060
+ (v) => Number.parseInt(v, 10)
1400
2061
  ).action(async (opts) => {
1401
2062
  await chatCommand({
1402
2063
  model: opts.model,
1403
2064
  system: opts.system,
1404
2065
  transcript: opts.transcript,
1405
- harvest: !!opts.harvest
2066
+ harvest: !!opts.harvest,
2067
+ branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0
1406
2068
  });
1407
2069
  });
1408
2070
  program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).action(async (task, opts) => {