reasonix 0.0.1
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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/chunk-XILYSYPT.js +261 -0
- package/dist/chunk-XILYSYPT.js.map +1 -0
- package/dist/cli/chunk-OSNTDDD6.js +262 -0
- package/dist/cli/chunk-OSNTDDD6.js.map +1 -0
- package/dist/cli/client-OWZXRMOE.js +10 -0
- package/dist/cli/client-OWZXRMOE.js.map +1 -0
- package/dist/cli/index.js +1089 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client-4JTJKRDV.js +9 -0
- package/dist/client-4JTJKRDV.js.map +1 -0
- package/dist/index.d.ts +396 -0
- package/dist/index.js +716 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DeepSeekClient
|
|
4
|
+
} from "./chunk-OSNTDDD6.js";
|
|
5
|
+
|
|
6
|
+
// src/cli/index.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/harvest.ts
|
|
10
|
+
function emptyPlanState() {
|
|
11
|
+
return { subgoals: [], hypotheses: [], uncertainties: [], rejectedPaths: [] };
|
|
12
|
+
}
|
|
13
|
+
async function harvest(_reasoningContent) {
|
|
14
|
+
return emptyPlanState();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/memory.ts
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
var ImmutablePrefix = class {
|
|
20
|
+
system;
|
|
21
|
+
toolSpecs;
|
|
22
|
+
fewShots;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
this.system = opts.system;
|
|
25
|
+
this.toolSpecs = Object.freeze([...opts.toolSpecs ?? []]);
|
|
26
|
+
this.fewShots = Object.freeze([...opts.fewShots ?? []]);
|
|
27
|
+
}
|
|
28
|
+
toMessages() {
|
|
29
|
+
return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
|
|
30
|
+
}
|
|
31
|
+
tools() {
|
|
32
|
+
return this.toolSpecs.map((t) => structuredClone(t));
|
|
33
|
+
}
|
|
34
|
+
get fingerprint() {
|
|
35
|
+
const blob = JSON.stringify({
|
|
36
|
+
system: this.system,
|
|
37
|
+
tools: this.toolSpecs,
|
|
38
|
+
shots: this.fewShots
|
|
39
|
+
});
|
|
40
|
+
return createHash("sha256").update(blob).digest("hex").slice(0, 16);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var AppendOnlyLog = class {
|
|
44
|
+
_entries = [];
|
|
45
|
+
append(message) {
|
|
46
|
+
if (!message || typeof message !== "object" || !("role" in message)) {
|
|
47
|
+
throw new Error(`invalid log entry: ${JSON.stringify(message)}`);
|
|
48
|
+
}
|
|
49
|
+
this._entries.push(message);
|
|
50
|
+
}
|
|
51
|
+
extend(messages) {
|
|
52
|
+
for (const m of messages) this.append(m);
|
|
53
|
+
}
|
|
54
|
+
get entries() {
|
|
55
|
+
return this._entries;
|
|
56
|
+
}
|
|
57
|
+
toMessages() {
|
|
58
|
+
return this._entries.map((e) => ({ ...e }));
|
|
59
|
+
}
|
|
60
|
+
get length() {
|
|
61
|
+
return this._entries.length;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var VolatileScratch = class {
|
|
65
|
+
reasoning = null;
|
|
66
|
+
planState = null;
|
|
67
|
+
notes = [];
|
|
68
|
+
reset() {
|
|
69
|
+
this.reasoning = null;
|
|
70
|
+
this.planState = null;
|
|
71
|
+
this.notes = [];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/repair/scavenge.ts
|
|
76
|
+
function scavengeToolCalls(reasoningContent, opts) {
|
|
77
|
+
if (!reasoningContent) return { calls: [], notes: [] };
|
|
78
|
+
const max = opts.maxCalls ?? 4;
|
|
79
|
+
const notes = [];
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const candidate of iterateJsonObjects(reasoningContent)) {
|
|
82
|
+
if (out.length >= max) break;
|
|
83
|
+
const call = coerceToToolCall(candidate, opts.allowedNames);
|
|
84
|
+
if (call) {
|
|
85
|
+
out.push(call);
|
|
86
|
+
notes.push(`scavenged call: ${call.function.name}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { calls: out, notes };
|
|
90
|
+
}
|
|
91
|
+
function* iterateJsonObjects(text) {
|
|
92
|
+
for (let i = 0; i < text.length; i++) {
|
|
93
|
+
if (text[i] !== "{") continue;
|
|
94
|
+
let depth = 0;
|
|
95
|
+
let inString = false;
|
|
96
|
+
let escaped = false;
|
|
97
|
+
for (let j = i; j < text.length; j++) {
|
|
98
|
+
const c = text[j];
|
|
99
|
+
if (escaped) {
|
|
100
|
+
escaped = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (inString) {
|
|
104
|
+
if (c === "\\") {
|
|
105
|
+
escaped = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (c === '"') inString = false;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (c === '"') inString = true;
|
|
112
|
+
else if (c === "{") depth++;
|
|
113
|
+
else if (c === "}") {
|
|
114
|
+
depth--;
|
|
115
|
+
if (depth === 0) {
|
|
116
|
+
yield text.slice(i, j + 1);
|
|
117
|
+
i = j;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function coerceToToolCall(candidateJson, allowedNames) {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(candidateJson);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
132
|
+
if (typeof parsed.name === "string" && allowedNames.has(parsed.name)) {
|
|
133
|
+
const args = parsed.arguments;
|
|
134
|
+
return {
|
|
135
|
+
function: {
|
|
136
|
+
name: parsed.name,
|
|
137
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (parsed.type === "function" && parsed.function && typeof parsed.function.name === "string" && allowedNames.has(parsed.function.name)) {
|
|
142
|
+
const args = parsed.function.arguments;
|
|
143
|
+
return {
|
|
144
|
+
type: "function",
|
|
145
|
+
function: {
|
|
146
|
+
name: parsed.function.name,
|
|
147
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (typeof parsed.tool_name === "string" && allowedNames.has(parsed.tool_name)) {
|
|
152
|
+
return {
|
|
153
|
+
function: {
|
|
154
|
+
name: parsed.tool_name,
|
|
155
|
+
arguments: JSON.stringify(parsed.tool_args ?? {})
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/repair/storm.ts
|
|
163
|
+
var StormBreaker = class {
|
|
164
|
+
windowSize;
|
|
165
|
+
threshold;
|
|
166
|
+
recent = [];
|
|
167
|
+
constructor(windowSize = 6, threshold = 3) {
|
|
168
|
+
this.windowSize = windowSize;
|
|
169
|
+
this.threshold = threshold;
|
|
170
|
+
}
|
|
171
|
+
inspect(call) {
|
|
172
|
+
const sig = signature(call);
|
|
173
|
+
if (!sig) return { suppress: false };
|
|
174
|
+
const count = this.recent.reduce(
|
|
175
|
+
(n, [name, args]) => name === sig[0] && args === sig[1] ? n + 1 : n,
|
|
176
|
+
0
|
|
177
|
+
);
|
|
178
|
+
if (count >= this.threshold - 1) {
|
|
179
|
+
return {
|
|
180
|
+
suppress: true,
|
|
181
|
+
reason: `call-storm suppressed: ${sig[0]} called with identical args ${count + 1} times within window=${this.windowSize}`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
this.recent.push(sig);
|
|
185
|
+
while (this.recent.length > this.windowSize) this.recent.shift();
|
|
186
|
+
return { suppress: false };
|
|
187
|
+
}
|
|
188
|
+
reset() {
|
|
189
|
+
this.recent.length = 0;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
function signature(call) {
|
|
193
|
+
const name = call.function?.name;
|
|
194
|
+
if (!name) return null;
|
|
195
|
+
return [name, call.function?.arguments ?? ""];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/repair/truncation.ts
|
|
199
|
+
function repairTruncatedJson(input) {
|
|
200
|
+
const notes = [];
|
|
201
|
+
if (!input || !input.trim()) {
|
|
202
|
+
return { repaired: "{}", changed: input !== "{}", notes: ["empty input \u2192 {}"] };
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
JSON.parse(input);
|
|
206
|
+
return { repaired: input, changed: false, notes: [] };
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
const stack = [];
|
|
210
|
+
let escaped = false;
|
|
211
|
+
let inString = false;
|
|
212
|
+
let lastSignificant = -1;
|
|
213
|
+
for (let i = 0; i < input.length; i++) {
|
|
214
|
+
const c = input[i];
|
|
215
|
+
if (!/\s/.test(c)) lastSignificant = i;
|
|
216
|
+
if (escaped) {
|
|
217
|
+
escaped = false;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (inString) {
|
|
221
|
+
if (c === "\\") {
|
|
222
|
+
escaped = true;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (c === '"') {
|
|
226
|
+
inString = false;
|
|
227
|
+
stack.pop();
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (c === '"') {
|
|
232
|
+
inString = true;
|
|
233
|
+
stack.push('"');
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (c === "{" || c === "[") stack.push(c);
|
|
237
|
+
else if (c === "}" || c === "]") stack.pop();
|
|
238
|
+
}
|
|
239
|
+
let s = input.slice(0, lastSignificant + 1);
|
|
240
|
+
if (/,$/.test(s)) {
|
|
241
|
+
s = s.replace(/,$/, "");
|
|
242
|
+
notes.push("trimmed trailing comma");
|
|
243
|
+
}
|
|
244
|
+
if (/"\s*:\s*$/.test(s)) {
|
|
245
|
+
s += " null";
|
|
246
|
+
notes.push("filled dangling key with null");
|
|
247
|
+
}
|
|
248
|
+
if (inString) {
|
|
249
|
+
s += '"';
|
|
250
|
+
stack.pop();
|
|
251
|
+
notes.push("closed unterminated string");
|
|
252
|
+
}
|
|
253
|
+
while (stack.length > 0) {
|
|
254
|
+
const top = stack.pop();
|
|
255
|
+
if (top === "{") s += "}";
|
|
256
|
+
else if (top === "[") s += "]";
|
|
257
|
+
else if (top === '"') s += '"';
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
JSON.parse(s);
|
|
261
|
+
return { repaired: s, changed: true, notes };
|
|
262
|
+
} catch (err) {
|
|
263
|
+
notes.push(`fallback to {}: ${err.message}`);
|
|
264
|
+
return { repaired: "{}", changed: true, notes };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/repair/index.ts
|
|
269
|
+
var ToolCallRepair = class {
|
|
270
|
+
storm;
|
|
271
|
+
opts;
|
|
272
|
+
constructor(opts) {
|
|
273
|
+
this.opts = opts;
|
|
274
|
+
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
|
|
275
|
+
}
|
|
276
|
+
process(declaredCalls, reasoningContent) {
|
|
277
|
+
const report = {
|
|
278
|
+
scavenged: 0,
|
|
279
|
+
truncationsFixed: 0,
|
|
280
|
+
stormsBroken: 0,
|
|
281
|
+
notes: []
|
|
282
|
+
};
|
|
283
|
+
const scavenged = scavengeToolCalls(reasoningContent, {
|
|
284
|
+
allowedNames: this.opts.allowedToolNames,
|
|
285
|
+
maxCalls: this.opts.maxScavenge ?? 4
|
|
286
|
+
});
|
|
287
|
+
const seenSignatures = new Set(declaredCalls.map(signature2));
|
|
288
|
+
const merged = [...declaredCalls];
|
|
289
|
+
for (const sc of scavenged.calls) {
|
|
290
|
+
if (!seenSignatures.has(signature2(sc))) {
|
|
291
|
+
merged.push(sc);
|
|
292
|
+
report.scavenged++;
|
|
293
|
+
seenSignatures.add(signature2(sc));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
report.notes.push(...scavenged.notes);
|
|
297
|
+
for (const call of merged) {
|
|
298
|
+
const args = call.function?.arguments ?? "";
|
|
299
|
+
const r = repairTruncatedJson(args);
|
|
300
|
+
if (r.changed) {
|
|
301
|
+
call.function.arguments = r.repaired;
|
|
302
|
+
report.truncationsFixed++;
|
|
303
|
+
report.notes.push(...r.notes.map((n) => `[${call.function.name}] ${n}`));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const filtered = [];
|
|
307
|
+
for (const call of merged) {
|
|
308
|
+
const verdict = this.storm.inspect(call);
|
|
309
|
+
if (verdict.suppress) {
|
|
310
|
+
report.stormsBroken++;
|
|
311
|
+
if (verdict.reason) report.notes.push(verdict.reason);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
filtered.push(call);
|
|
315
|
+
}
|
|
316
|
+
return { calls: filtered, report };
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function signature2(call) {
|
|
320
|
+
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// src/telemetry.ts
|
|
324
|
+
var DEEPSEEK_PRICING = {
|
|
325
|
+
"deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
|
|
326
|
+
"deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
|
|
327
|
+
};
|
|
328
|
+
var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
|
|
329
|
+
function costUsd(model, usage) {
|
|
330
|
+
const p = DEEPSEEK_PRICING[model];
|
|
331
|
+
if (!p) return 0;
|
|
332
|
+
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
333
|
+
}
|
|
334
|
+
function claudeEquivalentCost(usage) {
|
|
335
|
+
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
336
|
+
}
|
|
337
|
+
var SessionStats = class {
|
|
338
|
+
turns = [];
|
|
339
|
+
record(turn, model, usage) {
|
|
340
|
+
const cost = costUsd(model, usage);
|
|
341
|
+
const stats = {
|
|
342
|
+
turn,
|
|
343
|
+
model,
|
|
344
|
+
usage,
|
|
345
|
+
cost,
|
|
346
|
+
cacheHitRatio: usage.cacheHitRatio
|
|
347
|
+
};
|
|
348
|
+
this.turns.push(stats);
|
|
349
|
+
return stats;
|
|
350
|
+
}
|
|
351
|
+
get totalCost() {
|
|
352
|
+
return this.turns.reduce((sum, t) => sum + t.cost, 0);
|
|
353
|
+
}
|
|
354
|
+
get totalClaudeEquivalent() {
|
|
355
|
+
return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
|
|
356
|
+
}
|
|
357
|
+
get savingsVsClaude() {
|
|
358
|
+
const c = this.totalClaudeEquivalent;
|
|
359
|
+
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
360
|
+
}
|
|
361
|
+
get aggregateCacheHitRatio() {
|
|
362
|
+
let hit = 0;
|
|
363
|
+
let miss = 0;
|
|
364
|
+
for (const t of this.turns) {
|
|
365
|
+
hit += t.usage.promptCacheHitTokens;
|
|
366
|
+
miss += t.usage.promptCacheMissTokens;
|
|
367
|
+
}
|
|
368
|
+
const denom = hit + miss;
|
|
369
|
+
return denom > 0 ? hit / denom : 0;
|
|
370
|
+
}
|
|
371
|
+
summary() {
|
|
372
|
+
return {
|
|
373
|
+
turns: this.turns.length,
|
|
374
|
+
totalCostUsd: round(this.totalCost, 6),
|
|
375
|
+
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
376
|
+
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
377
|
+
cacheHitRatio: round(this.aggregateCacheHitRatio, 4)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
function round(n, digits) {
|
|
382
|
+
const f = 10 ** digits;
|
|
383
|
+
return Math.round(n * f) / f;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/tools.ts
|
|
387
|
+
var ToolRegistry = class {
|
|
388
|
+
_tools = /* @__PURE__ */ new Map();
|
|
389
|
+
register(def) {
|
|
390
|
+
if (!def.name) throw new Error("tool requires a name");
|
|
391
|
+
this._tools.set(def.name, def);
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
has(name) {
|
|
395
|
+
return this._tools.has(name);
|
|
396
|
+
}
|
|
397
|
+
get(name) {
|
|
398
|
+
return this._tools.get(name);
|
|
399
|
+
}
|
|
400
|
+
get size() {
|
|
401
|
+
return this._tools.size;
|
|
402
|
+
}
|
|
403
|
+
specs() {
|
|
404
|
+
return [...this._tools.values()].map((t) => ({
|
|
405
|
+
type: "function",
|
|
406
|
+
function: {
|
|
407
|
+
name: t.name,
|
|
408
|
+
description: t.description ?? "",
|
|
409
|
+
parameters: t.parameters ?? { type: "object", properties: {} }
|
|
410
|
+
}
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
async dispatch(name, argumentsRaw) {
|
|
414
|
+
const tool = this._tools.get(name);
|
|
415
|
+
if (!tool) {
|
|
416
|
+
return JSON.stringify({ error: `unknown tool: ${name}` });
|
|
417
|
+
}
|
|
418
|
+
let args;
|
|
419
|
+
try {
|
|
420
|
+
args = typeof argumentsRaw === "string" ? argumentsRaw.trim() ? JSON.parse(argumentsRaw) : {} : argumentsRaw ?? {};
|
|
421
|
+
} catch (err) {
|
|
422
|
+
return JSON.stringify({
|
|
423
|
+
error: `invalid tool arguments JSON: ${err.message}`
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const result = await tool.fn(args);
|
|
428
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
return JSON.stringify({
|
|
431
|
+
error: `${err.name}: ${err.message}`
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/loop.ts
|
|
438
|
+
var CacheFirstLoop = class {
|
|
439
|
+
client;
|
|
440
|
+
prefix;
|
|
441
|
+
tools;
|
|
442
|
+
model;
|
|
443
|
+
maxToolIters;
|
|
444
|
+
stream;
|
|
445
|
+
log = new AppendOnlyLog();
|
|
446
|
+
scratch = new VolatileScratch();
|
|
447
|
+
stats = new SessionStats();
|
|
448
|
+
repair;
|
|
449
|
+
_turn = 0;
|
|
450
|
+
constructor(opts) {
|
|
451
|
+
this.client = opts.client;
|
|
452
|
+
this.prefix = opts.prefix;
|
|
453
|
+
this.tools = opts.tools ?? new ToolRegistry();
|
|
454
|
+
this.model = opts.model ?? "deepseek-chat";
|
|
455
|
+
this.maxToolIters = opts.maxToolIters ?? 8;
|
|
456
|
+
this.stream = opts.stream ?? true;
|
|
457
|
+
const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
|
|
458
|
+
this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
|
|
459
|
+
}
|
|
460
|
+
buildMessages(pendingUser) {
|
|
461
|
+
const msgs = [...this.prefix.toMessages(), ...this.log.toMessages()];
|
|
462
|
+
if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
|
|
463
|
+
return msgs;
|
|
464
|
+
}
|
|
465
|
+
async *step(userInput) {
|
|
466
|
+
this._turn++;
|
|
467
|
+
this.scratch.reset();
|
|
468
|
+
let pendingUser = userInput;
|
|
469
|
+
const toolSpecs = this.prefix.tools();
|
|
470
|
+
for (let iter = 0; iter < this.maxToolIters; iter++) {
|
|
471
|
+
const messages = this.buildMessages(pendingUser);
|
|
472
|
+
let assistantContent = "";
|
|
473
|
+
let reasoningContent = "";
|
|
474
|
+
let toolCalls = [];
|
|
475
|
+
let usage = null;
|
|
476
|
+
try {
|
|
477
|
+
if (this.stream) {
|
|
478
|
+
const callBuf = /* @__PURE__ */ new Map();
|
|
479
|
+
for await (const chunk of this.client.stream({
|
|
480
|
+
model: this.model,
|
|
481
|
+
messages,
|
|
482
|
+
tools: toolSpecs.length ? toolSpecs : void 0
|
|
483
|
+
})) {
|
|
484
|
+
if (chunk.contentDelta) {
|
|
485
|
+
assistantContent += chunk.contentDelta;
|
|
486
|
+
yield {
|
|
487
|
+
turn: this._turn,
|
|
488
|
+
role: "assistant_delta",
|
|
489
|
+
content: chunk.contentDelta
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
if (chunk.reasoningDelta) {
|
|
493
|
+
reasoningContent += chunk.reasoningDelta;
|
|
494
|
+
yield {
|
|
495
|
+
turn: this._turn,
|
|
496
|
+
role: "assistant_delta",
|
|
497
|
+
content: "",
|
|
498
|
+
reasoningDelta: chunk.reasoningDelta
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (chunk.toolCallDelta) {
|
|
502
|
+
const d = chunk.toolCallDelta;
|
|
503
|
+
const cur = callBuf.get(d.index) ?? {
|
|
504
|
+
id: d.id,
|
|
505
|
+
type: "function",
|
|
506
|
+
function: { name: "", arguments: "" }
|
|
507
|
+
};
|
|
508
|
+
if (d.id) cur.id = d.id;
|
|
509
|
+
if (d.name) cur.function.name = (cur.function.name ?? "") + d.name;
|
|
510
|
+
if (d.argumentsDelta)
|
|
511
|
+
cur.function.arguments = (cur.function.arguments ?? "") + d.argumentsDelta;
|
|
512
|
+
callBuf.set(d.index, cur);
|
|
513
|
+
}
|
|
514
|
+
if (chunk.usage) usage = chunk.usage;
|
|
515
|
+
}
|
|
516
|
+
toolCalls = [...callBuf.values()];
|
|
517
|
+
} else {
|
|
518
|
+
const resp = await this.client.chat({
|
|
519
|
+
model: this.model,
|
|
520
|
+
messages,
|
|
521
|
+
tools: toolSpecs.length ? toolSpecs : void 0
|
|
522
|
+
});
|
|
523
|
+
assistantContent = resp.content;
|
|
524
|
+
reasoningContent = resp.reasoningContent ?? "";
|
|
525
|
+
toolCalls = resp.toolCalls;
|
|
526
|
+
usage = resp.usage;
|
|
527
|
+
}
|
|
528
|
+
} catch (err) {
|
|
529
|
+
yield {
|
|
530
|
+
turn: this._turn,
|
|
531
|
+
role: "error",
|
|
532
|
+
content: "",
|
|
533
|
+
error: err.message
|
|
534
|
+
};
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const turnStats = this.stats.record(
|
|
538
|
+
this._turn,
|
|
539
|
+
this.model,
|
|
540
|
+
usage ?? new (await import("./client-OWZXRMOE.js")).Usage()
|
|
541
|
+
);
|
|
542
|
+
if (pendingUser !== null) {
|
|
543
|
+
this.log.append({ role: "user", content: pendingUser });
|
|
544
|
+
pendingUser = null;
|
|
545
|
+
}
|
|
546
|
+
this.scratch.reasoning = reasoningContent || null;
|
|
547
|
+
const planState = await harvest(reasoningContent || null);
|
|
548
|
+
const { calls: repairedCalls, report } = this.repair.process(
|
|
549
|
+
toolCalls,
|
|
550
|
+
reasoningContent || null
|
|
551
|
+
);
|
|
552
|
+
this.log.append(this.assistantMessage(assistantContent, repairedCalls));
|
|
553
|
+
yield {
|
|
554
|
+
turn: this._turn,
|
|
555
|
+
role: "assistant_final",
|
|
556
|
+
content: assistantContent,
|
|
557
|
+
stats: turnStats,
|
|
558
|
+
planState,
|
|
559
|
+
repair: report
|
|
560
|
+
};
|
|
561
|
+
if (repairedCalls.length === 0) {
|
|
562
|
+
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
for (const call of repairedCalls) {
|
|
566
|
+
const name = call.function?.name ?? "";
|
|
567
|
+
const args = call.function?.arguments ?? "{}";
|
|
568
|
+
const result = await this.tools.dispatch(name, args);
|
|
569
|
+
this.log.append({
|
|
570
|
+
role: "tool",
|
|
571
|
+
tool_call_id: call.id ?? "",
|
|
572
|
+
name,
|
|
573
|
+
content: result
|
|
574
|
+
});
|
|
575
|
+
yield { turn: this._turn, role: "tool", content: result, toolName: name };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
|
|
579
|
+
}
|
|
580
|
+
async run(userInput, onEvent) {
|
|
581
|
+
let final = "";
|
|
582
|
+
for await (const ev of this.step(userInput)) {
|
|
583
|
+
onEvent?.(ev);
|
|
584
|
+
if (ev.role === "assistant_final") final = ev.content;
|
|
585
|
+
if (ev.role === "done") break;
|
|
586
|
+
}
|
|
587
|
+
return final;
|
|
588
|
+
}
|
|
589
|
+
assistantMessage(content, toolCalls) {
|
|
590
|
+
const msg = { role: "assistant", content };
|
|
591
|
+
if (toolCalls.length > 0) msg.tool_calls = toolCalls;
|
|
592
|
+
return msg;
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/env.ts
|
|
597
|
+
import { readFileSync } from "fs";
|
|
598
|
+
import { resolve } from "path";
|
|
599
|
+
function loadDotenv(path = ".env") {
|
|
600
|
+
let raw;
|
|
601
|
+
try {
|
|
602
|
+
raw = readFileSync(resolve(process.cwd(), path), "utf8");
|
|
603
|
+
} catch {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
607
|
+
const trimmed = line.trim();
|
|
608
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
609
|
+
const eq = trimmed.indexOf("=");
|
|
610
|
+
if (eq === -1) continue;
|
|
611
|
+
const key = trimmed.slice(0, eq).trim();
|
|
612
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
613
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
614
|
+
value = value.slice(1, -1);
|
|
615
|
+
}
|
|
616
|
+
if (process.env[key] === void 0) process.env[key] = value;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/index.ts
|
|
621
|
+
var VERSION = "0.0.1";
|
|
622
|
+
|
|
623
|
+
// src/cli/commands/chat.tsx
|
|
624
|
+
import { render } from "ink";
|
|
625
|
+
import React6 from "react";
|
|
626
|
+
|
|
627
|
+
// src/cli/ui/App.tsx
|
|
628
|
+
import { createWriteStream } from "fs";
|
|
629
|
+
import { Box as Box5, Static, useApp } from "ink";
|
|
630
|
+
import React5, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
631
|
+
|
|
632
|
+
// src/cli/ui/EventLog.tsx
|
|
633
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
634
|
+
import React2 from "react";
|
|
635
|
+
|
|
636
|
+
// src/cli/ui/markdown.tsx
|
|
637
|
+
import { Box, Text } from "ink";
|
|
638
|
+
import React from "react";
|
|
639
|
+
function stripMath(s) {
|
|
640
|
+
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, " ");
|
|
641
|
+
}
|
|
642
|
+
var INLINE_RE = /(\*\*([^*\n]+?)\*\*|`([^`\n]+?)`|(?<![*\w])\*([^*\n]+?)\*(?!\w))/g;
|
|
643
|
+
function InlineMd({ text }) {
|
|
644
|
+
const parts = [];
|
|
645
|
+
let last = 0;
|
|
646
|
+
let idx = 0;
|
|
647
|
+
for (const m of text.matchAll(INLINE_RE)) {
|
|
648
|
+
const start = m.index ?? 0;
|
|
649
|
+
if (start > last) {
|
|
650
|
+
parts.push(/* @__PURE__ */ React.createElement(Text, { key: `t${idx++}` }, text.slice(last, start)));
|
|
651
|
+
}
|
|
652
|
+
if (m[2] !== void 0) {
|
|
653
|
+
parts.push(
|
|
654
|
+
/* @__PURE__ */ React.createElement(Text, { key: `b${idx++}`, bold: true }, m[2])
|
|
655
|
+
);
|
|
656
|
+
} else if (m[3] !== void 0) {
|
|
657
|
+
parts.push(
|
|
658
|
+
/* @__PURE__ */ React.createElement(Text, { key: `c${idx++}`, color: "yellow" }, m[3])
|
|
659
|
+
);
|
|
660
|
+
} else if (m[4] !== void 0) {
|
|
661
|
+
parts.push(
|
|
662
|
+
/* @__PURE__ */ React.createElement(Text, { key: `i${idx++}`, italic: true }, m[4])
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
last = start + m[0].length;
|
|
666
|
+
}
|
|
667
|
+
if (last < text.length) {
|
|
668
|
+
parts.push(/* @__PURE__ */ React.createElement(Text, { key: `t${idx++}` }, text.slice(last)));
|
|
669
|
+
}
|
|
670
|
+
return /* @__PURE__ */ React.createElement(Text, null, parts);
|
|
671
|
+
}
|
|
672
|
+
function parseBlocks(raw) {
|
|
673
|
+
const lines = raw.split(/\r?\n/);
|
|
674
|
+
const out = [];
|
|
675
|
+
let para = [];
|
|
676
|
+
let inCode = false;
|
|
677
|
+
let codeLang = "";
|
|
678
|
+
let codeBuf = [];
|
|
679
|
+
let listBuf = null;
|
|
680
|
+
const flushPara = () => {
|
|
681
|
+
if (para.length) {
|
|
682
|
+
out.push({ kind: "paragraph", text: para.join(" ") });
|
|
683
|
+
para = [];
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
const flushList = () => {
|
|
687
|
+
if (listBuf) {
|
|
688
|
+
out.push(listBuf);
|
|
689
|
+
listBuf = null;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
for (const rawLine of lines) {
|
|
693
|
+
const line = rawLine.replace(/\s+$/g, "");
|
|
694
|
+
const fence = line.match(/^```(\w*)/);
|
|
695
|
+
if (fence) {
|
|
696
|
+
if (inCode) {
|
|
697
|
+
out.push({ kind: "code", lang: codeLang, text: codeBuf.join("\n") });
|
|
698
|
+
codeBuf = [];
|
|
699
|
+
codeLang = "";
|
|
700
|
+
inCode = false;
|
|
701
|
+
} else {
|
|
702
|
+
flushPara();
|
|
703
|
+
flushList();
|
|
704
|
+
inCode = true;
|
|
705
|
+
codeLang = fence[1] ?? "";
|
|
706
|
+
}
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (inCode) {
|
|
710
|
+
codeBuf.push(rawLine);
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (line.trim() === "") {
|
|
714
|
+
flushPara();
|
|
715
|
+
flushList();
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
719
|
+
flushPara();
|
|
720
|
+
flushList();
|
|
721
|
+
out.push({ kind: "hr" });
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const hm = line.match(/^(#{1,6})\s+(.+)$/);
|
|
725
|
+
if (hm) {
|
|
726
|
+
flushPara();
|
|
727
|
+
flushList();
|
|
728
|
+
out.push({ kind: "heading", level: hm[1].length, text: hm[2].trim() });
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const bm = line.match(/^\s*[-*+]\s+(.+)$/);
|
|
732
|
+
if (bm) {
|
|
733
|
+
flushPara();
|
|
734
|
+
if (!listBuf || listBuf.ordered) {
|
|
735
|
+
flushList();
|
|
736
|
+
listBuf = { kind: "bullet", items: [], ordered: false, start: 1 };
|
|
737
|
+
}
|
|
738
|
+
listBuf.items.push(bm[1]);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const om = line.match(/^\s*(\d+)\.\s+(.+)$/);
|
|
742
|
+
if (om) {
|
|
743
|
+
flushPara();
|
|
744
|
+
if (!listBuf || !listBuf.ordered) {
|
|
745
|
+
flushList();
|
|
746
|
+
listBuf = { kind: "bullet", items: [], ordered: true, start: Number(om[1]) };
|
|
747
|
+
}
|
|
748
|
+
listBuf.items.push(om[2]);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
flushList();
|
|
752
|
+
para.push(line);
|
|
753
|
+
}
|
|
754
|
+
if (inCode && codeBuf.length) {
|
|
755
|
+
out.push({ kind: "code", lang: codeLang, text: codeBuf.join("\n") });
|
|
756
|
+
}
|
|
757
|
+
flushPara();
|
|
758
|
+
flushList();
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
function BlockView({ block }) {
|
|
762
|
+
switch (block.kind) {
|
|
763
|
+
case "heading":
|
|
764
|
+
return /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, /* @__PURE__ */ React.createElement(InlineMd, { text: block.text }));
|
|
765
|
+
case "paragraph":
|
|
766
|
+
return /* @__PURE__ */ React.createElement(InlineMd, { text: block.text });
|
|
767
|
+
case "bullet":
|
|
768
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React.createElement(Box, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React.createElement(Text, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React.createElement(InlineMd, { text: item }))));
|
|
769
|
+
case "code":
|
|
770
|
+
return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, block.text));
|
|
771
|
+
case "hr":
|
|
772
|
+
return /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function Markdown({ text }) {
|
|
776
|
+
const cleaned = stripMath(text);
|
|
777
|
+
const blocks = React.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
778
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/cli/ui/EventLog.tsx
|
|
782
|
+
var EventRow = React2.memo(function EventRow2({ event }) {
|
|
783
|
+
if (event.role === "user") {
|
|
784
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React2.createElement(Text2, null, event.text));
|
|
785
|
+
}
|
|
786
|
+
if (event.role === "assistant") {
|
|
787
|
+
if (event.streaming) return /* @__PURE__ */ React2.createElement(StreamingAssistant, { event });
|
|
788
|
+
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, 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);
|
|
789
|
+
}
|
|
790
|
+
if (event.role === "tool") {
|
|
791
|
+
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)));
|
|
792
|
+
}
|
|
793
|
+
if (event.role === "error") {
|
|
794
|
+
return /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, event.text));
|
|
795
|
+
}
|
|
796
|
+
if (event.role === "info") {
|
|
797
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, event.text));
|
|
798
|
+
}
|
|
799
|
+
return /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, null, event.text));
|
|
800
|
+
});
|
|
801
|
+
function ReasoningBlock({ reasoning }) {
|
|
802
|
+
const max = 220;
|
|
803
|
+
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
804
|
+
const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
|
|
805
|
+
return /* @__PURE__ */ React2.createElement(Box2, { marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
|
|
806
|
+
}
|
|
807
|
+
function StreamingAssistant({ event }) {
|
|
808
|
+
const tail = lastLine(event.text, 140);
|
|
809
|
+
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
810
|
+
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)"));
|
|
811
|
+
}
|
|
812
|
+
function lastLine(s, maxChars) {
|
|
813
|
+
const flat = s.replace(/\s+/g, " ").trim();
|
|
814
|
+
if (!flat) return "";
|
|
815
|
+
return flat.length <= maxChars ? flat : `\u2026${flat.slice(-maxChars)}`;
|
|
816
|
+
}
|
|
817
|
+
function StatsLine({ stats }) {
|
|
818
|
+
const hit = (stats.cacheHitRatio * 100).toFixed(1);
|
|
819
|
+
return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
|
|
820
|
+
}
|
|
821
|
+
function truncate(s, max) {
|
|
822
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// src/cli/ui/PromptInput.tsx
|
|
826
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
827
|
+
import TextInput from "ink-text-input";
|
|
828
|
+
import React3 from "react";
|
|
829
|
+
function PromptInput({
|
|
830
|
+
value,
|
|
831
|
+
onChange,
|
|
832
|
+
onSubmit,
|
|
833
|
+
disabled,
|
|
834
|
+
placeholder
|
|
835
|
+
}) {
|
|
836
|
+
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", " "), disabled ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, placeholder ?? "\u2026waiting for response\u2026") : /* @__PURE__ */ React3.createElement(
|
|
837
|
+
TextInput,
|
|
838
|
+
{
|
|
839
|
+
value,
|
|
840
|
+
onChange,
|
|
841
|
+
onSubmit,
|
|
842
|
+
placeholder: placeholder ?? 'type a message, or "/exit"'
|
|
843
|
+
}
|
|
844
|
+
));
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/cli/ui/StatsPanel.tsx
|
|
848
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
849
|
+
import React4 from "react";
|
|
850
|
+
function StatsPanel({ summary, model, prefixHash }) {
|
|
851
|
+
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
852
|
+
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
853
|
+
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), "%"))));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// src/cli/ui/App.tsx
|
|
857
|
+
var FLUSH_INTERVAL_MS = 60;
|
|
858
|
+
function App({ model, system, transcript }) {
|
|
859
|
+
const { exit } = useApp();
|
|
860
|
+
const [historical, setHistorical] = useState([]);
|
|
861
|
+
const [streaming, setStreaming] = useState(null);
|
|
862
|
+
const [input, setInput] = useState("");
|
|
863
|
+
const [busy, setBusy] = useState(false);
|
|
864
|
+
const [summary, setSummary] = useState({
|
|
865
|
+
turns: 0,
|
|
866
|
+
totalCostUsd: 0,
|
|
867
|
+
claudeEquivalentUsd: 0,
|
|
868
|
+
savingsVsClaudePct: 0,
|
|
869
|
+
cacheHitRatio: 0
|
|
870
|
+
});
|
|
871
|
+
const transcriptRef = useRef(null);
|
|
872
|
+
if (transcript && !transcriptRef.current) {
|
|
873
|
+
transcriptRef.current = createWriteStream(transcript, { flags: "a" });
|
|
874
|
+
}
|
|
875
|
+
useEffect(() => {
|
|
876
|
+
return () => {
|
|
877
|
+
transcriptRef.current?.end();
|
|
878
|
+
};
|
|
879
|
+
}, []);
|
|
880
|
+
const loopRef = useRef(null);
|
|
881
|
+
const loop = useMemo(() => {
|
|
882
|
+
if (loopRef.current) return loopRef.current;
|
|
883
|
+
const client = new DeepSeekClient();
|
|
884
|
+
const prefix = new ImmutablePrefix({ system });
|
|
885
|
+
const l = new CacheFirstLoop({ client, prefix, model });
|
|
886
|
+
loopRef.current = l;
|
|
887
|
+
return l;
|
|
888
|
+
}, [model, system]);
|
|
889
|
+
const prefixHash = loop.prefix.fingerprint;
|
|
890
|
+
const writeTranscript = useCallback((ev) => {
|
|
891
|
+
transcriptRef.current?.write(
|
|
892
|
+
`${JSON.stringify({
|
|
893
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
894
|
+
turn: ev.turn,
|
|
895
|
+
role: ev.role,
|
|
896
|
+
content: ev.content,
|
|
897
|
+
tool: ev.toolName
|
|
898
|
+
})}
|
|
899
|
+
`
|
|
900
|
+
);
|
|
901
|
+
}, []);
|
|
902
|
+
const handleSubmit = useCallback(
|
|
903
|
+
async (raw) => {
|
|
904
|
+
const text = raw.trim();
|
|
905
|
+
if (!text || busy) return;
|
|
906
|
+
setInput("");
|
|
907
|
+
if (text === "/exit" || text === "/quit") {
|
|
908
|
+
transcriptRef.current?.end();
|
|
909
|
+
exit();
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (text === "/clear") {
|
|
913
|
+
setHistorical([]);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
917
|
+
const assistantId = `a-${Date.now()}`;
|
|
918
|
+
const streamRef = { id: assistantId, text: "", reasoning: "" };
|
|
919
|
+
const contentBuf = { current: "" };
|
|
920
|
+
const reasoningBuf = { current: "" };
|
|
921
|
+
setStreaming({ id: assistantId, role: "assistant", text: "", streaming: true });
|
|
922
|
+
setBusy(true);
|
|
923
|
+
const flush = () => {
|
|
924
|
+
if (!contentBuf.current && !reasoningBuf.current) return;
|
|
925
|
+
streamRef.text += contentBuf.current;
|
|
926
|
+
streamRef.reasoning += reasoningBuf.current;
|
|
927
|
+
contentBuf.current = "";
|
|
928
|
+
reasoningBuf.current = "";
|
|
929
|
+
setStreaming({
|
|
930
|
+
id: assistantId,
|
|
931
|
+
role: "assistant",
|
|
932
|
+
text: streamRef.text,
|
|
933
|
+
reasoning: streamRef.reasoning || void 0,
|
|
934
|
+
streaming: true
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
const timer = setInterval(flush, FLUSH_INTERVAL_MS);
|
|
938
|
+
try {
|
|
939
|
+
for await (const ev of loop.step(text)) {
|
|
940
|
+
writeTranscript(ev);
|
|
941
|
+
if (ev.role === "assistant_delta") {
|
|
942
|
+
if (ev.content) contentBuf.current += ev.content;
|
|
943
|
+
if (ev.reasoningDelta) reasoningBuf.current += ev.reasoningDelta;
|
|
944
|
+
} else if (ev.role === "assistant_final") {
|
|
945
|
+
flush();
|
|
946
|
+
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
947
|
+
setStreaming(null);
|
|
948
|
+
setHistorical((prev) => [
|
|
949
|
+
...prev,
|
|
950
|
+
{
|
|
951
|
+
id: assistantId,
|
|
952
|
+
role: "assistant",
|
|
953
|
+
text: ev.content || streamRef.text,
|
|
954
|
+
reasoning: streamRef.reasoning || void 0,
|
|
955
|
+
stats: ev.stats,
|
|
956
|
+
repair: repairNote || void 0,
|
|
957
|
+
streaming: false
|
|
958
|
+
}
|
|
959
|
+
]);
|
|
960
|
+
} else if (ev.role === "tool") {
|
|
961
|
+
flush();
|
|
962
|
+
setHistorical((prev) => [
|
|
963
|
+
...prev,
|
|
964
|
+
{
|
|
965
|
+
id: `t-${Date.now()}-${Math.random()}`,
|
|
966
|
+
role: "tool",
|
|
967
|
+
text: ev.content,
|
|
968
|
+
toolName: ev.toolName
|
|
969
|
+
}
|
|
970
|
+
]);
|
|
971
|
+
} else if (ev.role === "error") {
|
|
972
|
+
setHistorical((prev) => [
|
|
973
|
+
...prev,
|
|
974
|
+
{ id: `e-${Date.now()}`, role: "error", text: ev.error ?? ev.content }
|
|
975
|
+
]);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
flush();
|
|
979
|
+
} finally {
|
|
980
|
+
clearInterval(timer);
|
|
981
|
+
setStreaming(null);
|
|
982
|
+
setSummary(loop.stats.summary());
|
|
983
|
+
setBusy(false);
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
[busy, exit, loop, writeTranscript]
|
|
987
|
+
);
|
|
988
|
+
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 }));
|
|
989
|
+
}
|
|
990
|
+
function describeRepair(repair) {
|
|
991
|
+
const parts = [];
|
|
992
|
+
if (repair.scavenged) parts.push(`scavenged ${repair.scavenged}`);
|
|
993
|
+
if (repair.truncationsFixed) parts.push(`repaired ${repair.truncationsFixed} truncation`);
|
|
994
|
+
if (repair.stormsBroken) parts.push(`broke ${repair.stormsBroken} storm`);
|
|
995
|
+
return parts.length ? `[repair] ${parts.join(", ")}` : "";
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/cli/commands/chat.tsx
|
|
999
|
+
async function chatCommand(opts) {
|
|
1000
|
+
loadDotenv();
|
|
1001
|
+
if (!process.env.DEEPSEEK_API_KEY) {
|
|
1002
|
+
console.error("DEEPSEEK_API_KEY is not set. Copy .env.example to .env and fill it in.");
|
|
1003
|
+
process.exit(1);
|
|
1004
|
+
}
|
|
1005
|
+
const { waitUntilExit } = render(
|
|
1006
|
+
/* @__PURE__ */ React6.createElement(App, { model: opts.model, system: opts.system, transcript: opts.transcript }),
|
|
1007
|
+
{ exitOnCtrlC: true }
|
|
1008
|
+
);
|
|
1009
|
+
await waitUntilExit();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/cli/commands/run.ts
|
|
1013
|
+
async function runCommand(opts) {
|
|
1014
|
+
loadDotenv();
|
|
1015
|
+
const client = new DeepSeekClient();
|
|
1016
|
+
const prefix = new ImmutablePrefix({ system: opts.system });
|
|
1017
|
+
const loop = new CacheFirstLoop({ client, prefix, model: opts.model });
|
|
1018
|
+
for await (const ev of loop.step(opts.task)) {
|
|
1019
|
+
if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
|
|
1020
|
+
if (ev.role === "tool") process.stdout.write(`
|
|
1021
|
+
[tool ${ev.toolName}] ${ev.content}
|
|
1022
|
+
`);
|
|
1023
|
+
if (ev.role === "error") process.stderr.write(`
|
|
1024
|
+
[error] ${ev.error}
|
|
1025
|
+
`);
|
|
1026
|
+
if (ev.role === "done") process.stdout.write("\n");
|
|
1027
|
+
}
|
|
1028
|
+
const s = loop.stats.summary();
|
|
1029
|
+
process.stdout.write(
|
|
1030
|
+
`
|
|
1031
|
+
\u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
|
|
1032
|
+
`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// src/cli/commands/stats.ts
|
|
1037
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
1038
|
+
function statsCommand(opts) {
|
|
1039
|
+
if (!existsSync(opts.transcript)) {
|
|
1040
|
+
console.error(`no such transcript: ${opts.transcript}`);
|
|
1041
|
+
process.exit(1);
|
|
1042
|
+
}
|
|
1043
|
+
const lines = readFileSync2(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
1044
|
+
let assistantTurns = 0;
|
|
1045
|
+
let toolCalls = 0;
|
|
1046
|
+
let lastTurn = 0;
|
|
1047
|
+
for (const line of lines) {
|
|
1048
|
+
try {
|
|
1049
|
+
const rec = JSON.parse(line);
|
|
1050
|
+
if (rec.role === "assistant_final") assistantTurns++;
|
|
1051
|
+
if (rec.role === "tool") toolCalls++;
|
|
1052
|
+
if (typeof rec.turn === "number") lastTurn = Math.max(lastTurn, rec.turn);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
console.log(`transcript: ${opts.transcript}`);
|
|
1057
|
+
console.log(`assistant turns: ${assistantTurns}`);
|
|
1058
|
+
console.log(`tool invocations: ${toolCalls}`);
|
|
1059
|
+
console.log(`last turn index: ${lastTurn}`);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/cli/commands/version.ts
|
|
1063
|
+
function versionCommand() {
|
|
1064
|
+
console.log(`reasonix ${VERSION}`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/cli/index.ts
|
|
1068
|
+
var DEFAULT_SYSTEM = "You are Reasonix, a helpful DeepSeek-powered assistant. Be concise and accurate. Use tools when available.";
|
|
1069
|
+
var program = new Command();
|
|
1070
|
+
program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION);
|
|
1071
|
+
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").action(async (opts) => {
|
|
1072
|
+
await chatCommand({
|
|
1073
|
+
model: opts.model,
|
|
1074
|
+
system: opts.system,
|
|
1075
|
+
transcript: opts.transcript
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
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) => {
|
|
1079
|
+
await runCommand({ task, model: opts.model, system: opts.system });
|
|
1080
|
+
});
|
|
1081
|
+
program.command("stats <transcript>").description("Summarize a JSONL transcript produced by `reasonix chat --transcript`.").action((transcript) => {
|
|
1082
|
+
statsCommand({ transcript });
|
|
1083
|
+
});
|
|
1084
|
+
program.command("version").description("Print Reasonix version.").action(versionCommand);
|
|
1085
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1086
|
+
console.error(err);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
});
|
|
1089
|
+
//# sourceMappingURL=index.js.map
|