@pikiloom/kernel 0.1.3 → 0.1.4

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.
@@ -1,5 +1,5 @@
1
1
  import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, DriverEvent, TuiInput, TuiSpec } from '../contracts/driver.js';
2
- import type { UniversalPlan } from '../protocol/index.js';
2
+ import type { UniversalUsage, UniversalPlan } from '../protocol/index.js';
3
3
  export declare class ClaudeDriver implements AgentDriver {
4
4
  private readonly bin;
5
5
  readonly id = "claude";
@@ -14,6 +14,17 @@ export declare class ClaudeDriver implements AgentDriver {
14
14
  run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
15
15
  private usage;
16
16
  }
17
+ export interface ClaudeUsageState {
18
+ input: number | null;
19
+ output: number | null;
20
+ cached: number | null;
21
+ cacheCreation?: number | null;
22
+ contextWindow?: number | null;
23
+ turnOutputTokensBase?: number | null;
24
+ }
25
+ export declare function claudeUsageOf(s: ClaudeUsageState): UniversalUsage;
26
+ export declare function claudeContextWindowFromModel(model: unknown): number | null;
27
+ export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
17
28
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
18
29
  export declare function claudeUserMessage(text: string): string;
19
30
  export declare function emitClaudeImages(blocks: any[], s: any, emit: (e: DriverEvent) => void): void;
@@ -46,6 +46,8 @@ export class ClaudeDriver {
46
46
  sessionId: null, model: null,
47
47
  stopReason: null, error: null,
48
48
  input: null, output: null, cached: null,
49
+ cacheCreation: null,
50
+ contextWindow: null, turnOutputTokensBase: 0,
49
51
  subAgents: new Map(),
50
52
  tools: new Map(),
51
53
  };
@@ -141,9 +143,45 @@ export class ClaudeDriver {
141
143
  });
142
144
  }
143
145
  usage(s) {
144
- return { inputTokens: s.input, outputTokens: s.output, cachedInputTokens: s.cached, contextPercent: null };
146
+ return claudeUsageOf(s);
145
147
  }
146
148
  }
149
+ export function claudeUsageOf(s) {
150
+ const used = (s.input ?? 0) + (s.cached ?? 0) + (s.cacheCreation ?? 0) + (s.output ?? 0);
151
+ const window = s.contextWindow ?? null;
152
+ const contextPercent = window && used > 0 ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
153
+ const turnOutput = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
154
+ return {
155
+ inputTokens: s.input,
156
+ outputTokens: s.output,
157
+ cachedInputTokens: s.cached,
158
+ contextUsedTokens: used > 0 ? used : null,
159
+ contextPercent,
160
+ turnOutputTokens: turnOutput > 0 ? turnOutput : null,
161
+ };
162
+ }
163
+ // Advertised context window by Claude model id (best-effort; unknown -> null so the
164
+ // percent simply stays absent rather than wrong). Anchor-free so vendor-prefixed ids
165
+ // (us.anthropic.claude-…) still match.
166
+ export function claudeContextWindowFromModel(model) {
167
+ const id = String(model ?? '').trim().toLowerCase();
168
+ if (!id)
169
+ return null;
170
+ if (id === 'haiku' || /claude-haiku-/.test(id))
171
+ return 200_000;
172
+ if (id === 'opus' || id === 'sonnet' || id === 'fable')
173
+ return 1_000_000;
174
+ if (/claude-(opus|sonnet)-/.test(id) || /claude-fable-/.test(id))
175
+ return 1_000_000;
176
+ return null;
177
+ }
178
+ // Usable window = advertised minus Claude's max-output (20k) + autocompact (13k) reserve.
179
+ const CLAUDE_USABLE_WINDOW_RESERVE = 33_000;
180
+ export function claudeEffectiveContextWindow(advertised) {
181
+ if (advertised == null)
182
+ return null;
183
+ return advertised <= CLAUDE_USABLE_WINDOW_RESERVE ? advertised : advertised - CLAUDE_USABLE_WINDOW_RESERVE;
184
+ }
147
185
  // Parse one claude stream-json event into kernel DriverEvents (pure + exported for
148
186
  // hermetic testing). Faithful to pikiloom's claudeParse shapes.
149
187
  export function handleClaudeEvent(ev, s, emit) {
@@ -178,16 +216,21 @@ export function handleClaudeEvent(ev, s, emit) {
178
216
  emit({ type: 'session', sessionId: ev.session_id });
179
217
  }
180
218
  s.model = ev.model ?? s.model;
219
+ s.contextWindow = claudeEffectiveContextWindow(claudeContextWindowFromModel(s.model)) ?? s.contextWindow;
181
220
  return;
182
221
  }
