reasonix 0.0.3 → 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 {
@@ -347,6 +660,74 @@ function repairTruncatedJson(input) {
347
660
  }
348
661
  }
349
662
 
663
+ // src/repair/flatten.ts
664
+ function analyzeSchema(schema) {
665
+ if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
666
+ let leafCount = 0;
667
+ let maxDepth = 0;
668
+ walk(schema, 0, (depth, isLeaf) => {
669
+ if (isLeaf) leafCount++;
670
+ if (depth > maxDepth) maxDepth = depth;
671
+ });
672
+ return {
673
+ shouldFlatten: leafCount > 10 || maxDepth > 2,
674
+ leafCount,
675
+ maxDepth
676
+ };
677
+ }
678
+ function flattenSchema(schema) {
679
+ const flatProps = {};
680
+ const required = [];
681
+ collect("", schema, flatProps, required, true);
682
+ return {
683
+ type: "object",
684
+ properties: flatProps,
685
+ required
686
+ };
687
+ }
688
+ function nestArguments(flatArgs) {
689
+ const out = {};
690
+ for (const [key, value] of Object.entries(flatArgs)) {
691
+ setByPath(out, key.split("."), value);
692
+ }
693
+ return out;
694
+ }
695
+ function walk(schema, depth, visit) {
696
+ if (schema.type === "object" && schema.properties) {
697
+ for (const child of Object.values(schema.properties)) {
698
+ walk(child, depth + 1, visit);
699
+ }
700
+ return;
701
+ }
702
+ if (schema.type === "array" && schema.items) {
703
+ walk(schema.items, depth + 1, visit);
704
+ return;
705
+ }
706
+ visit(depth, true);
707
+ }
708
+ function collect(prefix, schema, out, required, isRootRequired) {
709
+ if (schema.type === "object" && schema.properties) {
710
+ const requiredSet = new Set(schema.required ?? []);
711
+ for (const [key, child] of Object.entries(schema.properties)) {
712
+ const nextPrefix = prefix ? `${prefix}.${key}` : key;
713
+ const childRequired = isRootRequired && requiredSet.has(key);
714
+ collect(nextPrefix, child, out, required, childRequired);
715
+ }
716
+ return;
717
+ }
718
+ out[prefix] = schema;
719
+ if (isRootRequired) required.push(prefix);
720
+ }
721
+ function setByPath(target, path, value) {
722
+ let cur = target;
723
+ for (let i = 0; i < path.length - 1; i++) {
724
+ const key = path[i];
725
+ if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
726
+ cur = cur[key];
727
+ }
728
+ cur[path[path.length - 1]] = value;
729
+ }
730
+
350
731
  // src/repair/index.ts
