@pikiloom/kernel 0.1.4 → 0.1.5

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.
@@ -232,6 +232,23 @@ export function handleClaudeEvent(ev, s, emit) {
232
232
  s.cacheCreation = u?.cache_creation_input_tokens ?? 0;
233
233
  s.output = 0;
234
234
  }
235
+ else if (inner.type === 'content_block_start') {
236
+ // Claude emits multiple text/thinking blocks per turn (one set per tool-use round). Insert a
237
+ // paragraph break before a NEW block when prior content exists, so the live preview shows
238
+ // breaks between segments instead of running them together. Mirrors the legacy driver; the
239
+ // separator is emitted as a delta so the runtime's accumulated snapshot stays in sync with s.
240
+ const bt = inner.content_block?.type;
241
+ if (bt === 'text' && s.text && !s.text.endsWith('\n\n')) {
242
+ const sep = s.text.endsWith('\n') ? '\n' : '\n\n';
243
+ s.text += sep;
244
+ emit({ type: 'text', delta: sep });
245
+ }
246
+ else if (bt === 'thinking' && s.reasoning && !s.reasoning.endsWith('\n\n')) {
247
+ const sep = s.reasoning.endsWith('\n') ? '\n' : '\n\n';
248
+ s.reasoning += sep;
249
+ emit({ type: 'reasoning', delta: sep });
250
+ }
251
+ }
235
252
  else if (inner.type === 'content_block_delta') {
236
253
  const d = inner.delta || {};
237
254
  if (d.type === 'text_delta' && d.text) {
@@ -1,5 +1,22 @@
1
- import type { AgentDriver, AgentTurnInput, DriverContext, DriverResult, TuiInput, TuiSpec } from '../contracts/driver.js';
1
+ import type { AgentDriver, AgentTurnInput, DriverContext, DriverEvent, DriverResult, TuiInput, TuiSpec } from '../contracts/driver.js';
2
2
  import type { UniversalUsage } from '../protocol/index.js';
3
+ export declare function codexToolSummary(item: any): {
4
+ id: string;
5
+ name: string;
6
+ summary: string;
7
+ } | null;
8
+ export interface CodexContentState {
9
+ text: string;
10
+ reasoning: string;
11
+ streamedReasoning: boolean;
12
+ msgs: string[];
13
+ thinkParts: string[];
14
+ }
15
+ export declare function codexReasoningItemText(item: any): string;
16
+ export declare function captureCodexAgentMessage(item: any, s: CodexContentState, deltaItems: Set<string>, phases: Map<string, string>, emit: (e: DriverEvent) => void): void;
17
+ export declare function captureCodexReasoning(text: string, s: CodexContentState, emit: (e: DriverEvent) => void): void;
18
+ export declare function codexFinalText(s: CodexContentState): string;
19
+ export declare function codexFinalReasoning(s: CodexContentState): string;
3
20
  export declare class CodexDriver implements AgentDriver {
4
21
  private readonly bin;
5
22
  readonly id = "codex";
@@ -20,7 +20,12 @@ class AppServer {
20
20
  onServerRequest(cb) { this.requestResponder = cb; }
21
21
  async start() {
22
22
  const args = ['app-server'];
23
- const overrides = this.configOverrides.some(c => /^features\.goals\s*=/.test(c)) ? this.configOverrides : [...this.configOverrides, 'features.goals=true'];
23
+ // Do NOT force model_reasoning_summary codex reasoning summaries stay OFF by default
24
+ // (respecting ~/.codex/config.toml, which is the original behavior). A caller that wants
25
+ // thinking can pass `model_reasoning_summary=...` via configOverrides; we never inject it.
26
+ const overrides = [...this.configOverrides];
27
+ if (!overrides.some(c => /^features\.goals\s*=/.test(c)))
28
+ overrides.push('features.goals=true');
24
29
  for (const c of overrides)
25
30
  args.push('-c', c);
26
31
  try {
@@ -126,7 +131,12 @@ function codexFileChangeSummary(item) {
126
131
  }
127
132
  // Normalize a codex app-server item -> {id, name, summary}. The summary mirrors pikiloom's
128
133
  // vocabulary ("Run shell: <cmd>", "Edit <path>") so the activity projection reads naturally.
129
- function codexToolSummary(item) {
134
+ // Only ACTUAL tool calls become Activity rows. agentMessage (the answer text) and reasoning
135
+ // (thinking) are CONTENT — they render below as message text / thinking, never as tools. The old
136
+ // fallback to `item.type` wrongly turned every item (incl. agentMessage/reasoning) into a bogus
137
+ // "tool" in the Activity card. Mirrors the legacy driver's isCodexToolCallItem whitelist.
138
+ const CODEX_TOOL_CALL_TYPES = new Set(['dynamicToolCall', 'mcpToolCall', 'collabAgentToolCall']);
139
+ export function codexToolSummary(item) {
130
140
  const id = String(item?.id || '');
131
141
  if (!id)
132
142
  return null;
@@ -136,8 +146,13 @@ function codexToolSummary(item) {
136
146
  }
137
147
  if (item.type === 'fileChange' || item.type === 'patch')
138
148
  return { id, name: 'edit', summary: codexFileChangeSummary(item) };
139
- const tool = typeof item?.tool === 'string' ? item.tool : typeof item?.name === 'string' ? item.name : item?.type;
140
- return tool ? { id, name: String(tool), summary: String(tool) } : null;
149
+ if (CODEX_TOOL_CALL_TYPES.has(item.type)) {
150
+ const raw = typeof item.tool === 'string' && item.tool.trim() ? item.tool.trim()
151
+ : typeof item.name === 'string' && item.name.trim() ? item.name.trim() : '';
152
+ const name = raw ? (raw.split('.').pop() || raw) : 'tool';
153
+ return { id, name, summary: raw ? `Use ${name}` : 'Use tool' };
154
+ }
155
+ return null;
141
156
  }
142
157
  // codex `item/tool/requestUserInput` params -> a normalized UniversalInteraction
143
158
  // (faithful to the legacy codex driver's toAgentInteraction shape).
@@ -160,6 +175,51 @@ function codexUserInputToInteraction(params, promptId) {
160
175
  return null;
161
176
  return { promptId, kind: 'user-input', title: 'User Input Required', hint: 'Use the buttons when available, or reply with text.', questions };
162
177
  }
178
+ // Reasoning text from a completed `reasoning` item (item/completed or rawResponseItem/completed):
179
+ // summary/content arrays of plain strings or {text} objects.
180
+ export function codexReasoningItemText(item) {
181
+ const parts = [
182
+ ...(Array.isArray(item?.summary) ? item.summary : []),
183
+ ...(Array.isArray(item?.content) ? item.content : []),
184
+ ];
185
+ return parts.map((p) => (typeof p === 'string' ? p : p?.text || '')).filter(Boolean).join('\n').trim();
186
+ }
187
+ // A completed final_answer agentMessage that did NOT stream deltas: append + emit it live.
188
+ // deltaItems holds the ids already streamed, so a completed item echoing a streamed one is
189
+ // not double-counted (matches the legacy driver's deltaSeenForItem guard).
190
+ export function captureCodexAgentMessage(item, s, deltaItems, phases, emit) {
191
+ const phase = item?.phase || (item?.id ? phases.get(item.id) : null) || 'final_answer';
192
+ if (phase !== 'final_answer')
193
+ return;
194
+ const text = typeof item?.text === 'string' ? item.text.trim() : '';
195
+ if (!text)
196
+ return;
197
+ s.msgs.push(text);
198
+ if (item.id && deltaItems.has(item.id))
199
+ return;
200
+ const delta = s.text.trim() ? `\n\n${text}` : text;
201
+ s.text += delta;
202
+ emit({ type: 'text', delta });
203
+ }
204
+ // A completed reasoning item: keep it for the end-of-turn fallback, and stream it live only
205
+ // when nothing arrived as reasoning deltas (so the streamed path is never double-emitted).
206
+ export function captureCodexReasoning(text, s, emit) {
207
+ if (!text)
208
+ return;
209
+ s.thinkParts.push(text);
210
+ if (s.streamedReasoning)
211
+ return;
212
+ const delta = s.reasoning.trim() ? `\n\n${text}` : text;
213
+ s.reasoning += delta;
214
+ emit({ type: 'reasoning', delta });
215
+ }
216
+ // End-of-turn finalizers: prefer streamed text/reasoning, fall back to completed-item parts.
217
+ export function codexFinalText(s) {
218
+ return s.text.trim() ? s.text : s.msgs.join('\n\n');
219
+ }
220
+ export function codexFinalReasoning(s) {
221
+ return s.reasoning.trim() ? s.reasoning : s.thinkParts.join('\n\n');
222
+ }
163
223
  export class CodexDriver {
164
224
  bin;
165
225
  id = 'codex';
@@ -173,9 +233,10 @@ export class CodexDriver {
173
233
  // to the native account.
174
234
  const config = [...(input.configOverrides || [])];
175
235
  const srv = new AppServer(this.bin, config, input.env);
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 };
236
+ const state = { text: '', reasoning: '', streamedReasoning: false, msgs: [], thinkParts: [], sessionId: input.sessionId ?? null, input: null, output: null, cached: null, contextUsed: null, contextWindow: null, status: null, error: null, turnId: null };
177
237
  const phases = new Map();
178
238
  const toolSummaries = new Map();
239
+ const deltaItems = new Set();
179
240
  let steerRegistered = false;
180
241
  const ok = await srv.start();
181
242
  if (!ok)
@@ -239,6 +300,8 @@ export class CodexDriver {
239
300
  const phase = params?.itemId ? (phases.get(params.itemId) || 'final_answer') : 'final_answer';
240
301
  if (phase === 'final_answer' && params?.delta) {
241
302
  state.text += params.delta;
303
+ if (params.itemId)
304
+ deltaItems.add(params.itemId);
242
305
  ctx.emit({ type: 'text', delta: params.delta });
243
306
  }
244
307
  break;
@@ -247,16 +310,28 @@ export class CodexDriver {
247
310
  case 'item/reasoning/summaryTextDelta':
248
311
  if (params?.delta) {
249
312
  state.reasoning += params.delta;
313
+ state.streamedReasoning = true;
250
314
  ctx.emit({ type: 'reasoning', delta: params.delta });
251
315
  }
252
316
  break;
253
317
  case 'item/completed': {
254
318
  const item = params?.item || {};
319
+ // Final answer / reasoning delivered as a completed item (no preceding deltas).
320
+ if (item.type === 'agentMessage')
321
+ captureCodexAgentMessage(item, state, deltaItems, phases, ctx.emit);
322
+ else if (item.type === 'reasoning')
323
+ captureCodexReasoning(codexReasoningItemText(item), state, ctx.emit);
255
324
  const t = codexToolSummary(item);
256
325
  if (t && toolSummaries.has(t.id))
257
326
  ctx.emit({ type: 'tool', call: { id: t.id, name: t.name, summary: toolSummaries.get(t.id) || t.summary, status: item.status === 'failed' ? 'failed' : 'done' } });
258
327
  break;
259
328
  }
329
+ case 'rawResponseItem/completed': {
330
+ const item = params?.item || {};
331
+ if (item?.type === 'reasoning')
332
+ captureCodexReasoning(codexReasoningItemText(item), state, ctx.emit);
333
+ break;
334
+ }
260
335
  case 'turn/plan/updated': {
261
336
  const plan = planFromUpdate(params);
262
337
  if (plan)
@@ -307,10 +382,11 @@ export class CodexDriver {
307
382
  await turnDone;
308
383
  const usage = codexUsageOf(state);
309
384
  const ok2 = (state.status === 'completed' || state.status == null) && !state.error && !ctx.signal.aborted;
385
+ const finalReasoning = codexFinalReasoning(state);
310
386
  return {
311
387
  ok: ok2,
312
- text: state.text,
313
- reasoning: state.reasoning || undefined,
388
+ text: codexFinalText(state),
389
+ reasoning: finalReasoning || undefined,
314
390
  error: state.error || (ctx.signal.aborted ? 'Interrupted by user.' : null),
315
391
  stopReason: ctx.signal.aborted ? 'interrupted' : (state.status || 'end_turn'),
316
392
  sessionId: state.sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikiloom/kernel",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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",