@ai-sdk/harness-claude-code 1.0.41 → 1.0.42

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.
@@ -34,6 +34,15 @@ import { argv, stdout } from 'node:process';
34
34
  import * as claudeAgentSdk from '@anthropic-ai/claude-agent-sdk';
35
35
  import * as mcpServerModule from '@modelcontextprotocol/sdk/server/mcp.js';
36
36
  import { toClaudeSkillsOption } from './claude-skills-option';
37
+ import {
38
+ createClaudeStreamEventState,
39
+ createEmitStreamEvent,
40
+ defaultUsage,
41
+ emitFinishStep,
42
+ finishApprovalStep,
43
+ mapUsage,
44
+ type ClaudeMessage,
45
+ } from './create-emit-stream-event';
37
46
  import { jsonSchemaToZodShape } from './json-schema-to-zod';
38
47
 
39
48
  /*
@@ -90,7 +99,6 @@ const PUBLIC_TO_NATIVE: Readonly<Record<string, string>> = {
90
99
  };
91
100
 
92
101
  const PUBLIC_TOOL_NAMES = Object.keys(PUBLIC_TO_NATIVE);
93
- const UNRECOVERABLE_API_RETRY_STATUSES = new Set([401, 403, 404]);
94
102
 
95
103
  const NATIVE_TOOL_KINDS: Readonly<
96
104
  Record<string, 'readonly' | 'edit' | 'bash'>
@@ -305,50 +313,7 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
305
313
  });
306
314
  }
307
315
 
308
- /*
309
- * Map of native tool-use id → tool name. Claude assistant messages emit
310
- * `tool_use` blocks with both `id` and `name`; the matching `tool_result`
311
- * block on a later user message carries only `tool_use_id`, so without this
312
- * map the tool-result event would have to emit `toolName: 'unknown'`.
313
- */
314
- const nativeToolCallNames = new Map<string, string>();
315
- const approvalRequestedToolUseIds = new Set<string>();
316
- const partialBlocks = new Map<
317
- number,
318
- { id: string; kind: 'text' | 'thinking' }
319
- >();
320
- let stepUsage: Record<string, unknown> | undefined;
321
- let pendingStepToolUseIds = new Set<string>();
322
- let pendingStepUsage: Record<string, unknown> | undefined;
323
- let stepOpen = false;
324
-
325
- const emitFinishStep = (usage: Record<string, unknown> | undefined): void => {
326
- emit({
327
- type: 'finish-step',
328
- finishReason: { unified: 'stop', raw: 'stop' },
329
- usage: usage ?? defaultUsage(),
330
- });
331
- stepUsage = usage ?? stepUsage;
332
- pendingStepUsage = undefined;
333
- pendingStepToolUseIds = new Set();
334
- stepOpen = false;
335
- };
336
-
337
- const closeStepIfReady = (): void => {
338
- if (!stepOpen || pendingStepToolUseIds.size > 0 || partialBlocks.size > 0) {
339
- return;
340
- }
341
- emitFinishStep(pendingStepUsage);
342
- };
343
-
344
- /*
345
- * Tool-use ids that originated from the MCP server hosting user-supplied
346
- * tools. The MCP handler emits its own `tool-call`/`tool-result` pair with
347
- * the user-facing tool name and a synthetic id, so the duplicate
348
- * `tool_result` block Claude reports for the underlying native id must be
349
- * suppressed.
350
- */
351
- const mcpToolUseIds = new Set<string>();
316
+ const streamEventState = createClaudeStreamEventState();
352
317
 
353
318
  const mcpServers: Record<string, unknown> = {};
354
319
  if (start.tools && start.tools.length > 0) {
@@ -415,12 +380,10 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
415
380
  turn,
416
381
  emit,
417
382
  finishApprovalStep: approvalId => {
418
- stepOpen = true;
419
- pendingStepToolUseIds.delete(approvalId);
420
- closeStepIfReady();
383
+ finishApprovalStep({ state: streamEventState, emit, approvalId });
421
384
  },
422
- nativeToolCallNames,
423
- approvalRequestedToolUseIds,
385
+ nativeToolCallNames: streamEventState.nativeToolCallNames,
386
+ approvalRequestedToolUseIds: streamEventState.approvalRequestedToolUseIds,
424
387
  });
