@pikiloom/kernel 0.2.8 → 0.2.10

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.
@@ -25,8 +25,10 @@ export interface ClaudeUsageState {
25
25
  cacheCreation?: number | null;
26
26
  contextWindow?: number | null;
27
27
  turnOutputTokensBase?: number | null;
28
+ thinkingEstTokens?: number | null;
28
29
  }
29
30
  export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
31
+ export declare function applyClaudeThinkingEstimate(s: any, ev: any): boolean;
30
32
  export declare function claudeContextWindowFromModel(model: unknown): number | null;
31
33
  export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
32
34
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
@@ -60,7 +60,7 @@ export class ClaudeDriver {
60
60
  stopReason: null, error: null,
61
61
  input: null, output: null, cached: null,
62
62
  cacheCreation: null,
63
- contextWindow: null, turnOutputTokensBase: 0,
63
+ contextWindow: null, turnOutputTokensBase: 0, thinkingEstTokens: 0,
64
64
  subAgents: new Map(),
65
65
  tools: new Map(),
66
66
  taskList: new Map(),
@@ -288,10 +288,16 @@ export class ClaudeDriver {
288
288
  }
289
289
  }
290
290
  export function claudeUsageOf(s) {
291
- const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + (s.output ?? 0);
291
+ // While a message is still streaming, the CLI's live thinking estimate (system/thinking_tokens)
292
+ // is often the ONLY output signal — subscription accounts stream no plaintext thinking and no
293
+ // usage until the message settles. Fold it into the derived numbers (never into the raw
294
+ // outputTokens) so the row ticks during silent extended thinking; the real per-message
295
+ // output_tokens supersedes it at message_delta.
296
+ const effOutput = Math.max(s.output ?? 0, s.thinkingEstTokens ?? 0);
297
+ const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + effOutput;
292
298
  const window = s.contextWindow ?? null;
293
299
  const contextPercent = window && used > 0 ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
294
- const turnOutput = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
300
+ const turnOutput = (s.turnOutputTokensBase ?? 0) + effOutput;
295
301
  return {
296
302
  inputTokens: s.input,
297
303
  outputTokens: s.output,
@@ -301,6 +307,19 @@ export function claudeUsageOf(s) {
301
307
  turnOutputTokens: turnOutput > 0 ? turnOutput : null,
302
308
  };
303
309
  }
310
+ // Accumulate the CLI's live thinking-token estimate onto driver state. Prefer the per-event
311
+ // delta (correct whether the CLI's running total is per-message or per-turn); fall back to a
312
+ // monotonic max of the running total. Returns true when the estimate advanced.
313
+ export function applyClaudeThinkingEstimate(s, ev) {
314
+ const prev = s.thinkingEstTokens ?? 0;
315
+ const delta = Number(ev?.estimated_tokens_delta);
316
+ const total = Number(ev?.estimated_tokens);
317
+ if (Number.isFinite(delta) && delta > 0)
318
+ s.thinkingEstTokens = prev + delta;
319
+ else if (Number.isFinite(total))
320
+ s.thinkingEstTokens = Math.max(prev, total);
321
+ return (s.thinkingEstTokens ?? 0) > prev;
322
+ }
304
323
  // Advertised context window by Claude model id (best-effort; unknown -> null so the
305
324
  // percent simply stays absent rather than wrong). Anchor-free so vendor-prefixed ids
306
325
  // (us.anthropic.claude-…) still match.
@@ -359,6 +378,13 @@ export function handleClaudeEvent(ev, s, emit) {
359
378
  }
360
379
  s.model = ev.model ?? s.model;
361
380
  s.contextWindow = claudeEffectiveContextWindow(claudeContextWindowFromModel(s.model)) ?? s.contextWindow;
381
+ // Live thinking progress (system/thinking_tokens, ~every 1.4s of sustained thinking): during
382
+ // extended thinking a subscription account streams no plaintext (signature_delta only) and no
383
+ // usage until the message settles, so without projecting these the terminal shows a dead
384
+ // spinner for the whole thinking phase.
385
+ if (ev.subtype === 'thinking_tokens' && applyClaudeThinkingEstimate(s, ev)) {
386
+ emit({ type: 'usage', usage: claudeUsageOf(s) });
387
+ }
362
388
  return;
363
389
  }