183
222
  if (t === 'stream_event') {
184
223
  const inner = ev.event || {};
185
224
  if (inner.type === 'message_start') {
186
225
  const u = inner.message?.usage;
187
- if (u) {
188
- s.input = u.input_tokens ?? s.input;
189
- s.cached = u.cache_read_input_tokens ?? s.cached;
190
- }
226
+ // Claude emits one message per tool-use round within a single turn. Carry the prior
227
+ // message's output into the per-turn base, then reset to the new message's prompt size
228
+ // so contextUsedTokens tracks current occupancy while turnOutputTokens sums the turn.
229
+ s.turnOutputTokensBase = (s.turnOutputTokensBase ?? 0) + (s.output ?? 0);
230
+ s.input = u?.input_tokens ?? 0;
231
+ s.cached = u?.cache_read_input_tokens ?? 0;
232
+ s.cacheCreation = u?.cache_creation_input_tokens ?? 0;
233
+ s.output = 0;
191
234
  }
192
235
  else if (inner.type === 'content_block_delta') {
193
236
  const d = inner.delta || {};
@@ -211,7 +254,9 @@ export function handleClaudeEvent(ev, s, emit) {
211
254
  s.input = u.input_tokens;
212
255
  if (u.cache_read_input_tokens != null)
213
256
  s.cached = u.cache_read_input_tokens;
214
- emit({ type: 'usage', usage: { inputTokens: s.input, outputTokens: s.output, cachedInputTokens: s.cached, contextPercent: null } });
257
+ if (u.cache_creation_input_tokens != null)
258
+ s.cacheCreation = u.cache_creation_input_tokens;
259
+ emit({ type: 'usage', usage: claudeUsageOf(s) });
215
260
  }
216
261
  if (inner.delta?.stop_reason)
217
262
  s.stopReason = inner.delta.stop_reason;
@@ -312,6 +357,8 @@ export function handleClaudeEvent(ev, s, emit) {
312
357
  const cached = u.cache_read_input_tokens ?? u.cached_input_tokens;
313
358
  if (s.cached == null && cached != null)
314
359
  s.cached = cached;
360
+ if (s.cacheCreation == null && u.cache_creation_input_tokens != null)
361
+ s.cacheCreation = u.cache_creation_input_tokens;
315
362
  }
316
363
  return;
317
364
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, TuiInput, TuiSpec } from '../contracts/driver.js';
2
+ import type { UniversalUsage } from '../protocol/index.js';
2
3
  export declare class CodexDriver implements AgentDriver {
3
4
  private readonly bin;
4
5
  readonly id = "codex";
@@ -12,3 +13,12 @@ export declare class CodexDriver implements AgentDriver {
12
13
  run(input: AgentTurnInput, ctx: DriverContext): Promise<DriverResult>;
13
14
  tui(input: TuiInput): TuiSpec;
14
15
  }
16
+ export interface CodexUsageState {
17
+ input: number | null;
18
+ output: number | null;
19
+ cached: number | null;
20
+ contextUsed?: number | null;
21
+ contextWindow?: number | null;
22
+ }
23
+ export declare function applyCodexTokenUsage(s: CodexUsageState, rawUsage: any): void;
24
+ export declare function codexUsageOf(s: CodexUsageState): UniversalUsage;
@@ -173,7 +173,7 @@ export class CodexDriver {
173
173
  // to the native account.
174
174
  const config = [...(input.configOverrides || [])];
175
175
  const srv = new AppServer(this.bin, config, input.env);
176
- const state = { text: '', reasoning: '', sessionId: input.sessionId ?? null, input: null, output: null, cached: null, status: null, error: null, turnId: null };
176
+ const state = { text: '', reasoning: '', sessionId: input.sessionId ?? null, input: null, output: null, cached: null, contextUsed: null, contextWindow: null, status: null, error: null, turnId: null };
177
177
  const phases = new Map();
178
178
  const toolSummaries = new Map();
179
179
  let steerRegistered = false;
@@ -264,14 +264,8 @@ export class CodexDriver {
264
264
  break;
265
265
  }
266
266
  case 'thread/tokenUsage/updated': {
267
- const u = params?.tokenUsage || params?.usage || {};
268
- if (u.input_tokens != null || u.inputTokens != null)
269
- state.input = u.input_tokens ?? u.inputTokens;
270
- if (u.output_tokens != null || u.outputTokens != null)
271
- state.output = u.output_tokens ?? u.outputTokens;
272
- if (u.cached_input_tokens != null || u.cachedInputTokens != null)
273
- state.cached = u.cached_input_tokens ?? u.cachedInputTokens;
274
- ctx.emit({ type: 'usage', usage: { inputTokens: state.input, outputTokens: state.output, cachedInputTokens: state.cached, contextPercent: null } });
267
+ applyCodexTokenUsage(state, params?.tokenUsage || params?.usage);
268
+ ctx.emit({ type: 'usage', usage: codexUsageOf(state) });
275
269
  break;
276
270
  }
277
271
  case 'turn/completed': {
@@ -279,6 +273,8 @@ export class CodexDriver {
279
273
  state.status = turn.status ?? 'completed';
280
274
  if (turn.error)
281
275
  state.error = turn.error.message || turn.error.code || 'turn error';
276
+ applyCodexTokenUsage(state, params?.tokenUsage || turn.tokenUsage || turn.usage);
277
+ ctx.emit({ type: 'usage', usage: codexUsageOf(state) });
282
278
  settle();
283
279
  break;
284
280
  }
@@ -309,7 +305,7 @@ export class CodexDriver {
309
305
  if (turnResp.error)
310
306
  return { ok: false, text: state.text, error: turnResp.error.message || 'turn/start failed', stopReason: 'error', sessionId: state.sessionId };
311
307
  await turnDone;
312
- const usage = { inputTokens: state.input, outputTokens: state.output, cachedInputTokens: state.cached, contextPercent: null };
308
+ const usage = codexUsageOf(state);
313
309
  const ok2 = (state.status === 'completed' || state.status == null) && !state.error && !ctx.signal.aborted;
314
310
  return {
315
311
  ok: ok2,
@@ -344,3 +340,76 @@ function buildTurnInput(prompt, attachments) {
344
340
  input.push({ type: 'text', text: prompt });
345
341
  return input;
346
342
  }
343
+ function codexNum(...vals) {
344
+ for (const v of vals) {
345
+ const n = typeof v === 'number' ? v : Number(v);
346
+ if (Number.isFinite(n))
347
+ return n;
348
+ }
349
+ return null;
350
+ }
351
+ // Context occupancy from one usage record: prefer total_tokens, else input(+output).
352
+ function codexContextUsed(raw) {
353
+ if (!raw || typeof raw !== 'object')
354
+ return null;
355
+ const total = codexNum(raw.total_tokens, raw.totalTokens);
356
+ if (total != null && total >= 0)
357
+ return total;
358
+ const i = codexNum(raw.input_tokens, raw.inputTokens);
359
+ const o = codexNum(raw.output_tokens, raw.outputTokens);
360
+ if (i != null && o != null)
361
+ return i + o;
362
+ return i;
363
+ }
364
+ // Fold a codex tokenUsage payload into driver state: last-turn counts, context
365
+ // occupancy, and the model window. Tolerant of nested {info:{…}} and flat shapes.
366
+ export function applyCodexTokenUsage(s, rawUsage) {
367
+ if (!rawUsage || typeof rawUsage !== 'object')
368
+ return;
369
+ const info = rawUsage.info && typeof rawUsage.info === 'object' ? rawUsage.info : rawUsage;
370
+ const last = info.last ?? info.lastTokenUsage ?? info.last_token_usage ?? rawUsage.last;
371
+ const li = codexNum(last?.input_tokens, last?.inputTokens);
372
+ const lo = codexNum(last?.output_tokens, last?.outputTokens);
373
+ const lc = codexNum(last?.cached_input_tokens, last?.cachedInputTokens);
374
+ if (li != null)
375
+ s.input = li;
376
+ if (lo != null)
377
+ s.output = lo;
378
+ if (lc != null)
379
+ s.cached = lc;
380
+ const used = codexContextUsed(last);
381
+ if (used != null)
382
+ s.contextUsed = used;
383
+ // Cumulative total as fallback for the per-turn counts (the trailing `?? rawUsage`
384
+ // also catches the flat shape, where there is no nested `last`).
385
+ const total = info.total ?? info.totalTokenUsage ?? info.total_token_usage ?? rawUsage.total ?? rawUsage;
386
+ if (total && typeof total === 'object') {
387
+ const ti = codexNum(total.input_tokens, total.inputTokens);
388
+ const to = codexNum(total.output_tokens, total.outputTokens);
389
+ const tc = codexNum(total.cached_input_tokens, total.cachedInputTokens);
390
+ if (li == null && ti != null)
391
+ s.input = ti;
392
+ if (lo == null && to != null)
393
+ s.output = to;
394
+ if (lc == null && tc != null)
395
+ s.cached = tc;
396
+ }
397
+ const cw = codexNum(info.modelContextWindow, info.model_context_window, rawUsage.modelContextWindow, rawUsage.model_context_window);
398
+ if (cw != null && cw > 0)
399
+ s.contextWindow = cw;
400
+ }
401
+ export function codexUsageOf(s) {
402
+ const fallback = (s.input ?? 0) + (s.cached ?? 0);
403
+ const used = s.contextUsed ?? (fallback > 0 ? fallback : null);
404
+ const window = s.contextWindow ?? null;
405
+ const contextPercent = used != null && window ? Math.min(99.9, Math.round((used / window) * 1000) / 10) : null;
406
+ const turnOutput = s.output ?? 0;
407
+ return {
408
+ inputTokens: s.input,
409
+ outputTokens: s.output,
410
+ cachedInputTokens: s.cached,
411
+ contextUsedTokens: used,
412
+ contextPercent,
413
+ turnOutputTokens: turnOutput > 0 ? turnOutput : null,
414
+ };
415
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
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",