kfc-code-cli 0.0.1-alpha.14 → 0.0.1-alpha.16

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/main.mjs +1334 -228
  2. package/package.json +1 -3
package/dist/main.mjs CHANGED
@@ -2312,6 +2312,9 @@ function createChatStreamingCallbacks(deps) {
2312
2312
  });
2313
2313
  },
2314
2314
  onPrefetchedResult: (toolCallId, result) => {
2315
+ prefetchedResults.set(toolCallId, Promise.resolve(result));
2316
+ },
2317
+ onPrefetchedResultPromise: (toolCallId, result) => {
2315
2318
  prefetchedResults.set(toolCallId, result);
2316
2319
  },
2317
2320
  onAtomicPart: async (atomic) => {
@@ -2355,25 +2358,29 @@ function createChatStreamingCallbacks(deps) {
2355
2358
  * Pass 1 runs inside the step envelope and classifies every LLM-emitted
2356
2359
  * tool call. Fallback branches write a durable tool_call + synthetic
2357
2360
  * tool_result before step_end. Pass 2 runs after step_end and executes
2358
- * allowed tools. SoulEvents for tool.result are deferred and flushed in
2359
- * provider order so UI consumers always see a balanced tool.call
2360
- * tool.result stream even though WAL writes are split across the step
2361
- * boundary.
2361
+ * allowed tools. Consecutive `isConcurrencySafe` tools execute as a
2362
+ * parallel batch; live tool.result events fire as soon as each sanitized
2363
+ * result is available, while durable context writes remain in provider
2364
+ * tool-call order for the next LLM request.
2362
2365
  */
2363
2366
  const GRACE_TIMEOUT_MS = 2e3;
2364
- function flushDeferredToolResults(response, deferred, emit) {
2365
- for (const toolCall of response.toolCalls) {
2366
- const result = deferred.get(toolCall.id);
2367
- if (result !== void 0) {
2368
- emitToolResultEvent(emit, toolCall.id, result.output, result.isError);
2369
- deferred.delete(toolCall.id);
2370
- }
2371
- }
2367
+ function streamingPreparationKeepsPrefixOpen(preparation) {
2368
+ if (preparation.kind === "handled") return true;
2369
+ return preparation.kind === "pending" && preparation.pending.preparedResult !== void 0;
2372
2370
  }
2373
- async function classifyToolCalls(step, response, deferred) {
2371
+ async function classifyToolCalls(step, response) {
2374
2372
  const { config, context, emit, signal, turnId, currentStep, stepUuid, toolCallByProviderId } = step;
2375
2373
  const pending = [];
2376
2374
  for (const toolCall of response.toolCalls) {
2375
+ const streamed = step.streamingPreparations.get(toolCall.id);
2376
+ if (streamed !== void 0) {
2377
+ const prepared = await streamed;
2378
+ if (prepared.kind === "pending") {
2379
+ pending.push(prepared.pending);
2380
+ continue;
2381
+ }
2382
+ if (prepared.kind === "handled") continue;
2383
+ }
2377
2384
  emit({
2378
2385
  type: "tool.call",
2379
2386
  toolCallId: toolCall.id,
@@ -2384,29 +2391,20 @@ async function classifyToolCalls(step, response, deferred) {
2384
2391
  if (tool === void 0) {
2385
2392
  const output = `Tool "${toolCall.name}" not found`;
2386
2393
  await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2387
- deferred.set(toolCall.id, {
2388
- output,
2389
- isError: true
2390
- });
2394
+ emitToolResultEvent(emit, toolCall.id, output, true);
2391
2395
  continue;
2392
2396
  }
2393
2397
  if (toolCall.error !== void 0) {
2394
2398
  const output = `Invalid input for tool "${toolCall.name}": ${toolCall.error}`;
2395
2399
  await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2396
- deferred.set(toolCall.id, {
2397
- output,
2398
- isError: true
2399
- });
2400
+ emitToolResultEvent(emit, toolCall.id, output, true);
2400
2401
  continue;
2401
2402
  }
2402
2403
  const parsed = tool.inputSchema.safeParse(toolCall.args);
2403
2404
  if (!parsed.success) {
2404
2405
  const output = `Invalid input for tool "${toolCall.name}": ${parsed.error.message}`;
2405
2406
  await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2406
- deferred.set(toolCall.id, {
2407
- output,
2408
- isError: true
2409
- });
2407
+ emitToolResultEvent(emit, toolCall.id, output, true);
2410
2408
  continue;
2411
2409
  }
2412
2410
  let hookResult;
@@ -2414,7 +2412,6 @@ async function classifyToolCalls(step, response, deferred) {
2414
2412
  hookResult = await config.beforeToolCall({
2415
2413
  toolCall,
2416
2414
  args: parsed.data,
2417
- assistantMessage: response.message,
2418
2415
  context,
2419
2416
  turnId,
2420
2417
  stepNumber: currentStep,
@@ -2424,19 +2421,13 @@ async function classifyToolCalls(step, response, deferred) {
2424
2421
  } catch (error) {
2425
2422
  const output = `beforeToolCall hook failed for "${toolCall.name}": ${errorMessage$1(error)}`;
2426
2423
  await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2427
- deferred.set(toolCall.id, {
2428
- output,
2429
- isError: true
2430
- });
2424
+ emitToolResultEvent(emit, toolCall.id, output, true);
2431
2425
  continue;
2432
2426
  }
2433
2427
  if (hookResult?.block === true) {
2434
2428
  const output = hookResult.reason ?? `Tool call "${toolCall.name}" was blocked`;
2435
2429
  await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2436
- deferred.set(toolCall.id, {
2437
- output,
2438
- isError: true
2439
- });
2430
+ emitToolResultEvent(emit, toolCall.id, output, true);
2440
2431
  continue;
2441
2432
  }
2442
2433
  pending.push({
@@ -2448,112 +2439,241 @@ async function classifyToolCalls(step, response, deferred) {
2448
2439
  }
2449
2440
  return pending;
2450
2441
  }
2451
- async function executePendingCalls(step, pending, deferred) {
2442
+ async function prepareStreamingToolCall(step, toolCall) {
2452
2443
  const { config, context, emit, signal, turnId, currentStep, stepUuid, toolCallByProviderId } = step;
2444
+ const tool = findTool(config.tools, toolCall.name);
2445
+ if (tool === void 0 || toolCall.error !== void 0) return { kind: "declined" };
2446
+ const parsed = tool.inputSchema.safeParse(toolCall.args);
2447
+ if (!parsed.success || !isStreamingPrefetchSafe(tool, parsed.data)) return { kind: "declined" };
2448
+ emit({
2449
+ type: "tool.call",
2450
+ toolCallId: toolCall.id,
2451
+ name: toolCall.name,
2452
+ args: toToolCallArgs(toolCall.args)
2453
+ });
2454
+ let hookResult;
2455
+ if (config.beforeToolCall !== void 0) try {
2456
+ hookResult = await config.beforeToolCall({
2457
+ toolCall,
2458
+ args: parsed.data,
2459
+ context,
2460
+ turnId,
2461
+ stepNumber: currentStep,
2462
+ stepUuid,
2463
+ toolCallByProviderId
2464
+ }, signal);
2465
+ } catch (error) {
2466
+ const output = `beforeToolCall hook failed for "${toolCall.name}": ${errorMessage$1(error)}`;
2467
+ await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2468
+ emitToolResultEvent(emit, toolCall.id, output, true);
2469
+ return { kind: "handled" };
2470
+ }
2471
+ if (hookResult?.block === true) {
2472
+ const output = hookResult.reason ?? `Tool call "${toolCall.name}" was blocked`;
2473
+ await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2474
+ emitToolResultEvent(emit, toolCall.id, output, true);
2475
+ return { kind: "handled" };
2476
+ }
2477
+ const base = {
2478
+ toolCall,
2479
+ tool,
2480
+ parsedArgs: parsed.data,
2481
+ hookResult
2482
+ };
2483
+ if (!isStreamingPrefetchSafe(tool, getEffectiveInput(base))) return {
2484
+ kind: "pending",
2485
+ pending: base
2486
+ };
2487
+ const preparedResult = preparePendingToolResultCore(step, base).then((prepared) => {
2488
+ emitToolResultEvent(emit, prepared.toolCallId, prepared.output, prepared.isError);
2489
+ return prepared;
2490
+ });
2491
+ return {
2492
+ kind: "pending",
2493
+ pending: {
2494
+ ...base,
2495
+ preparedResult,
2496
+ resultEventEmitted: true
2497
+ }
2498
+ };
2499
+ }
2500
+ async function executePendingCalls(step, pending) {
2501
+ const { emit, signal } = step;
2453
2502
  const postContentCallbacks = [];
2454
- for (const [index, item] of pending.entries()) {
2503
+ for (let index = 0; index < pending.length;) {
2455
2504
  if (index > 0) signal.throwIfAborted();
2456
- const { toolCall, tool, parsedArgs, hookResult } = item;
2457
- let effectiveInput = parsedArgs;
2458
- if (hookResult?.block === true) {
2459
- const output = hookResult.reason ?? `Tool call "${toolCall.name}" was blocked`;
2460
- deferred.set(toolCall.id, {
2461
- output,
2462
- isError: true
2463
- });
2464
- await writeFallbackToolCallAndResult(context, stepUuid, turnId, currentStep, toolCall, output);
2465
- continue;
2466
- }
2467
- if (hookResult?.updatedInput !== void 0) effectiveInput = hookResult.updatedInput;
2468
- let toolResult;
2469
- try {
2470
- const prefetched = step.prefetchedResults.get(toolCall.id);
2471
- if (prefetched !== void 0) toolResult = prefetched;
2472
- else toolResult = await raceExecuteWithGraceTimeout(tool.execute({
2473
- turnId,
2474
- toolCallId: toolCall.id,
2475
- args: effectiveInput,
2476
- signal,
2477
- onUpdate: (update) => {
2478
- emit({
2479
- type: "tool.progress",
2480
- toolCallId: toolCall.id,
2481
- update
2482
- });
2483
- },
2484
- wireUuid: toolCallByProviderId.get(toolCall.id)
2485
- }), signal, toolCall.name);
2486
- } catch (error) {
2487
- const output = isAbortError$3(error) || signal.aborted ? `Tool "${toolCall.name}" was aborted` : `Tool "${toolCall.name}" failed: ${errorMessage$1(error)}`;
2488
- const syntheticResult = {
2489
- content: output,
2490
- isError: true
2491
- };
2492
- deferred.set(toolCall.id, {
2493
- output,
2494
- isError: true
2495
- });
2496
- await context.appendToolResult(toolCallByProviderId.get(toolCall.id), toolCall.id, {
2497
- output,
2498
- isError: true
2499
- });
2500
- if (config.afterToolCall !== void 0) try {
2501
- await config.afterToolCall({
2502
- toolCall,
2503
- args: effectiveInput,
2504
- result: syntheticResult,
2505
- context
2506
- }, signal);
2507
- } catch {}
2505
+ const item = pending[index];
2506
+ if (item === void 0) break;
2507
+ if (isConcurrencySafe(item) && !signal.aborted) {
2508
+ const batch = [];
2509
+ while (index < pending.length) {
2510
+ const next = pending[index];
2511
+ if (next === void 0 || !isConcurrencySafe(next)) break;
2512
+ batch.push(next);
2513
+ index += 1;
2514
+ }
2515
+ let batchStopped = false;
2516
+ const shouldEmit = () => !batchStopped;
2517
+ const tasks = batch.map((batchItem) => prepareAndEmitToolResult(step, batchItem, shouldEmit));
2518
+ for (const task of tasks) {
2519
+ const outcome = await task;
2520
+ if (!outcome.ok) {
2521
+ batchStopped = true;
2522
+ throw outcome.error instanceof Error ? outcome.error : new Error(errorMessage$1(outcome.error));
2523
+ }
2524
+ const prepared = outcome.prepared;
2525
+ await appendPreparedToolResult(step, prepared, postContentCallbacks);
2526
+ if (prepared.throwAfterCommit !== void 0) {
2527
+ batchStopped = true;
2528
+ throw prepared.throwAfterCommit;
2529
+ }
2530
+ if (signal.aborted) {
2531
+ batchStopped = true;
2532
+ signal.throwIfAborted();
2533
+ }
2534
+ }
2508
2535
  continue;
2509
2536
  }
2510
- let finalResult = toolResult;
2511
- let afterError;
2537
+ const prepared = await preparePendingToolResult(step, item);
2538
+ if (item.resultEventEmitted !== true) emitToolResultEvent(emit, prepared.toolCallId, prepared.output, prepared.isError);
2539
+ await appendPreparedToolResult(step, prepared, postContentCallbacks);
2540
+ if (prepared.throwAfterCommit !== void 0) throw prepared.throwAfterCommit;
2541
+ index += 1;
2542
+ }
2543
+ for (const callback of postContentCallbacks) await callback();
2544
+ }
2545
+ async function preparePendingToolResult(step, item) {
2546
+ if (item.preparedResult !== void 0) return item.preparedResult;
2547
+ return preparePendingToolResultCore(step, item);
2548
+ }
2549
+ async function preparePendingToolResultCore(step, item) {
2550
+ const { config, context, signal } = step;
2551
+ const { toolCall, hookResult } = item;
2552
+ const effectiveInput = getEffectiveInput(item);
2553
+ if (hookResult?.block === true) return makeSyntheticPrepared(step, toolCall, hookResult.reason ?? `Tool call "${toolCall.name}" was blocked`);
2554
+ let toolResult;
2555
+ try {
2556
+ toolResult = await executeOrAwaitPrefetch(step, item, effectiveInput);
2557
+ } catch (error) {
2558
+ const output = isAbortError$3(error) || signal.aborted ? `Tool "${toolCall.name}" was aborted` : `Tool "${toolCall.name}" failed: ${errorMessage$1(error)}`;
2559
+ const syntheticResult = {
2560
+ content: output,
2561
+ isError: true
2562
+ };
2512
2563
  if (config.afterToolCall !== void 0) try {
2513
- const afterResult = await config.afterToolCall({
2564
+ await config.afterToolCall({
2514
2565
  toolCall,
2515
2566
  args: effectiveInput,
2516
- result: toolResult,
2567
+ result: syntheticResult,
2517
2568
  context
2518
2569
  }, signal);
2519
- if (afterResult?.resultOverride !== void 0) finalResult = afterResult.resultOverride;
2520
- } catch (error) {
2521
- afterError = error;
2522
- }
2523
- if (afterError !== void 0) {
2524
- if (isAbortError$3(afterError) || signal.aborted) {
2525
- const output = `Tool "${toolCall.name}" aborted during afterToolCall hook.`;
2526
- deferred.set(toolCall.id, {
2527
- output,
2528
- isError: true
2529
- });
2530
- await context.appendToolResult(toolCallByProviderId.get(toolCall.id), toolCall.id, {
2531
- output,
2532
- isError: true
2533
- });
2534
- throw afterError instanceof Error ? afterError : new Error(errorMessage$1(afterError));
2535
- }
2536
- const output = `afterToolCall hook failed for "${toolCall.name}": ${errorMessage$1(afterError)}`;
2537
- deferred.set(toolCall.id, {
2538
- output,
2539
- isError: true
2540
- });
2541
- await context.appendToolResult(toolCallByProviderId.get(toolCall.id), toolCall.id, {
2542
- output,
2543
- isError: true
2570
+ } catch {}
2571
+ return makeSyntheticPrepared(step, toolCall, output);
2572
+ }
2573
+ let finalResult = toolResult;
2574
+ let afterError;
2575
+ if (config.afterToolCall !== void 0) try {
2576
+ const afterResult = await config.afterToolCall({
2577
+ toolCall,
2578
+ args: effectiveInput,
2579
+ result: toolResult,
2580
+ context
2581
+ }, signal);
2582
+ if (afterResult?.resultOverride !== void 0) finalResult = afterResult.resultOverride;
2583
+ } catch (error) {
2584
+ afterError = error;
2585
+ }
2586
+ if (afterError !== void 0) {
2587
+ if (isAbortError$3(afterError) || signal.aborted) return makeSyntheticPrepared(step, toolCall, `Tool "${toolCall.name}" aborted during afterToolCall hook.`, afterError instanceof Error ? afterError : new Error(errorMessage$1(afterError)));
2588
+ return makeSyntheticPrepared(step, toolCall, `afterToolCall hook failed for "${toolCall.name}": ${errorMessage$1(afterError)}`);
2589
+ }
2590
+ const adapted = adaptToolResult(finalResult);
2591
+ const output = typeof adapted.output === "string" ? adapted.output : JSON.stringify(adapted.output);
2592
+ return {
2593
+ toolCallId: toolCall.id,
2594
+ adapted,
2595
+ output,
2596
+ isError: finalResult.isError === true,
2597
+ postContent: !finalResult.isError ? finalResult.postContent : void 0,
2598
+ throwAfterCommit: void 0
2599
+ };
2600
+ }
2601
+ async function prepareAndEmitToolResult(step, item, shouldEmit) {
2602
+ try {
2603
+ const prepared = await preparePendingToolResult(step, item);
2604
+ if (item.resultEventEmitted !== true && shouldEmit()) emitToolResultEvent(step.emit, prepared.toolCallId, prepared.output, prepared.isError);
2605
+ return {
2606
+ ok: true,
2607
+ prepared
2608
+ };
2609
+ } catch (error) {
2610
+ return {
2611
+ ok: false,
2612
+ error
2613
+ };
2614
+ }
2615
+ }
2616
+ async function executeOrAwaitPrefetch(step, item, effectiveInput) {
2617
+ const { emit, signal, turnId, toolCallByProviderId } = step;
2618
+ const { toolCall, tool } = item;
2619
+ const prefetched = step.prefetchedResults.get(toolCall.id);
2620
+ if (prefetched !== void 0) try {
2621
+ return await raceExecuteWithGraceTimeout(prefetched, signal, tool.name);
2622
+ } catch (error) {
2623
+ if (signal.aborted) throw error;
2624
+ }
2625
+ return raceExecuteWithGraceTimeout(tool.execute({
2626
+ turnId,
2627
+ toolCallId: toolCall.id,
2628
+ args: effectiveInput,
2629
+ signal,
2630
+ onUpdate: (update) => {
2631
+ emit({
2632
+ type: "tool.progress",
2633
+ toolCallId: toolCall.id,
2634
+ update
2544
2635
  });
2545
- continue;
2546
- }
2547
- const adapted = adaptToolResult(finalResult);
2548
- const adaptedText = typeof adapted.output === "string" ? adapted.output : JSON.stringify(adapted.output);
2549
- deferred.set(toolCall.id, {
2550
- output: adaptedText,
2551
- isError: finalResult.isError === true
2552
- });
2553
- await context.appendToolResult(toolCallByProviderId.get(toolCall.id), toolCall.id, adapted);
2554
- if (!finalResult.isError && finalResult.postContent) postContentCallbacks.push(finalResult.postContent);
2636
+ },
2637
+ wireUuid: toolCallByProviderId.get(toolCall.id)
2638
+ }), signal, tool.name);
2639
+ }
2640
+ function makeSyntheticPrepared(step, toolCall, output, throwAfterCommit) {
2641
+ return {
2642
+ toolCallId: toolCall.id,
2643
+ adapted: {
2644
+ output,
2645
+ isError: true
2646
+ },
2647
+ output,
2648
+ isError: true,
2649
+ postContent: void 0,
2650
+ throwAfterCommit
2651
+ };
2652
+ }
2653
+ async function appendPreparedToolResult(step, prepared, postContentCallbacks) {
2654
+ const parentUuid = step.toolCallByProviderId.get(prepared.toolCallId);
2655
+ await step.context.appendToolResult(parentUuid, prepared.toolCallId, prepared.adapted);
2656
+ if (prepared.postContent !== void 0) postContentCallbacks.push(prepared.postContent);
2657
+ }
2658
+ function getEffectiveInput(item) {
2659
+ return item.hookResult?.updatedInput ?? item.parsedArgs;
2660
+ }
2661
+ function isConcurrencySafe(item) {
2662
+ if (item.hookResult?.block === true) return false;
2663
+ if (item.tool.isConcurrencySafe === void 0) return false;
2664
+ try {
2665
+ return item.tool.isConcurrencySafe(getEffectiveInput(item));
2666
+ } catch {
2667
+ return false;
2668
+ }
2669
+ }
2670
+ function isStreamingPrefetchSafe(tool, input) {
2671
+ if (tool.isStreamingPrefetchSafe === void 0) return false;
2672
+ try {
2673
+ return tool.isStreamingPrefetchSafe(input);
2674
+ } catch {
2675
+ return false;
2555
2676
  }
2556
- for (const callback of postContentCallbacks) await callback();
2557
2677
  }
2558
2678
  function findTool(tools, name) {
2559
2679
  return tools?.find((tool) => tool.name === name);
@@ -2643,33 +2763,7 @@ async function executeSoulStep(deps) {
2643
2763
  const stepUuid = randomUUID();
2644
2764
  const toolCallByProviderId = /* @__PURE__ */ new Map();
2645
2765
  const prefetchedResults = /* @__PURE__ */ new Map();
2646
- await context.appendStepBegin({
2647
- uuid: stepUuid,
2648
- turnId,
2649
- step: currentStep
2650
- });
2651
- const response = await runtime.kosong.chat({
2652
- messages,
2653
- tools: visibleTools,
2654
- model,
2655
- systemPrompt: context.systemPrompt,
2656
- effort: overrides?.effort,
2657
- signal,
2658
- ...createChatStreamingCallbacks({
2659
- emit,
2660
- context,
2661
- turnId,
2662
- currentStep,
2663
- stepUuid,
2664
- prefetchedResults
2665
- }),
2666
- contextWindow: config.contextWindow
2667
- });
2668
- recordUsage(response.usage);
2669
- const deferredResults = /* @__PURE__ */ new Map();
2670
- const flushDeferredResults = () => {
2671
- flushDeferredToolResults(response, deferredResults, emit);
2672
- };
2766
+ const streamingPreparations = /* @__PURE__ */ new Map();
2673
2767
  const step = {
2674
2768
  config,
2675
2769
  context,
@@ -2679,22 +2773,51 @@ async function executeSoulStep(deps) {
2679
2773
  currentStep,
2680
2774
  stepUuid,
2681
2775
  toolCallByProviderId,
2682
- prefetchedResults
2776
+ prefetchedResults,
2777
+ streamingPreparations
2683
2778
  };
2779
+ const streamingScheduler = createStreamingToolCallScheduler(step, streamingPreparations);
2780
+ await context.appendStepBegin({
2781
+ uuid: stepUuid,
2782
+ turnId,
2783
+ step: currentStep
2784
+ });
2785
+ let response;
2684
2786
  try {
2685
- const pending = await classifyToolCalls(step, response, deferredResults);
2686
- await context.appendStepEnd({
2687
- uuid: stepUuid,
2688
- turnId,
2689
- step: currentStep,
2690
- usage: toStepEndUsage(response.usage),
2691
- finishReason: response.stopReason
2787
+ response = await runtime.kosong.chat({
2788
+ messages,
2789
+ tools: visibleTools,
2790
+ model,
2791
+ systemPrompt: context.systemPrompt,
2792
+ effort: overrides?.effort,
2793
+ signal,
2794
+ ...createChatStreamingCallbacks({
2795
+ emit,
2796
+ context,
2797
+ turnId,
2798
+ currentStep,
2799
+ stepUuid,
2800
+ prefetchedResults
2801
+ }),
2802
+ onToolCallReady: (toolCall, info) => {
2803
+ streamingScheduler.markReady(toolCall, info);
2804
+ },
2805
+ contextWindow: config.contextWindow
2692
2806
  });
2693
- await executePendingCalls(step, pending, deferredResults);
2694
- signal.throwIfAborted();
2695
2807
  } finally {
2696
- flushDeferredResults();
2808
+ streamingScheduler.finish();
2697
2809
  }
2810
+ recordUsage(response.usage);
2811
+ const pending = await classifyToolCalls(step, response);
2812
+ await context.appendStepEnd({
2813
+ uuid: stepUuid,
2814
+ turnId,
2815
+ step: currentStep,
2816
+ usage: toStepEndUsage(response.usage),
2817
+ finishReason: response.stopReason
2818
+ });
2819
+ await executePendingCalls(step, pending);
2820
+ signal.throwIfAborted();
2698
2821
  emit({
2699
2822
  type: "step.end",
2700
2823
  step: currentStep
@@ -2710,6 +2833,86 @@ async function executeSoulStep(deps) {
2710
2833
  } catch {}
2711
2834
  return { stopReason };
2712
2835
  }
2836
+ function createStreamingToolCallScheduler(step, streamingPreparations) {
2837
+ let nextOrdinal = 0;
2838
+ let nextSyntheticOrdinal = 0;
2839
+ let prefixOpen = true;
2840
+ let finished = false;
2841
+ let pumping = false;
2842
+ const queue = /* @__PURE__ */ new Map();
2843
+ const declined = () => ({ kind: "declined" });
2844
+ const drainDeclined = () => {
2845
+ for (const slot of queue.values()) slot.resolve(declined());
2846
+ queue.clear();
2847
+ };
2848
+ const assignOrdinal = (info) => {
2849
+ const ordinal = info?.ordinal;
2850
+ if (ordinal !== void 0 && Number.isSafeInteger(ordinal) && ordinal >= 0) {
2851
+ nextSyntheticOrdinal = Math.max(nextSyntheticOrdinal, ordinal + 1);
2852
+ return ordinal;
2853
+ }
2854
+ while (nextSyntheticOrdinal < nextOrdinal || queue.has(nextSyntheticOrdinal)) nextSyntheticOrdinal += 1;
2855
+ const synthetic = nextSyntheticOrdinal;
2856
+ nextSyntheticOrdinal += 1;
2857
+ return synthetic;
2858
+ };
2859
+ const pump = async () => {
2860
+ if (pumping) return;
2861
+ pumping = true;
2862
+ try {
2863
+ while (true) {
2864
+ if (finished || !prefixOpen) break;
2865
+ const slot = queue.get(nextOrdinal);
2866
+ if (slot === void 0) break;
2867
+ queue.delete(nextOrdinal);
2868
+ nextOrdinal += 1;
2869
+ let preparation;
2870
+ try {
2871
+ preparation = await prepareStreamingToolCall(step, slot.toolCall);
2872
+ } catch {
2873
+ preparation = declined();
2874
+ }
2875
+ slot.resolve(preparation);
2876
+ if (!streamingPreparationKeepsPrefixOpen(preparation)) {
2877
+ prefixOpen = false;
2878
+ drainDeclined();
2879
+ break;
2880
+ }
2881
+ }
2882
+ } finally {
2883
+ pumping = false;
2884
+ if (!finished && prefixOpen && queue.has(nextOrdinal)) pump();
2885
+ }
2886
+ };
2887
+ return {
2888
+ markReady(toolCall, info) {
2889
+ if (streamingPreparations.has(toolCall.id)) return;
2890
+ const preparation = new Promise((resolve) => {
2891
+ if (finished || !prefixOpen) {
2892
+ resolve(declined());
2893
+ return;
2894
+ }
2895
+ const ordinal = assignOrdinal(info);
2896
+ if (ordinal < nextOrdinal || queue.has(ordinal)) {
2897
+ prefixOpen = false;
2898
+ resolve(declined());
2899
+ drainDeclined();
2900
+ return;
2901
+ }
2902
+ queue.set(ordinal, {
2903
+ toolCall,
2904
+ resolve
2905
+ });
2906
+ pump();
2907
+ });
2908
+ streamingPreparations.set(toolCall.id, preparation);
2909
+ },
2910
+ finish() {
2911
+ finished = true;
2912
+ drainDeclined();
2913
+ }
2914
+ };
2915
+ }
2713
2916
  function toStepEndUsage(usage) {
2714
2917
  return {
2715
2918
  input_tokens: usage.input,
@@ -3885,10 +4088,10 @@ var ToolStreamingPrefetchScope = class {
3885
4088
  }
3886
4089
  executeStreaming(toolCall, signal) {
3887
4090
  const tool = this.currentTools.get(toolCall.name);
3888
- if (tool === void 0 || tool.isConcurrencySafe === void 0) return void 0;
4091
+ if (tool === void 0 || tool.isStreamingPrefetchSafe === void 0) return void 0;
3889
4092
  let safe;
3890
4093
  try {
3891
- safe = tool.isConcurrencySafe(toolCall.args);
4094
+ safe = tool.isStreamingPrefetchSafe(toolCall.args);
3892
4095
  } catch {
3893
4096
  return;
3894
4097
  }
@@ -4600,7 +4803,9 @@ var StreamingKosongWrapper = class {
4600
4803
  const sub = new AbortController();
4601
4804
  const inFlight = /* @__PURE__ */ new Map();
4602
4805
  const completed = /* @__PURE__ */ new Map();
4806
+ const upstreamOnToolCallReady = params.onToolCallReady;
4603
4807
  const onPrefetchedResult = params.onPrefetchedResult;
4808
+ const onPrefetchedResultPromise = params.onPrefetchedResultPromise;
4604
4809
  const onCallerAbort = () => {
4605
4810
  try {
4606
4811
  this.streaming.discardStreaming("aborted");
@@ -4609,19 +4814,28 @@ var StreamingKosongWrapper = class {
4609
4814
  };
4610
4815
  if (params.signal.aborted) sub.abort();
4611
4816
  else params.signal.addEventListener("abort", onCallerAbort);
4612
- const onToolCallReady = (toolCall) => {
4817
+ const onToolCallReady = (toolCall, info) => {
4818
+ if (upstreamOnToolCallReady !== void 0) {
4819
+ upstreamOnToolCallReady(toolCall, info);
4820
+ return;
4821
+ }
4613
4822
  if (inFlight.has(toolCall.id) || completed.has(toolCall.id)) return;
4614
4823
  const pending = this.streaming.executeStreaming(toolCall, sub.signal);
4615
4824
  if (pending === void 0) return;
4616
4825
  inFlight.set(toolCall.id, pending);
4826
+ try {
4827
+ onPrefetchedResultPromise?.(toolCall.id, pending);
4828
+ } catch {}
4617
4829
  pending.then((result) => {
4618
4830
  inFlight.delete(toolCall.id);
4619
4831
  completed.set(toolCall.id, result);
4620
4832
  try {
4621
4833
  onPrefetchedResult?.(toolCall.id, result);
4622
4834
  } catch {}
4835
+ return null;
4623
4836
  }).catch(() => {
4624
4837
  inFlight.delete(toolCall.id);
4838
+ return null;
4625
4839
  });
4626
4840
  };
4627
4841
  const unbind = typeof this.streaming.bindStreaming === "function" ? this.streaming.bindStreaming({
@@ -4634,18 +4848,31 @@ var StreamingKosongWrapper = class {
4634
4848
  if (!sub.signal.aborted) sub.abort();
4635
4849
  }
4636
4850
  }) : () => {};
4851
+ let deferredCleanup = false;
4852
+ const cleanup = (clearCompleted) => {
4853
+ if (clearCompleted) completed.clear();
4854
+ unbind();
4855
+ params.signal.removeEventListener("abort", onCallerAbort);
4856
+ };
4637
4857
  try {
4638
4858
  const wrappedParams = {
4639
4859
  ...params,
4640
4860
  onToolCallReady
4641
4861
  };
4642
4862
  const response = await this.raw.chat(wrappedParams);
4643
- if (inFlight.size > 0) await Promise.allSettled(inFlight.values());
4863
+ if (inFlight.size > 0 && onPrefetchedResultPromise === void 0) await Promise.allSettled(inFlight.values());
4864
+ if (inFlight.size > 0 && onPrefetchedResultPromise !== void 0) {
4865
+ deferredCleanup = true;
4866
+ const pendingAtReturn = [...inFlight.values()];
4867
+ Promise.allSettled(pendingAtReturn).finally(() => {
4868
+ cleanup(true);
4869
+ });
4870
+ return response;
4871
+ }
4644
4872
  completed.clear();
4645
4873
  return response;
4646
4874
  } finally {
4647
- unbind();
4648
- params.signal.removeEventListener("abort", onCallerAbort);
4875
+ if (!deferredCleanup) cleanup(false);
4649
4876
  }
4650
4877
  }
4651
4878
  };
@@ -4703,6 +4930,7 @@ var DefaultToolExecutionScopeFactory = class {
4703
4930
  if (inner.maxResultSizeChars !== void 0) wrapped.maxResultSizeChars = inner.maxResultSizeChars;
4704
4931
  if (inner.display !== void 0) wrapped.display = inner.display;
4705
4932
  if (inner.isConcurrencySafe !== void 0) wrapped.isConcurrencySafe = inner.isConcurrencySafe.bind(inner);
4933
+ if (inner.isStreamingPrefetchSafe !== void 0) wrapped.isStreamingPrefetchSafe = inner.isStreamingPrefetchSafe.bind(inner);
4706
4934
  return wrapped;
4707
4935
  };
4708
4936
  return {
@@ -4817,6 +5045,7 @@ function copyToolMetadata(inner, wrapped) {
4817
5045
  if (inner.maxResultSizeChars !== void 0) wrapped.maxResultSizeChars = inner.maxResultSizeChars;
4818
5046
  if (inner.display !== void 0) wrapped.display = inner.display;
4819
5047
  if (inner.isConcurrencySafe !== void 0) wrapped.isConcurrencySafe = inner.isConcurrencySafe.bind(inner);
5048
+ if (inner.isStreamingPrefetchSafe !== void 0) wrapped.isStreamingPrefetchSafe = inner.isStreamingPrefetchSafe.bind(inner);
4820
5049
  }
4821
5050
  async function maybeExecutePlanFileTool(toolName, args, planMode) {
4822
5051
  if (!isRecord$10(args) || typeof args["path"] !== "string") return void 0;
@@ -5722,7 +5951,7 @@ function createSoulPlusToolRegistry(options) {
5722
5951
  };
5723
5952
  }
5724
5953
  function registerSubagentTool(options) {
5725
- if (options.hasSubagentInfra && options.isToolEnabled("Agent")) options.toolRegistry.push(new AgentTool(options.soulRegistry, "agent_main"));
5954
+ if (options.hasSubagentInfra && options.isToolEnabled("Agent")) options.toolRegistry.push(new AgentTool(options.soulRegistry, "agent_main", void 0, options.agentTypeRegistry));
5726
5955
  }
5727
5956
  function registerSkillTool(options) {
5728
5957
  if (options.skillManager === void 0) return;
@@ -7443,6 +7672,10 @@ function isToolCall(part) {
7443
7672
  function isToolCallPart(part) {
7444
7673
  return part.type === "tool_call_part";
7445
7674
  }
7675
+ /** Check if a streamed part marks a tool call as complete. */
7676
+ function isToolCallDone(part) {
7677
+ return part.type === "tool_call_done";
7678
+ }
7446
7679
  /**
7447
7680
  * Merge `source` into `target` in-place for streaming accumulation.
7448
7681
  *
@@ -7568,13 +7801,13 @@ var APIEmptyResponseError = class extends ChatProviderError {
7568
7801
  * (e.g. TextPart + TextPart, ToolCall + ToolCallPart) are merged in-place so
7569
7802
  * the returned message always contains fully-assembled parts.
7570
7803
  *
7571
- * **onToolCall firing**: The {@link GenerateCallbacks.onToolCall} callback is
7572
- * fired once per fully-assembled tool call **after** the stream completes, in
7573
- * the order they appear in `message.toolCalls`. Tool calls are never dispatched
7574
- * mid-stream because parallel tool call streams may interleave arguments
7575
- * (e.g. `tc0-header tc1-header tc0-args tc1-args`), so the arguments of
7576
- * any individual tool call are only guaranteed to be complete once the entire
7577
- * stream has drained.
7804
+ * **tool-call callback firing**: {@link GenerateCallbacks.onToolCallReady}
7805
+ * may fire before stream end when a provider emits an explicit
7806
+ * `tool_call_done`, or when a linear provider opts into Python-style
7807
+ * merge-boundary readiness. Indexed streams without those signals can still
7808
+ * receive later routed argument deltas, so they are only marked ready after the
7809
+ * stream drains. {@link GenerateCallbacks.onToolCall} remains deferred until
7810
+ * after the stream completes, in final message order.
7578
7811
  *
7579
7812
  * @param provider - The chat provider to generate from.
7580
7813
  * @param systemPrompt - System-level instruction prepended to the request.
@@ -7595,16 +7828,23 @@ async function generate$1(provider, systemPrompt, tools, history, callbacks, opt
7595
7828
  toolCalls: []
7596
7829
  };
7597
7830
  let pendingPart = null;
7831
+ const readyToolCallIds = /* @__PURE__ */ new Set();
7832
+ const readyStreamIndexes = /* @__PURE__ */ new Set();
7598
7833
  const toolCallIndexMap = /* @__PURE__ */ new Map();
7599
7834
  if (options?.signal?.aborted) throwAbortError();
7600
7835
  const stream = await provider.generate(systemPrompt, tools, history, options);
7601
7836
  await throwIfAborted(options?.signal, stream);
7602
7837
  for await (const part of stream) {
7603
7838
  await throwIfAborted(options?.signal, stream);
7839
+ if (isLateArgumentDelta(part, readyStreamIndexes)) throw new Error(`Received tool-call arguments for stream index ${String(part.index)} after it was marked ready`);
7604
7840
  if (callbacks?.onMessagePart !== void 0) {
7605
7841
  await callbacks.onMessagePart(deepCopyPart(part));
7606
7842
  await throwIfAborted(options?.signal, stream);
7607
7843
  }
7844
+ if (isToolCallDone(part)) {
7845
+ pendingPart = await handleToolCallDone(message, pendingPart, part, toolCallIndexMap, callbacks, readyToolCallIds, readyStreamIndexes, options?.signal, stream);
7846
+ continue;
7847
+ }
7608
7848
  if (isToolCallPart(part) && part.index !== void 0 && !isPendingToolCallAtIndex(pendingPart, part.index)) {
7609
7849
  const arrayIdx = toolCallIndexMap.get(part.index);
7610
7850
  if (arrayIdx !== void 0) {
@@ -7615,7 +7855,10 @@ async function generate$1(provider, systemPrompt, tools, history, callbacks, opt
7615
7855
  }
7616
7856
  if (pendingPart === null) pendingPart = part;
7617
7857
  else if (!mergeInPlace(pendingPart, part)) {
7618
- flushPart(message, pendingPart, toolCallIndexMap);
7858
+ const flushed = flushPart(message, pendingPart, toolCallIndexMap);
7859
+ if (flushed !== void 0 && canReadyFromMergeBoundary(provider, flushed) && hasCompleteJsonArguments(flushed.toolCall)) {
7860
+ if (await emitToolCallReady(callbacks, readyToolCallIds, flushed.toolCall, { ordinal: flushed.ordinal }, options?.signal, stream) && flushed.streamIndex !== void 0) readyStreamIndexes.add(flushed.streamIndex);
7861
+ }
7619
7862
  pendingPart = part;
7620
7863
  }
7621
7864
  }
@@ -7626,6 +7869,7 @@ async function generate$1(provider, systemPrompt, tools, history, callbacks, opt
7626
7869
  const hasText = message.content.some((p) => p.type === "text" && p.text.trim().length > 0);
7627
7870
  const hasToolCalls = message.toolCalls.length > 0;
7628
7871
  if (hasThink && !hasText && !hasToolCalls) throw new APIEmptyResponseError(`The API returned a response containing only thinking content without any text or tool calls. This usually indicates the stream was interrupted or the output token budget was exhausted during reasoning. Provider: ${provider.name}, model: ${provider.modelName}`);
7872
+ if (callbacks?.onToolCallReady !== void 0) for (const [ordinal, toolCall] of message.toolCalls.entries()) await emitToolCallReady(callbacks, readyToolCallIds, toolCall, { ordinal }, options?.signal, stream);
7629
7873
  if (callbacks?.onToolCall !== void 0) for (const toolCall of message.toolCalls) {
7630
7874
  await throwIfAborted(options?.signal, stream);
7631
7875
  await callbacks.onToolCall(toolCall);
@@ -7659,13 +7903,29 @@ async function throwIfAborted(signal, stream) {
7659
7903
  function isPendingToolCallAtIndex(pending, index) {
7660
7904
  return pending !== null && isToolCall(pending) && pending._streamIndex === index;
7661
7905
  }
7906
+ function isLateArgumentDelta(part, readyStreamIndexes) {
7907
+ return isToolCallPart(part) && part.index !== void 0 && readyStreamIndexes.has(part.index);
7908
+ }
7909
+ function hasCompleteJsonArguments(toolCall) {
7910
+ const raw = toolCall.function.arguments;
7911
+ if (raw === null || raw.length === 0) return false;
7912
+ try {
7913
+ JSON.parse(raw);
7914
+ return true;
7915
+ } catch {
7916
+ return false;
7917
+ }
7918
+ }
7919
+ function canReadyFromMergeBoundary(provider, flushed) {
7920
+ return flushed.streamIndex === void 0 || provider.toolCallReadyStrategy === "merge-boundary";
7921
+ }
7662
7922
  /**
7663
7923
  * Append a fully-merged part to the message.
7664
7924
  *
7665
7925
  * - ContentPart -> message.content
7666
7926
  * - ToolCall -> message.toolCalls (the `_streamIndex` routing key is
7667
7927
  * registered in the map and stripped before storage).
7668
- * - ToolCallPart -> ignored (orphaned delta)
7928
+ * - ToolCallPart / ToolCallDone -> ignored (control or orphaned delta)
7669
7929
  */
7670
7930
  function flushPart(message, part, toolCallIndexMap) {
7671
7931
  if (isContentPart$1(part)) {
@@ -7680,10 +7940,48 @@ function flushPart(message, part, toolCallIndexMap) {
7680
7940
  function: part.function,
7681
7941
  extras: part.extras
7682
7942
  };
7683
- const arrayIdx = message.toolCalls.length;
7943
+ const ordinal = message.toolCalls.length;
7684
7944
  message.toolCalls.push(storedCall);
7685
- if (streamIndex !== void 0) toolCallIndexMap.set(streamIndex, arrayIdx);
7686
- }
7945
+ if (streamIndex !== void 0) toolCallIndexMap.set(streamIndex, ordinal);
7946
+ return {
7947
+ toolCall: storedCall,
7948
+ streamIndex,
7949
+ ordinal
7950
+ };
7951
+ }
7952
+ }
7953
+ async function handleToolCallDone(message, pendingPart, part, toolCallIndexMap, callbacks, readyToolCallIds, readyStreamIndexes, signal, stream) {
7954
+ let target = getToolCallEntryByStreamIndex(message, toolCallIndexMap, part.index);
7955
+ let nextPending = pendingPart;
7956
+ if (target === void 0 && isPendingToolCallAtIndex(pendingPart, part.index)) {
7957
+ const flushed = flushPart(message, pendingPart, toolCallIndexMap);
7958
+ target = flushed !== void 0 ? {
7959
+ toolCall: flushed.toolCall,
7960
+ ordinal: flushed.ordinal
7961
+ } : void 0;
7962
+ nextPending = null;
7963
+ }
7964
+ if (target === void 0) return nextPending;
7965
+ if ("arguments" in part) target.toolCall.function.arguments = part.arguments ?? null;
7966
+ if (part.name !== void 0 && part.name.length > 0) target.toolCall.function.name = part.name;
7967
+ if (await emitToolCallReady(callbacks, readyToolCallIds, target.toolCall, { ordinal: target.ordinal }, signal, stream)) readyStreamIndexes.add(part.index);
7968
+ return nextPending;
7969
+ }
7970
+ function getToolCallEntryByStreamIndex(message, toolCallIndexMap, streamIndex) {
7971
+ const arrayIdx = toolCallIndexMap.get(streamIndex);
7972
+ if (arrayIdx === void 0) return void 0;
7973
+ const toolCall = message.toolCalls[arrayIdx];
7974
+ return toolCall === void 0 ? void 0 : {
7975
+ toolCall,
7976
+ ordinal: arrayIdx
7977
+ };
7978
+ }
7979
+ async function emitToolCallReady(callbacks, readyToolCallIds, toolCall, info, signal, stream) {
7980
+ if (callbacks?.onToolCallReady === void 0 || readyToolCallIds.has(toolCall.id)) return false;
7981
+ await throwIfAborted(signal, stream);
7982
+ readyToolCallIds.add(toolCall.id);
7983
+ await callbacks.onToolCallReady(toolCall, info);
7984
+ return true;
7687
7985
  }
7688
7986
  /**
7689
7987
  * Produce a shallow-ish copy of a StreamedMessagePart.
@@ -21342,7 +21640,8 @@ var SoulPlus = class {
21342
21640
  toolRegistry,
21343
21641
  isToolEnabled,
21344
21642
  hasSubagentInfra,
21345
- soulRegistry
21643
+ soulRegistry,
21644
+ agentTypeRegistry: deps.agentTypeRegistry
21346
21645
  });
21347
21646
  registerSkillTool({
21348
21647
  toolRegistry,
@@ -21811,13 +22110,13 @@ var KosongAdapter = class {
21811
22110
  const onDelta = params.onDelta;
21812
22111
  const onThinkDelta = params.onThinkDelta;
21813
22112
  const onToolCallPart = params.onToolCallPart;
22113
+ const onToolCallReady = params.onToolCallReady;
21814
22114
  const onAtomicPart = params.onAtomicPart;
21815
22115
  const needMessagePart = onDelta !== void 0 || onThinkDelta !== void 0 || onToolCallPart !== void 0;
21816
22116
  const toolCallsByStreamIndex = /* @__PURE__ */ new Map();
21817
22117
  let lastToolCall;
21818
- let result;
21819
- try {
21820
- result = await generate$1(activeProvider, params.systemPrompt, kosongTools, params.messages, needMessagePart ? { onMessagePart: async (part) => {
22118
+ const callbacks = needMessagePart || onToolCallReady !== void 0 ? {
22119
+ ...needMessagePart ? { onMessagePart: async (part) => {
21821
22120
  if (part.type === "text" && onDelta !== void 0) onDelta(part.text);
21822
22121
  else if (part.type === "think" && onThinkDelta !== void 0) onThinkDelta(part.think);
21823
22122
  else if (part.type === "function") {
@@ -21826,16 +22125,30 @@ var KosongAdapter = class {
21826
22125
  name: part.function.name
21827
22126
  };
21828
22127
  if (part._streamIndex !== void 0) toolCallsByStreamIndex.set(part._streamIndex, lastToolCall);
21829
- } else if (isToolCallPart(part) && onToolCallPart !== void 0) {
22128
+ } else if (isToolCallPart(part)) {
21830
22129
  const target = part.index !== void 0 ? toolCallsByStreamIndex.get(part.index) : lastToolCall;
21831
- if (target !== void 0) onToolCallPart({
22130
+ if (target !== void 0 && onToolCallPart !== void 0) onToolCallPart({
21832
22131
  type: "tool_call_part",
21833
22132
  tool_call_id: target.id,
21834
22133
  name: target.name,
21835
22134
  ...part.argumentsPart !== null ? { arguments_chunk: part.argumentsPart } : {}
21836
22135
  });
21837
22136
  }
21838
- } } : void 0, { signal: params.signal });
22137
+ } } : {},
22138
+ ...onToolCallReady !== void 0 ? { onToolCallReady: async (toolCall, info) => {
22139
+ const parsed = parseToolArgs(toolCall.function.arguments);
22140
+ if (parsed.error !== void 0) return;
22141
+ onToolCallReady({
22142
+ id: toolCall.id,
22143
+ name: toolCall.function.name,
22144
+ rawArgs: toolCall.function.arguments,
22145
+ args: parsed.args
22146
+ }, info);
22147
+ } } : {}
22148
+ } : void 0;
22149
+ let result;
22150
+ try {
22151
+ result = await generate$1(activeProvider, params.systemPrompt, kosongTools, params.messages, callbacks, { signal: params.signal });
21839
22152
  } catch (error) {
21840
22153
  if (isContextOverflowProviderError(error)) throw new ContextOverflowError(extractMessage(error));
21841
22154
  throw error;
@@ -24261,6 +24574,7 @@ var ReadTool = class {
24261
24574
  inputSchema = ReadInputSchema;
24262
24575
  maxResultSizeChars = Number.POSITIVE_INFINITY;
24263
24576
  isConcurrencySafe = (_input) => true;
24577
+ isStreamingPrefetchSafe = (_input) => true;
24264
24578
  display = {
24265
24579
  getUserFacingName: () => "Read",
24266
24580
  getInputDisplay: (input) => ({
@@ -29183,6 +29497,7 @@ var GrepTool = class {
29183
29497
  description = "Search file contents using regular expressions (powered by ripgrep).";
29184
29498
  inputSchema = GrepInputSchema;
29185
29499
  isConcurrencySafe = (_input) => true;
29500
+ isStreamingPrefetchSafe = (_input) => true;
29186
29501
  constructor(kaos, workspace) {
29187
29502
  this.kaos = kaos;
29188
29503
  this.workspace = workspace;
@@ -29583,6 +29898,7 @@ var GlobTool = class {
29583
29898
  description = GLOB_DESCRIPTION;
29584
29899
  inputSchema = GlobInputSchema;
29585
29900
  isConcurrencySafe = (_input) => true;
29901
+ isStreamingPrefetchSafe = (_input) => true;
29586
29902
  constructor(kaos, workspace) {
29587
29903
  this.kaos = kaos;
29588
29904
  this.workspace = workspace;
@@ -32187,7 +32503,7 @@ function registerProcessHandlers(ctx) {
32187
32503
  //#region ../../packages/kimi-core/src/wire-protocol/outbound/external-tool-proxy.ts
32188
32504
  function buildExternalToolProxy(options) {
32189
32505
  const inputSchema = z.unknown();
32190
- return {
32506
+ const tool = {
32191
32507
  name: options.name,
32192
32508
  description: options.description,
32193
32509
  inputSchema,
@@ -32217,6 +32533,8 @@ function buildExternalToolProxy(options) {
32217
32533
  }
32218
32534
  }
32219
32535
  };
32536
+ if (options.concurrencySafe === true) tool.isConcurrencySafe = () => true;
32537
+ return tool;
32220
32538
  }
32221
32539
  //#endregion
32222
32540
  //#region ../../packages/kimi-core/src/wire-protocol/request/handlers/tools.ts
@@ -32229,12 +32547,14 @@ const TOOLS_HANDLER_DESCRIPTORS = [
32229
32547
  if (reverse === void 0) throw new Error("session.registerTool requires a reverse-RPC channel (no `server` transport wired)");
32230
32548
  ctx.sessionState.get(msg.session_id).externalTools.set(payload.name, {
32231
32549
  description: payload.description ?? "",
32232
- input_schema: payload.input_schema
32550
+ input_schema: payload.input_schema,
32551
+ concurrency_safe: payload.concurrency_safe === true
32233
32552
  });
32234
32553
  await ctx.sessionApplication.registerDynamicTool(msg.session_id, buildExternalToolProxy({
32235
32554
  name: payload.name,
32236
32555
  description: payload.description ?? "",
32237
32556
  inputSchema: payload.input_schema,
32557
+ concurrencySafe: payload.concurrency_safe === true,
32238
32558
  sendToolCall: async (call, signal) => {
32239
32559
  const data = (await reverse.sendRequest("tool.call", msg.session_id, {
32240
32560
  id: call.id,
@@ -32453,7 +32773,7 @@ function registerDefaultWireHandlers(deps) {
32453
32773
  pathConfig,
32454
32774
  runtimeProvider: () => deps.runtimeProvider?.() ?? runtime,
32455
32775
  toolsProvider: (ctx) => deps.toolsProvider?.(ctx) ?? tools,
32456
- ...deps.enabledToolNames !== void 0 || deps.enabledToolNamesProvider !== void 0 ? { enabledToolNamesProvider: async () => deps.enabledToolNamesProvider !== void 0 ? await deps.enabledToolNamesProvider() : deps.enabledToolNames } : {},
32776
+ ...deps.enabledToolNames !== void 0 || deps.enabledToolNamesProvider !== void 0 ? { enabledToolNamesProvider: async () => deps.enabledToolNamesProvider !== void 0 ? deps.enabledToolNamesProvider() : deps.enabledToolNames } : {},
32457
32777
  defaultModelProvider: () => deps.defaultModelProvider?.() ?? defaultModel,
32458
32778
  ...deps.defaultSystemPromptProvider !== void 0 ? { defaultSystemPromptProvider: deps.defaultSystemPromptProvider } : {},
32459
32779
  compactionProviderProvider: () => deps.compactionProviderProvider?.() ?? compactionProvider,
@@ -33287,7 +33607,7 @@ function normalizeSessionTimestampSeconds(value) {
33287
33607
  async function readSessionInfo(paths, sessionId) {
33288
33608
  const statePath = paths.statePath(sessionId);
33289
33609
  const cache = new StateCache(statePath);
33290
- const [state, lastActivity] = await Promise.all([cache.read(), stat(statePath).then((st) => Math.floor(st.mtimeMs / 1e3), () => void 0)]);
33610
+ const [state, lastActivity] = await Promise.all([cache.read(), stat(statePath).then((st) => Math.floor(st.mtimeMs / 1e3), () => {})]);
33291
33611
  if (state !== null) return {
33292
33612
  session_id: state.session_id,
33293
33613
  created_at: state.created_at,
@@ -47647,6 +47967,7 @@ var AnthropicStreamedMessage = class {
47647
47967
  }
47648
47968
  }
47649
47969
  async *_convertStreamResponse(response) {
47970
+ const toolUseBlockIndexes = /* @__PURE__ */ new Set();
47650
47971
  try {
47651
47972
  for await (const event of response) {
47652
47973
  const evt = event;
@@ -47680,6 +48001,7 @@ var AnthropicStreamedMessage = class {
47680
48001
  };
47681
48002
  break;
47682
48003
  case "tool_use":
48004
+ toolUseBlockIndexes.add(blockIndex);
47683
48005
  yield {
47684
48006
  type: "function",
47685
48007
  id: block.id,
@@ -47723,6 +48045,15 @@ var AnthropicStreamedMessage = class {
47723
48045
  };
47724
48046
  break;
47725
48047
  }
48048
+ } else if (eventType === "content_block_stop") {
48049
+ const blockIndex = evt.index;
48050
+ if (blockIndex !== void 0 && toolUseBlockIndexes.has(blockIndex)) {
48051
+ toolUseBlockIndexes.delete(blockIndex);
48052
+ yield {
48053
+ type: "tool_call_done",
48054
+ index: blockIndex
48055
+ };
48056
+ }
47726
48057
  } else if (eventType === "message_delta") {
47727
48058
  const deltaUsage = evt.usage;
47728
48059
  if (deltaUsage !== void 0) {
@@ -76942,6 +77273,7 @@ var KimiStreamedMessage = class {
76942
77273
  };
76943
77274
  var KimiChatProvider = class {
76944
77275
  name = "kimi";
77276
+ toolCallReadyStrategy = "merge-boundary";
76945
77277
  _model;
76946
77278
  _stream;
76947
77279
  _apiKey;
@@ -77718,7 +78050,7 @@ var OpenAIResponsesStreamedMessage = class {
77718
78050
  } else if (chunkType === "response.output_item.added") {
77719
78051
  const item = chunk["item"];
77720
78052
  if (item["type"] === "function_call") {
77721
- const itemId = item["id"];
78053
+ const streamIndex = item["id"] ?? chunk["output_index"];
77722
78054
  const tc = {
77723
78055
  type: "function",
77724
78056
  id: item["call_id"] || crypto.randomUUID(),
@@ -77727,7 +78059,7 @@ var OpenAIResponsesStreamedMessage = class {
77727
78059
  arguments: item["arguments"] ?? null
77728
78060
  }
77729
78061
  };
77730
- if (itemId !== void 0) tc._streamIndex = itemId;
78062
+ if (streamIndex !== void 0) tc._streamIndex = streamIndex;
77731
78063
  yield tc;
77732
78064
  }
77733
78065
  } else if (chunkType === "response.output_item.done") {
@@ -77740,15 +78072,41 @@ var OpenAIResponsesStreamedMessage = class {
77740
78072
  };
77741
78073
  if (encContent !== void 0) thinkPart.encrypted = encContent;
77742
78074
  yield thinkPart;
78075
+ } else if (item["type"] === "function_call") {
78076
+ const streamIndex = item["id"] ?? chunk["output_index"];
78077
+ if (streamIndex !== void 0) {
78078
+ const donePart = {
78079
+ type: "tool_call_done",
78080
+ index: streamIndex
78081
+ };
78082
+ const argumentsValue = item["arguments"];
78083
+ if (typeof argumentsValue === "string") donePart.arguments = argumentsValue;
78084
+ const name = item["name"];
78085
+ if (typeof name === "string") donePart.name = name;
78086
+ yield donePart;
78087
+ }
77743
78088
  }
77744
78089
  } else if (chunkType === "response.function_call_arguments.delta") {
77745
- const itemId = chunk["item_id"];
78090
+ const streamIndex = chunk["item_id"] ?? chunk["output_index"];
77746
78091
  const part = {
77747
78092
  type: "tool_call_part",
77748
78093
  argumentsPart: chunk["delta"]
77749
78094
  };
77750
- if (itemId !== void 0) part.index = itemId;
78095
+ if (streamIndex !== void 0) part.index = streamIndex;
77751
78096
  yield part;
78097
+ } else if (chunkType === "response.function_call_arguments.done") {
78098
+ const streamIndex = chunk["item_id"] ?? chunk["output_index"];
78099
+ if (streamIndex !== void 0) {
78100
+ const donePart = {
78101
+ type: "tool_call_done",
78102
+ index: streamIndex
78103
+ };
78104
+ const argumentsValue = chunk["arguments"];
78105
+ if (typeof argumentsValue === "string") donePart.arguments = argumentsValue;
78106
+ const name = chunk["name"];
78107
+ if (typeof name === "string") donePart.name = name;
78108
+ yield donePart;
78109
+ }
77752
78110
  } else if (chunkType === "response.reasoning_summary_part.added") yield {
77753
78111
  type: "think",
77754
78112
  think: ""
@@ -78182,12 +78540,14 @@ var DeferredOAuthChatProvider = class DeferredOAuthChatProvider {
78182
78540
  name;
78183
78541
  modelName;
78184
78542
  thinkingEffort;
78543
+ toolCallReadyStrategy;
78185
78544
  options;
78186
78545
  constructor(options) {
78187
78546
  this.options = options;
78188
78547
  this.name = options.providerConfig.type;
78189
78548
  this.modelName = options.modelOverride ?? options.providerConfig.defaultModel ?? "";
78190
78549
  this.thinkingEffort = options.thinkingEffort ?? null;
78550
+ this.toolCallReadyStrategy = this.createProviderWithApiKey("__oauth_token_placeholder__").toolCallReadyStrategy;
78191
78551
  }
78192
78552
  async generate(systemPrompt, tools, history, options) {
78193
78553
  return (await this.materializeProvider()).generate(systemPrompt, tools, history, options);
@@ -78208,20 +78568,20 @@ var DeferredOAuthChatProvider = class DeferredOAuthChatProvider {
78208
78568
  });
78209
78569
  }
78210
78570
  getCapability(model) {
78211
- return createProvider(this.options.providerName, {
78212
- ...this.options.providerConfig,
78213
- apiKey: "__oauth_token_placeholder__"
78214
- }, this.options.modelOverride, this.options.defaultHeaders).getCapability?.(model) ?? UNKNOWN_CAPABILITY;
78571
+ return this.createProviderWithApiKey("__oauth_token_placeholder__").getCapability?.(model) ?? UNKNOWN_CAPABILITY;
78215
78572
  }
78216
78573
  async materializeProvider() {
78217
78574
  const accessToken = await this.options.oauthResolver(this.options.providerName);
78218
- let provider = createProvider(this.options.providerName, {
78219
- ...this.options.providerConfig,
78220
- apiKey: accessToken
78221
- }, this.options.modelOverride, this.options.defaultHeaders);
78575
+ let provider = this.createProviderWithApiKey(accessToken);
78222
78576
  if (this.thinkingEffort !== null) provider = provider.withThinking(this.thinkingEffort);
78223
78577
  return provider;
78224
78578
  }
78579
+ createProviderWithApiKey(apiKey) {
78580
+ return createProvider(this.options.providerName, {
78581
+ ...this.options.providerConfig,
78582
+ apiKey
78583
+ }, this.options.modelOverride, this.options.defaultHeaders);
78584
+ }
78225
78585
  };
78226
78586
  //#endregion
78227
78587
  //#region ../../packages/kimi-core/src/soul-plus/subagent/agent-type-registry.ts
@@ -91723,8 +92083,8 @@ async function postForm(url, params, deviceHeaders, options) {
91723
92083
  body,
91724
92084
  signal
91725
92085
  });
91726
- } catch (err) {
91727
- throw new OAuthError(`OAuth request to ${url} failed: ${err instanceof Error ? err.message : String(err)}`);
92086
+ } catch (error) {
92087
+ throw new OAuthError(`OAuth request to ${url} failed: ${error instanceof Error ? error.message : String(error)}`);
91728
92088
  }
91729
92089
  const status = response.status;
91730
92090
  let data = {};
@@ -91803,8 +92163,8 @@ async function refreshAccessToken(config, refreshToken, options = {}) {
91803
92163
  grant_type: "refresh_token",
91804
92164
  refresh_token: refreshToken
91805
92165
  }, getDeviceHeaders()));
91806
- } catch (err) {
91807
- lastError = err instanceof Error ? err : new OAuthError(String(err));
92166
+ } catch (error) {
92167
+ lastError = error instanceof Error ? error : new OAuthError(String(error));
91808
92168
  if (attempt < maxRetries - 1) {
91809
92169
  await sleep(backoff(attempt));
91810
92170
  continue;
@@ -96466,6 +96826,15 @@ var ToolCallComponent = class extends Container {
96466
96826
  subagentUsage;
96467
96827
  subagentResultSummary;
96468
96828
  subagentError;
96829
+ /**
96830
+ * 当 ToolCallComponent 被 group 容器(`AgentGroupComponent` /
96831
+ * `ReadGroupComponent`)借走做"隐身状态容器"时,由 group 注册回调;任何
96832
+ * 状态变化(subagent meta / phase / sub-tool / result 等)都会触发它,
96833
+ * 让 group 走节流重渲染。`undefined` 表示没人订阅,对独立单卡渲染路径
96834
+ * 无副作用。一个 ToolCallComponent 同时只可能在一个 group 里,所以
96835
+ * 单 listener slot 足够;后到的 listener 会覆盖前一个。
96836
+ */
96837
+ onSnapshotChange;
96469
96838
  constructor(toolCall, result, colors, ui, markdownTheme) {
96470
96839
  super();
96471
96840
  this.toolCall = toolCall;
@@ -96491,6 +96860,7 @@ var ToolCallComponent = class extends Container {
96491
96860
  this.result = result;
96492
96861
  this.headerText.setText(this.buildHeader());
96493
96862
  this.rebuildBody();
96863
+ this.notifySnapshotChange();
96494
96864
  }
96495
96865
  updateToolCall(toolCall) {
96496
96866
  this.toolCall = toolCall;
@@ -96548,9 +96918,72 @@ var ToolCallComponent = class extends Container {
96548
96918
  this.subagentAgentId = agentId;
96549
96919
  this.subagentAgentName = agentName;
96550
96920
  this.rebuildContent();
96921
+ this.notifySnapshotChange();
96551
96922
  this.ui?.requestRender();
96552
96923
  }
96553
96924
  /**
96925
+ * 让 group 容器(AgentGroup / ReadGroup)订阅本卡的状态变化。注册同时
96926
+ * 立即触发一次回调,使 group 拿到当前快照(无需再调一次 getSubagentSnapshot
96927
+ * / getReadSnapshot)。传入 `undefined` 解绑。
96928
+ */
96929
+ setSnapshotListener(cb) {
96930
+ this.onSnapshotChange = cb;
96931
+ if (cb !== void 0) cb();
96932
+ }
96933
+ getSubagentSnapshot() {
96934
+ const finished = this.finishedSubCalls.length + this.hiddenSubCallCount;
96935
+ const tokens = this.subagentUsage ? this.subagentUsage.input + this.subagentUsage.output : 0;
96936
+ const latestActivity = computeLatestActivity(this.ongoingSubCalls, this.finishedSubCalls, this.subagentText);
96937
+ const derivedPhase = this.result !== void 0 ? this.result.is_error ? "failed" : "done" : this.subagentPhase;
96938
+ return {
96939
+ toolCallId: this.toolCall.id,
96940
+ toolName: this.toolCall.name,
96941
+ toolCallDescription: str(this.toolCall.args["description"]) || str(this.toolCall.description),
96942
+ agentName: this.subagentAgentName,
96943
+ phase: derivedPhase,
96944
+ toolCount: finished,
96945
+ tokens,
96946
+ isError: derivedPhase === "failed",
96947
+ errorText: this.subagentError ?? (derivedPhase === "failed" ? this.result?.output : void 0),
96948
+ latestActivity
96949
+ };
96950
+ }
96951
+ /**
96952
+ * 给 `ReadGroupComponent` 用:累加同 step 多张 Read 卡的 line 数。
96953
+ * `lines` 与单卡 chip(`pluralize(countNonEmptyLines(...), 'line')`)保持
96954
+ * 一致,避免 group 汇总和单卡读数不对齐。
96955
+ */
96956
+ getReadSnapshot() {
96957
+ const args = this.toolCall.args;
96958
+ const filePathRaw = args["file_path"] ?? args["path"];
96959
+ const filePath = typeof filePathRaw === "string" ? filePathRaw : void 0;
96960
+ if (this.result === void 0) return {
96961
+ toolCallId: this.toolCall.id,
96962
+ filePath,
96963
+ phase: "pending",
96964
+ lines: 0
96965
+ };
96966
+ if (this.result.is_error === true) return {
96967
+ toolCallId: this.toolCall.id,
96968
+ filePath,
96969
+ phase: "failed",
96970
+ lines: 0
96971
+ };
96972
+ return {
96973
+ toolCallId: this.toolCall.id,
96974
+ filePath,
96975
+ phase: "done",
96976
+ lines: countNonEmptyLines(this.result.output)
96977
+ };
96978
+ }
96979
+ get toolCallView() {
96980
+ return this.toolCall;
96981
+ }
96982
+ /** 给本组件内部调用:state 任何变化都通知监听者(如果已绑定 group)。 */
96983
+ notifySnapshotChange() {
96984
+ this.onSnapshotChange?.();
96985
+ }
96986
+ /**
96554
96987
  * 来自 wire 'subagent.spawned'。子 agent 已被注册,但内部活动事件
96555
96988
  * (content.delta / tool.call) 可能尚未到达——把 UI 切到 'spawning'
96556
96989
  * 占位状态(除非 runInBackground,这种情况直接进 'backgrounded')。
@@ -96561,6 +96994,7 @@ var ToolCallComponent = class extends Container {
96561
96994
  this.subagentRunInBackground = meta.runInBackground;
96562
96995
  this.subagentPhase = meta.runInBackground ? "backgrounded" : "spawning";
96563
96996
  this.rebuildContent();
96997
+ this.notifySnapshotChange();
96564
96998
  this.ui?.requestRender();
96565
96999
  }
96566
97000
  /**
@@ -96572,6 +97006,7 @@ var ToolCallComponent = class extends Container {
96572
97006
  this.subagentUsage = payload.usage;
96573
97007
  this.subagentResultSummary = payload.resultSummary.length > 0 ? payload.resultSummary : void 0;
96574
97008
  this.rebuildContent();
97009
+ this.notifySnapshotChange();
96575
97010
  this.ui?.requestRender();
96576
97011
  }
96577
97012
  /**
@@ -96581,12 +97016,14 @@ var ToolCallComponent = class extends Container {
96581
97016
  this.subagentPhase = "failed";
96582
97017
  this.subagentError = payload.error;
96583
97018
  this.rebuildContent();
97019
+ this.notifySnapshotChange();
96584
97020
  this.ui?.requestRender();
96585
97021
  }
96586
97022
  appendSubagentText(text) {
96587
97023
  this.subagentText += text;
96588
97024
  if (this.subagentPhase === void 0 || this.subagentPhase === "spawning") this.subagentPhase = "running";
96589
97025
  this.rebuildContent();
97026
+ this.notifySnapshotChange();
96590
97027
  this.ui?.requestRender();
96591
97028
  }
96592
97029
  appendSubToolCall(call) {
@@ -96598,6 +97035,7 @@ var ToolCallComponent = class extends Container {
96598
97035
  });
96599
97036
  if (this.subagentPhase === void 0 || this.subagentPhase === "spawning") this.subagentPhase = "running";
96600
97037
  this.rebuildContent();
97038
+ this.notifySnapshotChange();
96601
97039
  this.ui?.requestRender();
96602
97040
  }
96603
97041
  appendSubToolCallDelta(delta) {
@@ -96610,6 +97048,7 @@ var ToolCallComponent = class extends Container {
96610
97048
  streamingArguments: nextArgsText
96611
97049
  });
96612
97050
  this.rebuildContent();
97051
+ this.notifySnapshotChange();
96613
97052
  this.ui?.requestRender();
96614
97053
  }
96615
97054
  finishSubToolCall(result) {
@@ -96627,6 +97066,7 @@ var ToolCallComponent = class extends Container {
96627
97066
  this.hiddenSubCallCount += 1;
96628
97067
  }
96629
97068
  this.rebuildContent();
97069
+ this.notifySnapshotChange();
96630
97070
  this.ui?.requestRender();
96631
97071
  }
96632
97072
  buildHeader() {
@@ -96651,7 +97091,7 @@ var ToolCallComponent = class extends Container {
96651
97091
  const tone = isError ? chalk.hex(colors.error) : chalk.hex(colors.primary);
96652
97092
  return `${bullet}${tone.bold(label)}`;
96653
97093
  }
96654
- const verb = isFinished ? "Used" : "Using";
97094
+ const verb = isFinished ? "Used" : toolCall.streamingArguments !== void 0 ? "Preparing" : "Using";
96655
97095
  const keyArg = extractKeyArgument(toolCall.name, toolCall.args);
96656
97096
  const toolRef = chalk.hex(colors.primary).bold(toolCall.name);
96657
97097
  const argStr = keyArg ? chalk.dim(` (${keyArg})`) : "";
@@ -96907,6 +97347,30 @@ var ToolCallComponent = class extends Container {
96907
97347
  return true;
96908
97348
  }
96909
97349
  };
97350
+ /**
97351
+ * 推算 group line 第二级"最新活动"行:优先级
97352
+ * 1. 最近的 ongoing sub-tool(`Using {name} ({keyArg})`)
97353
+ * 2. 最近的 finished sub-tool(`Used {name} ({keyArg})`)
97354
+ * 3. subagent 累计文本的非空末行
97355
+ */
97356
+ function computeLatestActivity(ongoing, finished, text) {
97357
+ if (ongoing.size > 0) {
97358
+ const lastOngoing = [...ongoing.values()].at(-1);
97359
+ if (lastOngoing !== void 0) return formatActivityLine("Using", lastOngoing.name, lastOngoing.args);
97360
+ }
97361
+ if (finished.length > 0) {
97362
+ const last = finished.at(-1);
97363
+ if (last !== void 0) return formatActivityLine("Used", last.name, last.args);
97364
+ }
97365
+ if (text.length > 0) {
97366
+ const tail = text.split("\n").toReversed().find((l) => l.trim().length > 0);
97367
+ if (tail !== void 0) return tail.trim();
97368
+ }
97369
+ }
97370
+ function formatActivityLine(verb, toolName, args) {
97371
+ const keyArg = extractKeyArgument(toolName, args);
97372
+ return keyArg ? `${verb} ${toolName} (${keyArg})` : `${verb} ${toolName}`;
97373
+ }
96910
97374
  //#endregion
96911
97375
  //#region src/tui/components/messages/user-message.ts
96912
97376
  /**
@@ -97602,6 +98066,7 @@ function clearTranscriptAndRedraw(state) {
97602
98066
  state.transcriptContainer.clear();
97603
98067
  state.pendingToolComponents.clear();
97604
98068
  state.streamingComponent = void 0;
98069
+ state.streamingTranscriptEntry = void 0;
97605
98070
  state.todoPanel.clear();
97606
98071
  state.todoPanelContainer.clear();
97607
98072
  state.imageStore.clear();
@@ -97677,7 +98142,19 @@ function handleSubagentSourceEvent(ectx, payload) {
97677
98142
  }
97678
98143
  }
97679
98144
  function handleSubagentSpawned(ectx, data) {
97680
- const tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
98145
+ if (data.run_in_background) {
98146
+ ectx.state.backgroundAgents.add(data.agent_id);
98147
+ syncBackgroundAgentBadge(ectx);
98148
+ return;
98149
+ }
98150
+ let tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
98151
+ if (tc === void 0) {
98152
+ const toolCall = ectx.state.activeToolCalls.get(data.parent_tool_call_id);
98153
+ if (toolCall !== void 0) {
98154
+ ectx.onToolCallStart(toolCall);
98155
+ tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
98156
+ }
98157
+ }
97681
98158
  if (tc === void 0) return;
97682
98159
  tc.onSubagentSpawned({
97683
98160
  agentId: data.agent_id,
@@ -97686,6 +98163,7 @@ function handleSubagentSpawned(ectx, data) {
97686
98163
  });
97687
98164
  }
97688
98165
  function handleSubagentCompleted(ectx, data) {
98166
+ if (ectx.state.backgroundAgents.delete(data.agent_id)) syncBackgroundAgentBadge(ectx);
97689
98167
  const tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
97690
98168
  if (tc === void 0) return;
97691
98169
  tc.onSubagentCompleted({
@@ -97694,10 +98172,15 @@ function handleSubagentCompleted(ectx, data) {
97694
98172
  });
97695
98173
  }
97696
98174
  function handleSubagentFailed(ectx, data) {
98175
+ if (ectx.state.backgroundAgents.delete(data.agent_id)) syncBackgroundAgentBadge(ectx);
97697
98176
  const tc = ectx.state.pendingToolComponents.get(data.parent_tool_call_id);
97698
98177
  if (tc === void 0) return;
97699
98178
  tc.onSubagentFailed({ error: data.error });
97700
98179
  }
98180
+ function syncBackgroundAgentBadge(ectx) {
98181
+ ectx.state.footer.setBackgroundAgentCount(ectx.state.backgroundAgents.size);
98182
+ ectx.state.ui.requestRender();
98183
+ }
97701
98184
  //#endregion
97702
98185
  //#region src/utils/git/git-ls-files.ts
97703
98186
  /**
@@ -98705,6 +99188,13 @@ var FooterComponent = class {
98705
99188
  gitCache;
98706
99189
  gitCacheWorkDir;
98707
99190
  transientHint = null;
99191
+ /**
99192
+ * 在跑的 background subagent 数量。`subagent.spawned (run_in_background)`
99193
+ * 入集合 → 加 1;`subagent.completed/failed` → 减 1(详见
99194
+ * `handlers/subagent.ts`)。0 时整块隐藏;>=1 时在 line1 的 cwd 左侧
99195
+ * 插入 `[N agents running]` badge(位于 `agent(model ○)` 与 cwd 之间)。
99196
+ */
99197
+ backgroundAgentCount = 0;
98708
99198
  constructor(state, colors) {
98709
99199
  this.state = state;
98710
99200
  this.colors = colors;
@@ -98730,6 +99220,14 @@ var FooterComponent = class {
98730
99220
  setTransientHint(hint) {
98731
99221
  this.transientHint = hint;
98732
99222
  }
99223
+ /**
99224
+ * Sync the background-agent badge with the live count. Called by
99225
+ * `handlers/subagent.ts` after `subagent.spawned/completed/failed` mutate
99226
+ * `state.backgroundAgents`. n=0 hides the badge entirely.
99227
+ */
99228
+ setBackgroundAgentCount(n) {
99229
+ this.backgroundAgentCount = Math.max(0, n);
99230
+ }
98733
99231
  invalidate() {}
98734
99232
  attach(handlers, requestRender) {
98735
99233
  const unsubStatus = handlers.onStatusUpdate((data) => {
@@ -98771,6 +99269,7 @@ var FooterComponent = class {
98771
99269
  const dot = state.thinking ? "●" : "○";
98772
99270
  left.push(chalk.hex(colors.text)(`agent(${model} ${dot})`));
98773
99271
  }
99272
+ if (this.backgroundAgentCount > 0) left.push(chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)} agents running]`));
98774
99273
  const cwd = shortenCwd(state.workDir);
98775
99274
  if (cwd) left.push(chalk.hex(colors.status)(cwd));
98776
99275
  const git = this.gitCache.getStatus();
@@ -99011,11 +99510,15 @@ function createTUIState(options) {
99011
99510
  phaseSpinner: void 0,
99012
99511
  activeThinkingComponent: void 0,
99013
99512
  streamingComponent: void 0,
99513
+ streamingTranscriptEntry: void 0,
99014
99514
  activeCompactionBlock: void 0,
99015
99515
  toolOutputExpanded: false,
99016
99516
  lastActivityMode: void 0,
99017
99517
  lastHistoryContent: void 0,
99018
99518
  pendingToolComponents: /* @__PURE__ */ new Map(),
99519
+ pendingAgentGroup: null,
99520
+ pendingReadGroup: null,
99521
+ backgroundAgents: /* @__PURE__ */ new Set(),
99019
99522
  sessions: [],
99020
99523
  loadingSessions: false,
99021
99524
  showingSessionPicker: false,
@@ -99026,6 +99529,7 @@ function createTUIState(options) {
99026
99529
  fdPath: detectFdPath(),
99027
99530
  gitLsFilesCache: createGitLsFilesCache(initialAppState.workDir),
99028
99531
  currentTurnId: void 0,
99532
+ currentStep: 0,
99029
99533
  assistantDraft: "",
99030
99534
  assistantStreamActive: false,
99031
99535
  thinkingDraft: "",
@@ -99196,6 +99700,8 @@ function sendMessageInternal(state, addEntry, input, options) {
99196
99700
  state.currentTurnId = void 0;
99197
99701
  state.assistantDraft = "";
99198
99702
  state.assistantStreamActive = false;
99703
+ state.streamingComponent = void 0;
99704
+ state.streamingTranscriptEntry = void 0;
99199
99705
  state.thinkingDraft = "";
99200
99706
  disposeActiveThinkingComponent(state);
99201
99707
  state.activeToolCalls.clear();
@@ -99257,6 +99763,8 @@ function sendSkillActivation(state, addEntry, skillName, skillArgs, fullPrompt)
99257
99763
  state.currentTurnId = void 0;
99258
99764
  state.assistantDraft = "";
99259
99765
  state.assistantStreamActive = false;
99766
+ state.streamingComponent = void 0;
99767
+ state.streamingTranscriptEntry = void 0;
99260
99768
  state.thinkingDraft = "";
99261
99769
  disposeActiveThinkingComponent(state);
99262
99770
  state.activeToolCalls.clear();
@@ -99433,13 +99941,186 @@ async function performReload(state, action, hooks) {
99433
99941
  state.ui.requestRender();
99434
99942
  }
99435
99943
  //#endregion
99944
+ //#region src/tui/components/messages/agent-group.ts
99945
+ const THROTTLE_MS$1 = 200;
99946
+ var AgentGroupComponent = class extends Container {
99947
+ entries = [];
99948
+ headerText;
99949
+ bodyContainer;
99950
+ throttleTimer = null;
99951
+ lastFlushPhases = /* @__PURE__ */ new Map();
99952
+ constructor(colors, ui) {
99953
+ super();
99954
+ this.colors = colors;
99955
+ this.ui = ui;
99956
+ this.addChild(new Spacer(1));
99957
+ this.headerText = new Text("", 0, 0);
99958
+ this.addChild(this.headerText);
99959
+ this.bodyContainer = new Container();
99960
+ this.addChild(this.bodyContainer);
99961
+ }
99962
+ size() {
99963
+ return this.entries.length;
99964
+ }
99965
+ /**
99966
+ * 把一个独立 `ToolCallComponent` 借进 group 做"隐身状态容器"。注册
99967
+ * snapshot 监听 → 状态有变即触发节流刷新。多次 attach 同一个 toolCallId
99968
+ * 是 noop。
99969
+ */
99970
+ attach(toolCallId, tc) {
99971
+ if (this.entries.some((e) => e.toolCallId === toolCallId)) return;
99972
+ this.entries.push({
99973
+ toolCallId,
99974
+ tc
99975
+ });
99976
+ tc.setSnapshotListener(() => {
99977
+ this.scheduleRender();
99978
+ });
99979
+ this.flushRender();
99980
+ }
99981
+ /**
99982
+ * 立刻调度一次重绘。phase 真正跨阶段切换(spawning ↔ running ↔ done/failed)
99983
+ * 时强制立刷新;其他变更(latestActivity / tokens / toolCount)走节流。
99984
+ */
99985
+ scheduleRender() {
99986
+ if (this.detectPhaseTransition()) {
99987
+ this.flushRender();
99988
+ return;
99989
+ }
99990
+ if (this.throttleTimer !== null) return;
99991
+ this.throttleTimer = setTimeout(() => {
99992
+ this.throttleTimer = null;
99993
+ this.flushRender();
99994
+ }, THROTTLE_MS$1);
99995
+ }
99996
+ /**
99997
+ * 比较当前各 child 的 phase 与上次刷新时的 phase 快照。任意一个发生
99998
+ * 变化即视为 phase transition,触发立刷新。
99999
+ */
100000
+ detectPhaseTransition() {
100001
+ let changed = false;
100002
+ for (const e of this.entries) {
100003
+ const phase = e.tc.getSubagentSnapshot().phase;
100004
+ if (this.lastFlushPhases.get(e.toolCallId) !== phase) {
100005
+ changed = true;
100006
+ break;
100007
+ }
100008
+ }
100009
+ return changed;
100010
+ }
100011
+ flushRender() {
100012
+ if (this.throttleTimer !== null) {
100013
+ clearTimeout(this.throttleTimer);
100014
+ this.throttleTimer = null;
100015
+ }
100016
+ const snapshots = this.entries.map((e) => e.tc.getSubagentSnapshot());
100017
+ this.headerText.setText(this.buildHeader(snapshots));
100018
+ this.bodyContainer.clear();
100019
+ snapshots.forEach((snap, idx) => {
100020
+ const isLast = idx === snapshots.length - 1;
100021
+ this.appendLines(snap, isLast);
100022
+ });
100023
+ this.lastFlushPhases.clear();
100024
+ this.entries.forEach((entry, i) => {
100025
+ const snap = snapshots[i];
100026
+ if (snap !== void 0) this.lastFlushPhases.set(entry.toolCallId, snap.phase);
100027
+ });
100028
+ this.invalidate();
100029
+ this.ui?.requestRender();
100030
+ }
100031
+ buildHeader(snapshots) {
100032
+ const colors = this.colors;
100033
+ const total = snapshots.length;
100034
+ const done = snapshots.filter((s) => s.phase === "done").length;
100035
+ const failed = snapshots.filter((s) => s.phase === "failed").length;
100036
+ const finished = done + failed;
100037
+ const allDone = finished === total;
100038
+ const bullet = allDone ? chalk.hex(colors.success)("⏺ ") : chalk.hex(colors.roleAssistant)("⏺ ");
100039
+ if (allDone) {
100040
+ const types = new Set(snapshots.map((s) => s.agentName).filter((n) => n !== void 0));
100041
+ const headerLabel = types.size === 1 ? `${String(total)} ${[...types][0]} agents finished` : `${String(total)} agents finished`;
100042
+ const tail = formatHeaderTail(snapshots.reduce((acc, s) => acc + s.toolCount, 0), snapshots.reduce((acc, s) => acc + s.tokens, 0));
100043
+ return `${bullet}${chalk.hex(colors.primary).bold(headerLabel)}${tail}`;
100044
+ }
100045
+ let headerText = `Running ${String(total)} agents`;
100046
+ if (finished > 0) {
100047
+ const running = total - finished;
100048
+ const parts = [];
100049
+ if (done > 0) parts.push(`${String(done)} done`);
100050
+ if (failed > 0) parts.push(`${String(failed)} failed`);
100051
+ if (running > 0) parts.push(`${String(running)} running`);
100052
+ headerText = `Running ${String(total)} agents (${parts.join(", ")})`;
100053
+ }
100054
+ return `${bullet}${chalk.hex(colors.primary).bold(headerText)}`;
100055
+ }
100056
+ appendLines(snap, isLast) {
100057
+ const colors = this.colors;
100058
+ const dim = chalk.dim;
100059
+ const branch1 = isLast ? "└─" : "├─";
100060
+ const agentType = snap.agentName ?? "agent";
100061
+ const desc = snap.toolCallDescription || "(no description)";
100062
+ const tail = formatLineTail(snap, colors);
100063
+ const line1 = ` ${branch1} ${chalk.hex(colors.primary)(agentType)} ${dim(`· ${desc}`)}${formatStats(snap)}${tail}`;
100064
+ this.bodyContainer.addChild(new Text(line1, 0, 0));
100065
+ const branch2 = isLast ? " " : "│ ";
100066
+ if (snap.phase === "failed") {
100067
+ const errLine = (snap.errorText ?? "Failed").split("\n").at(0) ?? "Failed";
100068
+ const errStr = chalk.hex(colors.error)(`Error: ${errLine}`);
100069
+ this.bodyContainer.addChild(new Text(` ${branch2} ${errStr}`, 0, 0));
100070
+ return;
100071
+ }
100072
+ if (snap.phase === "done" || snap.phase === "backgrounded") return;
100073
+ const activity = snap.latestActivity ?? "Initializing…";
100074
+ this.bodyContainer.addChild(new Text(` ${branch2} ${dim(activity)}`, 0, 0));
100075
+ }
100076
+ /**
100077
+ * 给测试 / 开发期使用,立刻冲掉节流计时器并刷新。生产代码不需要主动调。
100078
+ */
100079
+ flushNow() {
100080
+ this.flushRender();
100081
+ }
100082
+ /** 释放节流计时器,避免组件被销毁后还触发刷新。 */
100083
+ dispose() {
100084
+ if (this.throttleTimer !== null) {
100085
+ clearTimeout(this.throttleTimer);
100086
+ this.throttleTimer = null;
100087
+ }
100088
+ for (const e of this.entries) e.tc.setSnapshotListener(void 0);
100089
+ }
100090
+ };
100091
+ function formatStats(snap) {
100092
+ const dim = chalk.dim;
100093
+ return dim(`${` · ${String(snap.toolCount)} tool${snap.toolCount === 1 ? "" : "s"}`}${snap.tokens > 0 ? ` · ${formatTokens(snap.tokens)}` : ""}`);
100094
+ }
100095
+ function formatLineTail(snap, colors) {
100096
+ if (snap.phase === "done") return chalk.dim(" · ") + chalk.hex(colors.success)("✓ Completed");
100097
+ if (snap.phase === "failed") return chalk.dim(" · ") + chalk.hex(colors.error)("✗ Failed");
100098
+ if (snap.phase === "backgrounded") return chalk.dim(" · ◐ backgrounded");
100099
+ return "";
100100
+ }
100101
+ function formatHeaderTail(toolCount, tokens) {
100102
+ const dim = chalk.dim;
100103
+ const parts = [];
100104
+ if (toolCount > 0) parts.push(`${String(toolCount)} tool${toolCount === 1 ? "" : "s"}`);
100105
+ if (tokens > 0) parts.push(formatTokens(tokens));
100106
+ return parts.length > 0 ? dim(` · ${parts.join(" · ")}`) : "";
100107
+ }
100108
+ function formatTokens(n) {
100109
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M tok`;
100110
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k tok`;
100111
+ return `${String(n)} tok`;
100112
+ }
100113
+ //#endregion
99436
100114
  //#region src/tui/actions/replay-ops.ts
99437
100115
  async function hydrateTranscriptFromReplay(state, hooks, sessionId) {
99438
100116
  setState(state, { isReplaying: true }, hooks);
99439
100117
  try {
99440
100118
  const replay = await state.client.replayRecords(sessionId);
99441
100119
  const projection = projectReplayRecords(replay.records);
99442
- for (const entry of projection.entries) appendTranscriptEntry(state, entry);
100120
+ hydrateProjectedEntries(state, projection.entries);
100121
+ state.backgroundAgents = new Set(projection.backgroundAgents);
100122
+ state.footer.setBackgroundAgentCount(state.backgroundAgents.size);
100123
+ state.ui.requestRender();
99443
100124
  if (replay.warnings !== void 0 && replay.warnings.length > 0) emitError(state, `Replay completed with ${String(replay.warnings.length)} warning(s).`);
99444
100125
  return true;
99445
100126
  } catch (error) {
@@ -99457,7 +100138,8 @@ function projectReplayRecords(records) {
99457
100138
  assistant: {
99458
100139
  thinking: [],
99459
100140
  text: []
99460
- }
100141
+ },
100142
+ backgroundAgents: /* @__PURE__ */ new Set()
99461
100143
  };
99462
100144
  for (const envelope of records) {
99463
100145
  if (envelope.source?.kind === "subagent") {
@@ -99467,7 +100149,10 @@ function projectReplayRecords(records) {
99467
100149
  projectMainRecord(state, envelope.record);
99468
100150
  }
99469
100151
  flushAssistant(state);
99470
- return { entries: state.entries };
100152
+ return {
100153
+ entries: state.entries,
100154
+ backgroundAgents: state.backgroundAgents
100155
+ };
99471
100156
  }
99472
100157
  function projectMainRecord(state, rawRecord) {
99473
100158
  const record = asRecord(rawRecord);
@@ -99509,6 +100194,22 @@ function projectMainRecord(state, rawRecord) {
99509
100194
  text: []
99510
100195
  };
99511
100196
  return;
100197
+ case "subagent_spawned": {
100198
+ const data = record.data;
100199
+ if (!isObject(data)) return;
100200
+ if (data["run_in_background"] !== true) return;
100201
+ const agentId = stringValue(data["agent_id"]);
100202
+ if (agentId !== void 0) state.backgroundAgents.add(agentId);
100203
+ return;
100204
+ }
100205
+ case "subagent_completed":
100206
+ case "subagent_failed": {
100207
+ const data = record.data;
100208
+ if (!isObject(data)) return;
100209
+ const agentId = stringValue(data["agent_id"]);
100210
+ if (agentId !== void 0) state.backgroundAgents.delete(agentId);
100211
+ return;
100212
+ }
99512
100213
  default: return;
99513
100214
  }
99514
100215
  }
@@ -99557,15 +100258,19 @@ function projectSubagentRecord(state, envelope) {
99557
100258
  function projectToolCall(state, record) {
99558
100259
  const tool = toolCallFromRecord(record);
99559
100260
  if (tool === void 0) return;
100261
+ const turnId = stringValue(record.turn_id);
100262
+ const step = typeof record.step === "number" ? record.step : void 0;
99560
100263
  const toolCall = {
99561
100264
  id: tool.id,
99562
100265
  name: tool.name,
99563
100266
  args: tool.args,
99564
- description: tool.description
100267
+ description: tool.description,
100268
+ ...step !== void 0 ? { step } : {},
100269
+ ...turnId !== void 0 ? { turnId } : {}
99565
100270
  };
99566
100271
  state.toolCalls.set(tool.id, toolCall);
99567
100272
  state.entries.push(entry("tool_call", "", "plain", {
99568
- turnId: stringValue(record.turn_id),
100273
+ turnId,
99569
100274
  toolCallData: toolCall
99570
100275
  }));
99571
100276
  }
@@ -99680,6 +100385,61 @@ function isObject(value) {
99680
100385
  function stringValue(value) {
99681
100386
  return typeof value === "string" ? value : void 0;
99682
100387
  }
100388
+ /**
100389
+ * 把 projection 出的扁平 entries 注入 live state。其中相邻、同
100390
+ * `(turnId, step)` 的 ≥2 个 Agent tool_call 合并成一个 AgentGroupComponent,
100391
+ * 与 live 路径行为一致。其他 entry 走原本 `appendTranscriptEntry`。
100392
+ *
100393
+ * 注意:和 `tryAttachAgentToolCall` 不同,这里**不**写 `state.pendingAgentGroup`
100394
+ * —— replay 完成后该字段必须是 null(live 事件接管时从 zero 开始)。
100395
+ */
100396
+ function hydrateProjectedEntries(state, entries) {
100397
+ let i = 0;
100398
+ while (i < entries.length) {
100399
+ const cur = entries[i];
100400
+ if (cur === void 0) {
100401
+ i += 1;
100402
+ continue;
100403
+ }
100404
+ const tc = cur.toolCallData;
100405
+ if (cur.kind === "tool_call" && tc !== void 0 && tc.name === "Agent" && tc.step !== void 0) {
100406
+ const batch = [cur];
100407
+ let j = i + 1;
100408
+ while (j < entries.length) {
100409
+ const next = entries[j];
100410
+ if (next === void 0) break;
100411
+ const nextTc = next.toolCallData;
100412
+ if (next.kind === "tool_call" && nextTc !== void 0 && nextTc.name === "Agent" && nextTc.step === tc.step && nextTc.turnId === tc.turnId) {
100413
+ batch.push(next);
100414
+ j++;
100415
+ continue;
100416
+ }
100417
+ break;
100418
+ }
100419
+ if (batch.length >= 2) {
100420
+ attachAgentBatchAsGroup(state, batch);
100421
+ i = j;
100422
+ continue;
100423
+ }
100424
+ }
100425
+ appendTranscriptEntry(state, cur);
100426
+ i++;
100427
+ }
100428
+ }
100429
+ function attachAgentBatchAsGroup(state, batch) {
100430
+ const group = new AgentGroupComponent(state.colors, state.ui);
100431
+ state.transcriptContainer.addChild(group);
100432
+ for (const item of batch) {
100433
+ const tc = item.toolCallData;
100434
+ if (tc === void 0) continue;
100435
+ state.transcriptEntries.push(item);
100436
+ const component = new ToolCallComponent(tc, tc.result, state.colors, state.ui, state.markdownTheme);
100437
+ if (state.toolOutputExpanded) component.setExpanded(true);
100438
+ state.pendingToolComponents.set(tc.id, component);
100439
+ group.attach(tc.id, component);
100440
+ }
100441
+ state.ui.requestRender();
100442
+ }
99683
100443
  //#endregion
99684
100444
  //#region src/tui/utils/proctitle.ts
99685
100445
  /**
@@ -99842,6 +100602,8 @@ function releaseSessionSideEffects(state) {
99842
100602
  state.currentTurnId = void 0;
99843
100603
  state.assistantDraft = "";
99844
100604
  state.assistantStreamActive = false;
100605
+ state.streamingComponent = void 0;
100606
+ state.streamingTranscriptEntry = void 0;
99845
100607
  state.thinkingDraft = "";
99846
100608
  disposeActiveThinkingComponent(state);
99847
100609
  }
@@ -99909,6 +100671,100 @@ var CompactionComponent = class extends Container {
99909
100671
  }
99910
100672
  };
99911
100673
  //#endregion
100674
+ //#region src/tui/actions/agent-group-ops.ts
100675
+ /**
100676
+ * Agent 合并组挂载策略 & 打断规则。
100677
+ *
100678
+ * `tryAttachAgentToolCall`:在 `onToolCallStart` 里给 Agent 工具走的拦截
100679
+ * 点。如果当前 step 已有 `solo` 但还没升级成 group,第二个同 step 的
100680
+ * Agent 到达时会就地把 solo 替换成 `AgentGroupComponent` 并把两者塞进去。
100681
+ *
100682
+ * `closePendingAgentGroup`:所有打断点(非 Agent 工具 / step.begin /
100683
+ * turn.end / step.interrupted / session.error / assistant text)调用,
100684
+ * 清空 pending 指针。已挂出去的 group 不被销毁——它在 transcript 里
100685
+ * 作为独立组件继续工作,只是不再有新的 Agent 合并进来。
100686
+ */
100687
+ /**
100688
+ * 把一个新创建的 Agent ToolCallComponent 挂到合适位置(合并组或单卡)。
100689
+ * 调用方负责创建 `tc` 与登记 `pendingToolComponents`,本函数只决定
100690
+ * 挂载策略并把 tc 放到 transcript 里。
100691
+ *
100692
+ * @returns true 如果走了 group 路径(已经挂好),调用方不应再调
100693
+ * `transcriptContainer.addChild(tc)`;false 表示让调用方按
100694
+ * 原本的单卡路径自行 addChild。
100695
+ */
100696
+ function tryAttachAgentToolCall(state, toolCall, tc) {
100697
+ if (toolCall.name !== "Agent") {
100698
+ closePendingAgentGroup(state);
100699
+ return false;
100700
+ }
100701
+ const step = toolCall.step ?? state.currentStep;
100702
+ const turnId = toolCall.turnId ?? state.currentTurnId;
100703
+ const pending = state.pendingAgentGroup;
100704
+ if (pending !== null && (pending.step !== step || pending.turnId !== turnId)) state.pendingAgentGroup = null;
100705
+ const cur = state.pendingAgentGroup;
100706
+ if (cur === null) {
100707
+ state.pendingAgentGroup = {
100708
+ step,
100709
+ turnId,
100710
+ solo: tc
100711
+ };
100712
+ state.transcriptContainer.addChild(tc);
100713
+ state.ui.requestRender();
100714
+ return true;
100715
+ }
100716
+ if (cur.group !== void 0) {
100717
+ cur.group.attach(toolCall.id, tc);
100718
+ return true;
100719
+ }
100720
+ const solo = cur.solo;
100721
+ if (solo === void 0) {
100722
+ state.pendingAgentGroup = {
100723
+ step,
100724
+ turnId,
100725
+ solo: tc
100726
+ };
100727
+ state.transcriptContainer.addChild(tc);
100728
+ state.ui.requestRender();
100729
+ return true;
100730
+ }
100731
+ const group = upgradeSoloToGroup$1(state, solo);
100732
+ group.attach(toolCall.id, tc);
100733
+ state.pendingAgentGroup = {
100734
+ step,
100735
+ turnId,
100736
+ group
100737
+ };
100738
+ state.ui.requestRender();
100739
+ return true;
100740
+ }
100741
+ /**
100742
+ * 任意打断信号触发本函数。仅清空 pending 指针——已挂出去的 solo 单卡
100743
+ * 或 group 都保持原状。
100744
+ */
100745
+ function closePendingAgentGroup(state) {
100746
+ if (state.pendingAgentGroup === null) return;
100747
+ state.pendingAgentGroup = null;
100748
+ }
100749
+ /**
100750
+ * 把已挂在 transcriptContainer 的 solo `ToolCallComponent` 原地替换成新建
100751
+ * 的 `AgentGroupComponent`,并把 solo 塞进 group 里继续做"隐身状态容器"。
100752
+ *
100753
+ * pi-tui 没暴露 insertChildAt / replaceChildAt,但 `Container.children`
100754
+ * 是 public 数组。这里直接 splice,再调一次 `invalidate()` 触发差分渲染。
100755
+ */
100756
+ function upgradeSoloToGroup$1(state, solo) {
100757
+ const group = new AgentGroupComponent(state.colors, state.ui);
100758
+ const children = state.transcriptContainer.children;
100759
+ const idx = children.indexOf(solo);
100760
+ if (idx >= 0) {
100761
+ children[idx] = group;
100762
+ state.transcriptContainer.invalidate();
100763
+ } else state.transcriptContainer.addChild(group);
100764
+ group.attach(solo.toolCallView.id, solo);
100765
+ return group;
100766
+ }
100767
+ //#endregion
99912
100768
  //#region src/tui/actions/plan-ops.ts
99913
100769
  async function refreshPlanFromCore(state) {
99914
100770
  try {
@@ -99918,6 +100774,218 @@ async function refreshPlanFromCore(state) {
99918
100774
  }
99919
100775
  }
99920
100776
  //#endregion
100777
+ //#region src/tui/components/messages/read-group.ts
100778
+ const THROTTLE_MS = 200;
100779
+ var ReadGroupComponent = class extends Container {
100780
+ entries = [];
100781
+ headerText;
100782
+ bodyContainer;
100783
+ throttleTimer = null;
100784
+ lastFlushPhases = /* @__PURE__ */ new Map();
100785
+ constructor(colors, ui) {
100786
+ super();
100787
+ this.colors = colors;
100788
+ this.ui = ui;
100789
+ this.addChild(new Spacer(1));
100790
+ this.headerText = new Text("", 0, 0);
100791
+ this.addChild(this.headerText);
100792
+ this.bodyContainer = new Container();
100793
+ this.addChild(this.bodyContainer);
100794
+ }
100795
+ size() {
100796
+ return this.entries.length;
100797
+ }
100798
+ /**
100799
+ * 把一个独立 `ToolCallComponent` 借进 group 做隐身状态容器。注册
100800
+ * snapshot 监听 → 状态有变即触发节流刷新。多次 attach 同一个 toolCallId
100801
+ * 是 noop。
100802
+ */
100803
+ attach(toolCallId, tc) {
100804
+ if (this.entries.some((e) => e.toolCallId === toolCallId)) return;
100805
+ this.entries.push({
100806
+ toolCallId,
100807
+ tc
100808
+ });
100809
+ tc.setSnapshotListener(() => {
100810
+ this.scheduleRender();
100811
+ });
100812
+ this.flushRender();
100813
+ }
100814
+ /**
100815
+ * pending → done/failed 是用户最关心的转变,立刷新;其它情况节流。
100816
+ */
100817
+ scheduleRender() {
100818
+ if (this.detectPhaseTransition()) {
100819
+ this.flushRender();
100820
+ return;
100821
+ }
100822
+ if (this.throttleTimer !== null) return;
100823
+ this.throttleTimer = setTimeout(() => {
100824
+ this.throttleTimer = null;
100825
+ this.flushRender();
100826
+ }, THROTTLE_MS);
100827
+ }
100828
+ detectPhaseTransition() {
100829
+ for (const e of this.entries) {
100830
+ const phase = e.tc.getReadSnapshot().phase;
100831
+ if (this.lastFlushPhases.get(e.toolCallId) !== phase) return true;
100832
+ }
100833
+ return false;
100834
+ }
100835
+ flushRender() {
100836
+ if (this.throttleTimer !== null) {
100837
+ clearTimeout(this.throttleTimer);
100838
+ this.throttleTimer = null;
100839
+ }
100840
+ const snapshots = this.entries.map((e) => e.tc.getReadSnapshot());
100841
+ let pending = 0;
100842
+ let failed = 0;
100843
+ let totalLines = 0;
100844
+ for (const snap of snapshots) if (snap.phase === "pending") pending += 1;
100845
+ else if (snap.phase === "failed") failed += 1;
100846
+ else totalLines += snap.lines;
100847
+ this.headerText.setText(this.buildHeader(snapshots.length, pending, failed, totalLines));
100848
+ this.bodyContainer.clear();
100849
+ snapshots.forEach((snap, idx) => {
100850
+ const isLast = idx === snapshots.length - 1;
100851
+ this.bodyContainer.addChild(new Text(this.buildBodyLine(snap, isLast), 0, 0));
100852
+ });
100853
+ this.lastFlushPhases.clear();
100854
+ this.entries.forEach((entry, i) => {
100855
+ const snap = snapshots[i];
100856
+ if (snap !== void 0) this.lastFlushPhases.set(entry.toolCallId, snap.phase);
100857
+ });
100858
+ this.invalidate();
100859
+ this.ui?.requestRender();
100860
+ }
100861
+ buildHeader(total, pending, failed, totalLines) {
100862
+ const colors = this.colors;
100863
+ const dim = chalk.dim;
100864
+ if (pending > 0) return `${chalk.hex(colors.roleAssistant)("⏺ ")}${chalk.hex(colors.primary).bold(`Reading ${String(total)} files…`)}`;
100865
+ if (failed === total) return `${chalk.hex(colors.error)("✗ ")}${chalk.hex(colors.error).bold(`Read ${String(total)} files`)}${chalk.hex(colors.error)(" · failed")}`;
100866
+ return `${chalk.hex(colors.success)("⏺ ")}${chalk.hex(colors.primary).bold(`Read ${String(total)} files`)}${dim(` · ${String(totalLines)} ${totalLines === 1 ? "line" : "lines"}`)}${failed > 0 ? chalk.hex(colors.error)(` · ${String(failed)} failed`) : ""}`;
100867
+ }
100868
+ buildBodyLine(snap, isLast) {
100869
+ const colors = this.colors;
100870
+ const dim = chalk.dim;
100871
+ const branch = isLast ? "└─" : "├─";
100872
+ const path = snap.filePath ?? "(unknown)";
100873
+ const pathPart = chalk.hex(colors.text)(path);
100874
+ let tail;
100875
+ if (snap.phase === "pending") tail = dim(" · reading…");
100876
+ else if (snap.phase === "failed") tail = chalk.hex(colors.error)(" · failed");
100877
+ else tail = dim(` · ${String(snap.lines)} ${snap.lines === 1 ? "line" : "lines"}`);
100878
+ return ` ${branch} ${pathPart}${tail}`;
100879
+ }
100880
+ /** 给测试 / 开发期使用,立刻冲掉节流计时器并刷新。生产代码不需要主动调。 */
100881
+ flushNow() {
100882
+ this.flushRender();
100883
+ }
100884
+ /** 释放节流计时器,避免组件被销毁后还触发刷新。 */
100885
+ dispose() {
100886
+ if (this.throttleTimer !== null) {
100887
+ clearTimeout(this.throttleTimer);
100888
+ this.throttleTimer = null;
100889
+ }
100890
+ for (const e of this.entries) e.tc.setSnapshotListener(void 0);
100891
+ }
100892
+ };
100893
+ //#endregion
100894
+ //#region src/tui/actions/read-group-ops.ts
100895
+ /**
100896
+ * Read 合并组挂载策略 & 打断规则。
100897
+ *
100898
+ * 与 `agent-group-ops.ts` 完全同构,只是 batch key 从「Agent 工具」换成
100899
+ * 「Read 工具」。
100900
+ *
100901
+ * `tryAttachReadToolCall`:在 `onToolCallStart` 里给 Read 工具走的拦截
100902
+ * 点。如果当前 step 已有 `solo` 但还没升级成 group,第二个同 step 的
100903
+ * Read 到达时会就地把 solo 替换成 `ReadGroupComponent` 并把两者塞进去。
100904
+ *
100905
+ * `closePendingReadGroup`:所有打断点(非 Read 工具 / step.begin /
100906
+ * turn.end / step.interrupted / session.error / assistant text /
100907
+ * thinking)调用,清空 pending 指针。已挂出去的 group 不被销毁——它在
100908
+ * transcript 里作为独立组件继续工作,只是不再有新的 Read 合并进来。
100909
+ */
100910
+ /**
100911
+ * 把一个新创建的 Read ToolCallComponent 挂到合适位置(合并组或单卡)。
100912
+ * 调用方负责创建 `tc` 与登记 `pendingToolComponents`,本函数只决定
100913
+ * 挂载策略并把 tc 放到 transcript 里。
100914
+ *
100915
+ * @returns true 如果走了 group 路径(已经挂好),调用方不应再调
100916
+ * `transcriptContainer.addChild(tc)`;false 表示让调用方按
100917
+ * 原本的单卡路径自行 addChild。
100918
+ */
100919
+ function tryAttachReadToolCall(state, toolCall, tc) {
100920
+ if (toolCall.name !== "Read") {
100921
+ closePendingReadGroup(state);
100922
+ return false;
100923
+ }
100924
+ const step = toolCall.step ?? state.currentStep;
100925
+ const turnId = toolCall.turnId ?? state.currentTurnId;
100926
+ const pending = state.pendingReadGroup;
100927
+ if (pending !== null && (pending.step !== step || pending.turnId !== turnId)) state.pendingReadGroup = null;
100928
+ const cur = state.pendingReadGroup;
100929
+ if (cur === null) {
100930
+ state.pendingReadGroup = {
100931
+ step,
100932
+ turnId,
100933
+ solo: tc
100934
+ };
100935
+ state.transcriptContainer.addChild(tc);
100936
+ state.ui.requestRender();
100937
+ return true;
100938
+ }
100939
+ if (cur.group !== void 0) {
100940
+ cur.group.attach(toolCall.id, tc);
100941
+ return true;
100942
+ }
100943
+ const solo = cur.solo;
100944
+ if (solo === void 0) {
100945
+ state.pendingReadGroup = {
100946
+ step,
100947
+ turnId,
100948
+ solo: tc
100949
+ };
100950
+ state.transcriptContainer.addChild(tc);
100951
+ state.ui.requestRender();
100952
+ return true;
100953
+ }
100954
+ const group = upgradeSoloToGroup(state, solo);
100955
+ group.attach(toolCall.id, tc);
100956
+ state.pendingReadGroup = {
100957
+ step,
100958
+ turnId,
100959
+ group
100960
+ };
100961
+ state.ui.requestRender();
100962
+ return true;
100963
+ }
100964
+ /**
100965
+ * 任意打断信号触发本函数。仅清空 pending 指针——已挂出去的 solo 单卡
100966
+ * 或 group 都保持原状。
100967
+ */
100968
+ function closePendingReadGroup(state) {
100969
+ if (state.pendingReadGroup === null) return;
100970
+ state.pendingReadGroup = null;
100971
+ }
100972
+ /**
100973
+ * 把已挂在 transcriptContainer 的 solo `ToolCallComponent` 原地替换成新建
100974
+ * 的 `ReadGroupComponent`,并把 solo 塞进 group 里继续做隐身状态容器。
100975
+ * 与 agent-group-ops 同构:直接 splice transcriptContainer.children。
100976
+ */
100977
+ function upgradeSoloToGroup(state, solo) {
100978
+ const group = new ReadGroupComponent(state.colors, state.ui);
100979
+ const children = state.transcriptContainer.children;
100980
+ const idx = children.indexOf(solo);
100981
+ if (idx >= 0) {
100982
+ children[idx] = group;
100983
+ state.transcriptContainer.invalidate();
100984
+ } else state.transcriptContainer.addChild(group);
100985
+ group.attach(solo.toolCallView.id, solo);
100986
+ return group;
100987
+ }
100988
+ //#endregion
99921
100989
  //#region src/tui/actions/stream-ops.ts
99922
100990
  /**
99923
100991
  * Streaming / tool-call / compaction render hooks.
@@ -99927,11 +100995,23 @@ async function refreshPlanFromCore(state) {
99927
100995
  * on `state` is touched — state-ops.ts owns AppState / LivePane.
99928
100996
  */
99929
100997
  function onStreamingTextStart(state) {
100998
+ closePendingAgentGroup(state);
100999
+ closePendingReadGroup(state);
101000
+ const entry = {
101001
+ id: nextTranscriptId(),
101002
+ kind: "assistant",
101003
+ turnId: state.currentTurnId,
101004
+ renderMode: "markdown",
101005
+ content: ""
101006
+ };
99930
101007
  state.streamingComponent = new AssistantMessageComponent(state.markdownTheme, state.colors);
101008
+ state.streamingTranscriptEntry = entry;
101009
+ state.transcriptEntries.push(entry);
99931
101010
  state.transcriptContainer.addChild(state.streamingComponent);
99932
101011
  state.ui.requestRender();
99933
101012
  }
99934
101013
  function onStreamingTextUpdate(state, fullText) {
101014
+ if (state.streamingTranscriptEntry !== void 0) state.streamingTranscriptEntry.content = fullText;
99935
101015
  if (state.streamingComponent) {
99936
101016
  state.streamingComponent.updateContent(fullText);
99937
101017
  state.ui.requestRender();
@@ -99939,9 +101019,12 @@ function onStreamingTextUpdate(state, fullText) {
99939
101019
  }
99940
101020
  function onStreamingTextEnd(state) {
99941
101021
  state.streamingComponent = void 0;
101022
+ state.streamingTranscriptEntry = void 0;
99942
101023
  }
99943
101024
  function onThinkingUpdate(state, fullText) {
99944
101025
  if (state.activeThinkingComponent === void 0) {
101026
+ closePendingAgentGroup(state);
101027
+ closePendingReadGroup(state);
99945
101028
  state.activeThinkingComponent = new ThinkingComponent(fullText, state.colors, true, "live", state.ui);
99946
101029
  state.transcriptContainer.addChild(state.activeThinkingComponent);
99947
101030
  } else state.activeThinkingComponent.setText(fullText);
@@ -99958,8 +101041,14 @@ function onToolCallStart(state, toolCall) {
99958
101041
  const tc = new ToolCallComponent(toolCall, void 0, state.colors, state.ui, state.markdownTheme);
99959
101042
  if (state.toolOutputExpanded) tc.setExpanded(true);
99960
101043
  state.pendingToolComponents.set(toolCall.id, tc);
99961
- state.transcriptContainer.addChild(tc);
99962
- state.ui.requestRender();
101044
+ if (toolCall.name !== "Agent") closePendingAgentGroup(state);
101045
+ if (toolCall.name !== "Read") closePendingReadGroup(state);
101046
+ let handled = tryAttachAgentToolCall(state, toolCall, tc);
101047
+ if (!handled) handled = tryAttachReadToolCall(state, toolCall, tc);
101048
+ if (!handled) {
101049
+ state.transcriptContainer.addChild(tc);
101050
+ state.ui.requestRender();
101051
+ }
99963
101052
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") (async () => {
99964
101053
  const snapshot = await refreshPlanFromCore(state);
99965
101054
  tc.setPlanInfo(snapshot);
@@ -100342,6 +101431,9 @@ async function dispatchSlashCommand(input, state, buildCtx, addEntry) {
100342
101431
  //#region src/tui/handlers/turn-lifecycle.ts
100343
101432
  function handleTurnBegin(ectx, _data) {
100344
101433
  ectx.state.streamingToolCallArguments.clear();
101434
+ ectx.state.currentStep = 0;
101435
+ closePendingAgentGroup(ectx.state);
101436
+ closePendingReadGroup(ectx.state);
100345
101437
  ectx.patchLivePane({
100346
101438
  mode: "waiting",
100347
101439
  thinkingText: "",
@@ -100360,9 +101452,15 @@ function handleTurnEnd(ectx, _data, sendQueued) {
100360
101452
  const todos = ectx.state.todoPanel.getTodos();
100361
101453
  if (todos.length > 0 && todos.every((t) => t.status === "done")) ectx.setTodoList([]);
100362
101454
  ectx.state.streamingToolCallArguments.clear();
101455
+ closePendingAgentGroup(ectx.state);
101456
+ closePendingReadGroup(ectx.state);
100363
101457
  finalizeTurn(ectx, sendQueued);
100364
101458
  }
100365
- function handleStepBegin(ectx) {
101459
+ function handleStepBegin(ectx, data) {
101460
+ ectx.state.currentStep = data.step;
101461
+ closePendingAgentGroup(ectx.state);
101462
+ closePendingReadGroup(ectx.state);
101463
+ flushTurnBuffers(ectx, "waiting");
100366
101464
  ectx.patchLivePane({
100367
101465
  mode: "waiting",
100368
101466
  pendingToolCall: null,
@@ -100375,6 +101473,8 @@ function handleStepBegin(ectx) {
100375
101473
  });
100376
101474
  }
100377
101475
  function handleStepInterrupted(ectx, data) {
101476
+ closePendingAgentGroup(ectx.state);
101477
+ closePendingReadGroup(ectx.state);
100378
101478
  flushTurnBuffers(ectx, "idle");
100379
101479
  const reason = data.reason;
100380
101480
  if (reason === "error") return;
@@ -100466,7 +101566,9 @@ function handleToolCall(ectx, data) {
100466
101566
  id: data.id,
100467
101567
  name: data.name,
100468
101568
  args: data.args,
100469
- description: data.description
101569
+ description: data.description,
101570
+ step: ectx.state.currentStep,
101571
+ turnId: ectx.state.currentTurnId
100470
101572
  };
100471
101573
  const existing = ectx.state.activeToolCalls.get(data.id);
100472
101574
  ectx.state.activeToolCalls.set(data.id, toolCall);
@@ -100475,7 +101577,7 @@ function handleToolCall(ectx, data) {
100475
101577
  if (existingComponent !== void 0) existingComponent.updateToolCall(toolCall);
100476
101578
  else if (existing === void 0) {
100477
101579
  flushTurnBuffers(ectx, "tool");
100478
- ectx.onToolCallStart(toolCall);
101580
+ if (data.name !== "Agent") ectx.onToolCallStart(toolCall);
100479
101581
  }
100480
101582
  ectx.patchLivePane({
100481
101583
  mode: "tool",
@@ -100498,13 +101600,15 @@ function handleToolCallDelta(ectx, data) {
100498
101600
  id,
100499
101601
  name,
100500
101602
  args: parseStreamingArgs(argumentsText),
100501
- streamingArguments: argumentsText
101603
+ streamingArguments: argumentsText,
101604
+ step: ectx.state.currentStep,
101605
+ turnId: ectx.state.currentTurnId
100502
101606
  };
100503
101607
  ectx.state.activeToolCalls.set(id, toolCall);
100504
- if (ectx.state.thinkingDraft.length > 0) flushTurnBuffers(ectx, "tool");
101608
+ if (ectx.state.thinkingDraft.length > 0 || ectx.state.assistantStreamActive) flushTurnBuffers(ectx, "tool");
100505
101609
  const existingComponent = ectx.state.pendingToolComponents.get(id);
100506
101610
  if (existingComponent !== void 0) existingComponent.updateToolCall(toolCall);
100507
- else ectx.onToolCallStart(toolCall);
101611
+ else if (name !== "Agent") ectx.onToolCallStart(toolCall);
100508
101612
  ectx.patchLivePane({
100509
101613
  mode: "tool",
100510
101614
  pendingToolCall: toolCall,
@@ -100575,6 +101679,8 @@ function handleSessionMetaChanged(ectx, data) {
100575
101679
  for (const fn of ectx.state.sessionMetaChangedListeners) fn(data);
100576
101680
  }
100577
101681
  function handleSessionError(ectx, data) {
101682
+ closePendingAgentGroup(ectx.state);
101683
+ closePendingReadGroup(ectx.state);
100578
101684
  flushTurnBuffers(ectx, "idle");
100579
101685
  const detail = data.error_type !== void 0 ? ` (${data.error_type})` : "";
100580
101686
  ectx.addTranscriptEntry(makeEntry(ectx, "status", `Error${detail}: ${data.error}`, "plain", { color: ectx.colors.error }));
@@ -100643,7 +101749,7 @@ function dispatchEvent(event, ectx, sendQueued) {
100643
101749
  handleTurnEnd(ectx, event.data, sendQueued);
100644
101750
  break;
100645
101751
  case "step.begin":
100646
- handleStepBegin(ectx);
101752
+ handleStepBegin(ectx, event.data);
100647
101753
  break;
100648
101754
  case "step.interrupted":
100649
101755
  handleStepInterrupted(ectx, event.data);