425
388
 
426
389
  const q = claudeSdk.query({
@@ -467,15 +430,13 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
467
430
 
468
431
  let turnUsage: Record<string, unknown> | undefined;
469
432
  let totalCostUsd: number | undefined;
470
- let observedTerminalError: string | undefined;
471
433
  let emittedTerminalError = false;
472
434
  let emittedTerminalFinish = false;
473
- let streamStarted = false;
474
435
 
475
436
  const emitTerminalError = (message: string | undefined): void => {
476
437
  const normalized = message?.trim();
477
438
  if (!normalized || emittedTerminalError || emittedTerminalFinish) return;
478
- observedTerminalError = normalized;
439
+ streamEventState.observedTerminalError = normalized;
479
440
  emittedTerminalError = true;
480
441
  turn.emitError({
481
442
  error: normalized,
@@ -485,182 +446,28 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
485
446
  abortCtl.abort();
486
447
  };
487
448
 
449
+ const emitStreamEvent = createEmitStreamEvent({
450
+ state: streamEventState,
451
+ emit,
452
+ emitWarning: turn.emitWarning,
453
+ emitTerminalError,
454
+ onCompactionBoundary: boundary => compaction.onBoundary(boundary),
455
+ toCommonName,
456
+ });
457
+
488
458
  try {
489
459
  for await (const msg of q as AsyncIterable<ClaudeMessage>) {
490
460
  if (abortCtl.signal.aborted) break;
491
461
 
492
462
  const type = msg.type;
493
463
 
494
- // Emit `stream-start` once, on the first message, carrying the model the
495
- // CLI resolved to (the `system`/`init` message reports it — this is the
496
- // default model when none was configured).
497
- if (!streamStarted) {
498
- const initModel =
499
- type === 'system' &&
500
- msg.subtype === 'init' &&
501
- typeof (msg as { model?: unknown }).model === 'string'
502
- ? (msg as { model: string }).model
503
- : undefined;
504
- emit({
505
- type: 'stream-start',
506
- ...(initModel ? { modelId: initModel } : {}),
507
- });
508
- streamStarted = true;
509
- }
510
-
511
- if (type === 'system' && msg.subtype === 'api_retry') {
512
- if (
513
- typeof msg.error_status === 'number' &&
514
- UNRECOVERABLE_API_RETRY_STATUSES.has(msg.error_status)
515
- ) {
516
- emitTerminalError(
517
- `HTTP ${msg.error_status}: ${
518
- msg.error ?? 'provider request failed'
519
- }`,
520
- );
521
- continue;
522
- }
523
-
524
- turn.emitWarning({ message: formatApiRetryWarning(msg) });
525
- continue;
526
- }
527
-
528
- if (typeof msg.error === 'string' && msg.error.trim()) {
529
- observedTerminalError = msg.error.trim();
530
- }
531
-
532
- if (
533
- type === 'auth_status' &&
534
- typeof msg.error === 'string' &&
535
- msg.error.trim()
536
- ) {
537
- emitTerminalError(msg.error);
538
- continue;
539
- }
540
-
541
- if (
542
- type === 'system' &&
543
- msg.subtype === 'task_updated' &&
544
- msg.patch?.status === 'failed' &&
545
- typeof msg.patch.error === 'string'
546
- ) {
547
- emitTerminalError(msg.patch.error);
548
- continue;
549
- }
550
-
551
- if (type === 'system' && msg.subtype === 'compact_boundary') {
552
- const meta = msg.compact_metadata;
553
- if (meta) {
554
- compaction.onBoundary({
555
- trigger: meta.trigger,
556
- ...(typeof meta.pre_tokens === 'number'
557
- ? { tokensBefore: meta.pre_tokens }
558
- : {}),
559
- ...(typeof meta.post_tokens === 'number'
560
- ? { tokensAfter: meta.post_tokens }
561
- : {}),
562
- });
563
- }
564
- continue;
565
- }
566
-
567
- if (type === 'stream_event') {
568
- handleStreamEvent(msg.event, partialBlocks, emit);
569
- continue;
570
- }
571
-
572
- if (type === 'assistant' && msg.message?.content) {
573
- const usage = mapUsage(msg.message.usage);
574
- const toolUseIds: string[] = [];
575
- let opensStep = false;
576
- for (const block of msg.message.content) {
577
- if (
578
- block.type === 'tool_use' &&
579
- typeof block.id === 'string' &&
580
- typeof block.name === 'string'
581
- ) {
582
- toolUseIds.push(block.id);
583
- const mcpPrefix = 'mcp__harness-tools__';
584
- if (block.name.startsWith(mcpPrefix)) {
585
- pendingStepToolUseIds.add(block.id);
586
- mcpToolUseIds.add(block.id);
587
- opensStep = true;
588
- continue;
589
- }
590
- nativeToolCallNames.set(block.id, block.name);
591
- if (approvalRequestedToolUseIds.has(block.id)) {
592
- continue;
593
- }
594
- pendingStepToolUseIds.add(block.id);
595
- opensStep = true;
596
- emit({
597
- type: 'tool-call',
598
- toolCallId: block.id,
599
- toolName: toCommonName(block.name),
600
- nativeName: block.name,
601
- input: JSON.stringify(block.input ?? {}),
602
- providerExecuted: true,
603
- });
604
- }
605
- }
606
- if (opensStep || toolUseIds.length === 0) {
607
- stepOpen = true;
608
- if (usage) pendingStepUsage = usage;
609
- }
610
- continue;
611
- }
612
-
613
- if (type === 'user' && msg.message?.content) {
614
- for (const block of msg.message.content) {
615
- if (
616
- block.type === 'tool_result' &&
617
- typeof block.tool_use_id === 'string'
618
- ) {
619
- if (mcpToolUseIds.has(block.tool_use_id)) {
620
- mcpToolUseIds.delete(block.tool_use_id);
621
- pendingStepToolUseIds.delete(block.tool_use_id);
622
- continue;
623
- }
624
- approvalRequestedToolUseIds.delete(block.tool_use_id);
625
- const nativeName =
626
- nativeToolCallNames.get(block.tool_use_id) ?? 'unknown';
627
- nativeToolCallNames.delete(block.tool_use_id);
628
- const toolName = toCommonName(nativeName);
629
- const isError = !!block.is_error;
630
- const content = stringifyContent(block.content);
631
- /*
632
- * Claude Code's Bash tool does not report the command's real
633
- * numeric exit code — the SDK exposes only stdout/stderr text and
634
- * an is_error flag. Consumers (and the example UI) render bash
635
- * failures from an `exitCode` field on a structured result, the
636
- * shape Codex's shell tool provides natively. To match it, derive
637
- * a binary code from is_error: 1 on failure, 0 on success. This is
638
- * a stand-in for failed/succeeded, not the process's true exit
639
- * status.
640
- */
641
- const result =
642
- toolName === 'bash'
643
- ? { exitCode: isError ? 1 : 0, stdout: content }
644
- : content;
645
- emit({
646
- type: 'tool-result',
647
- toolCallId: block.tool_use_id,
648
- toolName,
649
- result,
650
- isError,
651
- });
652
- pendingStepToolUseIds.delete(block.tool_use_id);
653
- }
654
- }
655
- closeStepIfReady();
656
- continue;
657
- }
464
+ emitStreamEvent(msg);
658
465
 
659
466
  if (type === 'result') {
660
467
  if (msg.subtype === 'success') {
661
468
  const emptyResult = !msg.result?.trim?.();
662
- if (emptyResult && observedTerminalError) {
663
- emitTerminalError(observedTerminalError);
469
+ if (emptyResult && streamEventState.observedTerminalError) {
470
+ emitTerminalError(streamEventState.observedTerminalError);
664
471
  continue;
665
472
  }
666
473
  const usage = msg.usage ?? msg.message?.usage;
@@ -669,13 +476,19 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
669
476
  if (typeof msg.total_cost_usd === 'number') {
670
477
  totalCostUsd = (totalCostUsd ?? 0) + msg.total_cost_usd;
671
478
  }
672
- if (stepOpen) emitFinishStep(harnessUsage ?? pendingStepUsage);
479
+ if (streamEventState.stepOpen) {
480
+ emitFinishStep({
481
+ state: streamEventState,
482
+ emit,
483
+ usage: harnessUsage ?? streamEventState.pendingStepUsage,
484
+ });
485
+ }
673
486
  queryInput.close();
674
487
  break;
675
488
  } else {
676
489
  emitTerminalError(
677
490
  (Array.isArray(msg.errors) ? msg.errors.join('\n') : undefined) ||
678
- observedTerminalError ||
491
+ streamEventState.observedTerminalError ||
679
492
  msg.result ||
680
493
  'Unknown error',
681
494
  );
@@ -698,119 +511,13 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
698
511
  emit({
699
512
  type: 'finish',
700
513
  finishReason: { unified: 'stop', raw: 'stop' },
701
- totalUsage: turnUsage ?? stepUsage ?? defaultUsage(),
514
+ totalUsage: turnUsage ?? streamEventState.stepUsage ?? defaultUsage(),
702
515
  ...(totalCostUsd !== undefined
703
516
  ? { harnessMetadata: { 'claude-code': { costUsd: totalCostUsd } } }
704
517
  : {}),
705
518
  });
706
519
  }
707
520
 
708
- type ClaudeMessage = {
709
- type?: string;
710
- subtype?: string;
711
- error?: string;
712
- error_status?: number | null;
713
- attempt?: number;
714
- max_retries?: number;
715
- retry_delay_ms?: number;
716
- patch?: { status?: string; error?: string };
717
- compact_metadata?: {
718
- trigger: 'manual' | 'auto';
719
- pre_tokens?: number;
720
- post_tokens?: number;
721
- };
722
- event?: {
723
- type?: string;
724
- index?: number;
725
- content_block?: { type?: string };
726
- delta?: { type?: string; text?: string; thinking?: string };
727
- };
728
- message?: {
729
- content?: ReadonlyArray<MessageBlock>;
730
- usage?: Record<string, unknown>;
731
- };
732
- result?: string;
733
- errors?: ReadonlyArray<string>;
734
- usage?: Record<string, unknown>;
735
- total_cost_usd?: number;
736
- };
737
-
738
- function formatApiRetryWarning(msg: ClaudeMessage): string {
739
- const details: string[] = [];
740
- if (typeof msg.attempt === 'number') {
741
- const maxRetries =
742
- typeof msg.max_retries === 'number' ? `/${msg.max_retries}` : '';
743
- details.push(`attempt ${msg.attempt}${maxRetries}`);
744
- }
745
- if (typeof msg.error_status === 'number') {
746
- details.push(`HTTP ${msg.error_status}`);
747
- }
748
- if (typeof msg.retry_delay_ms === 'number') {
749
- details.push(`retrying in ${msg.retry_delay_ms}ms`);
750
- }
751
- if (msg.error) details.push(msg.error);
752
- return details.length > 0
753
- ? `Claude Code API retry: ${details.join('; ')}`
754
- : 'Claude Code API retry';
755
- }
756
-
757
- function handleStreamEvent(
758
- event: ClaudeMessage['event'] | undefined,
759
- partialBlocks: Map<number, { id: string; kind: 'text' | 'thinking' }>,
760
- send: Emit,
761
- ): void {
762
- if (!event || typeof event.index !== 'number') return;
763
- const index = event.index;
764
-
765
- if (event.type === 'content_block_start') {
766
- const blockType = event.content_block?.type;
767
- if (blockType === 'text') {
768
- const id = randomUUID();
769
- partialBlocks.set(index, { id, kind: 'text' });
770
- send({ type: 'text-start', id });
771
- } else if (blockType === 'thinking') {
772
- const id = randomUUID();
773
- partialBlocks.set(index, { id, kind: 'thinking' });
774
- send({ type: 'reasoning-start', id });
775
- }
776
- return;
777
- }
778
-
779
- if (event.type === 'content_block_delta') {
780
- const block = partialBlocks.get(index);
781
- if (!block) return;
782
- if (
783
- block.kind === 'text' &&
784
- event.delta?.type === 'text_delta' &&
785
- typeof event.delta.text === 'string'
786
- ) {
787
- send({ type: 'text-delta', id: block.id, delta: event.delta.text });
788
- } else if (
789
- block.kind === 'thinking' &&
790
- event.delta?.type === 'thinking_delta' &&
791
- typeof event.delta.thinking === 'string'
792
- ) {
793
- send({
794
- type: 'reasoning-delta',
795
- id: block.id,
796
- delta: event.delta.thinking,
797
- });
798
- }
799
- return;
800
- }
801
-
802
- if (event.type === 'content_block_stop') {
803
- const block = partialBlocks.get(index);
804
- if (!block) return;
805
- partialBlocks.delete(index);
806
- if (block.kind === 'text') {
807
- send({ type: 'text-end', id: block.id });
808
- } else {
809
- send({ type: 'reasoning-end', id: block.id });
810
- }
811
- }
812
- }
813
-
814
521
  function createQueryInput({
815
522
  initialUserMessage,
816
523
  pendingUserMessages,
@@ -873,64 +580,6 @@ function createQueryInput({
873
580
  };
874
581
  }
875
582
 
876
- type MessageBlock = {
877
- type: string;
878
- text?: string;
879
- thinking?: string;
880
- id?: string;
881
- name?: string;
882
- input?: unknown;
883
- tool_use_id?: string;
884
- content?: unknown;
885
- is_error?: boolean;
886
- };
887
-
888
- function stringifyContent(content: unknown): string {
889
- if (typeof content === 'string') return content;
890
- if (Array.isArray(content)) {
891
- return content
892
- .map(entry =>
893
- entry && typeof entry === 'object' && 'text' in entry
894
- ? String((entry as { text?: unknown }).text ?? '')
895
- : JSON.stringify(entry),
896
- )
897
- .join('');
898
- }
899
- return JSON.stringify(content);
900
- }
901
-
902
- function mapUsage(usage: unknown): Record<string, unknown> | undefined {
903
- if (!usage || typeof usage !== 'object') return undefined;
904
- const u = usage as {
905
- input_tokens?: number;
906
- cache_creation_input_tokens?: number;
907
- cache_read_input_tokens?: number;
908
- output_tokens?: number;
909
- };
910
- return {
911
- inputTokens: {
912
- total:
913
- (u.input_tokens ?? 0) +
914
- (u.cache_creation_input_tokens ?? 0) +
915
- (u.cache_read_input_tokens ?? 0),
916
- noCache: u.input_tokens ?? 0,
917
- cacheRead: u.cache_read_input_tokens ?? 0,
918
- cacheWrite: u.cache_creation_input_tokens ?? 0,
919
- },
920
- outputTokens: {
921
- total: u.output_tokens ?? 0,
922
- text: u.output_tokens ?? 0,
923
- },
924
- };
925
- }
926
-
927
- function defaultUsage(): Record<string, unknown> {
928
- return {
929
- inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
930
- outputTokens: { total: 0, text: 0 },
931
- };
932
- }
933
-
934
583
  function parseArgs(args: string[]): {
935
584
  workdir?: string;
936
585
  bridgeStateDir?: string;