@mrciphersmith/keryx 0.3.17 → 0.3.18

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.
Files changed (2) hide show
  1. package/dist/cli.js +90 -17
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17641,7 +17641,7 @@ var package_default;
17641
17641
  var init_package = __esm(() => {
17642
17642
  package_default = {
17643
17643
  name: "@mrciphersmith/keryx",
17644
- version: "0.3.17",
17644
+ version: "0.3.18",
17645
17645
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
17646
17646
  private: false,
17647
17647
  publishConfig: {
@@ -28955,7 +28955,7 @@ function retryableFor2(kind, fallback) {
28955
28955
  const concrete = defaultRetryable(kind);
28956
28956
  return concrete === undefined ? fallback : concrete;
28957
28957
  }
28958
- function mergeUsage2(promptTokens, completionTokens, totalTokens) {
28958
+ function mergeUsage2(promptTokens, completionTokens, totalTokens, cacheReadTokens) {
28959
28959
  const usage = { exact: true };
28960
28960
  if (promptTokens !== undefined) {
28961
28961
  usage.inputTokens = promptTokens;
@@ -28968,6 +28968,9 @@ function mergeUsage2(promptTokens, completionTokens, totalTokens) {
28968
28968
  } else if (promptTokens !== undefined || completionTokens !== undefined) {
28969
28969
  usage.totalTokens = (promptTokens ?? 0) + (completionTokens ?? 0);
28970
28970
  }
28971
+ if (cacheReadTokens !== undefined) {
28972
+ usage.cacheReadTokens = cacheReadTokens;
28973
+ }
28971
28974
  return usage;
28972
28975
  }
28973
28976
  function classifyHttpError2(status, headers, code) {
@@ -29394,6 +29397,14 @@ ${message.content}` });
29394
29397
  }
29395
29398
  return "idx:0";
29396
29399
  };
29400
+ const pendingToolCallStillAccumulating = () => {
29401
+ for (const acc of pendingTools.values()) {
29402
+ if (!acc.ended) {
29403
+ return acc.id;
29404
+ }
29405
+ }
29406
+ return;
29407
+ };
29397
29408
  const flushPendingToolEnds = () => {
29398
29409
  for (const acc of pendingTools.values()) {
29399
29410
  if (acc.ended) {
@@ -29487,7 +29498,7 @@ ${message.content}` });
29487
29498
  const usage = asRecord4(data.usage);
29488
29499
  bodies.push({
29489
29500
  kind: "usage_update",
29490
- usage: mergeUsage2(asNumber3(usage.prompt_tokens), asNumber3(usage.completion_tokens), asNumber3(usage.total_tokens))
29501
+ usage: mergeUsage2(asNumber3(usage.prompt_tokens), asNumber3(usage.completion_tokens), asNumber3(usage.total_tokens), asNumber3(asRecord4(usage.prompt_tokens_details).cached_tokens))
29491
29502
  });
29492
29503
  }
29493
29504
  const choice0 = asRecord4(asArray(data.choices)[0]);
@@ -29600,6 +29611,7 @@ ${message.content}` });
29600
29611
  parser.push(trailing);
29601
29612
  }
29602
29613
  const torn = parser.flush();
29614
+ const pendingToolCallId = pendingToolCallStillAccumulating();
29603
29615
  if (torn.length > 0) {
29604
29616
  malformed = {
29605
29617
  kind: "malformed",
@@ -29612,8 +29624,14 @@ ${message.content}` });
29612
29624
  retryable: retryableFor2("malformed", false),
29613
29625
  message: "empty response body"
29614
29626
  };
29627
+ } else if (pendingToolCallId !== undefined) {
29628
+ malformed = {
29629
+ kind: "malformed",
29630
+ retryable: retryableFor2("malformed", false),
29631
+ message: `${this.label} SSE stream ended while a tool call was still accumulating`,
29632
+ detail: { pendingToolCallId }
29633
+ };
29615
29634
  } else {
29616
- flushPendingToolEnds();
29617
29635
  if (sawStart && sawFinish) {
29618
29636
  flushThinkTagParser();
29619
29637
  flushFieldTagStripper();
@@ -30272,6 +30290,16 @@ class GeminiProvider {
30272
30290
  break;
30273
30291
  }
30274
30292
  const chunk = asRecord5(parsed);
30293
+ const errorEnvelope = asRecord5(chunk.error);
30294
+ if (Object.keys(errorEnvelope).length > 0) {
30295
+ const classified = classifyGeminiError(asNumber4(errorEnvelope.code) ?? 0, asString4(errorEnvelope.status));
30296
+ const envelopeMessage = asString4(errorEnvelope.message);
30297
+ malformed = {
30298
+ ...classified,
30299
+ message: redact(envelopeMessage !== undefined && envelopeMessage.length > 0 ? envelopeMessage : "Gemini stream carried an in-band error envelope")
30300
+ };
30301
+ break;
30302
+ }
30275
30303
  if (!sawFirstChunk) {
30276
30304
  sawFirstChunk = true;
30277
30305
  bodies.push({ kind: "model_start" });
@@ -30295,7 +30323,7 @@ class GeminiProvider {
30295
30323
  bodies.push({ kind: "text_delta", text });
30296
30324
  }
30297
30325
  } else if (callName !== undefined) {
30298
- const callId = asString4(functionCall.id) ?? callName;
30326
+ const callId = asString4(functionCall.id) ?? `${callName}#${functionCallIndexInRound}`;
30299
30327
  const argsInput = JSON.stringify(asRecord5(functionCall.args));
30300
30328
  bodies.push({ kind: "tool_call_start", toolCallId: callId, toolName: callName });
30301
30329
  bodies.push({ kind: "tool_call_end", toolCallId: callId, input: argsInput });
@@ -30512,7 +30540,7 @@ function retryableFor4(kind, fallback) {
30512
30540
  const concrete = defaultRetryable(kind);
30513
30541
  return concrete === undefined ? fallback : concrete;
30514
30542
  }
30515
- function mergeUsage4(inputTokens, outputTokens, totalTokens) {
30543
+ function mergeUsage4(inputTokens, outputTokens, totalTokens, cacheReadTokens) {
30516
30544
  const usage = { exact: true };
30517
30545
  if (inputTokens !== undefined) {
30518
30546
  usage.inputTokens = inputTokens;
@@ -30525,6 +30553,9 @@ function mergeUsage4(inputTokens, outputTokens, totalTokens) {
30525
30553
  } else if (inputTokens !== undefined || outputTokens !== undefined) {
30526
30554
  usage.totalTokens = (inputTokens ?? 0) + (outputTokens ?? 0);
30527
30555
  }
30556
+ if (cacheReadTokens !== undefined) {
30557
+ usage.cacheReadTokens = cacheReadTokens;
30558
+ }
30528
30559
  return usage;
30529
30560
  }
30530
30561
  function extractErrorFields(data) {
@@ -30942,9 +30973,10 @@ class OpenAiProvider {
30942
30973
  const outputTokens = asNumber5(usage.output_tokens);
30943
30974
  const totalTokens = asNumber5(usage.total_tokens);
30944
30975
  const reasoningTokens = asNumber5(asRecord6(usage.output_tokens_details).reasoning_tokens);
30976
+ const cacheReadTokens = asNumber5(asRecord6(usage.input_tokens_details).cached_tokens);
30945
30977
  const usageBody = {
30946
30978
  kind: "usage_update",
30947
- usage: mergeUsage4(inputTokens, outputTokens, totalTokens)
30979
+ usage: mergeUsage4(inputTokens, outputTokens, totalTokens, cacheReadTokens)
30948
30980
  };
30949
30981
  if (reasoningTokens !== undefined) {
30950
30982
  usageBody.unknownExtensions = { "openai.reasoning_tokens": reasoningTokens };
@@ -58148,8 +58180,10 @@ async function runAgentTurn(io, deps, history, userLine, options = {}) {
58148
58180
 
58149
58181
  ${buildControlMarkerInstruction(controlNonce)}`
58150
58182
  };
58183
+ const caughtToolErrors = [];
58151
58184
  try {
58152
- const result = await runAgentTurnCore(io, turnDeps, history, userLine, options);
58185
+ const coreResult = await runAgentTurnCore(io, turnDeps, history, userLine, options, caughtToolErrors);
58186
+ const result = caughtToolErrors.length > 0 ? { ...coreResult, caughtToolErrors } : coreResult;
58153
58187
  await fireStopHookBestEffort(io, deps, result);
58154
58188
  return result;
58155
58189
  } finally {
@@ -58231,7 +58265,7 @@ function computeReasoningDurationMs(startedAt, endedAt) {
58231
58265
  const endMs = Date.parse(endedAt);
58232
58266
  return !Number.isNaN(startMs) && !Number.isNaN(endMs) && endMs >= startMs ? endMs - startMs : undefined;
58233
58267
  }
58234
- async function runAgentTurnCore(io, deps, history, userLine, options = {}) {
58268
+ async function runAgentTurnCore(io, deps, history, userLine, options = {}, caughtToolErrors = []) {
58235
58269
  const now = deps.now ?? (() => new Date().toISOString());
58236
58270
  const controlNonce = deps.controlNonce ?? generateControlNonce();
58237
58271
  const scrub = (text) => scrubControlNonce(text, controlNonce);
@@ -58821,7 +58855,7 @@ ${shown.join(`
58821
58855
  }
58822
58856
  const spawnConcurrencyCandidates = calls.filter((call) => call.name === "spawn_subagent" && reservationByCallId.get(call.id)?.ok === true);
58823
58857
  const untrustedGateBlocksSpawns = untrustedContentSeen || batchContainsUntrustedWeb;
58824
- const concurrentSpawnResults = spawnConcurrencyCandidates.length >= 2 && !untrustedGateBlocksSpawns && maxToolCalls === undefined ? await runConcurrentSpawnBatch(spawnConcurrencyCandidates, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal) : undefined;
58858
+ const concurrentSpawnResults = spawnConcurrencyCandidates.length >= 2 && !untrustedGateBlocksSpawns && maxToolCalls === undefined ? await runConcurrentSpawnBatch(spawnConcurrencyCandidates, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal, caughtToolErrors) : undefined;
58825
58859
  const answeredToolIds = new Set;
58826
58860
  let anchorsToAnnounce;
58827
58861
  let repeatedFailureHint;
@@ -58921,7 +58955,19 @@ ${shown.join(`
58921
58955
  if (toolByName.has(call.name)) {
58922
58956
  turnExecutedToolCall = true;
58923
58957
  }
58924
- const result = concurrentSpawnResults?.get(call.id) ?? await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks);
58958
+ const precomputedResult = concurrentSpawnResults?.get(call.id);
58959
+ let result;
58960
+ if (precomputedResult !== undefined) {
58961
+ result = precomputedResult;
58962
+ } else {
58963
+ try {
58964
+ result = await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks);
58965
+ } catch (err) {
58966
+ const message = err instanceof Error ? err.message : String(err);
58967
+ result = { output: `${call.name} failed: ${message}`, isError: true };
58968
+ caughtToolErrors.push({ toolName: call.name, message });
58969
+ }
58970
+ }
58925
58971
  io.onToolResult?.(call.name, result);
58926
58972
  const modelOutput = redactSensitiveText(result.output);
58927
58973
  const untrusted = result.untrusted === true;
@@ -59371,7 +59417,7 @@ function isApprovalFor(response, fingerprint) {
59371
59417
  }
59372
59418
  return response.fingerprint === undefined || response.fingerprint === fingerprint;
59373
59419
  }
59374
- async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal) {
59420
+ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvocationCapacity, reserveInvocation, signal, caughtToolErrors) {
59375
59421
  const maxConcurrency = deps.maxSubagentConcurrency ?? DEFAULT_MAX_SUBAGENT_CONCURRENCY;
59376
59422
  const perTaskRuntimeMs = NOMINAL_CONCURRENT_SPAWN_RUNTIME_MS;
59377
59423
  const tasks = spawnCalls.map((call) => ({
@@ -59398,6 +59444,7 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
59398
59444
  output: `subagent call ${call.id} failed: sequential fallback error: ${message}`,
59399
59445
  isError: true
59400
59446
  });
59447
+ caughtToolErrors.push({ toolName: call.name, message });
59401
59448
  }
59402
59449
  }
59403
59450
  return results;
@@ -59425,6 +59472,9 @@ async function runConcurrentSpawnBatch(spawnCalls, toolByName, io, deps, hasInvo
59425
59472
  const results = new Map;
59426
59473
  for (const call of spawnCalls) {
59427
59474
  const settled = partialResults?.get(call.id);
59475
+ if (settled === undefined) {
59476
+ caughtToolErrors.push({ toolName: call.name, message });
59477
+ }
59428
59478
  results.set(call.id, settled ?? {
59429
59479
  output: `subagent call ${call.id} failed: concurrent wave error: ${message}`,
59430
59480
  isError: true
@@ -127701,7 +127751,7 @@ async function dispatchLocked(projectRoot, entry, flow, dispatch, deps, runId) {
127701
127751
  });
127702
127752
  const provider = guardUsage(built, () => meter.markUsageMissing());
127703
127753
  try {
127704
- const tools = buildUnattendedRoster(worktree, unattendedRunner(worktree, sandbox));
127754
+ const tools = [...buildUnattendedRoster(worktree, unattendedRunner(worktree, sandbox)), ...deps.extraTools ?? []];
127705
127755
  const toolNames = tools.map((t) => t.definition.name);
127706
127756
  const shellHooks = buildShellHookRuntime({
127707
127757
  projectRoot: worktree,
@@ -127756,6 +127806,9 @@ async function dispatchLocked(projectRoot, entry, flow, dispatch, deps, runId) {
127756
127806
  signal: controller.signal
127757
127807
  });
127758
127808
  finishReason = result.finishReason;
127809
+ if (result.caughtToolErrors !== undefined && result.caughtToolErrors.length > 0) {
127810
+ crashed = result.caughtToolErrors.map((e) => `${e.toolName}: ${e.message}`).join("; ");
127811
+ }
127759
127812
  } catch (error) {
127760
127813
  crashed = error instanceof Error ? error.message : String(error);
127761
127814
  } finally {
@@ -153368,7 +153421,12 @@ async function runLocked(projectRoot, entry, action, deps, runId, now) {
153368
153421
  const env = deps.grantedEnv ?? process.env;
153369
153422
  const extraSecrets = await ghAuthToken(binaries.realpaths["gh"], grantedCwd, env);
153370
153423
  const granted = action.grants.tools.map((id) => grantedToolSpec(id)).filter((spec) => spec !== undefined).map((spec) => grantedTool(spec, action, grantedCwd, env, grantedCalls, binaries.realpaths, extraSecrets, binaries.stats));
153371
- const tools = [...builtinReadOnlyTools(projectRoot), shellExecTool(workdir, sandboxedRunner(workdir, sandbox)), ...granted];
153424
+ const tools = [
153425
+ ...builtinReadOnlyTools(projectRoot),
153426
+ shellExecTool(workdir, sandboxedRunner(workdir, sandbox)),
153427
+ ...granted,
153428
+ ...deps.extraTools ?? []
153429
+ ];
153372
153430
  const offending = tools.filter((t) => UNATTENDED_EXCLUDED_TOOLS.includes(t.definition.name));
153373
153431
  if (offending.length > 0)
153374
153432
  throw new Error(`agent-task roster must not offer: ${offending.map((t) => t.definition.name).join(", ")}`);
@@ -153418,6 +153476,9 @@ async function runLocked(projectRoot, entry, action, deps, runId, now) {
153418
153476
  };
153419
153477
  const result = await runAgentTurn(io, agentDeps, history, buildAgentTaskPrompt(entry, action), { signal: controller.signal });
153420
153478
  finishReason = result.finishReason;
153479
+ if (result.caughtToolErrors !== undefined && result.caughtToolErrors.length > 0) {
153480
+ crashed = result.caughtToolErrors.map((e) => `${e.toolName}: ${e.message}`).join("; ");
153481
+ }
153421
153482
  } catch (error) {
153422
153483
  crashed = error instanceof Error ? error.message : String(error);
153423
153484
  } finally {
@@ -155507,14 +155568,19 @@ function attachUsageIo(io, chrome) {
155507
155568
  }
155508
155569
  });
155509
155570
  }
155510
- function estimateTaskCostUsd(profile, inputTokens, outputTokens) {
155571
+ var OPENAI_CACHED_INPUT_DISCOUNT = 0.5;
155572
+ function estimateTaskCostUsd(profile, providerId, inputTokens, outputTokens, cacheReadTokens) {
155511
155573
  if (profile === undefined)
155512
155574
  return;
155513
155575
  const priceIn = profile.priceInputPerMillion.value;
155514
155576
  const priceOut = profile.priceOutputPerMillion.value;
155515
155577
  if (priceIn === "unknown" || priceOut === "unknown")
155516
155578
  return;
155517
- return inputTokens / 1e6 * priceIn + outputTokens / 1e6 * priceOut;
155579
+ const cachedInputDiscount = providerId === "openai" || providerId === "openai-codex" ? OPENAI_CACHED_INPUT_DISCOUNT : 1;
155580
+ const cacheRead = cacheReadTokens !== undefined && cacheReadTokens > 0 ? Math.min(cacheReadTokens, inputTokens) : 0;
155581
+ const fullPriceInputTokens = inputTokens - cacheRead;
155582
+ const inputCost = fullPriceInputTokens / 1e6 * priceIn + cacheRead / 1e6 * priceIn * cachedInputDiscount;
155583
+ return inputCost + outputTokens / 1e6 * priceOut;
155518
155584
  }
155519
155585
  async function recordTurnTaskCostBestEffort(input) {
155520
155586
  if (input.inputTokens === 0 && input.outputTokens === 0)
@@ -155522,7 +155588,7 @@ async function recordTurnTaskCostBestEffort(input) {
155522
155588
  try {
155523
155589
  const profiles = loadModelProfiles(input.userConfigDir);
155524
155590
  const profile = profiles[profileKey(input.providerId, input.modelId)];
155525
- const costUsd = estimateTaskCostUsd(profile, input.inputTokens, input.outputTokens);
155591
+ const costUsd = estimateTaskCostUsd(profile, input.providerId, input.inputTokens, input.outputTokens, input.cacheReadTokens);
155526
155592
  await appendTaskCostRecord({
155527
155593
  providerId: input.providerId,
155528
155594
  modelId: input.modelId,
@@ -160296,10 +160362,12 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
160296
160362
  };
160297
160363
  let turnInputTokens = 0;
160298
160364
  let turnOutputTokens = 0;
160365
+ let turnCacheReadTokens = 0;
160299
160366
  const prevOnUsageForCost = io.onUsage;
160300
160367
  io.onUsage = (usage) => {
160301
160368
  turnInputTokens += usage.inputTokens ?? 0;
160302
160369
  turnOutputTokens += usage.outputTokens ?? 0;
160370
+ turnCacheReadTokens += usage.cacheReadTokens ?? 0;
160303
160371
  prevOnUsageForCost?.(usage);
160304
160372
  };
160305
160373
  const suggestNextStep = async () => {
@@ -160399,6 +160467,7 @@ Usage: /reasoning <${REASONING_EFFORT_LEVELS.join("|")}>
160399
160467
  modelId: deps.modelId,
160400
160468
  inputTokens: turnInputTokens,
160401
160469
  outputTokens: turnOutputTokens,
160470
+ ...turnCacheReadTokens > 0 ? { cacheReadTokens: turnCacheReadTokens } : {},
160402
160471
  success: !turnFailed,
160403
160472
  ...turnCategory !== undefined ? { category: turnCategory } : {}
160404
160473
  });
@@ -162012,7 +162081,9 @@ async function runAgentRepl(lines, rich, deps, metaprojectPort, sessionOpts, ini
162012
162081
  return settled;
162013
162082
  }
162014
162083
  const completion = new Promise((resolve) => {
162084
+ completionWaiters = [];
162015
162085
  completionWaiters.push(() => resolve({ kind: "completion" }));
162086
+ rich.onCompletionWaitersSize?.(completionWaiters.length);
162016
162087
  });
162017
162088
  const winner = await Promise.race([pendingLine, completion]);
162018
162089
  if (winner.kind !== "completion") {
@@ -162677,6 +162748,8 @@ ${GUTTER}${turnSeparator()}
162677
162748
  continue;
162678
162749
  }
162679
162750
  await closeSlateSession(slateSession, mintTimestampAttemptId);
162751
+ lastToolOutput = undefined;
162752
+ lastToolName = undefined;
162680
162753
  if (nextLive !== undefined) {
162681
162754
  live = nextLive;
162682
162755
  history = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.17",
3
+ "version": "0.3.18",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {