reasonix 0.4.9 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +448 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +77 -9
- package/dist/index.js +238 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -120,6 +120,23 @@ interface StreamChunk {
|
|
|
120
120
|
finishReason?: string;
|
|
121
121
|
raw: any;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Response shape for DeepSeek's `/user/balance` endpoint. One entry
|
|
125
|
+
* per currency the account is funded in (typically CNY, sometimes
|
|
126
|
+
* USD). `total_balance` is the spendable figure; `granted_balance`
|
|
127
|
+
* counts promotional credits that expire, `topped_up_balance` is
|
|
128
|
+
* what the user paid for and keeps.
|
|
129
|
+
*/
|
|
130
|
+
interface BalanceInfo {
|
|
131
|
+
currency: string;
|
|
132
|
+
total_balance: string;
|
|
133
|
+
granted_balance?: string;
|
|
134
|
+
topped_up_balance?: string;
|
|
135
|
+
}
|
|
136
|
+
interface UserBalance {
|
|
137
|
+
is_available: boolean;
|
|
138
|
+
balance_infos: BalanceInfo[];
|
|
139
|
+
}
|
|
123
140
|
interface DeepSeekClientOptions {
|
|
124
141
|
apiKey?: string;
|
|
125
142
|
baseUrl?: string;
|
|
@@ -136,6 +153,15 @@ declare class DeepSeekClient {
|
|
|
136
153
|
private readonly _fetch;
|
|
137
154
|
constructor(opts?: DeepSeekClientOptions);
|
|
138
155
|
private buildPayload;
|
|
156
|
+
/**
|
|
157
|
+
* Fetch the current DeepSeek account balance. Separate endpoint
|
|
158
|
+
* from chat completions, no billing impact. Returns null on any
|
|
159
|
+
* network/auth failure so callers can gate the balance display
|
|
160
|
+
* without a hard error — the rest of the session works regardless.
|
|
161
|
+
*/
|
|
162
|
+
getBalance(opts?: {
|
|
163
|
+
signal?: AbortSignal;
|
|
164
|
+
}): Promise<UserBalance | null>;
|
|
139
165
|
chat(opts: ChatRequestOptions): Promise<ChatResponse>;
|
|
140
166
|
stream(opts: ChatRequestOptions): AsyncGenerator<StreamChunk>;
|
|
141
167
|
}
|
|
@@ -172,7 +198,7 @@ interface HarvestOptions {
|
|
|
172
198
|
}
|
|
173
199
|
declare function emptyPlanState(): TypedPlanState;
|
|
174
200
|
declare function isPlanStateEmpty(s: TypedPlanState | null | undefined): boolean;
|
|
175
|
-
declare function harvest(reasoningContent: string | null | undefined, client?: DeepSeekClient, options?: HarvestOptions): Promise<TypedPlanState>;
|
|
201
|
+
declare function harvest(reasoningContent: string | null | undefined, client?: DeepSeekClient, options?: HarvestOptions, signal?: AbortSignal): Promise<TypedPlanState>;
|
|
176
202
|
|
|
177
203
|
/**
|
|
178
204
|
* Self-consistency branching.
|
|
@@ -373,6 +399,10 @@ declare class ToolCallRepair {
|
|
|
373
399
|
}
|
|
374
400
|
|
|
375
401
|
declare function costUsd(model: string, usage: Usage): number;
|
|
402
|
+
/** Input-side cost only (prompt, cache hit + miss). Used for the panel breakdown. */
|
|
403
|
+
declare function inputCostUsd(model: string, usage: Usage): number;
|
|
404
|
+
/** Output-side cost only (completion tokens). Used for the panel breakdown. */
|
|
405
|
+
declare function outputCostUsd(model: string, usage: Usage): number;
|
|
376
406
|
declare function claudeEquivalentCost(usage: Usage): number;
|
|
377
407
|
interface TurnStats {
|
|
378
408
|
turn: number;
|
|
@@ -384,7 +414,17 @@ interface TurnStats {
|
|
|
384
414
|
interface SessionSummary {
|
|
385
415
|
turns: number;
|
|
386
416
|
totalCostUsd: number;
|
|
417
|
+
/**
|
|
418
|
+
* Input-side (prompt) cost aggregated across the session. Split
|
|
419
|
+
* from totalCostUsd so the panel can render "cost $X (in $Y · out
|
|
420
|
+
* $Z)" — users asked for visibility into where the spend lands.
|
|
421
|
+
*/
|
|
422
|
+
totalInputCostUsd: number;
|
|
423
|
+
/** Output-side (completion) cost aggregated across the session. */
|
|
424
|
+
totalOutputCostUsd: number;
|
|
425
|
+
/** @deprecated Claude reference; kept for benchmarks + replay compat, no longer surfaced in the TUI. */
|
|
387
426
|
claudeEquivalentUsd: number;
|
|
427
|
+
/** @deprecated. Same as claudeEquivalentUsd — synthetic ratio, not a real measurement. */
|
|
388
428
|
savingsVsClaudePct: number;
|
|
389
429
|
cacheHitRatio: number;
|
|
390
430
|
/**
|
|
@@ -401,6 +441,8 @@ declare class SessionStats {
|
|
|
401
441
|
get totalCost(): number;
|
|
402
442
|
get totalClaudeEquivalent(): number;
|
|
403
443
|
get savingsVsClaude(): number;
|
|
444
|
+
get totalInputCost(): number;
|
|
445
|
+
get totalOutputCost(): number;
|
|
404
446
|
get aggregateCacheHitRatio(): number;
|
|
405
447
|
summary(): SessionSummary;
|
|
406
448
|
}
|
|
@@ -447,6 +489,13 @@ declare class ToolRegistry {
|
|
|
447
489
|
}
|
|
448
490
|
|
|
449
491
|
type EventRole = "assistant_delta" | "assistant_final"
|
|
492
|
+
/**
|
|
493
|
+
* Emitted as `tool_calls[].function.arguments` streams in. A tool
|
|
494
|
+
* call with a large arguments payload produces no `content` or
|
|
495
|
+
* `reasoning_content` bytes — this is the only signal the UI has
|
|
496
|
+
* that the stream is alive during that window.
|
|
497
|
+
*/
|
|
498
|
+
| "tool_call_delta"
|
|
450
499
|
/**
|
|
451
500
|
* Yielded immediately before a tool is dispatched. Lets the TUI put
|
|
452
501
|
* up a "▸ tool<X> running…" spinner while the tool's Promise is
|
|
@@ -454,7 +503,16 @@ type EventRole = "assistant_delta" | "assistant_final"
|
|
|
454
503
|
* takes more than a few hundred ms (a big `filesystem_edit_file`
|
|
455
504
|
* is a typical trigger).
|
|
456
505
|
*/
|
|
457
|
-
| "tool_start" | "tool" | "done" | "error" | "warning"
|
|
506
|
+
| "tool_start" | "tool" | "done" | "error" | "warning"
|
|
507
|
+
/**
|
|
508
|
+
* Transient "what's happening right now" indicator. Emitted during
|
|
509
|
+
* silent phases — between a tool result and the next iteration's
|
|
510
|
+
* first streaming byte, and right before harvest — so the TUI can
|
|
511
|
+
* show a spinner with explanatory text instead of looking frozen.
|
|
512
|
+
* The UI clears it on the next primary event (assistant_delta,
|
|
513
|
+
* tool_start, tool, assistant_final, error).
|
|
514
|
+
*/
|
|
515
|
+
| "status" | "branch_start" | "branch_progress" | "branch_done";
|
|
458
516
|
interface BranchSummary {
|
|
459
517
|
budget: number;
|
|
460
518
|
chosenIndex: number;
|
|
@@ -480,6 +538,8 @@ interface LoopEvent {
|
|
|
480
538
|
* what it returned. Needed by `reasonix diff` to explain divergences.
|
|
481
539
|
*/
|
|
482
540
|
toolArgs?: string;
|
|
541
|
+
/** Cumulative arguments-string length for `role === "tool_call_delta"`. */
|
|
542
|
+
toolCallArgsChars?: number;
|
|
483
543
|
stats?: TurnStats;
|
|
484
544
|
planState?: TypedPlanState;
|
|
485
545
|
repair?: RepairReport;
|
|
@@ -584,6 +644,20 @@ declare class CacheFirstLoop {
|
|
|
584
644
|
charsSaved: number;
|
|
585
645
|
};
|
|
586
646
|
private appendAndPersist;
|
|
647
|
+
/**
|
|
648
|
+
* Start a fresh conversation WITHOUT exiting. Drops every message
|
|
649
|
+
* in the in-memory log AND rewrites the session file to empty so
|
|
650
|
+
* a resume won't re-hydrate the old turns. Unlike `/forget`, which
|
|
651
|
+
* deletes the session entirely, this keeps the session name and
|
|
652
|
+
* config intact — it's the "new chat" button.
|
|
653
|
+
*
|
|
654
|
+
* The immutable prefix (system prompt + tool specs) is preserved
|
|
655
|
+
* — that's the cache-first invariant, not part of the conversation.
|
|
656
|
+
* Returns the number of messages dropped so the UI can show it.
|
|
657
|
+
*/
|
|
658
|
+
clearLog(): {
|
|
659
|
+
dropped: number;
|
|
660
|
+
};
|
|
587
661
|
/**
|
|
588
662
|
* Reconfigure model/harvest/branch/stream mid-session. The loop's log,
|
|
589
663
|
* scratch, and stats are preserved — only the per-turn behavior changes.
|
|
@@ -629,12 +703,6 @@ declare class CacheFirstLoop {
|
|
|
629
703
|
* Exported so tests can exercise it against concrete R1 outputs.
|
|
630
704
|
*/
|
|
631
705
|
declare function stripHallucinatedToolMarkup(s: string): string;
|
|
632
|
-
/**
|
|
633
|
-
* Truncate any tool-role message whose content exceeds the cap. User
|
|
634
|
-
* and assistant messages are left alone because (a) they're almost
|
|
635
|
-
* always small, (b) truncating user prompts would corrupt conversational
|
|
636
|
-
* intent in a way the user didn't author. Exported for tests.
|
|
637
|
-
*/
|
|
638
706
|
declare function healLoadedMessages(messages: ChatMessage[], maxChars: number): {
|
|
639
707
|
messages: ChatMessage[];
|
|
640
708
|
healedCount: number;
|
|
@@ -1692,4 +1760,4 @@ declare function redactKey(key: string): string;
|
|
|
1692
1760
|
|
|
1693
1761
|
declare const VERSION = "0.4.3";
|
|
1694
1762
|
|
|
1695
|
-
export { AppendOnlyLog, type ApplyResult, type ApplyStatus, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetPromptResult, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type ProgressNotificationParams, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, appendSessionMessage, applyEditBlock, applyEditBlocks, bridgeMcpTools, claudeEquivalentCost, codeSystemPrompt, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatLoopError, harvest, healLoadedMessages, inspectMcpServer, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, parseEditBlocks, parseMcpSpec, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, registerFilesystemTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, restoreSnapshots, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, truncateForModel, writeConfig, writeMeta, writeRecord };
|
|
1763
|
+
export { AppendOnlyLog, type ApplyResult, type ApplyStatus, type BranchOptions, type BranchProgress, type BranchResult, type BranchSample, type BranchSelector, type BranchSummary, type BridgeOptions, type BridgeResult, CODE_SYSTEM_PROMPT, CacheFirstLoop, type CacheFirstLoopOptions, type CallToolResult, type ChatMessage, type ChatResponse, DEFAULT_MAX_RESULT_CHARS, DeepSeekClient, type DeepSeekClientOptions, type RenderOptions as DiffRenderOptions, type DiffReport, type DiffSide, type EditBlock, type EditSnapshot, type EventRole, type FilesystemToolsOptions, type FlattenDecision, type FlattenOptions, type GetPromptResult, type HarvestOptions, ImmutablePrefix, type ImmutablePrefixOptions, type InitializeResult, type InspectionReport, type JSONSchema, type JsonRpcMessage, type JsonRpcRequest, type JsonRpcResponse, type ListPromptsResult, type ListResourcesResult, type ListToolsResult, type LoopEvent, MCP_PROTOCOL_VERSION, McpClient, type McpClientOptions, type McpContentBlock, type McpProgressHandler, type McpProgressInfo, type McpPrompt, type McpPromptArgument, type McpPromptMessage, type McpPromptResourceBlock, type McpResource, type McpResourceContents, type McpResourceContentsBlob, type McpResourceContentsText, type McpSpec, type McpTool, type McpToolSchema, type McpTransport, type ProgressNotificationParams, type ReadResourceResult, type ReadTranscriptResult, type ReasonixConfig, type ReconfigurableOptions, type RepairReport, type ReplayStats, type RetryInfo, type RetryOptions, type Role, type ScavengeOptions, type ScavengeResult, type SectionResult, type SessionInfo, SessionStats, type SessionSummary, type SseMcpSpec, SseTransport, type SseTransportOptions, type StdioMcpSpec, StdioTransport, type StdioTransportOptions, StormBreaker, type StreamChunk, type ToolCall, type ToolCallContext, ToolCallRepair, type ToolCallRepairOptions, type ToolDefinition, type ToolFunctionSpec, ToolRegistry, type ToolSpec, type TranscriptMeta, type TranscriptRecord, type TruncationRepairResult, type TurnPair, type TurnStats, type TypedPlanState, Usage, VERSION, VolatileScratch, aggregateBranchUsage, analyzeSchema, appendSessionMessage, applyEditBlock, applyEditBlocks, bridgeMcpTools, claudeEquivalentCost, codeSystemPrompt, computeReplayStats, costUsd, defaultConfigPath, defaultSelector, deleteSession, diffTranscripts, emptyPlanState, fetchWithRetry, flattenMcpResult, flattenSchema, formatLoopError, harvest, healLoadedMessages, inputCostUsd, inspectMcpServer, isJsonRpcError, isPlanStateEmpty, isPlausibleKey, listSessions, loadApiKey, loadDotenv, loadSessionMessages, nestArguments, openTranscriptFile, outputCostUsd, parseEditBlocks, parseMcpSpec, parseTranscript, readConfig, readTranscript, recordFromLoopEvent, redactKey, registerFilesystemTools, renderMarkdown as renderDiffMarkdown, renderSummaryTable as renderDiffSummary, repairTruncatedJson, replayFromFile, restoreSnapshots, runBranches, sanitizeName as sanitizeSessionName, saveApiKey, scavengeToolCalls, sessionPath, sessionsDir, similarity, snapshotBeforeEdits, stripHallucinatedToolMarkup, truncateForModel, writeConfig, writeMeta, writeRecord };
|
package/dist/index.js
CHANGED
|
@@ -133,6 +133,27 @@ var DeepSeekClient = class {
|
|
|
133
133
|
if (opts.responseFormat) payload.response_format = opts.responseFormat;
|
|
134
134
|
return payload;
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* Fetch the current DeepSeek account balance. Separate endpoint
|
|
138
|
+
* from chat completions, no billing impact. Returns null on any
|
|
139
|
+
* network/auth failure so callers can gate the balance display
|
|
140
|
+
* without a hard error — the rest of the session works regardless.
|
|
141
|
+
*/
|
|
142
|
+
async getBalance(opts = {}) {
|
|
143
|
+
try {
|
|
144
|
+
const resp = await this._fetch(`${this.baseUrl}/user/balance`, {
|
|
145
|
+
method: "GET",
|
|
146
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
147
|
+
signal: opts.signal
|
|
148
|
+
});
|
|
149
|
+
if (!resp.ok) return null;
|
|
150
|
+
const data = await resp.json();
|
|
151
|
+
if (!data || !Array.isArray(data.balance_infos)) return null;
|
|
152
|
+
return data;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
136
157
|
async chat(opts) {
|
|
137
158
|
const ctrl = new AbortController();
|
|
138
159
|
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
@@ -279,8 +300,9 @@ Constraints:
|
|
|
279
300
|
- Each item is plain text, at most {maxItemLen} characters, no markdown.
|
|
280
301
|
- Write in the same language as the trace (Chinese in \u2192 Chinese out, etc.).
|
|
281
302
|
- Do not quote back the trace; write short, specific phrases.`;
|
|
282
|
-
async function harvest(reasoningContent, client, options = {}) {
|
|
303
|
+
async function harvest(reasoningContent, client, options = {}, signal) {
|
|
283
304
|
if (!client || !reasoningContent) return emptyPlanState();
|
|
305
|
+
if (signal?.aborted) return emptyPlanState();
|
|
284
306
|
const minLen = options.minReasoningLen ?? 40;
|
|
285
307
|
const trimmed = reasoningContent.trim();
|
|
286
308
|
if (trimmed.length < minLen) return emptyPlanState();
|
|
@@ -300,7 +322,8 @@ async function harvest(reasoningContent, client, options = {}) {
|
|
|
300
322
|
],
|
|
301
323
|
responseFormat: { type: "json_object" },
|
|
302
324
|
temperature: 0,
|
|
303
|
-
maxTokens: 600
|
|
325
|
+
maxTokens: 600,
|
|
326
|
+
signal
|
|
304
327
|
});
|
|
305
328
|
return parsePlanState(resp.content, maxItems, maxItemLen);
|
|
306
329
|
} catch {
|
|
@@ -1089,6 +1112,16 @@ function costUsd(model, usage) {
|
|
|
1089
1112
|
if (!p) return 0;
|
|
1090
1113
|
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss + usage.completionTokens * p.output) / 1e6;
|
|
1091
1114
|
}
|
|
1115
|
+
function inputCostUsd(model, usage) {
|
|
1116
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1117
|
+
if (!p) return 0;
|
|
1118
|
+
return (usage.promptCacheHitTokens * p.inputCacheHit + usage.promptCacheMissTokens * p.inputCacheMiss) / 1e6;
|
|
1119
|
+
}
|
|
1120
|
+
function outputCostUsd(model, usage) {
|
|
1121
|
+
const p = DEEPSEEK_PRICING[model];
|
|
1122
|
+
if (!p) return 0;
|
|
1123
|
+
return usage.completionTokens * p.output / 1e6;
|
|
1124
|
+
}
|
|
1092
1125
|
function claudeEquivalentCost(usage) {
|
|
1093
1126
|
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1094
1127
|
}
|
|
@@ -1116,6 +1149,12 @@ var SessionStats = class {
|
|
|
1116
1149
|
const c = this.totalClaudeEquivalent;
|
|
1117
1150
|
return c > 0 ? 1 - this.totalCost / c : 0;
|
|
1118
1151
|
}
|
|
1152
|
+
get totalInputCost() {
|
|
1153
|
+
return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
|
|
1154
|
+
}
|
|
1155
|
+
get totalOutputCost() {
|
|
1156
|
+
return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
|
|
1157
|
+
}
|
|
1119
1158
|
get aggregateCacheHitRatio() {
|
|
1120
1159
|
let hit = 0;
|
|
1121
1160
|
let miss = 0;
|
|
@@ -1131,6 +1170,8 @@ var SessionStats = class {
|
|
|
1131
1170
|
return {
|
|
1132
1171
|
turns: this.turns.length,
|
|
1133
1172
|
totalCostUsd: round(this.totalCost, 6),
|
|
1173
|
+
totalInputCostUsd: round(this.totalInputCost, 6),
|
|
1174
|
+
totalOutputCostUsd: round(this.totalOutputCost, 6),
|
|
1134
1175
|
claudeEquivalentUsd: round(this.totalClaudeEquivalent, 6),
|
|
1135
1176
|
savingsVsClaudePct: round(this.savingsVsClaude * 100, 2),
|
|
1136
1177
|
cacheHitRatio: round(this.aggregateCacheHitRatio, 4),
|
|
@@ -1205,8 +1246,12 @@ var CacheFirstLoop = class {
|
|
|
1205
1246
|
for (const msg of messages) this.log.append(msg);
|
|
1206
1247
|
this.resumedMessageCount = messages.length;
|
|
1207
1248
|
if (healedCount > 0) {
|
|
1249
|
+
try {
|
|
1250
|
+
rewriteSession(this.sessionName, messages);
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1208
1253
|
process.stderr.write(
|
|
1209
|
-
`\u25B8 session "${this.sessionName}": healed ${healedCount}
|
|
1254
|
+
`\u25B8 session "${this.sessionName}": healed ${healedCount} entr${healedCount === 1 ? "y" : "ies"}${healedFrom > 0 ? ` (was ${healedFrom.toLocaleString()} chars oversized)` : " (dropped dangling tool_calls tail)"}. Rewrote session file.
|
|
1210
1255
|
`
|
|
1211
1256
|
);
|
|
1212
1257
|
}
|
|
@@ -1227,7 +1272,7 @@ var CacheFirstLoop = class {
|
|
|
1227
1272
|
*/
|
|
1228
1273
|
compact(tightCapChars = 4e3) {
|
|
1229
1274
|
const before = this.log.toMessages();
|
|
1230
|
-
const { messages, healedCount, healedFrom } =
|
|
1275
|
+
const { messages, healedCount, healedFrom } = shrinkOversizedToolResults(before, tightCapChars);
|
|
1231
1276
|
const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
|
|
1232
1277
|
const charsSaved = healedFrom - afterBytes;
|
|
1233
1278
|
if (healedCount > 0) {
|
|
@@ -1250,6 +1295,29 @@ var CacheFirstLoop = class {
|
|
|
1250
1295
|
}
|
|
1251
1296
|
}
|
|
1252
1297
|
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Start a fresh conversation WITHOUT exiting. Drops every message
|
|
1300
|
+
* in the in-memory log AND rewrites the session file to empty so
|
|
1301
|
+
* a resume won't re-hydrate the old turns. Unlike `/forget`, which
|
|
1302
|
+
* deletes the session entirely, this keeps the session name and
|
|
1303
|
+
* config intact — it's the "new chat" button.
|
|
1304
|
+
*
|
|
1305
|
+
* The immutable prefix (system prompt + tool specs) is preserved
|
|
1306
|
+
* — that's the cache-first invariant, not part of the conversation.
|
|
1307
|
+
* Returns the number of messages dropped so the UI can show it.
|
|
1308
|
+
*/
|
|
1309
|
+
clearLog() {
|
|
1310
|
+
const dropped = this.log.length;
|
|
1311
|
+
this.log.compactInPlace([]);
|
|
1312
|
+
if (this.sessionName) {
|
|
1313
|
+
try {
|
|
1314
|
+
rewriteSession(this.sessionName, []);
|
|
1315
|
+
} catch {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
this.scratch.reset();
|
|
1319
|
+
return { dropped };
|
|
1320
|
+
}
|
|
1253
1321
|
/**
|
|
1254
1322
|
* Reconfigure model/harvest/branch/stream mid-session. The loop's log,
|
|
1255
1323
|
* scratch, and stats are preserved — only the per-turn behavior changes.
|
|
@@ -1281,7 +1349,8 @@ var CacheFirstLoop = class {
|
|
|
1281
1349
|
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
1282
1350
|
}
|
|
1283
1351
|
buildMessages(pendingUser) {
|
|
1284
|
-
const
|
|
1352
|
+
const healed = healLoadedMessages(this.log.toMessages(), DEFAULT_MAX_RESULT_CHARS);
|
|
1353
|
+
const msgs = [...this.prefix.toMessages(), ...healed.messages];
|
|
1285
1354
|
if (pendingUser !== null) msgs.push({ role: "user", content: pendingUser });
|
|
1286
1355
|
return msgs;
|
|
1287
1356
|
}
|
|
@@ -1356,6 +1425,13 @@ var CacheFirstLoop = class {
|
|
|
1356
1425
|
yield { turn: this._turn, role: "done", content: stoppedMsg };
|
|
1357
1426
|
return;
|
|
1358
1427
|
}
|
|
1428
|
+
if (iter > 0) {
|
|
1429
|
+
yield {
|
|
1430
|
+
turn: this._turn,
|
|
1431
|
+
role: "status",
|
|
1432
|
+
content: "tool result uploaded \xB7 model thinking before next response\u2026"
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1359
1435
|
if (!warnedForIterBudget && iter >= warnAt) {
|
|
1360
1436
|
warnedForIterBudget = true;
|
|
1361
1437
|
yield {
|
|
@@ -1485,6 +1561,15 @@ var CacheFirstLoop = class {
|
|
|
1485
1561
|
if (d.argumentsDelta)
|
|
1486
1562
|
cur.function.arguments = (cur.function.arguments ?? "") + d.argumentsDelta;
|
|
1487
1563
|
callBuf.set(d.index, cur);
|
|
1564
|
+
if (cur.function.name) {
|
|
1565
|
+
yield {
|
|
1566
|
+
turn: this._turn,
|
|
1567
|
+
role: "tool_call_delta",
|
|
1568
|
+
content: "",
|
|
1569
|
+
toolName: cur.function.name,
|
|
1570
|
+
toolCallArgsChars: (cur.function.arguments ?? "").length
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1488
1573
|
}
|
|
1489
1574
|
if (chunk.usage) usage = chunk.usage;
|
|
1490
1575
|
}
|
|
@@ -1516,7 +1601,14 @@ var CacheFirstLoop = class {
|
|
|
1516
1601
|
pendingUser = null;
|
|
1517
1602
|
}
|
|
1518
1603
|
this.scratch.reasoning = reasoningContent || null;
|
|
1519
|
-
|
|
1604
|
+
if (!preHarvestedPlanState && this.harvestEnabled && (reasoningContent?.trim().length ?? 0) >= 40) {
|
|
1605
|
+
yield {
|
|
1606
|
+
turn: this._turn,
|
|
1607
|
+
role: "status",
|
|
1608
|
+
content: "extracting plan state from reasoning\u2026"
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const planState = preHarvestedPlanState ? preHarvestedPlanState : this.harvestEnabled ? await harvest(reasoningContent || null, this.client, this.harvestOptions, signal) : emptyPlanState();
|
|
1520
1612
|
const { calls: repairedCalls, report } = this.repair.process(
|
|
1521
1613
|
toolCalls,
|
|
1522
1614
|
reasoningContent || null,
|
|
@@ -1538,15 +1630,38 @@ var CacheFirstLoop = class {
|
|
|
1538
1630
|
}
|
|
1539
1631
|
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1540
1632
|
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1633
|
+
const before = usage.promptTokens;
|
|
1634
|
+
const compactResult = this.compact(4e3);
|
|
1635
|
+
if (compactResult.healedCount > 0) {
|
|
1636
|
+
const approxSaved = Math.round(compactResult.charsSaved / 4);
|
|
1637
|
+
const after = before - approxSaved;
|
|
1638
|
+
yield {
|
|
1639
|
+
turn: this._turn,
|
|
1640
|
+
role: "warning",
|
|
1641
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
|
|
1642
|
+
};
|
|
1643
|
+
} else {
|
|
1644
|
+
yield {
|
|
1645
|
+
turn: this._turn,
|
|
1646
|
+
role: "warning",
|
|
1647
|
+
content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
|
|
1648
|
+
before / ctxMax * 100
|
|
1649
|
+
)}%) \u2014 nothing to auto-compact. Forcing summary from what was gathered.`
|
|
1650
|
+
};
|
|
1651
|
+
const tail = this.log.entries[this.log.entries.length - 1];
|
|
1652
|
+
if (tail && tail.role === "assistant" && Array.isArray(tail.tool_calls) && tail.tool_calls.length > 0) {
|
|
1653
|
+
const kept = this.log.entries.slice(0, -1);
|
|
1654
|
+
this.log.compactInPlace([...kept]);
|
|
1655
|
+
if (this.sessionName) {
|
|
1656
|
+
try {
|
|
1657
|
+
rewriteSession(this.sessionName, kept);
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1550
1665
|
}
|
|
1551
1666
|
for (const call of repairedCalls) {
|
|
1552
1667
|
const name = call.function?.name ?? "";
|
|
@@ -1578,6 +1693,11 @@ var CacheFirstLoop = class {
|
|
|
1578
1693
|
}
|
|
1579
1694
|
async *forceSummaryAfterIterLimit(opts = { reason: "budget" }) {
|
|
1580
1695
|
try {
|
|
1696
|
+
yield {
|
|
1697
|
+
turn: this._turn,
|
|
1698
|
+
role: "status",
|
|
1699
|
+
content: "summarizing what was gathered\u2026"
|
|
1700
|
+
};
|
|
1581
1701
|
const messages = this.buildMessages(null);
|
|
1582
1702
|
messages.push({
|
|
1583
1703
|
role: "user",
|
|
@@ -1660,7 +1780,7 @@ function summarizeBranch(chosen, samples) {
|
|
|
1660
1780
|
temperatures: samples.map((s) => s.temperature)
|
|
1661
1781
|
};
|
|
1662
1782
|
}
|
|
1663
|
-
function
|
|
1783
|
+
function shrinkOversizedToolResults(messages, maxChars) {
|
|
1664
1784
|
let healedCount = 0;
|
|
1665
1785
|
let healedFrom = 0;
|
|
1666
1786
|
const out = messages.map((msg) => {
|
|
@@ -1673,6 +1793,51 @@ function healLoadedMessages(messages, maxChars) {
|
|
|
1673
1793
|
});
|
|
1674
1794
|
return { messages: out, healedCount, healedFrom };
|
|
1675
1795
|
}
|
|
1796
|
+
function healLoadedMessages(messages, maxChars) {
|
|
1797
|
+
const shrunk = shrinkOversizedToolResults(messages, maxChars);
|
|
1798
|
+
let healedCount = shrunk.healedCount;
|
|
1799
|
+
const out = [];
|
|
1800
|
+
const openCallIds = /* @__PURE__ */ new Set();
|
|
1801
|
+
let droppedAssistantCalls = 0;
|
|
1802
|
+
let droppedStrayTools = 0;
|
|
1803
|
+
for (let i = 0; i < shrunk.messages.length; i++) {
|
|
1804
|
+
const msg = shrunk.messages[i];
|
|
1805
|
+
if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
1806
|
+
const needed = /* @__PURE__ */ new Set();
|
|
1807
|
+
for (const call of msg.tool_calls) {
|
|
1808
|
+
if (call?.id) needed.add(call.id);
|
|
1809
|
+
}
|
|
1810
|
+
const candidates = [];
|
|
1811
|
+
let j = i + 1;
|
|
1812
|
+
while (j < shrunk.messages.length && needed.size > 0) {
|
|
1813
|
+
const nxt = shrunk.messages[j];
|
|
1814
|
+
if (nxt.role !== "tool") break;
|
|
1815
|
+
const id = nxt.tool_call_id ?? "";
|
|
1816
|
+
if (!needed.has(id)) break;
|
|
1817
|
+
needed.delete(id);
|
|
1818
|
+
candidates.push(nxt);
|
|
1819
|
+
j++;
|
|
1820
|
+
}
|
|
1821
|
+
if (needed.size === 0) {
|
|
1822
|
+
out.push(msg);
|
|
1823
|
+
for (const r of candidates) out.push(r);
|
|
1824
|
+
i = j - 1;
|
|
1825
|
+
} else {
|
|
1826
|
+
droppedAssistantCalls += 1;
|
|
1827
|
+
droppedStrayTools += candidates.length;
|
|
1828
|
+
i = j - 1;
|
|
1829
|
+
}
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (msg.role === "tool") {
|
|
1833
|
+
droppedStrayTools += 1;
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
out.push(msg);
|
|
1837
|
+
}
|
|
1838
|
+
healedCount += droppedAssistantCalls + droppedStrayTools;
|
|
1839
|
+
return { messages: out, healedCount, healedFrom: shrunk.healedFrom };
|
|
1840
|
+
}
|
|
1676
1841
|
function formatLoopError(err) {
|
|
1677
1842
|
const msg = err.message ?? "";
|
|
1678
1843
|
if (msg.includes("maximum context length")) {
|
|
@@ -1929,7 +2094,12 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1929
2094
|
}
|
|
1930
2095
|
const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
|
|
1931
2096
|
await fs.writeFile(abs, after, "utf8");
|
|
1932
|
-
|
|
2097
|
+
const rel = pathMod.relative(rootDir, abs);
|
|
2098
|
+
const header = `edited ${rel} (${args.search.length}\u2192${args.replace.length} chars)`;
|
|
2099
|
+
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
2100
|
+
const diff = renderEditDiff(args.search, args.replace, startLine);
|
|
2101
|
+
return `${header}
|
|
2102
|
+
${diff}`;
|
|
1933
2103
|
}
|
|
1934
2104
|
});
|
|
1935
2105
|
registry.register({
|
|
@@ -1967,6 +2137,51 @@ function registerFilesystemTools(registry, opts) {
|
|
|
1967
2137
|
});
|
|
1968
2138
|
return registry;
|
|
1969
2139
|
}
|
|
2140
|
+
function renderEditDiff(search, replace, startLine) {
|
|
2141
|
+
const a = search.split(/\r?\n/);
|
|
2142
|
+
const b = replace.split(/\r?\n/);
|
|
2143
|
+
const diff = lineDiff(a, b);
|
|
2144
|
+
const hunk = `@@ -${startLine},${a.length} +${startLine},${b.length} @@`;
|
|
2145
|
+
const body = diff.map((d) => `${d.op === " " ? " " : d.op} ${d.line}`).join("\n");
|
|
2146
|
+
return `${hunk}
|
|
2147
|
+
${body}`;
|
|
2148
|
+
}
|
|
2149
|
+
function lineDiff(a, b) {
|
|
2150
|
+
const n = a.length;
|
|
2151
|
+
const m = b.length;
|
|
2152
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
2153
|
+
for (let i2 = 1; i2 <= n; i2++) {
|
|
2154
|
+
for (let j2 = 1; j2 <= m; j2++) {
|
|
2155
|
+
if (a[i2 - 1] === b[j2 - 1]) dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
|
|
2156
|
+
else dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
const out = [];
|
|
2160
|
+
let i = n;
|
|
2161
|
+
let j = m;
|
|
2162
|
+
while (i > 0 && j > 0) {
|
|
2163
|
+
if (a[i - 1] === b[j - 1]) {
|
|
2164
|
+
out.unshift({ op: " ", line: a[i - 1] });
|
|
2165
|
+
i--;
|
|
2166
|
+
j--;
|
|
2167
|
+
} else if ((dp[i - 1][j] ?? 0) > (dp[i][j - 1] ?? 0)) {
|
|
2168
|
+
out.unshift({ op: "-", line: a[i - 1] });
|
|
2169
|
+
i--;
|
|
2170
|
+
} else {
|
|
2171
|
+
out.unshift({ op: "+", line: b[j - 1] });
|
|
2172
|
+
j--;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
while (i > 0) {
|
|
2176
|
+
out.unshift({ op: "-", line: a[i - 1] });
|
|
2177
|
+
i--;
|
|
2178
|
+
}
|
|
2179
|
+
while (j > 0) {
|
|
2180
|
+
out.unshift({ op: "+", line: b[j - 1] });
|
|
2181
|
+
j--;
|
|
2182
|
+
}
|
|
2183
|
+
return out;
|
|
2184
|
+
}
|
|
1970
2185
|
|
|
1971
2186
|
// src/env.ts
|
|
1972
2187
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -2134,6 +2349,8 @@ function computeReplayStats(records) {
|
|
|
2134
2349
|
}
|
|
2135
2350
|
function summarizeTurns(turns) {
|
|
2136
2351
|
const totalCost = turns.reduce((s, t) => s + t.cost, 0);
|
|
2352
|
+
const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);
|
|
2353
|
+
const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);
|
|
2137
2354
|
const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
|
|
2138
2355
|
let hit = 0;
|
|
2139
2356
|
let miss = 0;
|
|
@@ -2147,6 +2364,8 @@ function summarizeTurns(turns) {
|
|
|
2147
2364
|
return {
|
|
2148
2365
|
turns: turns.length,
|
|
2149
2366
|
totalCostUsd: round2(totalCost, 6),
|
|
2367
|
+
totalInputCostUsd: round2(totalInput, 6),
|
|
2368
|
+
totalOutputCostUsd: round2(totalOutput, 6),
|
|
2150
2369
|
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
2151
2370
|
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
2152
2371
|
cacheHitRatio: round2(cacheHitRatio, 4),
|
|
@@ -3377,6 +3596,7 @@ export {
|
|
|
3377
3596
|
formatLoopError,
|
|
3378
3597
|
harvest,
|
|
3379
3598
|
healLoadedMessages,
|
|
3599
|
+
inputCostUsd,
|
|
3380
3600
|
inspectMcpServer,
|
|
3381
3601
|
isJsonRpcError,
|
|
3382
3602
|
isPlanStateEmpty,
|
|
@@ -3387,6 +3607,7 @@ export {
|
|
|
3387
3607
|
loadSessionMessages,
|
|
3388
3608
|
nestArguments,
|
|
3389
3609
|
openTranscriptFile,
|
|
3610
|
+
outputCostUsd,
|
|
3390
3611
|
parseEditBlocks,
|
|
3391
3612
|
parseMcpSpec,
|
|
3392
3613
|
parseTranscript,
|