351
732
  var ToolCallRepair = class {
352
733
  storm;
@@ -468,9 +849,20 @@ function round(n, digits) {
468
849
  // src/tools.ts
469
850
  var ToolRegistry = class {
470
851
  _tools = /* @__PURE__ */ new Map();
852
+ _autoFlatten;
853
+ constructor(opts = {}) {
854
+ this._autoFlatten = opts.autoFlatten !== false;
855
+ }
471
856
  register(def) {
472
857
  if (!def.name) throw new Error("tool requires a name");
473
- this._tools.set(def.name, def);
858
+ const internal = { ...def };
859
+ if (this._autoFlatten && def.parameters) {
860
+ const decision = analyzeSchema(def.parameters);
861
+ if (decision.shouldFlatten) {
862
+ internal.flatSchema = flattenSchema(def.parameters);
863
+ }
864
+ }
865
+ this._tools.set(def.name, internal);
474
866
  return this;
475
867
  }
476
868
  has(name) {
@@ -482,13 +874,17 @@ var ToolRegistry = class {
482
874
  get size() {
483
875
  return this._tools.size;
484
876
  }
877
+ /** True if a registered tool's schema was flattened for the model. */
878
+ wasFlattened(name) {
879
+ return Boolean(this._tools.get(name)?.flatSchema);
880
+ }
485
881
  specs() {
486
882
  return [...this._tools.values()].map((t) => ({
487
883
  type: "function",
488
884
  function: {
489
885
  name: t.name,
490
886
  description: t.description ?? "",
491
- parameters: t.parameters ?? { type: "object", properties: {} }
887
+ parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
492
888
  }
493
889
  }));
494
890
  }
@@ -499,12 +895,15 @@ var ToolRegistry = class {
499
895
  }
500
896
  let args;
501
897
  try {
502
- args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) : {} : argumentsRaw ?? {};
898
+ args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) ?? {} : {} : argumentsRaw ?? {};
503
899
  } catch (err) {
504
900
  return JSON.stringify({
505
901
  error: `invalid tool arguments JSON: ${err.message}`
506
902
  });
507
903
  }
904
+ if (tool.flatSchema && args && typeof args === "object" && hasDotKey(args)) {
905
+ args = nestArguments(args);
906
+ }
508
907
  try {
509
908
  const result = await tool.fn(args);
510
909
  return typeof result === "string" ? result : JSON.stringify(result);
@@ -515,34 +914,85 @@ var ToolRegistry = class {
515
914
  }
516
915
  }
517
916
  };
917
+ function hasDotKey(obj) {
918
+ for (const k of Object.keys(obj)) {
919
+ if (k.includes(".")) return true;
920
+ }
921
+ return false;
922
+ }
518
923
 
519
924
  // src/loop.ts
520
925
  var CacheFirstLoop = class {
521
926
  client;
522
927
  prefix;
523
928
  tools;
524
- model;
525
929
  maxToolIters;
526
- stream;
527
- harvestEnabled;
528
- harvestOptions;
529
930
  log = new AppendOnlyLog();
530
931
  scratch = new VolatileScratch();
531
932
  stats = new SessionStats();
532
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;
533
942
  _turn = 0;
943
+ _streamPreference;
534
944
  constructor(opts) {
535
945
  this.client = opts.client;
536
946
  this.prefix = opts.prefix;
537
947
  this.tools = opts.tools ?? new ToolRegistry();
538
948
  this.model = opts.model ?? "deepseek-chat";
539
949
  this.maxToolIters = opts.maxToolIters ?? 8;
540
- this.stream = opts.stream ?? true;
541
- this.harvestEnabled = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
542
- 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;
543
963
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
544
964
  this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
545
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
+ }
546
996
  buildMessages(pendingUser) {
547
997
  const msgs = [...this.prefix.toMessages(), ...this.log.toMessages()];
548
998
  if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
@@ -559,8 +1009,85 @@ var CacheFirstLoop = class {
559
1009
  let reasoningContent = "";
560
1010
  let toolCalls = [];
561
1011
  let usage = null;
1012
+ let branchSummary;
1013
+ let preHarvestedPlanState;
562
1014
  try {
563
- 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) {
564
1091
  const callBuf = /* @__PURE__ */ new Map();
565
1092
  for await (const chunk of this.client.stream({
566
1093
  model: this.model,
@@ -620,17 +1147,13 @@ var CacheFirstLoop = class {
620
1147
  };
621
1148
  return;
622
1149
  }
623
- const turnStats = this.stats.record(
624
- this._turn,
625
- this.model,
626
- usage ?? new (await import("./client-RIVGDOJP.js")).Usage()
627
- );
1150
+ const turnStats = this.stats.record(this._turn, this.model, usage ?? new Usage());
628
1151
  if (pendingUser !== null) {
629
1152
  this.log.append({ role: "user", content: pendingUser });
630
1153
  pendingUser = null;
631
1154
  }
632
1155
  this.scratch.reasoning = reasoningContent || null;
633
- 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();
634
1157
  const { calls: repairedCalls, report } = this.repair.process(
635
1158
  toolCalls,
636
1159
  reasoningContent || null
@@ -642,7 +1165,8 @@ var CacheFirstLoop = class {
642
1165
  content: assistantContent,
643
1166
  stats: turnStats,
644
1167
  planState,
645
- repair: report
1168
+ repair: report,
1169
+ branch: branchSummary
646
1170
  };
647
1171
  if (repairedCalls.length === 0) {
648
1172
  yield { turn: this._turn, role: "done", content: assistantContent };
@@ -678,6 +1202,14 @@ var CacheFirstLoop = class {
678
1202
  return msg;
679
1203
  }
680
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
+ }
681
1213
 
682
1214
  // src/env.ts
683
1215
  import { readFileSync } from "fs";
@@ -751,22 +1283,67 @@ var VERSION = "0.0.1";
751
1283
 
752
1284
  // src/cli/commands/chat.tsx
753
1285
  import { render } from "ink";
754
- import React7, { useState as useState3 } from "react";
1286
+ import React7, { useState as useState4 } from "react";
755
1287
 
756
1288
  // src/cli/ui/App.tsx
757
1289
  import { createWriteStream } from "fs";
758
- import { Box as Box5, Static, useApp } from "ink";
759
- 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";
760
1292
 
761
1293
  // src/cli/ui/EventLog.tsx
762
1294
  import { Box as Box2, Text as Text2 } from "ink";
763
- import React2 from "react";
1295
+ import React2, { useEffect, useState } from "react";
764
1296
 
765
1297
  // src/cli/ui/markdown.tsx
766
1298
  import { Box, Text } from "ink";
767
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
+ }
768
1339
  function stripMath(s) {
769
- 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, " ");
770
1347
  }
771
1348
  var INLINE_RE = /(\*\*([^*\n]+?)\*\*|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
772
1349
  function InlineMd({ text }) {
@@ -914,7 +1491,7 @@ var EventRow = React2.memo(function EventRow2({ event }) {
914
1491
  }
915
1492
  if (event.role === "assistant") {
916
1493
  if (event.streaming) return /* @__PURE__ */ React2.createElement(StreamingAssistant, { event });
917
- 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);
918
1495
  }
919
1496
  if (event.role === "tool") {
920
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)));
@@ -927,6 +1504,14 @@ var EventRow = React2.memo(function EventRow2({ event }) {
927
1504
  }
928
1505
  return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
929
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
+ }
930
1515
  function PlanStateBlock({ planState }) {
931
1516
  const lines = [];
932
1517
  if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
@@ -941,10 +1526,29 @@ function ReasoningBlock({ reasoning }) {
941
1526
  const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
942
1527
  return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
943
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
+ }
944
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
+ }
945
1549
  const tail = lastLine(event.text, 140);
946
1550
  const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
947
- 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)"));
948
1552
  }
949
1553
  function lastLine(s, maxChars) {
950
1554
  const flat = s.replace(/\s+/g, " ").trim();
@@ -970,7 +1574,7 @@ function PromptInput({
970
1574
  disabled,
971
1575
  placeholder
972
1576
  }) {
973
- 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";
974
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(
975
1579
  TextInput,
976
1580
  {
@@ -986,21 +1590,120 @@ function PromptInput({
986
1590
  // src/cli/ui/StatsPanel.tsx
987
1591
  import { Box as Box4, Text as Text4 } from "ink";
988
1592
  import React4 from "react";
989
- function StatsPanel({ summary, model, prefixHash }) {
1593
+ function StatsPanel({
1594
+ summary,
1595
+ model,
1596
+ prefixHash,
1597
+ harvestOn,
1598
+ branchBudget
1599
+ }) {
990
1600
  const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
991
1601
  const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
992
- 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
+ }
993
1696
  }
994
1697
 
995
1698
  // src/cli/ui/App.tsx
996
1699
  var FLUSH_INTERVAL_MS = 60;
997
- function App({ model, system, transcript, harvest: harvest2 }) {
1700
+ function App({ model, system, transcript, harvest: harvest2, branch }) {
998
1701
  const { exit } = useApp();
999
- const [historical, setHistorical] = useState([]);
1000
- const [streaming, setStreaming] = useState(null);
1001
- const [input, setInput] = useState("");
1002
- const [busy, setBusy] = useState(false);
1003
- 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({
1004
1707
  turns: 0,
1005
1708
  totalCostUsd: 0,
1006
1709
  claudeEquivalentUsd: 0,
@@ -1011,7 +1714,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1011
1714
  if (transcript && !transcriptRef.current) {
1012
1715
  transcriptRef.current = createWriteStream(transcript, { flags: "a" });
1013
1716
  }
1014
- useEffect(() => {
1717
+ useEffect2(() => {
1015
1718
  return () => {
1016
1719
  transcriptRef.current?.end();
1017
1720
  };
@@ -1021,10 +1724,10 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1021
1724
  if (loopRef.current) return loopRef.current;
1022
1725
  const client = new DeepSeekClient();
1023
1726
  const prefix = new ImmutablePrefix({ system });
1024
- const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2 });
1727
+ const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2, branch });
1025
1728
  loopRef.current = l;
1026
1729
  return l;
1027
- }, [model, system, harvest2]);
1730
+ }, [model, system, harvest2, branch]);
1028
1731
  const prefixHash = loop.prefix.fingerprint;
1029
1732
  const writeTranscript = useCallback((ev) => {
1030
1733
  transcriptRef.current?.write(
@@ -1043,13 +1746,28 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1043
1746
  const text = raw.trim();
1044
1747
  if (!text || busy) return;
1045
1748
  setInput("");
1046
- if (text === "/exit" || text === "/quit") {
1047
- transcriptRef.current?.end();
1048
- exit();
1049
- return;
1050
- }
1051
- if (text === "/clear") {
1052
- 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
+ }
1053
1771
  return;
1054
1772
  }
1055
1773
  setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
@@ -1080,6 +1798,23 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1080
1798
  if (ev.role === "assistant_delta") {
1081
1799
  if (ev.content) contentBuf.current += ev.content;
1082
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") {
1083
1818
  } else if (ev.role === "assistant_final") {
1084
1819
  flush();
1085
1820
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
@@ -1092,6 +1827,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1092
1827
  text: ev.content || streamRef.text,
1093
1828
  reasoning: streamRef.reasoning || void 0,
1094
1829
  planState: ev.planState,
1830
+ branch: ev.branch,
1095
1831
  stats: ev.stats,
1096
1832
  repair: repairNote || void 0,
1097
1833
  streaming: false
@@ -1125,7 +1861,19 @@ function App({ model, system, transcript, harvest: harvest2 }) {
1125
1861
  },
1126
1862
  [busy, exit, loop, writeTranscript]
1127
1863
  );
1128
- 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"));
1129
1877
  }
1130
1878
  function describeRepair(repair) {
1131
1879
  const parts = [];
@@ -1136,12 +1884,12 @@ function describeRepair(repair) {
1136
1884
  }
1137
1885
 
1138
1886
  // src/cli/ui/Setup.tsx
1139
- 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";
1140
1888
  import TextInput2 from "ink-text-input";
1141
- import React6, { useState as useState2 } from "react";
1889
+ import React6, { useState as useState3 } from "react";
1142
1890
  function Setup({ onReady }) {
1143
- const [value, setValue] = useState2("");
1144
- const [error, setError] = useState2(null);
1891
+ const [value, setValue] = useState3("");
1892
+ const [error, setError] = useState3(null);
1145
1893
  const { exit } = useApp2();
1146
1894
  const handleSubmit = (raw) => {
1147
1895
  const trimmed = raw.trim();
@@ -1162,7 +1910,7 @@ function Setup({ onReady }) {
1162
1910
  }
1163
1911
  onReady(trimmed);
1164
1912
  };
1165
- 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(
1166
1914
  TextInput2,
1167
1915
  {
1168
1916
  value,
@@ -1171,12 +1919,12 @@ function Setup({ onReady }) {
1171
1919
  mask: "\u2022",
1172
1920
  placeholder: "sk-..."
1173
1921
  }
1174
- )), 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.)")));
1175
1923
  }
1176
1924
 
1177
1925
  // src/cli/commands/chat.tsx
1178
1926
  function Root({ initialKey, ...appProps }) {
1179
- const [key, setKey] = useState3(initialKey);
1927
+ const [key, setKey] = useState4(initialKey);
1180
1928
  if (!key) {
1181
1929
  return /* @__PURE__ */ React7.createElement(
1182
1930
  Setup,
@@ -1195,7 +1943,8 @@ function Root({ initialKey, ...appProps }) {
1195
1943
  model: appProps.model,
1196
1944
  system: appProps.system,
1197
1945
  transcript: appProps.transcript,
1198
- harvest: appProps.harvest
1946
+ harvest: appProps.harvest,
1947
+ branch: appProps.branch
1199
1948
  }
1200
1949
  );
1201
1950
  }
@@ -1305,12 +2054,17 @@ program.name("reasonix").description("DeepSeek-native agent framework \u2014 bui
1305
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(
1306
2055
  "--harvest",
1307
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)
1308
2061
  ).action(async (opts) => {
1309
2062
  await chatCommand({
1310
2063
  model: opts.model,
1311
2064
  system: opts.system,
1312
2065
  transcript: opts.transcript,
1313
- harvest: !!opts.harvest
2066
+ harvest: !!opts.harvest,
2067
+ branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0
1314
2068
  });
1315
2069
  });
1316
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) => {