reasonix 0.5.13 → 0.5.20

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/index.d.ts CHANGED
@@ -69,6 +69,14 @@ interface ChatMessage {
69
69
  name?: string;
70
70
  tool_call_id?: string;
71
71
  tool_calls?: ToolCall[];
72
+ /**
73
+ * R1 `reasoning_content` captured from the assistant's thinking turn.
74
+ * DeepSeek's thinking mode 400s with "reasoning_content in the
75
+ * thinking mode must be passed back" when a tool-loop continuation
76
+ * omits it from the preceding assistant message. Round-tripped for
77
+ * deepseek-reasoner turns with tool_calls; absent for deepseek-chat.
78
+ */
79
+ reasoning_content?: string | null;
72
80
  }
73
81
  interface RawUsage {
74
82
  prompt_tokens?: number;
@@ -790,6 +798,19 @@ interface LoopEvent {
790
798
  toolArgs?: string;
791
799
  /** Cumulative arguments-string length for `role === "tool_call_delta"`. */
792
800
  toolCallArgsChars?: number;
801
+ /**
802
+ * Zero-based index of the tool call this delta belongs to. Surfaces
803
+ * multi-tool turns: on a response emitting 4 write_file calls the UI
804
+ * can show "building call 3/?" instead of a context-free spinner.
805
+ */
806
+ toolCallIndex?: number;
807
+ /**
808
+ * Count of prior tool calls (this turn) whose arguments have finished
809
+ * streaming into valid JSON. Not all ready calls have been dispatched
810
+ * yet — dispatch still happens post-stream — but the user gets "2
811
+ * ready" progress feedback while later calls keep streaming.
812
+ */
813
+ toolCallReadyCount?: number;
793
814
  stats?: TurnStats;
794
815
  planState?: TypedPlanState;
795
816
  repair?: RepairReport;
@@ -922,7 +943,7 @@ declare class CacheFirstLoop {
922
943
  tokensSaved: number;
923
944
  charsSaved: number;
924
945
  };
925
- private appendAndPersist;
946
+ appendAndPersist(message: ChatMessage): void;
926
947
  /**
927
948
  * Start a fresh conversation WITHOUT exiting. Drops every message
928
949
  * in the in-memory log AND rewrites the session file to empty so
@@ -970,6 +991,15 @@ declare class CacheFirstLoop {
970
991
  private forceSummaryAfterIterLimit;
971
992
  run(userInput: string, onEvent?: (ev: LoopEvent) => void): Promise<string>;
972
993
  private assistantMessage;
994
+ /**
995
+ * Build a synthetic assistant message we insert into the log without
996
+ * a real API round trip (abort notices, future system injections).
997
+ * Reasoner models reject follow-up requests whose assistant history
998
+ * is missing `reasoning_content`, so we stamp an empty-string
999
+ * placeholder on reasoner sessions to satisfy the validator. V3
1000
+ * doesn't care — field stays absent there.
1001
+ */
1002
+ private syntheticAssistantMessage;
973
1003
  }
974
1004
  /**
975
1005
  * R1 occasionally hallucinates tool-call markup as plain text when the
@@ -1550,6 +1580,10 @@ interface SubagentEvent {
1550
1580
  kind: "start" | "progress" | "end";
1551
1581
  /** First ~30 chars of the task prompt — used for the TUI status row. */
1552
1582
  task: string;
1583
+ /** Skill that spawned this subagent, when applicable. Stamped on every event so the TUI/logger can attribute without extra plumbing. */
1584
+ skillName?: string;
1585
+ /** Model id the child loop ran on. Stamped alongside skillName. */
1586
+ model?: string;
1553
1587
  /** Iteration count inside the child loop (number of tool results so far). */
1554
1588
  iter?: number;
1555
1589
  /** Wall-clock ms since the subagent started. */
@@ -1560,6 +1594,10 @@ interface SubagentEvent {
1560
1594
  error?: string;
1561
1595
  /** Total turns the subagent took. Set on `end`. */
1562
1596
  turns?: number;
1597
+ /** Total USD spent inside the child loop. Set on `end`. */
1598
+ costUsd?: number;
1599
+ /** Aggregated child-loop Usage (sum across turns). Set on `end`. */
1600
+ usage?: Usage;
1563
1601
  }
1564
1602
  /**
1565
1603
  * Mutable ref the registration writes through. The TUI sets `.current`
@@ -3066,6 +3104,22 @@ interface UsageRecord {
3066
3104
  costUsd: number;
3067
3105
  /** What the same turn would have cost at Claude Sonnet 4.6 rates. */
3068
3106
  claudeEquivUsd: number;
3107
+ /**
3108
+ * Distinguishes ordinary parent-loop turns from subagent summary rows.
3109
+ * Absent on pre-0.5.14 records — treat as "turn" when missing.
3110
+ */
3111
+ kind?: "turn" | "subagent";
3112
+ /** Present when `kind === "subagent"`. Attribution metadata for the /stats roll-up. */
3113
+ subagent?: {
3114
+ /** Skill that spawned it, when the spawn came from a `runAs: subagent` skill. */
3115
+ skillName?: string;
3116
+ /** First ~60 chars of the task prompt — enough context to recognize a run, never the full text. */
3117
+ taskPreview: string;
3118
+ /** Tool calls the child loop dispatched before returning. */
3119
+ toolIters: number;
3120
+ /** Wall-clock ms. */
3121
+ durationMs: number;
3122
+ };
3069
3123
  }