364
390
  if (t === 'stream_event') {
@@ -368,11 +394,17 @@ export function handleClaudeEvent(ev, s, emit) {
368
394
  // Claude emits one message per tool-use round within a single turn. Carry the prior
369
395
  // message's output into the per-turn base, then reset to the new message's prompt size
370
396
  // so contextUsedTokens tracks current occupancy while turnOutputTokens sums the turn.
371
- s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
397
+ // A message that never delivered real output_tokens keeps its thinking estimate as the carry.
398
+ s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + Math.max(s.output ?? 0, s.thinkingEstTokens ?? 0);
372
399
  s.input = u?.input_tokens ?? 0;
373
400
  s.cached = u?.cache_read_input_tokens ?? 0;
374
401
  s.cacheCreation = u?.cache_creation_input_tokens ?? 0;
375
402
  s.output = 0;
403
+ s.thinkingEstTokens = 0;
404
+ // The prompt-side counts are known the moment the model accepts the request — emit them so
405
+ // the context row appears right away instead of only after the first message settles
406
+ // (minutes into a long silent thinking phase, the "looks stuck" report).
407
+ emit({ type: 'usage', usage: claudeUsageOf(s) });
376
408
  }
377
409
  else if (inner.type === 'content_block_start') {
378
410
  // Claude emits multiple text/thinking blocks per turn (one set per tool-use round). Insert a
@@ -407,8 +439,11 @@ export function handleClaudeEvent(ev, s, emit) {
407
439
  else if (inner.type === 'message_delta') {
408
440
  const u = inner.usage;
409
441
  if (u) {
410
- if (u.output_tokens != null)
442
+ // Real reported output supersedes the live thinking estimate for this message.
443
+ if (u.output_tokens != null) {
411
444
  s.output = u.output_tokens;
445
+ s.thinkingEstTokens = 0;
446
+ }
412
447
  if (u.input_tokens != null)
413
448
  s.input = u.input_tokens;
414
449
  if (u.cache_read_input_tokens != null)
@@ -515,8 +550,13 @@ export function handleClaudeEvent(ev, s, emit) {
515
550
  for (const b of contents) {
516
551
  if (b?.type !== 'tool_result')
517
552
  continue;
518
- emitClaudeImages(b.content || [], s, emit);
519
553
  const id = String(b.tool_use_id || '').trim();
554
+ // A Read result is the agent inspecting a file, not a deliverable — surfacing those spammed
555
+ // the chat with every image the agent looked at (the legacy driver has the same exclusion).
556
+ // Images from generating tools (MCP image-gen, screenshots, …) still surface as photos.
557
+ const tool = id ? s.tools?.get(id) : undefined;
558
+ if (tool?.name !== 'Read')
559
+ emitClaudeImages(b.content || [], s, emit);
520
560
  // TaskCreate result: the assigned task id arrives here; register the task and emit the plan.
521
561
  if (id && s.pendingTaskCreates?.has(id)) {
522
562
  const pending = s.pendingTaskCreates.get(id);
@@ -534,7 +574,6 @@ export function handleClaudeEvent(ev, s, emit) {
534
574
  }
535
575
  continue;
536
576
  }
537
- const tool = id ? s.tools?.get(id) : undefined;
538
577
  if (!tool)
539
578
  continue;
540
579
  const isError = !!b.is_error;
@@ -137,6 +137,9 @@ function codexFileChangeSummary(item) {
137
137
  // fallback to `item.type` wrongly turned every item (incl. agentMessage/reasoning) into a bogus
138
138
  // "tool" in the Activity card. Mirrors the legacy driver's isCodexToolCallItem whitelist.
139
139
  const CODEX_TOOL_CALL_TYPES = new Set(['dynamicToolCall', 'mcpToolCall', 'collabAgentToolCall']);
140
+ // Field-less placeholder summaries — a completed item with only one of these never overrides
141
+ // a more specific summary cached at item/started.
142
+ const CODEX_GENERIC_TOOL_SUMMARIES = new Set(['Search web', 'Run shell command', 'Edit files', 'Use tool']);
140
143
  export function codexToolSummary(item) {
141
144
  const id = String(item?.id || '');
142
145
  if (!id)
@@ -147,6 +150,20 @@ export function codexToolSummary(item) {
147
150
  }
148
151
  if (item.type === 'fileChange' || item.type === 'patch')
149
152
  return { id, name: 'edit', summary: codexFileChangeSummary(item) };
153
+ if (item.type === 'webSearch') {
154
+ // v2 webSearch items carry the query at top level and (on newer servers) an action
155
+ // (search / openPage / findInPage, snake_case on older ones). The query is often only
156
+ // known at item/completed — item/started may arrive query-less.
157
+ const action = item.action || {};
158
+ const kind = String(action.type || '');
159
+ const url = codexCommandPreview(action.url);
160
+ if ((kind === 'openPage' || kind === 'open_page') && url)
161
+ return { id, name: 'web_search', summary: `Open ${url}` };
162
+ if ((kind === 'findInPage' || kind === 'find_in_page') && url)
163
+ return { id, name: 'web_search', summary: `Find in ${url}` };
164
+ const query = codexCommandPreview(typeof item.query === 'string' && item.query.trim() ? item.query : action.query);
165
+ return { id, name: 'web_search', summary: query ? `Search web: ${query}` : 'Search web' };
166
+ }
150
167
  if (CODEX_TOOL_CALL_TYPES.has(item.type)) {
151
168
  const raw = typeof item.tool === 'string' && item.tool.trim() ? item.tool.trim()
152
169
  : typeof item.name === 'string' && item.name.trim() ? item.name.trim() : '';
@@ -343,8 +360,14 @@ export class CodexDriver {
343
360
  else if (item.type === 'reasoning')
344
361
  captureCodexReasoning(codexReasoningItemText(item), state, ctx.emit);
345
362
  const t = codexToolSummary(item);
346
- if (t && toolSummaries.has(t.id))
347
- ctx.emit({ type: 'tool', call: { id: t.id, name: t.name, summary: toolSummaries.get(t.id) || t.summary, status: item.status === 'failed' ? 'failed' : 'done' } });
363
+ if (t) {
364
+ // Prefer the completed item's summary when it is specific webSearch carries its
365
+ // query only at completion (item/started may be query-less or absent entirely,
366
+ // so no `toolSummaries.has` gate: a completed-only tool still gets its row).
367
+ const summary = !CODEX_GENERIC_TOOL_SUMMARIES.has(t.summary) ? t.summary : (toolSummaries.get(t.id) || t.summary);
368
+ toolSummaries.set(t.id, summary);
369
+ ctx.emit({ type: 'tool', call: { id: t.id, name: t.name, summary, status: item.status === 'failed' ? 'failed' : 'done' } });
370
+ }
348
371
  break;
349
372
  }
350
373
  case 'rawResponseItem/completed': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Heterogeneous coding agents (Claude / Codex / Gemini / ACP) -> an interaction-friendly, accumulating session snapshot + control handle, over pluggable surfaces (IM, Web, tunnel, TUI). The reusable core pikiloom itself is built on.",
5
5
  "type": "module",
6
6
  "license": "MIT",