reasonix 0.0.4 → 0.0.6
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/README.md +140 -58
- package/dist/cli/index.js +921 -70
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +138 -6
- package/dist/index.js +586 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-Y7L6L5QS.js +0 -262
- package/dist/chunk-Y7L6L5QS.js.map +0 -1
- package/dist/cli/chunk-T2ODXAJP.js +0 -263
- package/dist/cli/chunk-T2ODXAJP.js.map +0 -1
- package/dist/cli/client-RIVGDOJP.js +0 -10
- package/dist/cli/client-RIVGDOJP.js.map +0 -1
- package/dist/client-KEA2D52Q.js +0 -9
- package/dist/client-KEA2D52Q.js.map +0 -1
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 {
|
|
@@ -470,6 +783,93 @@ function signature2(call) {
|
|
|
470
783
|
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
471
784
|
}
|
|
472
785
|
|
|
786
|
+
// src/session.ts
|
|
787
|
+
import {
|
|
788
|
+
appendFileSync,
|
|
789
|
+
chmodSync,
|
|
790
|
+
existsSync,
|
|
791
|
+
mkdirSync,
|
|
792
|
+
readFileSync,
|
|
793
|
+
readdirSync,
|
|
794
|
+
statSync,
|
|
795
|
+
unlinkSync
|
|
796
|
+
} from "fs";
|
|
797
|
+
import { homedir } from "os";
|
|
798
|
+
import { dirname, join } from "path";
|
|
799
|
+
function sessionsDir() {
|
|
800
|
+
return join(homedir(), ".reasonix", "sessions");
|
|
801
|
+
}
|
|
802
|
+
function sessionPath(name) {
|
|
803
|
+
return join(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
804
|
+
}
|
|
805
|
+
function sanitizeName(name) {
|
|
806
|
+
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
807
|
+
return cleaned || "default";
|
|
808
|
+
}
|
|
809
|
+
function loadSessionMessages(name) {
|
|
810
|
+
const path = sessionPath(name);
|
|
811
|
+
if (!existsSync(path)) return [];
|
|
812
|
+
try {
|
|
813
|
+
const raw = readFileSync(path, "utf8");
|
|
814
|
+
const out = [];
|
|
815
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
816
|
+
const trimmed = line.trim();
|
|
817
|
+
if (!trimmed) continue;
|
|
818
|
+
try {
|
|
819
|
+
const msg = JSON.parse(trimmed);
|
|
820
|
+
if (msg && typeof msg === "object" && "role" in msg) out.push(msg);
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return out;
|
|
825
|
+
} catch {
|
|
826
|
+
return [];
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function appendSessionMessage(name, message) {
|
|
830
|
+
const path = sessionPath(name);
|
|
831
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
832
|
+
appendFileSync(path, `${JSON.stringify(message)}
|
|
833
|
+
`, "utf8");
|
|
834
|
+
try {
|
|
835
|
+
chmodSync(path, 384);
|
|
836
|
+
} catch {
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function listSessions() {
|
|
840
|
+
const dir = sessionsDir();
|
|
841
|
+
if (!existsSync(dir)) return [];
|
|
842
|
+
try {
|
|
843
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
844
|
+
return files.map((file) => {
|
|
845
|
+
const path = join(dir, file);
|
|
846
|
+
const stat = statSync(path);
|
|
847
|
+
const name = file.replace(/\.jsonl$/, "");
|
|
848
|
+
const messageCount = countLines(path);
|
|
849
|
+
return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
|
|
850
|
+
}).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
851
|
+
} catch {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
function deleteSession(name) {
|
|
856
|
+
const path = sessionPath(name);
|
|
857
|
+
try {
|
|
858
|
+
unlinkSync(path);
|
|
859
|
+
return true;
|
|
860
|
+
} catch {
|
|
861
|
+
return false;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function countLines(path) {
|
|
865
|
+
try {
|
|
866
|
+
const raw = readFileSync(path, "utf8");
|
|
867
|
+
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
868
|
+
} catch {
|
|
869
|
+
return 0;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
473
873
|
// src/telemetry.ts
|
|
474
874
|
var DEEPSEEK_PRICING = {
|
|
475
875
|
"deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
|
|
@@ -613,27 +1013,92 @@ var CacheFirstLoop = class {
|
|
|
613
1013
|
client;
|
|
614
1014
|
prefix;
|
|
615
1015
|
tools;
|
|
616
|
-
model;
|
|
617
1016
|
maxToolIters;
|
|
618
|
-
stream;
|
|
619
|
-
harvestEnabled;
|
|
620
|
-
harvestOptions;
|
|
621
1017
|
log = new AppendOnlyLog();
|
|
622
1018
|
scratch = new VolatileScratch();
|
|
623
1019
|
stats = new SessionStats();
|
|
624
1020
|
repair;
|
|
1021
|
+
// Mutable via configure() — slash commands in the TUI / library callers tweak
|
|
1022
|
+
// these mid-session so users don't have to restart to try harvest or branch.
|
|
1023
|
+
model;
|
|
1024
|
+
stream;
|
|
1025
|
+
harvestEnabled;
|
|
1026
|
+
harvestOptions;
|
|
1027
|
+
branchEnabled;
|
|
1028
|
+
branchOptions;
|
|
1029
|
+
sessionName;
|
|
1030
|
+
/** Number of messages that were pre-loaded from the session file. */
|
|
1031
|
+
resumedMessageCount;
|
|
625
1032
|
_turn = 0;
|
|
1033
|
+
_streamPreference;
|
|
626
1034
|
constructor(opts) {
|
|
627
1035
|
this.client = opts.client;
|
|
628
1036
|
this.prefix = opts.prefix;
|
|
629
1037
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
630
1038
|
this.model = opts.model ?? "deepseek-chat";
|
|
631
1039
|
this.maxToolIters = opts.maxToolIters ?? 8;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1040
|
+
if (typeof opts.branch === "number") {
|
|
1041
|
+
this.branchOptions = { budget: opts.branch };
|
|
1042
|
+
} else if (opts.branch && typeof opts.branch === "object") {
|
|
1043
|
+
this.branchOptions = opts.branch;
|
|
1044
|
+
} else {
|
|
1045
|
+
this.branchOptions = {};
|
|
1046
|
+
}
|
|
1047
|
+
this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
|
|
1048
|
+
const harvestForced = this.branchEnabled;
|
|
1049
|
+
this.harvestEnabled = harvestForced || opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
|
|
1050
|
+
this.harvestOptions = typeof opts.harvest === "object" && opts.harvest !== null ? opts.harvest : this.branchOptions.harvestOptions ?? {};
|
|
1051
|
+
this._streamPreference = opts.stream ?? true;
|
|
1052
|
+
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
635
1053
|
const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
|
|
636
1054
|
this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
|
|
1055
|
+
this.sessionName = opts.session ?? null;
|
|
1056
|
+
if (this.sessionName) {
|
|
1057
|
+
const prior = loadSessionMessages(this.sessionName);
|
|
1058
|
+
for (const msg of prior) this.log.append(msg);
|
|
1059
|
+
this.resumedMessageCount = prior.length;
|
|
1060
|
+
} else {
|
|
1061
|
+
this.resumedMessageCount = 0;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
appendAndPersist(message) {
|
|
1065
|
+
this.log.append(message);
|
|
1066
|
+
if (this.sessionName) {
|
|
1067
|
+
try {
|
|
1068
|
+
appendSessionMessage(this.sessionName, message);
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Reconfigure model/harvest/branch/stream mid-session. The loop's log,
|
|
1075
|
+
* scratch, and stats are preserved — only the per-turn behavior changes.
|
|
1076
|
+
* Used by the TUI's slash commands and by library callers who want to
|
|
1077
|
+
* flip a knob between turns.
|
|
1078
|
+
*/
|
|
1079
|
+
configure(opts) {
|
|
1080
|
+
if (opts.model !== void 0) this.model = opts.model;
|
|
1081
|
+
if (opts.stream !== void 0) this._streamPreference = opts.stream;
|
|
1082
|
+
if (opts.branch !== void 0) {
|
|
1083
|
+
if (typeof opts.branch === "number") {
|
|
1084
|
+
this.branchOptions = { budget: opts.branch };
|
|
1085
|
+
} else if (opts.branch && typeof opts.branch === "object") {
|
|
1086
|
+
this.branchOptions = opts.branch;
|
|
1087
|
+
} else {
|
|
1088
|
+
this.branchOptions = {};
|
|
1089
|
+
}
|
|
1090
|
+
this.branchEnabled = (this.branchOptions.budget ?? 1) > 1;
|
|
1091
|
+
}
|
|
1092
|
+
if (opts.harvest !== void 0) {
|
|
1093
|
+
const want = opts.harvest === true || typeof opts.harvest === "object" && opts.harvest !== null;
|
|
1094
|
+
this.harvestEnabled = want || this.branchEnabled;
|
|
1095
|
+
if (typeof opts.harvest === "object" && opts.harvest !== null) {
|
|
1096
|
+
this.harvestOptions = opts.harvest;
|
|
1097
|
+
}
|
|
1098
|
+
} else if (this.branchEnabled) {
|
|
1099
|
+
this.harvestEnabled = true;
|
|
1100
|
+
}
|
|
1101
|
+
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
637
1102
|
}
|
|
638
1103
|
buildMessages(pendingUser) {
|
|
639
1104
|
const msgs = [...this.prefix.toMessages(), ...this.log.toMessages()];
|
|
@@ -651,8 +1116,85 @@ var CacheFirstLoop = class {
|
|
|
651
1116
|
let reasoningContent = "";
|
|
652
1117
|
let toolCalls = [];
|
|
653
1118
|
let usage = null;
|
|
1119
|
+
let branchSummary;
|
|
1120
|
+
let preHarvestedPlanState;
|
|
654
1121
|
try {
|
|
655
|
-
if (this.
|
|
1122
|
+
if (this.branchEnabled) {
|
|
1123
|
+
const budget = this.branchOptions.budget ?? 1;
|
|
1124
|
+
yield {
|
|
1125
|
+
turn: this._turn,
|
|
1126
|
+
role: "branch_start",
|
|
1127
|
+
content: "",
|
|
1128
|
+
branchProgress: {
|
|
1129
|
+
completed: 0,
|
|
1130
|
+
total: budget,
|
|
1131
|
+
latestIndex: -1,
|
|
1132
|
+
latestTemperature: -1,
|
|
1133
|
+
latestUncertainties: -1
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
const queue = [];
|
|
1137
|
+
let waiter = null;
|
|
1138
|
+
const onSampleDone = (sample) => {
|
|
1139
|
+
if (waiter) {
|
|
1140
|
+
const w = waiter;
|
|
1141
|
+
waiter = null;
|
|
1142
|
+
w(sample);
|
|
1143
|
+
} else {
|
|
1144
|
+
queue.push(sample);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
const branchPromise = runBranches(
|
|
1148
|
+
this.client,
|
|
1149
|
+
{
|
|
1150
|
+
model: this.model,
|
|
1151
|
+
messages,
|
|
1152
|
+
tools: toolSpecs.length ? toolSpecs : void 0
|
|
1153
|
+
},
|
|
1154
|
+
{
|
|
1155
|
+
...this.branchOptions,
|
|
1156
|
+
harvestOptions: this.harvestOptions,
|
|
1157
|
+
onSampleDone
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
for (let k = 0; k < budget; k++) {
|
|
1161
|
+
const sample = queue.shift() ?? await new Promise((resolve2) => {
|
|
1162
|
+
waiter = resolve2;
|
|
1163
|
+
});
|
|
1164
|
+
yield {
|
|
1165
|
+
turn: this._turn,
|
|
1166
|
+
role: "branch_progress",
|
|
1167
|
+
content: "",
|
|
1168
|
+
branchProgress: {
|
|
1169
|
+
completed: k + 1,
|
|
1170
|
+
total: budget,
|
|
1171
|
+
latestIndex: sample.index,
|
|
1172
|
+
latestTemperature: sample.temperature,
|
|
1173
|
+
latestUncertainties: sample.planState.uncertainties.length
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const result = await branchPromise;
|
|
1178
|
+
assistantContent = result.chosen.response.content;
|
|
1179
|
+
reasoningContent = result.chosen.response.reasoningContent ?? "";
|
|
1180
|
+
toolCalls = result.chosen.response.toolCalls;
|
|
1181
|
+
const agg = aggregateBranchUsage(result.samples);
|
|
1182
|
+
usage = new Usage(
|
|
1183
|
+
agg.promptTokens,
|
|
1184
|
+
agg.completionTokens,
|
|
1185
|
+
agg.totalTokens,
|
|
1186
|
+
agg.promptCacheHitTokens,
|
|
1187
|
+
agg.promptCacheMissTokens
|
|
1188
|
+
);
|
|
1189
|
+
preHarvestedPlanState = result.chosen.planState;
|
|
1190
|
+
branchSummary = summarizeBranch(result.chosen, result.samples);
|
|
1191
|
+
yield {
|
|
1192
|
+
turn: this._turn,
|
|
1193
|
+
role: "branch_done",
|
|
1194
|
+
content: "",
|
|
1195
|
+
branch: branchSummary
|
|
1196
|
+
};
|
|
1197
|
+
} else if (this.stream) {
|
|
656
1198
|
const callBuf = /* @__PURE__ */ new Map();
|
|
657
1199
|
for await (const chunk of this.client.stream({
|
|
658
1200
|
model: this.model,
|
|
@@ -712,29 +1254,26 @@ var CacheFirstLoop = class {
|
|
|
712
1254
|
};
|
|
713
1255
|
return;
|
|
714
1256
|
}
|
|
715
|
-
const turnStats = this.stats.record(
|
|
716
|
-
this._turn,
|
|
717
|
-
this.model,
|
|
718
|
-
usage ?? new (await import("./client-RIVGDOJP.js")).Usage()
|
|
719
|
-
);
|
|
1257
|
+
const turnStats = this.stats.record(this._turn, this.model, usage ?? new Usage());
|
|
720
1258
|
if (pendingUser !== null) {
|
|
721
|
-
this.
|
|
1259
|
+
this.appendAndPersist({ role: "user", content: pendingUser });
|
|
722
1260
|
pendingUser = null;
|
|
723
1261
|
}
|
|
724
1262
|
this.scratch.reasoning = reasoningContent || null;
|
|
725
|
-
const planState = this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
|
|
1263
|
+
const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions) : emptyPlanState();
|
|
726
1264
|
const { calls: repairedCalls, report } = this.repair.process(
|
|
727
1265
|
toolCalls,
|
|
728
1266
|
reasoningContent || null
|
|
729
1267
|
);
|
|
730
|
-
this.
|
|
1268
|
+
this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
|
|
731
1269
|
yield {
|
|
732
1270
|
turn: this._turn,
|
|
733
1271
|
role: "assistant_final",
|
|
734
1272
|
content: assistantContent,
|
|
735
1273
|
stats: turnStats,
|
|
736
1274
|
planState,
|
|
737
|
-
repair: report
|
|
1275
|
+
repair: report,
|
|
1276
|
+
branch: branchSummary
|
|
738
1277
|
};
|
|
739
1278
|
if (repairedCalls.length === 0) {
|
|
740
1279
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
@@ -744,7 +1283,7 @@ var CacheFirstLoop = class {
|
|
|
744
1283
|
const name = call.function?.name ?? "";
|
|
745
1284
|
const args = call.function?.arguments ?? "{}";
|
|
746
1285
|
const result = await this.tools.dispatch(name, args);
|
|
747
|
-
this.
|
|
1286
|
+
this.appendAndPersist({
|
|
748
1287
|
role: "tool",
|
|
749
1288
|
tool_call_id: call.id ?? "",
|
|
750
1289
|
name,
|
|
@@ -770,14 +1309,22 @@ var CacheFirstLoop = class {
|
|
|
770
1309
|
return msg;
|
|
771
1310
|
}
|
|
772
1311
|
};
|
|
1312
|
+
function summarizeBranch(chosen, samples) {
|
|
1313
|
+
return {
|
|
1314
|
+
budget: samples.length,
|
|
1315
|
+
chosenIndex: chosen.index,
|
|
1316
|
+
uncertainties: samples.map((s) => s.planState.uncertainties.length),
|
|
1317
|
+
temperatures: samples.map((s) => s.temperature)
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
773
1320
|
|
|
774
1321
|
// src/env.ts
|
|
775
|
-
import { readFileSync } from "fs";
|
|
1322
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
776
1323
|
import { resolve } from "path";
|
|
777
1324
|
function loadDotenv(path = ".env") {
|
|
778
1325
|
let raw;
|
|
779
1326
|
try {
|
|
780
|
-
raw =
|
|
1327
|
+
raw = readFileSync2(resolve(process.cwd(), path), "utf8");
|
|
781
1328
|
} catch {
|
|
782
1329
|
return;
|
|
783
1330
|
}
|
|
@@ -796,15 +1343,15 @@ function loadDotenv(path = ".env") {
|
|
|
796
1343
|
}
|
|
797
1344
|
|
|
798
1345
|
// src/config.ts
|
|
799
|
-
import { chmodSync, mkdirSync, readFileSync as
|
|
800
|
-
import { homedir } from "os";
|
|
801
|
-
import { dirname, join } from "path";
|
|
1346
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
1347
|
+
import { homedir as homedir2 } from "os";
|
|
1348
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
802
1349
|
function defaultConfigPath() {
|
|
803
|
-
return
|
|
1350
|
+
return join2(homedir2(), ".reasonix", "config.json");
|
|
804
1351
|
}
|
|
805
1352
|
function readConfig(path = defaultConfigPath()) {
|
|
806
1353
|
try {
|
|
807
|
-
const raw =
|
|
1354
|
+
const raw = readFileSync3(path, "utf8");
|
|
808
1355
|
const parsed = JSON.parse(raw);
|
|
809
1356
|
if (parsed && typeof parsed === "object") return parsed;
|
|
810
1357
|
} catch {
|
|
@@ -812,10 +1359,10 @@ function readConfig(path = defaultConfigPath()) {
|
|
|
812
1359
|
return {};
|
|
813
1360
|
}
|
|
814
1361
|
function writeConfig(cfg, path = defaultConfigPath()) {
|
|
815
|
-
|
|
1362
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
816
1363
|
writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
817
1364
|
try {
|
|
818
|
-
|
|
1365
|
+
chmodSync2(path, 384);
|
|
819
1366
|
} catch {
|
|
820
1367
|
}
|
|
821
1368
|
}
|
|
@@ -843,22 +1390,67 @@ var VERSION = "0.0.1";
|
|
|
843
1390
|
|
|
844
1391
|
// src/cli/commands/chat.tsx
|
|
845
1392
|
import { render } from "ink";
|
|
846
|
-
import React7, { useState as
|
|
1393
|
+
import React7, { useState as useState4 } from "react";
|
|
847
1394
|
|
|
848
1395
|
// src/cli/ui/App.tsx
|
|
849
1396
|
import { createWriteStream } from "fs";
|
|
850
|
-
import { Box as Box5, Static, useApp } from "ink";
|
|
851
|
-
import React5, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
1397
|
+
import { Box as Box5, Static, Text as Text5, useApp } from "ink";
|
|
1398
|
+
import React5, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
852
1399
|
|
|
853
1400
|
// src/cli/ui/EventLog.tsx
|
|
854
1401
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
855
|
-
import React2 from "react";
|
|
1402
|
+
import React2, { useEffect, useState } from "react";
|
|
856
1403
|
|
|
857
1404
|
// src/cli/ui/markdown.tsx
|
|
858
1405
|
import { Box, Text } from "ink";
|
|
859
1406
|
import React from "react";
|
|
1407
|
+
var SUPERSCRIPT = {
|
|
1408
|
+
"0": "\u2070",
|
|
1409
|
+
"1": "\xB9",
|
|
1410
|
+
"2": "\xB2",
|
|
1411
|
+
"3": "\xB3",
|
|
1412
|
+
"4": "\u2074",
|
|
1413
|
+
"5": "\u2075",
|
|
1414
|
+
"6": "\u2076",
|
|
1415
|
+
"7": "\u2077",
|
|
1416
|
+
"8": "\u2078",
|
|
1417
|
+
"9": "\u2079",
|
|
1418
|
+
"+": "\u207A",
|
|
1419
|
+
"-": "\u207B",
|
|
1420
|
+
n: "\u207F"
|
|
1421
|
+
};
|
|
1422
|
+
var SUBSCRIPT = {
|
|
1423
|
+
"0": "\u2080",
|
|
1424
|
+
"1": "\u2081",
|
|
1425
|
+
"2": "\u2082",
|
|
1426
|
+
"3": "\u2083",
|
|
1427
|
+
"4": "\u2084",
|
|
1428
|
+
"5": "\u2085",
|
|
1429
|
+
"6": "\u2086",
|
|
1430
|
+
"7": "\u2087",
|
|
1431
|
+
"8": "\u2088",
|
|
1432
|
+
"9": "\u2089",
|
|
1433
|
+
"+": "\u208A",
|
|
1434
|
+
"-": "\u208B"
|
|
1435
|
+
};
|
|
1436
|
+
function toSuperscript(s) {
|
|
1437
|
+
let out = "";
|
|
1438
|
+
for (const c of s) out += SUPERSCRIPT[c] ?? c;
|
|
1439
|
+
return out;
|
|
1440
|
+
}
|
|
1441
|
+
function toSubscript(s) {
|
|
1442
|
+
let out = "";
|
|
1443
|
+
for (const c of s) out += SUBSCRIPT[c] ?? c;
|
|
1444
|
+
return out;
|
|
1445
|
+
}
|
|
860
1446
|
function stripMath(s) {
|
|
861
|
-
return s.replace(/\\\(\s*/g, "").replace(/\s*\\\)/g, "").replace(/\\\[\s*/g, "\n").replace(/\s*\\\]/g, "\n").replace(
|
|
1447
|
+
return s.replace(/\\\(\s*/g, "").replace(/\s*\\\)/g, "").replace(/\\\[\s*/g, "\n").replace(/\s*\\\]/g, "\n").replace(
|
|
1448
|
+
/\\[dt]?frac\s*\{((?:[^{}]|\{[^{}]*\})+)\}\s*\{((?:[^{}]|\{[^{}]*\})+)\}/g,
|
|
1449
|
+
(_m, num, den) => `(${num.trim()})/(${den.trim()})`
|
|
1450
|
+
).replace(
|
|
1451
|
+
/\\binom\s*\{([^{}]+)\}\s*\{([^{}]+)\}/g,
|
|
1452
|
+
(_m, n, k) => `C(${n.trim()},${k.trim()})`
|
|
1453
|
+
).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
1454
|
}
|
|
863
1455
|
var INLINE_RE = /(\*\*([^*\n]+?)\*\*|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
|
|
864
1456
|
function InlineMd({ text }) {
|
|
@@ -1006,7 +1598,7 @@ var EventRow = React2.memo(function EventRow2({ event }) {
|
|
|
1006
1598
|
}
|
|
1007
1599
|
if (event.role === "assistant") {
|
|
1008
1600
|
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);
|
|
1601
|
+
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
1602
|
}
|
|
1011
1603
|
if (event.role === "tool") {
|
|
1012
1604
|
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 +1611,14 @@ var EventRow = React2.memo(function EventRow2({ event }) {
|
|
|
1019
1611
|
}
|
|
1020
1612
|
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
|
|
1021
1613
|
});
|
|
1614
|
+
function BranchBlock({ branch }) {
|
|
1615
|
+
const per = branch.uncertainties.map((u, i) => {
|
|
1616
|
+
const marker = i === branch.chosenIndex ? "\u25B8" : " ";
|
|
1617
|
+
const t = (branch.temperatures[i] ?? 0).toFixed(1);
|
|
1618
|
+
return `${marker} #${i} T=${t} u=${u}`;
|
|
1619
|
+
}).join(" ");
|
|
1620
|
+
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)));
|
|
1621
|
+
}
|
|
1022
1622
|
function PlanStateBlock({ planState }) {
|
|
1023
1623
|
const lines = [];
|
|
1024
1624
|
if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
|
|
@@ -1033,10 +1633,29 @@ function ReasoningBlock({ reasoning }) {
|
|
|
1033
1633
|
const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
|
|
1034
1634
|
return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
|
|
1035
1635
|
}
|
|
1636
|
+
function Elapsed() {
|
|
1637
|
+
const [s, setS] = useState(0);
|
|
1638
|
+
useEffect(() => {
|
|
1639
|
+
const start = Date.now();
|
|
1640
|
+
const id = setInterval(() => setS(Math.floor((Date.now() - start) / 1e3)), 1e3);
|
|
1641
|
+
return () => clearInterval(id);
|
|
1642
|
+
}, []);
|
|
1643
|
+
const mm = String(Math.floor(s / 60)).padStart(2, "0");
|
|
1644
|
+
const ss = String(s % 60).padStart(2, "0");
|
|
1645
|
+
return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, `${mm}:${ss}`);
|
|
1646
|
+
}
|
|
1036
1647
|
function StreamingAssistant({ event }) {
|
|
1648
|
+
if (event.branchProgress) {
|
|
1649
|
+
const p = event.branchProgress;
|
|
1650
|
+
if (p.completed === 0) {
|
|
1651
|
+
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"));
|
|
1652
|
+
}
|
|
1653
|
+
const pct = Math.round(p.completed / p.total * 100);
|
|
1654
|
+
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"));
|
|
1655
|
+
}
|
|
1037
1656
|
const tail = lastLine(event.text, 140);
|
|
1038
1657
|
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)"));
|
|
1658
|
+
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
1659
|
}
|
|
1041
1660
|
function lastLine(s, maxChars) {
|
|
1042
1661
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -1062,7 +1681,7 @@ function PromptInput({
|
|
|
1062
1681
|
disabled,
|
|
1063
1682
|
placeholder
|
|
1064
1683
|
}) {
|
|
1065
|
-
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ??
|
|
1684
|
+
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
|
|
1066
1685
|
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
1686
|
TextInput,
|
|
1068
1687
|
{
|
|
@@ -1078,21 +1697,156 @@ function PromptInput({
|
|
|
1078
1697
|
// src/cli/ui/StatsPanel.tsx
|
|
1079
1698
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
1080
1699
|
import React4 from "react";
|
|
1081
|
-
function StatsPanel({
|
|
1700
|
+
function StatsPanel({
|
|
1701
|
+
summary,
|
|
1702
|
+
model,
|
|
1703
|
+
prefixHash,
|
|
1704
|
+
harvestOn,
|
|
1705
|
+
branchBudget
|
|
1706
|
+
}) {
|
|
1082
1707
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
1083
1708
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
1084
|
-
|
|
1709
|
+
const branchOn = (branchBudget ?? 1) > 1;
|
|
1710
|
+
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), "%"))));
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/cli/ui/slash.ts
|
|
1714
|
+
function parseSlash(text) {
|
|
1715
|
+
if (!text.startsWith("/")) return null;
|
|
1716
|
+
const parts = text.slice(1).trim().split(/\s+/);
|
|
1717
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
1718
|
+
if (!cmd) return null;
|
|
1719
|
+
return { cmd, args: parts.slice(1) };
|
|
1720
|
+
}
|
|
1721
|
+
function handleSlash(cmd, args, loop) {
|
|
1722
|
+
switch (cmd) {
|
|
1723
|
+
case "exit":
|
|
1724
|
+
case "quit":
|
|
1725
|
+
return { exit: true };
|
|
1726
|
+
case "clear":
|
|
1727
|
+
return { clear: true };
|
|
1728
|
+
case "help":
|
|
1729
|
+
case "?":
|
|
1730
|
+
return {
|
|
1731
|
+
info: [
|
|
1732
|
+
"Commands:",
|
|
1733
|
+
" /help this message",
|
|
1734
|
+
" /status show current settings",
|
|
1735
|
+
" /preset <fast|smart|max> one-tap presets \u2014 see below",
|
|
1736
|
+
" /model <id> deepseek-chat or deepseek-reasoner",
|
|
1737
|
+
" /harvest [on|off] Pillar 2: structured plan-state extraction",
|
|
1738
|
+
" /branch <N|off> run N parallel samples (N>=2), pick most confident",
|
|
1739
|
+
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
1740
|
+
" /forget delete the current session from disk",
|
|
1741
|
+
" /clear clear displayed history (log + session kept)",
|
|
1742
|
+
" /exit quit",
|
|
1743
|
+
"",
|
|
1744
|
+
"Presets:",
|
|
1745
|
+
" fast deepseek-chat no harvest no branch ~1\xA2/100turns \u2190 default",
|
|
1746
|
+
" smart reasoner harvest ~10x cost, slower",
|
|
1747
|
+
" max reasoner harvest branch 3 ~30x cost, slowest",
|
|
1748
|
+
"",
|
|
1749
|
+
"Sessions (auto-enabled by default, named 'default'):",
|
|
1750
|
+
" reasonix chat --session <name> use a different named session",
|
|
1751
|
+
" reasonix chat --no-session disable persistence for this run"
|
|
1752
|
+
].join("\n")
|
|
1753
|
+
};
|
|
1754
|
+
case "sessions": {
|
|
1755
|
+
const items = listSessions();
|
|
1756
|
+
if (items.length === 0) {
|
|
1757
|
+
return {
|
|
1758
|
+
info: "no saved sessions yet \u2014 chat normally and your messages will be saved automatically"
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
const lines = ["Saved sessions:"];
|
|
1762
|
+
for (const s of items) {
|
|
1763
|
+
const sizeKb = (s.size / 1024).toFixed(1);
|
|
1764
|
+
const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
|
|
1765
|
+
const marker = s.name === loop.sessionName ? "\u25B8" : " ";
|
|
1766
|
+
lines.push(
|
|
1767
|
+
` ${marker} ${s.name.padEnd(22)} ${String(s.messageCount).padStart(5)} msgs ${sizeKb.padStart(7)} KB ${when}`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
lines.push("");
|
|
1771
|
+
lines.push("Resume with: reasonix chat --session <name>");
|
|
1772
|
+
return { info: lines.join("\n") };
|
|
1773
|
+
}
|
|
1774
|
+
case "forget": {
|
|
1775
|
+
if (!loop.sessionName) {
|
|
1776
|
+
return { info: "not in a session \u2014 nothing to forget" };
|
|
1777
|
+
}
|
|
1778
|
+
const name = loop.sessionName;
|
|
1779
|
+
const ok = deleteSession(name);
|
|
1780
|
+
return {
|
|
1781
|
+
info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
case "status": {
|
|
1785
|
+
const branchBudget = loop.branchOptions.budget ?? 1;
|
|
1786
|
+
return {
|
|
1787
|
+
info: `model=${loop.model} harvest=${loop.harvestEnabled ? "on" : "off"} branch=${branchBudget > 1 ? branchBudget : "off"} stream=${loop.stream ? "on" : "off"}`
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
case "model": {
|
|
1791
|
+
const id = args[0];
|
|
1792
|
+
if (!id) return { info: "usage: /model <id> (try deepseek-chat or deepseek-reasoner)" };
|
|
1793
|
+
loop.configure({ model: id });
|
|
1794
|
+
return { info: `model \u2192 ${id}` };
|
|
1795
|
+
}
|
|
1796
|
+
case "harvest": {
|
|
1797
|
+
const arg = (args[0] ?? "").toLowerCase();
|
|
1798
|
+
const on = arg === "" ? !loop.harvestEnabled : arg === "on" || arg === "true" || arg === "1";
|
|
1799
|
+
loop.configure({ harvest: on });
|
|
1800
|
+
return { info: `harvest \u2192 ${loop.harvestEnabled ? "on" : "off"}` };
|
|
1801
|
+
}
|
|
1802
|
+
case "preset": {
|
|
1803
|
+
const name = (args[0] ?? "").toLowerCase();
|
|
1804
|
+
if (name === "fast" || name === "default") {
|
|
1805
|
+
loop.configure({ model: "deepseek-chat", harvest: false, branch: 1 });
|
|
1806
|
+
return { info: "preset \u2192 fast (deepseek-chat, no harvest, no branch)" };
|
|
1807
|
+
}
|
|
1808
|
+
if (name === "smart") {
|
|
1809
|
+
loop.configure({ model: "deepseek-reasoner", harvest: true, branch: 1 });
|
|
1810
|
+
return { info: "preset \u2192 smart (reasoner + harvest, ~10x cost vs fast)" };
|
|
1811
|
+
}
|
|
1812
|
+
if (name === "max" || name === "best") {
|
|
1813
|
+
loop.configure({ model: "deepseek-reasoner", harvest: true, branch: 3 });
|
|
1814
|
+
return {
|
|
1815
|
+
info: "preset \u2192 max (reasoner + harvest + branch3, ~30x cost vs fast, slowest)"
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
return { info: "usage: /preset <fast|smart|max>" };
|
|
1819
|
+
}
|
|
1820
|
+
case "branch": {
|
|
1821
|
+
const raw = (args[0] ?? "").toLowerCase();
|
|
1822
|
+
if (raw === "" || raw === "off" || raw === "0" || raw === "1") {
|
|
1823
|
+
loop.configure({ branch: 1 });
|
|
1824
|
+
return { info: "branch \u2192 off" };
|
|
1825
|
+
}
|
|
1826
|
+
const n = Number.parseInt(raw, 10);
|
|
1827
|
+
if (!Number.isFinite(n) || n < 2) {
|
|
1828
|
+
return { info: "usage: /branch <N> (N>=2, or 'off')" };
|
|
1829
|
+
}
|
|
1830
|
+
if (n > 8) {
|
|
1831
|
+
return { info: "branch budget capped at 8 to prevent runaway cost" };
|
|
1832
|
+
}
|
|
1833
|
+
loop.configure({ branch: n });
|
|
1834
|
+
return { info: `branch \u2192 ${n} (harvest auto-enabled; streaming disabled)` };
|
|
1835
|
+
}
|
|
1836
|
+
default:
|
|
1837
|
+
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
1838
|
+
}
|
|
1085
1839
|
}
|
|
1086
1840
|
|
|
1087
1841
|
// src/cli/ui/App.tsx
|
|
1088
1842
|
var FLUSH_INTERVAL_MS = 60;
|
|
1089
|
-
function App({ model, system, transcript, harvest: harvest2 }) {
|
|
1843
|
+
function App({ model, system, transcript, harvest: harvest2, branch, session }) {
|
|
1090
1844
|
const { exit } = useApp();
|
|
1091
|
-
const [historical, setHistorical] =
|
|
1092
|
-
const [streaming, setStreaming] =
|
|
1093
|
-
const [input, setInput] =
|
|
1094
|
-
const [busy, setBusy] =
|
|
1095
|
-
const [summary, setSummary] =
|
|
1845
|
+
const [historical, setHistorical] = useState2([]);
|
|
1846
|
+
const [streaming, setStreaming] = useState2(null);
|
|
1847
|
+
const [input, setInput] = useState2("");
|
|
1848
|
+
const [busy, setBusy] = useState2(false);
|
|
1849
|
+
const [summary, setSummary] = useState2({
|
|
1096
1850
|
turns: 0,
|
|
1097
1851
|
totalCostUsd: 0,
|
|
1098
1852
|
claudeEquivalentUsd: 0,
|
|
@@ -1103,7 +1857,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
|
|
|
1103
1857
|
if (transcript && !transcriptRef.current) {
|
|
1104
1858
|
transcriptRef.current = createWriteStream(transcript, { flags: "a" });
|
|
1105
1859
|
}
|
|
1106
|
-
|
|
1860
|
+
useEffect2(() => {
|
|
1107
1861
|
return () => {
|
|
1108
1862
|
transcriptRef.current?.end();
|
|
1109
1863
|
};
|
|
@@ -1113,10 +1867,43 @@ function App({ model, system, transcript, harvest: harvest2 }) {
|
|
|
1113
1867
|
if (loopRef.current) return loopRef.current;
|
|
1114
1868
|
const client = new DeepSeekClient();
|
|
1115
1869
|
const prefix = new ImmutablePrefix({ system });
|
|
1116
|
-
const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2 });
|
|
1870
|
+
const l = new CacheFirstLoop({ client, prefix, model, harvest: harvest2, branch, session });
|
|
1117
1871
|
loopRef.current = l;
|
|
1118
1872
|
return l;
|
|
1119
|
-
}, [model, system, harvest2]);
|
|
1873
|
+
}, [model, system, harvest2, branch, session]);
|
|
1874
|
+
const sessionBannerShown = useRef(false);
|
|
1875
|
+
useEffect2(() => {
|
|
1876
|
+
if (sessionBannerShown.current) return;
|
|
1877
|
+
sessionBannerShown.current = true;
|
|
1878
|
+
if (!session) {
|
|
1879
|
+
setHistorical((prev) => [
|
|
1880
|
+
...prev,
|
|
1881
|
+
{
|
|
1882
|
+
id: `sys-session-${Date.now()}`,
|
|
1883
|
+
role: "info",
|
|
1884
|
+
text: "\u25B8 ephemeral chat (no session persistence) \u2014 drop --no-session to enable"
|
|
1885
|
+
}
|
|
1886
|
+
]);
|
|
1887
|
+
} else if (loop.resumedMessageCount > 0) {
|
|
1888
|
+
setHistorical((prev) => [
|
|
1889
|
+
...prev,
|
|
1890
|
+
{
|
|
1891
|
+
id: `sys-resume-${Date.now()}`,
|
|
1892
|
+
role: "info",
|
|
1893
|
+
text: `\u25B8 resumed session "${session}" with ${loop.resumedMessageCount} prior messages \xB7 /forget to start over \xB7 /sessions to list`
|
|
1894
|
+
}
|
|
1895
|
+
]);
|
|
1896
|
+
} else {
|
|
1897
|
+
setHistorical((prev) => [
|
|
1898
|
+
...prev,
|
|
1899
|
+
{
|
|
1900
|
+
id: `sys-newsession-${Date.now()}`,
|
|
1901
|
+
role: "info",
|
|
1902
|
+
text: `\u25B8 session "${session}" (new) \u2014 auto-saved as you chat \xB7 /forget to delete \xB7 /sessions to list`
|
|
1903
|
+
}
|
|
1904
|
+
]);
|
|
1905
|
+
}
|
|
1906
|
+
}, [session, loop]);
|
|
1120
1907
|
const prefixHash = loop.prefix.fingerprint;
|
|
1121
1908
|
const writeTranscript = useCallback((ev) => {
|
|
1122
1909
|
transcriptRef.current?.write(
|
|
@@ -1135,13 +1922,28 @@ function App({ model, system, transcript, harvest: harvest2 }) {
|
|
|
1135
1922
|
const text = raw.trim();
|
|
1136
1923
|
if (!text || busy) return;
|
|
1137
1924
|
setInput("");
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1925
|
+
const slash = parseSlash(text);
|
|
1926
|
+
if (slash) {
|
|
1927
|
+
const result = handleSlash(slash.cmd, slash.args, loop);
|
|
1928
|
+
if (result.exit) {
|
|
1929
|
+
transcriptRef.current?.end();
|
|
1930
|
+
exit();
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
if (result.clear) {
|
|
1934
|
+
setHistorical([]);
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (result.info) {
|
|
1938
|
+
setHistorical((prev) => [
|
|
1939
|
+
...prev,
|
|
1940
|
+
{
|
|
1941
|
+
id: `sys-${Date.now()}`,
|
|
1942
|
+
role: "info",
|
|
1943
|
+
text: result.info
|
|
1944
|
+
}
|
|
1945
|
+
]);
|
|
1946
|
+
}
|
|
1145
1947
|
return;
|
|
1146
1948
|
}
|
|
1147
1949
|
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
@@ -1172,6 +1974,23 @@ function App({ model, system, transcript, harvest: harvest2 }) {
|
|
|
1172
1974
|
if (ev.role === "assistant_delta") {
|
|
1173
1975
|
if (ev.content) contentBuf.current += ev.content;
|
|
1174
1976
|
if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
|
|
1977
|
+
} else if (ev.role === "branch_start") {
|
|
1978
|
+
setStreaming({
|
|
1979
|
+
id: assistantId,
|
|
1980
|
+
role: "assistant",
|
|
1981
|
+
text: "",
|
|
1982
|
+
streaming: true,
|
|
1983
|
+
branchProgress: ev.branchProgress
|
|
1984
|
+
});
|
|
1985
|
+
} else if (ev.role === "branch_progress") {
|
|
1986
|
+
setStreaming({
|
|
1987
|
+
id: assistantId,
|
|
1988
|
+
role: "assistant",
|
|
1989
|
+
text: "",
|
|
1990
|
+
streaming: true,
|
|
1991
|
+
branchProgress: ev.branchProgress
|
|
1992
|
+
});
|
|
1993
|
+
} else if (ev.role === "branch_done") {
|
|
1175
1994
|
} else if (ev.role === "assistant_final") {
|
|
1176
1995
|
flush();
|
|
1177
1996
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
@@ -1184,6 +2003,7 @@ function App({ model, system, transcript, harvest: harvest2 }) {
|
|
|
1184
2003
|
text: ev.content || streamRef.text,
|
|
1185
2004
|
reasoning: streamRef.reasoning || void 0,
|
|
1186
2005
|
planState: ev.planState,
|
|
2006
|
+
branch: ev.branch,
|
|
1187
2007
|
stats: ev.stats,
|
|
1188
2008
|
repair: repairNote || void 0,
|
|
1189
2009
|
streaming: false
|
|
@@ -1217,7 +2037,19 @@ function App({ model, system, transcript, harvest: harvest2 }) {
|
|
|
1217
2037
|
},
|
|
1218
2038
|
[busy, exit, loop, writeTranscript]
|
|
1219
2039
|
);
|
|
1220
|
-
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(
|
|
2040
|
+
return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column" }, /* @__PURE__ */ React5.createElement(
|
|
2041
|
+
StatsPanel,
|
|
2042
|
+
{
|
|
2043
|
+
summary,
|
|
2044
|
+
model: loop.model,
|
|
2045
|
+
prefixHash,
|
|
2046
|
+
harvestOn: loop.harvestEnabled,
|
|
2047
|
+
branchBudget: loop.branchOptions.budget
|
|
2048
|
+
}
|
|
2049
|
+
), /* @__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));
|
|
2050
|
+
}
|
|
2051
|
+
function CommandStrip() {
|
|
2052
|
+
return /* @__PURE__ */ React5.createElement(Box5, { paddingX: 2 }, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /sessions \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
|
|
1221
2053
|
}
|
|
1222
2054
|
function describeRepair(repair) {
|
|
1223
2055
|
const parts = [];
|
|
@@ -1228,12 +2060,12 @@ function describeRepair(repair) {
|
|
|
1228
2060
|
}
|
|
1229
2061
|
|
|
1230
2062
|
// src/cli/ui/Setup.tsx
|
|
1231
|
-
import { Box as Box6, Text as
|
|
2063
|
+
import { Box as Box6, Text as Text6, useApp as useApp2 } from "ink";
|
|
1232
2064
|
import TextInput2 from "ink-text-input";
|
|
1233
|
-
import React6, { useState as
|
|
2065
|
+
import React6, { useState as useState3 } from "react";
|
|
1234
2066
|
function Setup({ onReady }) {
|
|
1235
|
-
const [value, setValue] =
|
|
1236
|
-
const [error, setError] =
|
|
2067
|
+
const [value, setValue] = useState3("");
|
|
2068
|
+
const [error, setError] = useState3(null);
|
|
1237
2069
|
const { exit } = useApp2();
|
|
1238
2070
|
const handleSubmit = (raw) => {
|
|
1239
2071
|
const trimmed = raw.trim();
|
|
@@ -1254,7 +2086,7 @@ function Setup({ onReady }) {
|
|
|
1254
2086
|
}
|
|
1255
2087
|
onReady(trimmed);
|
|
1256
2088
|
};
|
|
1257
|
-
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React6.createElement(
|
|
2089
|
+
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
2090
|
TextInput2,
|
|
1259
2091
|
{
|
|
1260
2092
|
value,
|
|
@@ -1263,12 +2095,12 @@ function Setup({ onReady }) {
|
|
|
1263
2095
|
mask: "\u2022",
|
|
1264
2096
|
placeholder: "sk-..."
|
|
1265
2097
|
}
|
|
1266
|
-
)), error ? /* @__PURE__ */ React6.createElement(Box6, { marginTop: 1 }, /* @__PURE__ */ React6.createElement(
|
|
2098
|
+
)), 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
2099
|
}
|
|
1268
2100
|
|
|
1269
2101
|
// src/cli/commands/chat.tsx
|
|
1270
2102
|
function Root({ initialKey, ...appProps }) {
|
|
1271
|
-
const [key, setKey] =
|
|
2103
|
+
const [key, setKey] = useState4(initialKey);
|
|
1272
2104
|
if (!key) {
|
|
1273
2105
|
return /* @__PURE__ */ React7.createElement(
|
|
1274
2106
|
Setup,
|
|
@@ -1287,7 +2119,9 @@ function Root({ initialKey, ...appProps }) {
|
|
|
1287
2119
|
model: appProps.model,
|
|
1288
2120
|
system: appProps.system,
|
|
1289
2121
|
transcript: appProps.transcript,
|
|
1290
|
-
harvest: appProps.harvest
|
|
2122
|
+
harvest: appProps.harvest,
|
|
2123
|
+
branch: appProps.branch,
|
|
2124
|
+
session: appProps.session
|
|
1291
2125
|
}
|
|
1292
2126
|
);
|
|
1293
2127
|
}
|
|
@@ -1360,13 +2194,13 @@ async function runCommand(opts) {
|
|
|
1360
2194
|
}
|
|
1361
2195
|
|
|
1362
2196
|
// src/cli/commands/stats.ts
|
|
1363
|
-
import { existsSync, readFileSync as
|
|
2197
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
|
|
1364
2198
|
function statsCommand(opts) {
|
|
1365
|
-
if (!
|
|
2199
|
+
if (!existsSync2(opts.transcript)) {
|
|
1366
2200
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
1367
2201
|
process.exit(1);
|
|
1368
2202
|
}
|
|
1369
|
-
const lines =
|
|
2203
|
+
const lines = readFileSync4(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
1370
2204
|
let assistantTurns = 0;
|
|
1371
2205
|
let toolCalls = 0;
|
|
1372
2206
|
let lastTurn = 0;
|
|
@@ -1397,12 +2231,29 @@ program.name("reasonix").description("DeepSeek-native agent framework \u2014 bui
|
|
|
1397
2231
|
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
2232
|
"--harvest",
|
|
1399
2233
|
"Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
|
|
1400
|
-
).
|
|
2234
|
+
).option(
|
|
2235
|
+
"--branch <n>",
|
|
2236
|
+
"Self-consistency: run N parallel samples per turn and pick the most confident (disables streaming; enables harvest)",
|
|
2237
|
+
(v) => Number.parseInt(v, 10)
|
|
2238
|
+
).option(
|
|
2239
|
+
"--session <name>",
|
|
2240
|
+
"Use a named session (default: 'default'). Resume the same session next time."
|
|
2241
|
+
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").action(async (opts) => {
|
|
2242
|
+
let session;
|
|
2243
|
+
if (opts.session === false) {
|
|
2244
|
+
session = void 0;
|
|
2245
|
+
} else if (typeof opts.session === "string" && opts.session.length > 0) {
|
|
2246
|
+
session = opts.session;
|
|
2247
|
+
} else {
|
|
2248
|
+
session = "default";
|
|
2249
|
+
}
|
|
1401
2250
|
await chatCommand({
|
|
1402
2251
|
model: opts.model,
|
|
1403
2252
|
system: opts.system,
|
|
1404
2253
|
transcript: opts.transcript,
|
|
1405
|
-
harvest: !!opts.harvest
|
|
2254
|
+
harvest: !!opts.harvest,
|
|
2255
|
+
branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
|
|
2256
|
+
session
|
|
1406
2257
|
});
|
|
1407
2258
|
});
|
|
1408
2259
|
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) => {
|