3070
3124
  /** Where the log lives. Tests override via `opts.path`. */
3071
3125
  declare function defaultUsageLogPath(homeDirOverride?: string): string;
@@ -3077,6 +3131,9 @@ interface AppendUsageInput {
3077
3131
  now?: number;
3078
3132
  /** Override the log path (tests). */
3079
3133
  path?: string;
3134
+ /** When appending a subagent summary row, set `kind: "subagent"` and populate `subagent`. */
3135
+ kind?: "turn" | "subagent";
3136
+ subagent?: UsageRecord["subagent"];
3080
3137
  }
3081
3138
  /**
3082
3139
  * Append one record and return it. Swallows disk errors — the TUI
@@ -3131,6 +3188,25 @@ interface UsageAggregate {
3131
3188
  firstSeen: number | null;
3132
3189
  /** Latest record's ts, or `null` when the log is empty. */
3133
3190
  lastSeen: number | null;
3191
+ /**
3192
+ * Subagent-specific rollup. Undefined when no subagent records exist
3193
+ * in the log so consumers can cheaply skip the section. Counts reflect
3194
+ * subagent SPAWNS (not internal child-loop turns) — one row per run.
3195
+ */
3196
+ subagents?: SubagentAggregate;
3197
+ }
3198
+ /** Rolled-up view of all `kind: "subagent"` records. */
3199
+ interface SubagentAggregate {
3200
+ total: number;
3201
+ costUsd: number;
3202
+ totalDurationMs: number;
3203
+ /** Per-skill breakdown. Records without `skillName` (raw spawn_subagent calls) group under `"(adhoc)"`. */
3204
+ bySkill: Array<{
3205
+ skillName: string;
3206
+ count: number;
3207
+ costUsd: number;
3208
+ durationMs: number;
3209
+ }>;
3134
3210
  }
3135
3211
  /**
3136
3212
  * Fold a flat record list into the dashboard shape — rolling windows
package/dist/index.js CHANGED
@@ -1563,6 +1563,11 @@ function deleteSession(name) {
1563
1563
  const path = sessionPath(name);
1564
1564
  try {
1565
1565
  unlinkSync(path);
1566
+ const sidecar = path.replace(/\.jsonl$/, ".pending.json");
1567
+ try {
1568
+ unlinkSync(sidecar);
1569
+ } catch {
1570
+ }
1566
1571
  return true;
1567
1572
  } catch {
1568
1573
  return false;
@@ -1590,13 +1595,18 @@ function countLines(path) {
1590
1595
 
1591
1596
  // src/telemetry.ts
1592
1597
  var DEEPSEEK_PRICING = {
1593
- "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 },
1594
- "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 }
1598
+ "deepseek-v4-flash": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
1599
+ "deepseek-v4-pro": { inputCacheHit: 0.139, inputCacheMiss: 1.667, output: 3.333 },
1600
+ // Compat aliases — priced as v4-flash per the deprecation notice.
1601
+ "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 },
1602
+ "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.139, output: 0.278 }
1595
1603
  };
1596
1604
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1597
1605
  var DEEPSEEK_CONTEXT_TOKENS = {
1598
- "deepseek-chat": 131072,
1599
- "deepseek-reasoner": 131072
1606
+ "deepseek-v4-flash": 1e6,
1607
+ "deepseek-v4-pro": 1e6,
1608
+ "deepseek-chat": 1e6,
1609
+ "deepseek-reasoner": 1e6
1600
1610
  };
1601
1611
  var DEFAULT_CONTEXT_TOKENS = 131072;
1602
1612
  function costUsd(model, usage) {
@@ -1924,7 +1934,7 @@ var CacheFirstLoop = class {
1924
1934
  content: `aborted at iter ${iter}/${this.maxToolIters} \u2014 stopped without producing a summary (press \u2191 + Enter or /retry to resume)`
1925
1935
  };
1926
1936
  const stoppedMsg = "[aborted by user (Esc) \u2014 no summary produced. Ask again or /retry when ready; prior tool output is still in the log.]";
1927
- this.appendAndPersist({ role: "assistant", content: stoppedMsg });
1937
+ this.appendAndPersist(this.syntheticAssistantMessage(stoppedMsg));
1928
1938
  yield {
1929
1939
  turn: this._turn,
1930
1940
  role: "assistant_final",
@@ -2060,6 +2070,7 @@ var CacheFirstLoop = class {
2060
2070
  };
2061
2071
  } else if (this.stream) {
2062
2072
  const callBuf = /* @__PURE__ */ new Map();
2073
+ const readyIndices = /* @__PURE__ */ new Set();
2063
2074
  for await (const chunk of this.client.stream({
2064
2075
  model: this.model,
2065
2076
  messages,
@@ -2095,13 +2106,18 @@ var CacheFirstLoop = class {
2095
2106
  if (d.argumentsDelta)
2096
2107
  cur.function.arguments = (cur.function.arguments ?? "") + d.argumentsDelta;
2097
2108
  callBuf.set(d.index, cur);
2109
+ if (!readyIndices.has(d.index) && cur.function.name && looksLikeCompleteJson(cur.function.arguments ?? "")) {
2110
+ readyIndices.add(d.index);
2111
+ }
2098
2112
  if (cur.function.name) {
2099
2113
  yield {
2100
2114
  turn: this._turn,
2101
2115
  role: "tool_call_delta",
2102
2116
  content: "",
2103
2117
  toolName: cur.function.name,
2104
- toolCallArgsChars: (cur.function.arguments ?? "").length
2118
+ toolCallArgsChars: (cur.function.arguments ?? "").length,
2119
+ toolCallIndex: d.index,
2120
+ toolCallReadyCount: readyIndices.size
2105
2121
  };
2106
2122
  }
2107
2123
  }
@@ -2152,7 +2168,9 @@ var CacheFirstLoop = class {
2152
2168
  reasoningContent || null,
2153
2169
  assistantContent || null
2154
2170
  );
2155
- this.appendAndPersist(this.assistantMessage(assistantContent, repairedCalls));
2171
+ this.appendAndPersist(
2172
+ this.assistantMessage(assistantContent, repairedCalls, reasoningContent)
2173
+ );
2156
2174
  yield {
2157
2175
  turn: this._turn,
2158
2176
  role: "assistant_final",
@@ -2314,7 +2332,7 @@ ${reason}`;
2314
2332
 
2315
2333
  ${summary}`;
2316
2334
  const summaryStats = this.stats.record(this._turn, this.model, resp.usage ?? new Usage());
2317
- this.appendAndPersist({ role: "assistant", content: summary });
2335
+ this.appendAndPersist(this.assistantMessage(summary, [], resp.reasoningContent ?? void 0));
2318
2336
  yield {
2319
2337
  turn: this._turn,
2320
2338
  role: "assistant_final",
@@ -2343,12 +2361,35 @@ ${summary}`;
2343
2361
  }
2344
2362
  return final;
2345
2363
  }
2346
- assistantMessage(content, toolCalls) {
2364
+ assistantMessage(content, toolCalls, reasoningContent) {
2347
2365
  const msg = { role: "assistant", content };
2348
2366
  if (toolCalls.length > 0) msg.tool_calls = toolCalls;
2367
+ if (reasoningContent && reasoningContent.length > 0) {
2368
+ msg.reasoning_content = reasoningContent;
2369
+ }
2370
+ return msg;
2371
+ }
2372
+ /**
2373
+ * Build a synthetic assistant message we insert into the log without
2374
+ * a real API round trip (abort notices, future system injections).
2375
+ * Reasoner models reject follow-up requests whose assistant history
2376
+ * is missing `reasoning_content`, so we stamp an empty-string
2377
+ * placeholder on reasoner sessions to satisfy the validator. V3
2378
+ * doesn't care — field stays absent there.
2379
+ */
2380
+ syntheticAssistantMessage(content) {
2381
+ const msg = { role: "assistant", content };
2382
+ if (isThinkingModeModel(this.model)) {
2383
+ msg.reasoning_content = "";
2384
+ }
2349
2385
  return msg;
2350
2386
  }
2351
2387
  };
2388
+ function isThinkingModeModel(model) {
2389
+ if (model.includes("reasoner")) return true;
2390
+ if (model === "deepseek-v4-flash" || model === "deepseek-v4-pro") return true;
2391
+ return false;
2392
+ }
2352
2393
  function stripHallucinatedToolMarkup(s) {
2353
2394
  let out = s;
2354
2395
  out = out.replace(/<|DSML|function_calls>[\s\S]*?<\/?|DSML|function_calls>/g, "");
@@ -2364,6 +2405,15 @@ function safeParseToolArgs(raw) {
2364
2405
  return raw;
2365
2406
  }
2366
2407
  }
2408
+ function looksLikeCompleteJson(s) {
2409
+ if (!s || !s.trim()) return false;
2410
+ try {
2411
+ JSON.parse(s);
2412
+ return true;
2413
+ } catch {
2414
+ return false;
2415
+ }
2416
+ }
2367
2417
  function* hookWarnings(outcomes, turn) {
2368
2418
  for (const o of outcomes) {
2369
2419
  if (o.decision === "pass") continue;
@@ -3961,11 +4011,14 @@ async function spawnSubagent(opts) {
3961
4011
  const maxToolIters = opts.maxToolIters ?? DEFAULT_MAX_ITERS;
3962
4012
  const maxResultChars = opts.maxResultChars ?? DEFAULT_MAX_RESULT_CHARS2;
3963
4013
  const sink = opts.sink;
4014
+ const skillName = opts.skillName;
3964
4015
  const startedAt = Date.now();
3965
4016
  const taskPreview = opts.task.length > 30 ? `${opts.task.slice(0, 30)}\u2026` : opts.task;
3966
4017
  sink?.current?.({
3967
4018
  kind: "start",
3968
4019
  task: taskPreview,
4020
+ skillName,
4021
+ model,
3969
4022
  iter: 0,
3970
4023
  elapsedMs: 0
3971
4024
  });
@@ -3995,6 +4048,8 @@ async function spawnSubagent(opts) {
3995
4048
  sink?.current?.({
3996
4049
  kind: "progress",
3997
4050
  task: taskPreview,
4051
+ skillName,
4052
+ model,
3998
4053
  iter: toolIter,
3999
4054
  elapsedMs: Date.now() - startedAt
4000
4055
  });
@@ -4017,17 +4072,22 @@ async function spawnSubagent(opts) {
4017
4072
  const elapsedMs = Date.now() - startedAt;
4018
4073
  const turns = childLoop.stats.turns.length;
4019
4074
  const costUsd2 = childLoop.stats.totalCost;
4075
+ const usage = aggregateChildUsage(childLoop);
4020
4076
  const truncated = final.length > maxResultChars ? `${final.slice(0, maxResultChars)}
4021
4077
 
4022
4078
  [\u2026truncated ${final.length - maxResultChars} chars; ask the subagent for a tighter summary if you need more.]` : final;
4023
4079
  sink?.current?.({
4024
4080
  kind: "end",
4025
4081
  task: taskPreview,
4082
+ skillName,
4083
+ model,
4026
4084
  iter: toolIter,
4027
4085
  elapsedMs,
4028
4086
  summary: errorMessage ? void 0 : truncated.slice(0, 120),
4029
4087
  error: errorMessage,
4030
- turns
4088
+ turns,
4089
+ costUsd: costUsd2,
4090
+ usage
4031
4091
  });
4032
4092
  return {
4033
4093
  success: !errorMessage,
@@ -4036,9 +4096,23 @@ async function spawnSubagent(opts) {
4036
4096
  turns,
4037
4097
  toolIters: toolIter,
4038
4098
  elapsedMs,
4039
- costUsd: costUsd2
4099
+ costUsd: costUsd2,
4100
+ model,
4101
+ skillName,
4102
+ usage
4040
4103
  };
4041
4104
  }
4105
+ function aggregateChildUsage(loop) {
4106
+ const agg = new Usage();
4107
+ for (const t of loop.stats.turns) {
4108
+ agg.promptTokens += t.usage.promptTokens;
4109
+ agg.completionTokens += t.usage.completionTokens;
4110
+ agg.totalTokens += t.usage.totalTokens;
4111
+ agg.promptCacheHitTokens += t.usage.promptCacheHitTokens;
4112
+ agg.promptCacheMissTokens += t.usage.promptCacheMissTokens;
4113
+ }
4114
+ return agg;
4115
+ }
4042
4116
  function formatSubagentResult(r) {
4043
4117
  if (!r.success) {
4044
4118
  return JSON.stringify({
@@ -4081,8 +4155,8 @@ function registerSubagentTool(parentRegistry, opts) {
4081
4155
  },
4082
4156
  model: {
4083
4157
  type: "string",
4084
- enum: ["deepseek-chat", "deepseek-reasoner"],
4085
- description: "Which DeepSeek model the subagent runs on. 'deepseek-chat' (V3) is the default \u2014 fast and cheap. Use 'deepseek-reasoner' (R1) only when the subtask genuinely needs planning or multi-step reasoning; it is roughly 5-10x more expensive."
4158
+ enum: ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-chat", "deepseek-reasoner"],
4159
+ description: "Which DeepSeek model the subagent runs on. 'deepseek-v4-flash' (default; thinking mode) is fast and cheap and is what the legacy 'deepseek-chat' / 'deepseek-reasoner' aliases route to today. Use 'deepseek-v4-pro' only when the subtask needs the strongest model \u2014 roughly 12\xD7 the input cost and 12\xD7 the output cost vs flash."
4086
4160
  }
4087
4161
  },
4088
4162
  required: ["task"]
@@ -6245,6 +6319,8 @@ function appendUsage(input) {
6245
6319
  costUsd: costUsd(input.model, input.usage),
6246
6320
  claudeEquivUsd: claudeEquivalentCost(input.usage)
6247
6321
  };
6322
+ if (input.kind === "subagent") record.kind = "subagent";
6323
+ if (input.subagent) record.subagent = input.subagent;
6248
6324
  const path = input.path ?? defaultUsageLogPath();
6249
6325
  try {
6250
6326
  mkdirSync6(dirname7(path), { recursive: true });
@@ -6318,6 +6394,10 @@ function aggregateUsage(records, opts = {}) {
6318
6394
  const sessionCounts = /* @__PURE__ */ new Map();
6319
6395
  let firstSeen = null;
6320
6396
  let lastSeen = null;
6397
+ const skillCounts = /* @__PURE__ */ new Map();
6398
+ let subagentTotal = 0;
6399
+ let subagentCost = 0;
6400
+ let subagentDuration = 0;
6321
6401
  for (const r of records) {
6322
6402
  addToBucket(all, r);
6323
6403
  if (r.ts >= today.since) addToBucket(today, r);
@@ -6328,15 +6408,34 @@ function aggregateUsage(records, opts = {}) {
6328
6408
  sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
6329
6409
  if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
6330
6410
  if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
6411
+ if (r.kind === "subagent") {
6412
+ subagentTotal += 1;
6413
+ subagentCost += r.costUsd;
6414
+ const dur = r.subagent?.durationMs ?? 0;
6415
+ subagentDuration += dur;
6416
+ const key = r.subagent?.skillName?.trim() || "(adhoc)";
6417
+ const prev = skillCounts.get(key) ?? { count: 0, costUsd: 0, durationMs: 0 };
6418
+ prev.count += 1;
6419
+ prev.costUsd += r.costUsd;
6420
+ prev.durationMs += dur;
6421
+ skillCounts.set(key, prev);
6422
+ }
6331
6423
  }
6332
6424
  const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
6333
6425
  const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
6426
+ const subagents = subagentTotal > 0 ? {
6427
+ total: subagentTotal,
6428
+ costUsd: subagentCost,
6429
+ totalDurationMs: subagentDuration,
6430
+ bySkill: Array.from(skillCounts.entries()).map(([skillName, v]) => ({ skillName, ...v })).sort((a, b) => b.count - a.count)
6431
+ } : void 0;
6334
6432
  return {
6335
6433
  buckets: [today, week, month, all],
6336
6434
  byModel,
6337
6435
  bySession,
6338
6436
  firstSeen,
6339
- lastSeen
6437
+ lastSeen,
6438
+ subagents
6340
6439
  };
6341
6440
  }
6342
6441
  function formatLogSize(path = defaultUsageLogPath()) {