fourmis-agents-sdk 0.3.1 → 0.4.1

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 (84) hide show
  1. package/README.md +126 -198
  2. package/dist/agent-loop.d.ts +21 -3
  3. package/dist/agent-loop.d.ts.map +1 -1
  4. package/dist/agent-loop.js +279 -90
  5. package/dist/agents/index.js +1079 -124
  6. package/dist/agents/tools.d.ts.map +1 -1
  7. package/dist/agents/tools.js +1079 -124
  8. package/dist/agents/types.d.ts +4 -0
  9. package/dist/agents/types.d.ts.map +1 -1
  10. package/dist/api.d.ts +8 -5
  11. package/dist/api.d.ts.map +1 -1
  12. package/dist/api.js +1663 -430
  13. package/dist/hooks.d.ts +19 -1
  14. package/dist/hooks.d.ts.map +1 -1
  15. package/dist/hooks.js +27 -2
  16. package/dist/index.d.ts +8 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1671 -431
  19. package/dist/mcp/client.d.ts +8 -1
  20. package/dist/mcp/client.d.ts.map +1 -1
  21. package/dist/mcp/client.js +134 -13
  22. package/dist/mcp/index.js +134 -13
  23. package/dist/mcp/types.d.ts +21 -1
  24. package/dist/mcp/types.d.ts.map +1 -1
  25. package/dist/permissions.d.ts.map +1 -1
  26. package/dist/permissions.js +7 -3
  27. package/dist/providers/anthropic.d.ts.map +1 -1
  28. package/dist/providers/anthropic.js +41 -2
  29. package/dist/providers/openai.d.ts +6 -0
  30. package/dist/providers/openai.d.ts.map +1 -1
  31. package/dist/providers/openai.js +36 -6
  32. package/dist/providers/registry.js +76 -8
  33. package/dist/providers/types.d.ts +4 -1
  34. package/dist/providers/types.d.ts.map +1 -1
  35. package/dist/query.d.ts +21 -2
  36. package/dist/query.d.ts.map +1 -1
  37. package/dist/query.js +69 -1
  38. package/dist/skills/index.js +23 -1
  39. package/dist/skills/skills.d.ts +16 -0
  40. package/dist/skills/skills.d.ts.map +1 -1
  41. package/dist/skills/skills.js +23 -1
  42. package/dist/tools/ask-user-question.d.ts +7 -0
  43. package/dist/tools/ask-user-question.d.ts.map +1 -0
  44. package/dist/tools/ask-user-question.js +48 -0
  45. package/dist/tools/bash.d.ts.map +1 -1
  46. package/dist/tools/bash.js +47 -2
  47. package/dist/tools/config.d.ts +7 -0
  48. package/dist/tools/config.d.ts.map +1 -0
  49. package/dist/tools/config.js +114 -0
  50. package/dist/tools/exit-plan-mode.d.ts +7 -0
  51. package/dist/tools/exit-plan-mode.d.ts.map +1 -0
  52. package/dist/tools/exit-plan-mode.js +34 -0
  53. package/dist/tools/index.d.ts +7 -0
  54. package/dist/tools/index.d.ts.map +1 -1
  55. package/dist/tools/index.js +506 -9
  56. package/dist/tools/notebook-edit.d.ts +7 -0
  57. package/dist/tools/notebook-edit.d.ts.map +1 -0
  58. package/dist/tools/notebook-edit.js +83 -0
  59. package/dist/tools/presets.d.ts +2 -1
  60. package/dist/tools/presets.d.ts.map +1 -1
  61. package/dist/tools/presets.js +22 -4
  62. package/dist/tools/read.d.ts.map +1 -1
  63. package/dist/tools/read.js +12 -1
  64. package/dist/tools/registry.d.ts +2 -0
  65. package/dist/tools/registry.d.ts.map +1 -1
  66. package/dist/tools/registry.js +10 -0
  67. package/dist/tools/todo-write.d.ts +7 -0
  68. package/dist/tools/todo-write.d.ts.map +1 -0
  69. package/dist/tools/todo-write.js +69 -0
  70. package/dist/tools/web-fetch.d.ts +6 -0
  71. package/dist/tools/web-fetch.d.ts.map +1 -0
  72. package/dist/tools/web-fetch.js +85 -0
  73. package/dist/tools/web-search.d.ts +7 -0
  74. package/dist/tools/web-search.d.ts.map +1 -0
  75. package/dist/tools/web-search.js +78 -0
  76. package/dist/types.d.ts +344 -42
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/utils/session-store.d.ts +1 -1
  79. package/dist/utils/session-store.d.ts.map +1 -1
  80. package/dist/utils/session-store.js +49 -2
  81. package/dist/utils/system-prompt.d.ts +2 -0
  82. package/dist/utils/system-prompt.d.ts.map +1 -1
  83. package/dist/utils/system-prompt.js +33 -4
  84. package/package.json +3 -2
@@ -483,10 +483,57 @@ function mergeUsage(a, b) {
483
483
  }
484
484
 
485
485
  // src/agent-loop.ts
486
+ function makeModelUsageEntry() {
487
+ return {
488
+ inputTokens: 0,
489
+ outputTokens: 0,
490
+ cacheReadInputTokens: 0,
491
+ cacheCreationInputTokens: 0,
492
+ totalCostUsd: 0
493
+ };
494
+ }
495
+ function makeErrorResult(params) {
496
+ return {
497
+ type: "result",
498
+ subtype: params.subtype,
499
+ duration_ms: Date.now() - params.startTime,
500
+ duration_api_ms: params.apiTimeMs,
501
+ is_error: true,
502
+ num_turns: params.turns,
503
+ stop_reason: null,
504
+ total_cost_usd: params.costUsd,
505
+ usage: params.usage,
506
+ modelUsage: params.modelUsage,
507
+ permission_denials: params.permissionDenials,
508
+ errors: params.errors,
509
+ uuid: uuid(),
510
+ session_id: params.sessionId
511
+ };
512
+ }
513
+ function extractStructuredJson(text) {
514
+ const trimmed = text.trim();
515
+ if (!trimmed) {
516
+ return { ok: false, error: "Empty result text; expected JSON output." };
517
+ }
518
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
519
+ const candidate = fenced ? fenced[1].trim() : trimmed;
520
+ try {
521
+ return { ok: true, value: JSON.parse(candidate) };
522
+ } catch (err) {
523
+ const message = err instanceof Error ? err.message : String(err);
524
+ return { ok: false, error: `Invalid JSON output: ${message}` };
525
+ }
526
+ }
486
527
  async function* agentLoop(prompt, options) {
487
528
  const {
488
529
  provider,
489
530
  model,
531
+ fallbackModel,
532
+ modelState,
533
+ maxThinkingTokensState,
534
+ thinking,
535
+ effort,
536
+ outputFormat,
490
537
  systemPrompt,
491
538
  tools,
492
539
  permissions,
@@ -494,7 +541,7 @@ async function* agentLoop(prompt, options) {
494
541
  sessionId,
495
542
  maxTurns,
496
543
  maxBudgetUsd,
497
- includeStreamEvents,
544
+ includePartialMessages,
498
545
  signal,
499
546
  env,
500
547
  debug,
@@ -502,14 +549,17 @@ async function* agentLoop(prompt, options) {
502
549
  mcpClient,
503
550
  previousMessages,
504
551
  sessionLogger,
505
- nativeMemoryTool
552
+ nativeMemoryTool,
553
+ initMeta
506
554
  } = options;
555
+ const effectiveModelState = modelState ?? { current: model };
507
556
  const startTime = Date.now();
508
557
  let apiTimeMs = 0;
509
558
  let turns = 0;
510
559
  let totalUsage = emptyTokenUsage();
511
560
  let costUsd = 0;
512
561
  const modelUsage = {};
562
+ const permissionDenials = [];
513
563
  if (mcpClient) {
514
564
  await mcpClient.connectAll();
515
565
  for (const tool of mcpClient.getTools()) {
@@ -527,56 +577,135 @@ async function* agentLoop(prompt, options) {
527
577
  sessionLogger("user", prompt, null);
528
578
  }
529
579
  yield {
530
- type: "init",
531
- sessionId,
532
- model,
533
- provider: provider.name,
580
+ type: "system",
581
+ subtype: "init",
582
+ apiKeySource: "user",
583
+ claude_code_version: "fourmis-agent-sdk",
584
+ session_id: sessionId,
585
+ model: effectiveModelState.current,
534
586
  tools: tools.list(),
535
587
  cwd,
588
+ mcp_servers: (mcpClient?.status() ?? []).map((s) => ({ name: s.name, status: s.status })),
589
+ permissionMode: permissions.getMode(),
590
+ agents: initMeta?.agents,
591
+ betas: initMeta?.betas,
592
+ slash_commands: initMeta?.slashCommands ?? [],
593
+ output_style: initMeta?.outputStyle ?? "default",
594
+ skills: initMeta?.skills ?? [],
595
+ plugins: (initMeta?.plugins ?? []).map((p) => ({ name: p.path.split("/").pop() ?? p.path, path: p.path })),
536
596
  uuid: uuid()
537
597
  };
538
598
  if (hooks) {
539
- await hooks.fire("SessionStart", { event: "SessionStart", session_id: sessionId }, undefined, { signal });
599
+ await hooks.fire("Setup", {
600
+ event: "Setup",
601
+ hook_event_name: "Setup",
602
+ trigger: "init",
603
+ session_id: sessionId,
604
+ cwd,
605
+ permission_mode: permissions.getMode()
606
+ }, undefined, { signal });
607
+ }
608
+ if (hooks) {
609
+ await hooks.fire("SessionStart", {
610
+ event: "SessionStart",
611
+ hook_event_name: "SessionStart",
612
+ session_id: sessionId,
613
+ source: "startup",
614
+ model: effectiveModelState.current,
615
+ cwd,
616
+ permission_mode: permissions.getMode()
617
+ }, undefined, { signal });
540
618
  }
541
619
  while (true) {
542
620
  if (signal.aborted) {
543
- yield makeError("error_execution", ["Aborted"], turns, costUsd, sessionId, startTime);
621
+ yield makeErrorResult({
622
+ subtype: "error_during_execution",
623
+ errors: ["Aborted"],
624
+ turns,
625
+ costUsd,
626
+ sessionId,
627
+ startTime,
628
+ apiTimeMs,
629
+ usage: totalUsage,
630
+ modelUsage,
631
+ permissionDenials
632
+ });
544
633
  return;
545
634
  }
546
635
  if (turns >= maxTurns) {
547
- yield makeError("error_max_turns", [`Reached maximum turns (${maxTurns})`], turns, costUsd, sessionId, startTime);
636
+ yield makeErrorResult({
637
+ subtype: "error_max_turns",
638
+ errors: [`Reached maximum turns (${maxTurns})`],
639
+ turns,
640
+ costUsd,
641
+ sessionId,
642
+ startTime,
643
+ apiTimeMs,
644
+ usage: totalUsage,
645
+ modelUsage,
646
+ permissionDenials
647
+ });
548
648
  return;
549
649
  }
550
650
  if (maxBudgetUsd > 0 && costUsd >= maxBudgetUsd) {
551
- yield makeError("error_max_budget", [`Reached budget limit ($${maxBudgetUsd})`], turns, costUsd, sessionId, startTime);
651
+ yield makeErrorResult({
652
+ subtype: "error_max_budget_usd",
653
+ errors: [],
654
+ turns,
655
+ costUsd,
656
+ sessionId,
657
+ startTime,
658
+ apiTimeMs,
659
+ usage: totalUsage,
660
+ modelUsage,
661
+ permissionDenials
662
+ });
552
663
  return;
553
664
  }
665
+ const activeModel = effectiveModelState.current;
554
666
  const toolDefs = tools.getDefinitions();
555
667
  const apiStart = Date.now();
556
668
  let assistantTextParts = [];
557
- let toolCalls = [];
669
+ const toolCalls = [];
558
670
  let turnUsage = emptyTokenUsage();
671
+ let turnStopReason = null;
559
672
  const nativeTools = nativeMemoryTool ? [nativeMemoryTool.definition] : undefined;
560
673
  try {
561
674
  const chunks = provider.chat({
562
- model,
675
+ model: activeModel,
563
676
  messages,
564
677
  tools: toolDefs.length > 0 ? toolDefs : undefined,
565
678
  systemPrompt,
566
679
  signal,
567
- nativeTools
680
+ nativeTools,
681
+ thinkingBudget: maxThinkingTokensState?.current,
682
+ thinking,
683
+ effort,
684
+ outputFormat
568
685
  });
569
686
  for await (const chunk of chunks) {
570
687
  switch (chunk.type) {
571
688
  case "text_delta":
572
689
  assistantTextParts.push(chunk.text);
573
- if (includeStreamEvents) {
574
- yield { type: "stream", subtype: "text_delta", text: chunk.text, uuid: uuid() };
690
+ if (includePartialMessages) {
691
+ yield {
692
+ type: "stream_event",
693
+ event: { type: "text_delta", text: chunk.text },
694
+ parent_tool_use_id: null,
695
+ uuid: uuid(),
696
+ session_id: sessionId
697
+ };
575
698
  }
576
699
  break;
577
700
  case "thinking_delta":
578
- if (includeStreamEvents) {
579
- yield { type: "stream", subtype: "thinking_delta", text: chunk.text, uuid: uuid() };
701
+ if (includePartialMessages) {
702
+ yield {
703
+ type: "stream_event",
704
+ event: { type: "thinking_delta", thinking: chunk.text },
705
+ parent_tool_use_id: null,
706
+ uuid: uuid(),
707
+ session_id: sessionId
708
+ };
580
709
  }
581
710
  break;
582
711
  case "tool_call":
@@ -586,33 +715,54 @@ async function* agentLoop(prompt, options) {
586
715
  turnUsage = mergeUsage(turnUsage, chunk.usage);
587
716
  break;
588
717
  case "done":
718
+ turnStopReason = chunk.stopReason ?? null;
589
719
  break;
590
720
  }
591
721
  }
592
722
  } catch (err) {
593
723
  const message = err instanceof Error ? err.message : String(err);
594
- yield makeError("error_execution", [`API error: ${message}`], turns, costUsd, sessionId, startTime);
724
+ if (fallbackModel && activeModel !== fallbackModel) {
725
+ effectiveModelState.current = fallbackModel;
726
+ yield {
727
+ type: "system",
728
+ subtype: "status",
729
+ status: null,
730
+ permissionMode: permissions.getMode(),
731
+ uuid: uuid(),
732
+ session_id: sessionId
733
+ };
734
+ continue;
735
+ }
736
+ yield makeErrorResult({
737
+ subtype: "error_during_execution",
738
+ errors: [`API error: ${message}`],
739
+ turns,
740
+ costUsd,
741
+ sessionId,
742
+ startTime,
743
+ apiTimeMs,
744
+ usage: totalUsage,
745
+ modelUsage,
746
+ permissionDenials
747
+ });
595
748
  return;
596
749
  }
597
750
  apiTimeMs += Date.now() - apiStart;
598
751
  turns++;
599
752
  totalUsage = mergeUsage(totalUsage, turnUsage);
600
- const turnCost = provider.calculateCost(model, turnUsage);
753
+ const turnCost = provider.calculateCost(activeModel, turnUsage);
601
754
  costUsd += turnCost;
602
- if (!modelUsage[model]) {
603
- modelUsage[model] = {
604
- inputTokens: 0,
605
- outputTokens: 0,
606
- cacheReadInputTokens: 0,
607
- cacheCreationInputTokens: 0,
608
- totalCostUsd: 0
609
- };
610
- }
611
- modelUsage[model].inputTokens += turnUsage.inputTokens;
612
- modelUsage[model].outputTokens += turnUsage.outputTokens;
613
- modelUsage[model].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
614
- modelUsage[model].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
615
- modelUsage[model].totalCostUsd += turnCost;
755
+ if (!modelUsage[activeModel]) {
756
+ modelUsage[activeModel] = makeModelUsageEntry();
757
+ }
758
+ modelUsage[activeModel].inputTokens += turnUsage.inputTokens;
759
+ modelUsage[activeModel].outputTokens += turnUsage.outputTokens;
760
+ modelUsage[activeModel].cacheReadInputTokens += turnUsage.cacheReadInputTokens;
761
+ modelUsage[activeModel].cacheCreationInputTokens += turnUsage.cacheCreationInputTokens;
762
+ modelUsage[activeModel].totalCostUsd += turnCost;
763
+ modelUsage[activeModel].webSearchRequests = (modelUsage[activeModel].webSearchRequests ?? 0) + (turnUsage.webSearchRequests ?? 0);
764
+ modelUsage[activeModel].costUSD = modelUsage[activeModel].totalCostUsd;
765
+ modelUsage[activeModel].contextWindow = provider.getContextWindow(activeModel);
616
766
  const assistantText = assistantTextParts.join("");
617
767
  const assistantContent = [];
618
768
  if (assistantText) {
@@ -630,28 +780,72 @@ async function* agentLoop(prompt, options) {
630
780
  if (sessionLogger) {
631
781
  sessionLogger("assistant", assistantContent, null);
632
782
  }
633
- if (assistantText) {
634
- yield { type: "text", text: assistantText, uuid: uuid() };
635
- }
783
+ yield {
784
+ type: "assistant",
785
+ message: {
786
+ role: "assistant",
787
+ content: assistantContent
788
+ },
789
+ parent_tool_use_id: null,
790
+ uuid: uuid(),
791
+ session_id: sessionId
792
+ };
636
793
  if (toolCalls.length === 0) {
794
+ let structuredOutput;
795
+ if (outputFormat?.type === "json_schema") {
796
+ const parsed = extractStructuredJson(assistantText);
797
+ if (!parsed.ok) {
798
+ yield makeErrorResult({
799
+ subtype: "error_max_structured_output_retries",
800
+ errors: [parsed.error],
801
+ turns,
802
+ costUsd,
803
+ sessionId,
804
+ startTime,
805
+ apiTimeMs,
806
+ usage: totalUsage,
807
+ modelUsage,
808
+ permissionDenials
809
+ });
810
+ return;
811
+ }
812
+ structuredOutput = parsed.value;
813
+ }
637
814
  if (hooks) {
638
- await hooks.fire("Stop", { event: "Stop", session_id: sessionId, text: assistantText || undefined }, undefined, { signal });
815
+ await hooks.fire("Stop", {
816
+ event: "Stop",
817
+ hook_event_name: "Stop",
818
+ session_id: sessionId,
819
+ text: assistantText || undefined,
820
+ stop_reason: turnStopReason ?? undefined
821
+ }, undefined, {
822
+ signal
823
+ });
639
824
  }
640
825
  if (hooks) {
641
- await hooks.fire("SessionEnd", { event: "SessionEnd", session_id: sessionId }, undefined, { signal });
826
+ await hooks.fire("SessionEnd", {
827
+ event: "SessionEnd",
828
+ hook_event_name: "SessionEnd",
829
+ session_id: sessionId,
830
+ reason: "other"
831
+ }, undefined, { signal });
642
832
  }
643
833
  yield {
644
834
  type: "result",
645
835
  subtype: "success",
646
- text: assistantText || null,
647
- turns,
648
- costUsd,
649
- durationMs: Date.now() - startTime,
650
- durationApiMs: apiTimeMs,
651
- sessionId,
836
+ duration_ms: Date.now() - startTime,
837
+ duration_api_ms: apiTimeMs,
838
+ is_error: false,
839
+ num_turns: turns,
840
+ result: assistantText,
841
+ stop_reason: turnStopReason,
842
+ total_cost_usd: costUsd,
652
843
  usage: totalUsage,
653
844
  modelUsage,
654
- uuid: uuid()
845
+ permission_denials: permissionDenials,
846
+ structured_output: structuredOutput,
847
+ uuid: uuid(),
848
+ session_id: sessionId
655
849
  };
656
850
  return;
657
851
  }
@@ -660,7 +854,13 @@ async function* agentLoop(prompt, options) {
660
854
  let hookDenied = false;
661
855
  let hookUpdatedInput;
662
856
  if (hooks) {
663
- const hookResult = await hooks.fire("PreToolUse", { event: "PreToolUse", tool_name: call.name, tool_input: call.input, session_id: sessionId }, call.id, { signal });
857
+ const hookResult = await hooks.fire("PreToolUse", {
858
+ event: "PreToolUse",
859
+ hook_event_name: "PreToolUse",
860
+ tool_name: call.name,
861
+ tool_input: call.input,
862
+ session_id: sessionId
863
+ }, call.id, { signal });
664
864
  if (hookResult) {
665
865
  if (hookResult.permissionDecision === "deny") {
666
866
  hookDenied = true;
@@ -672,14 +872,6 @@ async function* agentLoop(prompt, options) {
672
872
  }
673
873
  if (hookDenied) {
674
874
  const denyContent = "Denied by hook";
675
- yield {
676
- type: "tool_result",
677
- id: call.id,
678
- name: call.name,
679
- content: denyContent,
680
- isError: true,
681
- uuid: uuid()
682
- };
683
875
  toolResults.push({
684
876
  type: "tool_result",
685
877
  tool_use_id: call.id,
@@ -695,14 +887,11 @@ async function* agentLoop(prompt, options) {
695
887
  const permResult = await permissions.check(call.name, inputAfterHook ?? {}, { signal, toolUseId: call.id });
696
888
  if (permResult.behavior === "deny") {
697
889
  const denyContent = `Permission denied: ${permResult.message}`;
698
- yield {
699
- type: "tool_result",
700
- id: call.id,
701
- name: call.name,
702
- content: denyContent,
703
- isError: true,
704
- uuid: uuid()
705
- };
890
+ permissionDenials.push({
891
+ tool_name: call.name,
892
+ tool_use_id: call.id,
893
+ tool_input: inputAfterHook ?? {}
894
+ });
706
895
  toolResults.push({
707
896
  type: "tool_result",
708
897
  tool_use_id: call.id,
@@ -715,13 +904,6 @@ async function* agentLoop(prompt, options) {
715
904
  continue;
716
905
  }
717
906
  const toolInput = permResult.behavior === "allow" && permResult.updatedInput ? permResult.updatedInput : inputAfterHook;
718
- yield {
719
- type: "tool_use",
720
- id: call.id,
721
- name: call.name,
722
- input: toolInput,
723
- uuid: uuid()
724
- };
725
907
  let result;
726
908
  if (call.name === "memory" && nativeMemoryTool) {
727
909
  try {
@@ -740,28 +922,36 @@ async function* agentLoop(prompt, options) {
740
922
  };
741
923
  result = await tools.execute(call.name, toolInput, toolCtx);
742
924
  }
925
+ if (call.name === "ExitPlanMode") {
926
+ permissions.setMode("default");
927
+ }
743
928
  if (debug) {
744
929
  console.error(`[debug] Tool ${call.name}: ${result.isError ? "ERROR" : "OK"} (${result.content.length} chars)`);
745
930
  }
746
931
  if (hooks) {
747
932
  if (result.isError) {
748
- await hooks.fire("PostToolUseFailure", { event: "PostToolUseFailure", tool_name: call.name, tool_result: result.content, tool_error: true, session_id: sessionId }, call.id, { signal });
933
+ await hooks.fire("PostToolUseFailure", {
934
+ event: "PostToolUseFailure",
935
+ hook_event_name: "PostToolUseFailure",
936
+ tool_name: call.name,
937
+ tool_result: result.content,
938
+ tool_error: true,
939
+ session_id: sessionId
940
+ }, call.id, { signal });
749
941
  } else {
750
- const postResult = await hooks.fire("PostToolUse", { event: "PostToolUse", tool_name: call.name, tool_result: result.content, session_id: sessionId }, call.id, { signal });
942
+ const postResult = await hooks.fire("PostToolUse", {
943
+ event: "PostToolUse",
944
+ hook_event_name: "PostToolUse",
945
+ tool_name: call.name,
946
+ tool_result: result.content,
947
+ session_id: sessionId
948
+ }, call.id, { signal });
751
949
  if (postResult?.additionalContext) {
752
950
  result.content += `
753
951
  ${postResult.additionalContext}`;
754
952
  }
755
953
  }
756
954
  }
757
- yield {
758
- type: "tool_result",
759
- id: call.id,
760
- name: call.name,
761
- content: result.content,
762
- isError: result.isError,
763
- uuid: uuid()
764
- };
765
955
  toolResults.push({
766
956
  type: "tool_result",
767
957
  tool_use_id: call.id,
@@ -773,20 +963,19 @@ ${postResult.additionalContext}`;
773
963
  if (sessionLogger) {
774
964
  sessionLogger("user", toolResults, null);
775
965
  }
966
+ yield {
967
+ type: "user",
968
+ message: {
969
+ role: "user",
970
+ content: toolResults
971
+ },
972
+ parent_tool_use_id: null,
973
+ isSynthetic: true,
974
+ uuid: uuid(),
975
+ session_id: sessionId
976
+ };
776
977
  }
777
978
  }
778
- function makeError(subtype, errors, turns, costUsd, sessionId, startTime) {
779
- return {
780
- type: "result",
781
- subtype,
782
- errors,
783
- turns,
784
- costUsd,
785
- durationMs: Date.now() - startTime,
786
- sessionId,
787
- uuid: uuid()
788
- };
789
- }
790
979
 
791
980
  // src/utils/cost.ts
792
981
  var ANTHROPIC_PRICING = {
@@ -1043,6 +1232,44 @@ class AnthropicAdapter {
1043
1232
  max_tokens: maxTokens,
1044
1233
  stream: true
1045
1234
  };
1235
+ if (request.thinking) {
1236
+ switch (request.thinking.type) {
1237
+ case "adaptive":
1238
+ params.thinking = { type: "adaptive" };
1239
+ break;
1240
+ case "disabled":
1241
+ params.thinking = { type: "disabled" };
1242
+ break;
1243
+ case "enabled":
1244
+ params.thinking = {
1245
+ type: "enabled",
1246
+ budget_tokens: request.thinking.budgetTokens
1247
+ };
1248
+ break;
1249
+ }
1250
+ } else if (request.thinkingBudget !== undefined) {
1251
+ if (request.thinkingBudget <= 0) {
1252
+ params.thinking = { type: "disabled" };
1253
+ } else {
1254
+ params.thinking = {
1255
+ type: "enabled",
1256
+ budget_tokens: Math.max(1024, request.thinkingBudget)
1257
+ };
1258
+ }
1259
+ }
1260
+ const outputConfig = {};
1261
+ if (request.effort) {
1262
+ outputConfig.effort = request.effort;
1263
+ }
1264
+ if (request.outputFormat?.type === "json_schema") {
1265
+ outputConfig.format = {
1266
+ type: "json_schema",
1267
+ schema: request.outputFormat.schema
1268
+ };
1269
+ }
1270
+ if (Object.keys(outputConfig).length > 0) {
1271
+ params.output_config = outputConfig;
1272
+ }
1046
1273
  if (this.oauthMode) {
1047
1274
  const systemBlocks = [
1048
1275
  { type: "text", text: "You are Claude Code, Anthropic's official CLI for Claude." }
@@ -1167,11 +1394,12 @@ class AnthropicAdapter {
1167
1394
  switch (feature) {
1168
1395
  case "streaming":
1169
1396
  case "tool_calling":
1170
- case "image_input":
1171
- case "pdf_input":
1172
1397
  case "thinking":
1173
1398
  case "structured_output":
1174
1399
  return true;
1400
+ case "image_input":
1401
+ case "pdf_input":
1402
+ return false;
1175
1403
  default:
1176
1404
  return false;
1177
1405
  }
@@ -1242,6 +1470,22 @@ var CODEX_MODELS = new Set([
1242
1470
  "gpt-5-codex",
1243
1471
  "gpt-5-codex-mini"
1244
1472
  ]);
1473
+ var OPENAI_MAX_TOOL_NAME = 64;
1474
+ function sanitizeToolName(name) {
1475
+ const clean = name.replace(/[^a-zA-Z0-9_-]/g, "_");
1476
+ if (clean.length <= OPENAI_MAX_TOOL_NAME)
1477
+ return clean;
1478
+ const hash = simpleHash(name);
1479
+ return clean.slice(0, OPENAI_MAX_TOOL_NAME - 7) + "_" + hash;
1480
+ }
1481
+ function simpleHash(s) {
1482
+ let h = 2166136261;
1483
+ for (let i = 0;i < s.length; i++) {
1484
+ h ^= s.charCodeAt(i);
1485
+ h = Math.imul(h, 16777619);
1486
+ }
1487
+ return (h >>> 0).toString(16).padStart(8, "0").slice(0, 6);
1488
+ }
1245
1489
 
1246
1490
  class OpenAIAdapter {
1247
1491
  name = "openai";
@@ -1249,6 +1493,7 @@ class OpenAIAdapter {
1249
1493
  codexMode;
1250
1494
  accountId;
1251
1495
  currentAccessToken;
1496
+ toolNameMap = new Map;
1252
1497
  constructor(options) {
1253
1498
  const key = options?.apiKey ?? process.env.OPENAI_API_KEY;
1254
1499
  if (key) {
@@ -1281,6 +1526,15 @@ class OpenAIAdapter {
1281
1526
  }
1282
1527
  }
1283
1528
  async* chat(request) {
1529
+ this.toolNameMap.clear();
1530
+ if (request.tools) {
1531
+ for (const tool of request.tools) {
1532
+ const sanitized = sanitizeToolName(tool.name);
1533
+ if (sanitized !== tool.name) {
1534
+ this.toolNameMap.set(sanitized, tool.name);
1535
+ }
1536
+ }
1537
+ }
1284
1538
  if (this.codexMode) {
1285
1539
  yield* this.chatResponses(request);
1286
1540
  } else {
@@ -1382,7 +1636,7 @@ class OpenAIAdapter {
1382
1636
  yield {
1383
1637
  type: "tool_call",
1384
1638
  id: buf.id,
1385
- name: buf.name,
1639
+ name: this.resolveToolName(buf.name),
1386
1640
  input
1387
1641
  };
1388
1642
  }
@@ -1426,7 +1680,7 @@ class OpenAIAdapter {
1426
1680
  yield {
1427
1681
  type: "tool_call",
1428
1682
  id: item.call_id,
1429
- name: item.name,
1683
+ name: this.resolveToolName(item.name),
1430
1684
  input: parsedInput
1431
1685
  };
1432
1686
  }
@@ -1493,7 +1747,7 @@ class OpenAIAdapter {
1493
1747
  id: block.id,
1494
1748
  type: "function",
1495
1749
  function: {
1496
- name: block.name,
1750
+ name: sanitizeToolName(block.name),
1497
1751
  arguments: JSON.stringify(block.input)
1498
1752
  }
1499
1753
  });
@@ -1558,7 +1812,7 @@ class OpenAIAdapter {
1558
1812
  result.push({
1559
1813
  type: "function_call",
1560
1814
  call_id: tu.id,
1561
- name: tu.name,
1815
+ name: sanitizeToolName(tu.name),
1562
1816
  arguments: JSON.stringify(tu.input)
1563
1817
  });
1564
1818
  }
@@ -1590,7 +1844,7 @@ class OpenAIAdapter {
1590
1844
  return tools.map((tool) => ({
1591
1845
  type: "function",
1592
1846
  function: {
1593
- name: tool.name,
1847
+ name: sanitizeToolName(tool.name),
1594
1848
  description: tool.description,
1595
1849
  parameters: tool.inputSchema
1596
1850
  }
@@ -1599,12 +1853,15 @@ class OpenAIAdapter {
1599
1853
  convertToolsForResponses(tools) {
1600
1854
  return tools.map((tool) => ({
1601
1855
  type: "function",
1602
- name: tool.name,
1856
+ name: sanitizeToolName(tool.name),
1603
1857
  description: tool.description,
1604
1858
  parameters: tool.inputSchema,
1605
1859
  strict: false
1606
1860
  }));
1607
1861
  }
1862
+ resolveToolName(name) {
1863
+ return this.toolNameMap.get(name) ?? name;
1864
+ }
1608
1865
  mapStopReason(reason) {
1609
1866
  switch (reason) {
1610
1867
  case "stop":
@@ -2086,6 +2343,16 @@ class ToolRegistry {
2086
2343
  register(tool) {
2087
2344
  this.tools.set(tool.name, tool);
2088
2345
  }
2346
+ unregister(name) {
2347
+ this.tools.delete(name);
2348
+ }
2349
+ clearByPrefix(prefix) {
2350
+ for (const name of this.tools.keys()) {
2351
+ if (name.startsWith(prefix)) {
2352
+ this.tools.delete(name);
2353
+ }
2354
+ }
2355
+ }
2089
2356
  get(name) {
2090
2357
  return this.tools.get(name);
2091
2358
  }
@@ -2119,16 +2386,34 @@ class ToolRegistry {
2119
2386
  // src/tools/presets.ts
2120
2387
  var PRESETS = {
2121
2388
  coding: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
2389
+ claude_code: [
2390
+ "Bash",
2391
+ "Read",
2392
+ "Write",
2393
+ "Edit",
2394
+ "Glob",
2395
+ "Grep",
2396
+ "NotebookEdit",
2397
+ "WebFetch",
2398
+ "WebSearch",
2399
+ "TodoWrite",
2400
+ "Config",
2401
+ "AskUserQuestion",
2402
+ "ExitPlanMode"
2403
+ ],
2122
2404
  readonly: ["Read", "Glob", "Grep"],
2123
2405
  minimal: ["Read", "Write", "Edit", "Glob", "Grep"]
2124
2406
  };
2125
2407
  function resolveToolNames(tools) {
2126
2408
  if (!tools)
2127
- return PRESETS.coding;
2128
- if (typeof tools === "string") {
2129
- return PRESETS[tools] ?? [tools];
2409
+ return PRESETS.claude_code;
2410
+ if (Array.isArray(tools)) {
2411
+ return tools;
2412
+ }
2413
+ if (tools.type === "preset") {
2414
+ return PRESETS[tools.preset] ?? PRESETS.claude_code;
2130
2415
  }
2131
- return tools;
2416
+ throw new Error("Invalid tools option. Expected string[] or { type: 'preset', preset: 'claude_code' }.");
2132
2417
  }
2133
2418
 
2134
2419
  // src/tools/bash.ts
@@ -2152,17 +2437,58 @@ var BashTool = {
2152
2437
  timeout: {
2153
2438
  type: "number",
2154
2439
  description: "Timeout in milliseconds (max 600000)"
2440
+ },
2441
+ run_in_background: {
2442
+ type: "boolean",
2443
+ description: "Run command asynchronously and return immediately."
2444
+ },
2445
+ dangerouslyDisableSandbox: {
2446
+ type: "boolean",
2447
+ description: "If true, explicitly request unsandboxed execution."
2448
+ },
2449
+ _simulatedSedEdit: {
2450
+ type: "object",
2451
+ properties: {
2452
+ filePath: { type: "string" },
2453
+ newContent: { type: "string" }
2454
+ },
2455
+ description: "Internal field for precomputed edit previews."
2155
2456
  }
2156
2457
  },
2157
2458
  required: ["command"]
2158
2459
  },
2159
2460
  async execute(input, ctx) {
2160
- const { command, timeout: timeoutMs, description } = input;
2461
+ const {
2462
+ command,
2463
+ timeout: timeoutMs,
2464
+ run_in_background,
2465
+ description,
2466
+ dangerouslyDisableSandbox,
2467
+ _simulatedSedEdit
2468
+ } = input;
2161
2469
  if (!command || typeof command !== "string") {
2162
2470
  return { content: "Error: command is required", isError: true };
2163
2471
  }
2164
2472
  const timeout = Math.min(timeoutMs ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
2165
2473
  try {
2474
+ if (run_in_background) {
2475
+ const proc2 = Bun.spawn(["bash", "-c", command], {
2476
+ cwd: ctx.cwd,
2477
+ stdout: "ignore",
2478
+ stderr: "ignore",
2479
+ stdin: "ignore",
2480
+ env: { ...process.env, ...ctx.env }
2481
+ });
2482
+ return {
2483
+ content: `Background command started (pid ${proc2.pid ?? "unknown"}).`,
2484
+ metadata: {
2485
+ pid: proc2.pid ?? null,
2486
+ run_in_background: true,
2487
+ dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
2488
+ hasSimulatedSedEdit: !!_simulatedSedEdit
2489
+ }
2490
+ };
2491
+ }
2166
2492
  const proc = Bun.spawn(["bash", "-c", command], {
2167
2493
  cwd: ctx.cwd,
2168
2494
  stdout: "pipe",
@@ -2194,7 +2520,11 @@ var BashTool = {
2194
2520
  return {
2195
2521
  content: output,
2196
2522
  isError: exitCode !== 0 ? true : undefined,
2197
- metadata: { exitCode }
2523
+ metadata: {
2524
+ exitCode,
2525
+ dangerouslyDisableSandbox: dangerouslyDisableSandbox === true,
2526
+ hasSimulatedSedEdit: !!_simulatedSedEdit
2527
+ }
2198
2528
  };
2199
2529
  } catch (err) {
2200
2530
  const message = err instanceof Error ? err.message : String(err);
@@ -2223,12 +2553,17 @@ var ReadTool = {
2223
2553
  limit: {
2224
2554
  type: "number",
2225
2555
  description: "Number of lines to read"
2556
+ },
2557
+ pages: {
2558
+ type: "array",
2559
+ items: { type: "number" },
2560
+ description: "Optional PDF page numbers (1-based)."
2226
2561
  }
2227
2562
  },
2228
2563
  required: ["file_path"]
2229
2564
  },
2230
2565
  async execute(input, ctx) {
2231
- const { file_path, offset, limit } = input;
2566
+ const { file_path, offset, limit, pages } = input;
2232
2567
  if (!file_path) {
2233
2568
  return { content: "Error: file_path is required", isError: true };
2234
2569
  }
@@ -2239,6 +2574,12 @@ var ReadTool = {
2239
2574
  if (!exists) {
2240
2575
  return { content: `Error: File not found: ${resolvedPath}`, isError: true };
2241
2576
  }
2577
+ if (Array.isArray(pages) && pages.length > 0 && resolvedPath.toLowerCase().endsWith(".pdf")) {
2578
+ return {
2579
+ content: "Error: PDF page extraction is not implemented in this runtime.",
2580
+ isError: true
2581
+ };
2582
+ }
2242
2583
  const text = await file.text();
2243
2584
  const lines = text.split(`
2244
2585
  `);
@@ -2626,6 +2967,405 @@ async function collectFiles(dir, globPattern) {
2626
2967
  }
2627
2968
  return files;
2628
2969
  }
2970
+
2971
+ // src/tools/notebook-edit.ts
2972
+ import { readFile, writeFile } from "fs/promises";
2973
+ function toSourceLines(text) {
2974
+ const lines = text.split(`
2975
+ `);
2976
+ return lines.map((line, idx) => idx < lines.length - 1 ? `${line}
2977
+ ` : line);
2978
+ }
2979
+ var NotebookEditTool = {
2980
+ name: "NotebookEdit",
2981
+ description: "Edit a specific Jupyter notebook cell by id or index.",
2982
+ inputSchema: {
2983
+ type: "object",
2984
+ properties: {
2985
+ notebook_path: { type: "string", description: "Path to .ipynb file." },
2986
+ cell_id: { type: "string", description: "Cell id to edit." },
2987
+ cell_index: { type: "number", description: "Cell index to edit if id is not provided." },
2988
+ new_source: { type: "string", description: "New cell source content." }
2989
+ },
2990
+ required: ["notebook_path", "new_source"]
2991
+ },
2992
+ async execute(input, ctx) {
2993
+ const {
2994
+ notebook_path,
2995
+ cell_id,
2996
+ cell_index,
2997
+ new_source
2998
+ } = input ?? {};
2999
+ if (!notebook_path)
3000
+ return { content: "Error: notebook_path is required", isError: true };
3001
+ if (new_source === undefined)
3002
+ return { content: "Error: new_source is required", isError: true };
3003
+ const filePath = notebook_path.startsWith("/") ? notebook_path : `${ctx.cwd}/${notebook_path}`;
3004
+ try {
3005
+ const raw = await readFile(filePath, "utf-8");
3006
+ const notebook = JSON.parse(raw);
3007
+ if (!Array.isArray(notebook.cells)) {
3008
+ return { content: "Error: notebook has no cells array", isError: true };
3009
+ }
3010
+ let targetIndex = -1;
3011
+ if (cell_id) {
3012
+ targetIndex = notebook.cells.findIndex((c) => c.id === cell_id);
3013
+ } else if (typeof cell_index === "number") {
3014
+ targetIndex = cell_index;
3015
+ } else {
3016
+ targetIndex = 0;
3017
+ }
3018
+ if (targetIndex < 0 || targetIndex >= notebook.cells.length) {
3019
+ return {
3020
+ content: `Error: cell not found (id=${cell_id ?? "n/a"}, index=${String(cell_index ?? "n/a")})`,
3021
+ isError: true
3022
+ };
3023
+ }
3024
+ const cell = notebook.cells[targetIndex];
3025
+ cell.source = toSourceLines(new_source);
3026
+ await writeFile(filePath, JSON.stringify(notebook, null, 2) + `
3027
+ `, "utf-8");
3028
+ return {
3029
+ content: `Updated notebook cell ${targetIndex} in ${filePath}`
3030
+ };
3031
+ } catch (err) {
3032
+ const message = err instanceof Error ? err.message : String(err);
3033
+ return { content: `Error editing notebook: ${message}`, isError: true };
3034
+ }
3035
+ }
3036
+ };
3037
+
3038
+ // src/tools/web-fetch.ts
3039
+ var DEFAULT_TIMEOUT_MS = 20000;
3040
+ var MAX_OUTPUT = 80000;
3041
+ var WebFetchTool = {
3042
+ name: "WebFetch",
3043
+ description: "Fetches a URL and returns response text.",
3044
+ inputSchema: {
3045
+ type: "object",
3046
+ properties: {
3047
+ url: {
3048
+ type: "string",
3049
+ description: "The URL to fetch."
3050
+ },
3051
+ prompt: {
3052
+ type: "string",
3053
+ description: "Optional fetch intent/instructions."
3054
+ },
3055
+ timeout_ms: {
3056
+ type: "number",
3057
+ description: "Timeout in milliseconds (default 20000)."
3058
+ },
3059
+ max_length: {
3060
+ type: "number",
3061
+ description: "Maximum output length (default 80000)."
3062
+ }
3063
+ },
3064
+ required: ["url"]
3065
+ },
3066
+ async execute(input) {
3067
+ const { url, timeout_ms, max_length } = input ?? {};
3068
+ if (!url)
3069
+ return { content: "Error: url is required", isError: true };
3070
+ const timeout = Math.max(1000, timeout_ms ?? DEFAULT_TIMEOUT_MS);
3071
+ const outLimit = Math.max(1000, max_length ?? MAX_OUTPUT);
3072
+ const controller = new AbortController;
3073
+ const timer = setTimeout(() => controller.abort(), timeout);
3074
+ try {
3075
+ const res = await fetch(url, {
3076
+ method: "GET",
3077
+ signal: controller.signal,
3078
+ headers: {
3079
+ "user-agent": "fourmis-agent-sdk/1.0"
3080
+ }
3081
+ });
3082
+ const contentType = res.headers.get("content-type") ?? "unknown";
3083
+ let body = await res.text();
3084
+ if (body.length > outLimit) {
3085
+ body = body.slice(0, outLimit) + `
3086
+ ... (truncated)`;
3087
+ }
3088
+ return {
3089
+ content: [
3090
+ `Status: ${res.status} ${res.statusText}`,
3091
+ `Content-Type: ${contentType}`,
3092
+ "",
3093
+ body
3094
+ ].join(`
3095
+ `),
3096
+ isError: res.ok ? undefined : true
3097
+ };
3098
+ } catch (err) {
3099
+ const message = err instanceof Error ? err.message : String(err);
3100
+ return { content: `Error fetching URL: ${message}`, isError: true };
3101
+ } finally {
3102
+ clearTimeout(timer);
3103
+ }
3104
+ }
3105
+ };
3106
+
3107
+ // src/tools/web-search.ts
3108
+ var SEARCH_ENDPOINT = "https://duckduckgo.com/html/";
3109
+ function stripTags(input) {
3110
+ return input.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").trim();
3111
+ }
3112
+ var WebSearchTool = {
3113
+ name: "WebSearch",
3114
+ description: "Searches the web and returns top result links.",
3115
+ inputSchema: {
3116
+ type: "object",
3117
+ properties: {
3118
+ query: {
3119
+ type: "string",
3120
+ description: "Search query."
3121
+ },
3122
+ max_results: {
3123
+ type: "number",
3124
+ description: "Maximum results to return (default 5)."
3125
+ }
3126
+ },
3127
+ required: ["query"]
3128
+ },
3129
+ async execute(input) {
3130
+ const { query, max_results } = input ?? {};
3131
+ if (!query) {
3132
+ return { content: "Error: query is required", isError: true };
3133
+ }
3134
+ const limit = Math.max(1, Math.min(20, max_results ?? 5));
3135
+ try {
3136
+ const url = `${SEARCH_ENDPOINT}?q=${encodeURIComponent(query)}`;
3137
+ const res = await fetch(url, {
3138
+ headers: {
3139
+ "user-agent": "fourmis-agent-sdk/1.0"
3140
+ }
3141
+ });
3142
+ if (!res.ok) {
3143
+ return {
3144
+ content: `Error searching web: ${res.status} ${res.statusText}`,
3145
+ isError: true
3146
+ };
3147
+ }
3148
+ const html = await res.text();
3149
+ const matches = [...html.matchAll(/<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g)];
3150
+ if (matches.length === 0) {
3151
+ return { content: "No search results found." };
3152
+ }
3153
+ const lines = [];
3154
+ for (let i = 0;i < Math.min(limit, matches.length); i++) {
3155
+ const href = stripTags(matches[i][1]);
3156
+ const title = stripTags(matches[i][2]);
3157
+ lines.push(`${i + 1}. ${title}
3158
+ ${href}`);
3159
+ }
3160
+ return { content: lines.join(`
3161
+ `) };
3162
+ } catch (err) {
3163
+ const message = err instanceof Error ? err.message : String(err);
3164
+ return { content: `Error searching web: ${message}`, isError: true };
3165
+ }
3166
+ }
3167
+ };
3168
+
3169
+ // src/tools/ask-user-question.ts
3170
+ var AskUserQuestionTool = {
3171
+ name: "AskUserQuestion",
3172
+ description: "Ask the user a clarifying question and wait for their response.",
3173
+ inputSchema: {
3174
+ type: "object",
3175
+ properties: {
3176
+ question: {
3177
+ type: "string",
3178
+ description: "Question to ask the user."
3179
+ },
3180
+ options: {
3181
+ type: "array",
3182
+ items: { type: "string" },
3183
+ description: "Optional fixed choices."
3184
+ }
3185
+ },
3186
+ required: ["question"]
3187
+ },
3188
+ async execute(input) {
3189
+ const { question, options } = input ?? {};
3190
+ if (!question) {
3191
+ return { content: "Error: question is required", isError: true };
3192
+ }
3193
+ const choices = Array.isArray(options) && options.length > 0 ? ` Choices: ${options.join(" | ")}` : "";
3194
+ return {
3195
+ content: `User interaction is not available in this runtime. Unanswered question: ${question}.${choices}`,
3196
+ isError: true
3197
+ };
3198
+ }
3199
+ };
3200
+
3201
+ // src/tools/todo-write.ts
3202
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
3203
+ import { dirname as dirname2, join as join3 } from "path";
3204
+ var TodoWriteTool = {
3205
+ name: "TodoWrite",
3206
+ description: "Write/update task todo items for the current session.",
3207
+ inputSchema: {
3208
+ type: "object",
3209
+ properties: {
3210
+ todos: {
3211
+ type: "array",
3212
+ items: {
3213
+ type: "object",
3214
+ properties: {
3215
+ content: { type: "string" },
3216
+ status: { type: "string", enum: ["pending", "in_progress", "completed"] },
3217
+ activeForm: { type: "string" }
3218
+ },
3219
+ required: ["content", "status"]
3220
+ }
3221
+ }
3222
+ },
3223
+ required: ["todos"]
3224
+ },
3225
+ async execute(input, ctx) {
3226
+ const { todos } = input ?? {};
3227
+ if (!Array.isArray(todos)) {
3228
+ return { content: "Error: todos must be an array", isError: true };
3229
+ }
3230
+ for (const todo of todos) {
3231
+ if (!todo?.content || !todo?.status) {
3232
+ return { content: "Error: each todo requires content and status", isError: true };
3233
+ }
3234
+ }
3235
+ const filePath = join3(ctx.cwd, ".claude", "todos.json");
3236
+ try {
3237
+ await mkdir2(dirname2(filePath), { recursive: true });
3238
+ const payload = {
3239
+ updatedAt: new Date().toISOString(),
3240
+ todos
3241
+ };
3242
+ await writeFile2(filePath, JSON.stringify(payload, null, 2) + `
3243
+ `, "utf-8");
3244
+ return {
3245
+ content: `Saved ${todos.length} todo item(s) to ${filePath}`
3246
+ };
3247
+ } catch (err) {
3248
+ const message = err instanceof Error ? err.message : String(err);
3249
+ return { content: `Error writing todos: ${message}`, isError: true };
3250
+ }
3251
+ }
3252
+ };
3253
+
3254
+ // src/tools/config.ts
3255
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
3256
+ import { join as join4, dirname as dirname3 } from "path";
3257
+ function scopePath(cwd, scope) {
3258
+ if (scope === "project")
3259
+ return join4(cwd, ".claude", "settings.json");
3260
+ return join4(cwd, ".claude", "settings.local.json");
3261
+ }
3262
+ function setByPath(obj, keyPath, value) {
3263
+ const keys = keyPath.split(".").filter(Boolean);
3264
+ if (keys.length === 0)
3265
+ return;
3266
+ let current = obj;
3267
+ for (let i = 0;i < keys.length - 1; i++) {
3268
+ const key = keys[i];
3269
+ const next = current[key];
3270
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
3271
+ current[key] = {};
3272
+ }
3273
+ current = current[key];
3274
+ }
3275
+ current[keys[keys.length - 1]] = value;
3276
+ }
3277
+ function getByPath(obj, keyPath) {
3278
+ const keys = keyPath.split(".").filter(Boolean);
3279
+ let current = obj;
3280
+ for (const key of keys) {
3281
+ if (!current || typeof current !== "object" || Array.isArray(current))
3282
+ return;
3283
+ current = current[key];
3284
+ }
3285
+ return current;
3286
+ }
3287
+ var ConfigTool = {
3288
+ name: "Config",
3289
+ description: "Read or update .claude settings values.",
3290
+ inputSchema: {
3291
+ type: "object",
3292
+ properties: {
3293
+ action: {
3294
+ type: "string",
3295
+ enum: ["get", "set", "list"]
3296
+ },
3297
+ key: {
3298
+ type: "string",
3299
+ description: "Dot-path key (for get/set)."
3300
+ },
3301
+ value: {
3302
+ description: "Value for set action."
3303
+ },
3304
+ scope: {
3305
+ type: "string",
3306
+ enum: ["local", "project"]
3307
+ }
3308
+ },
3309
+ required: ["action"]
3310
+ },
3311
+ async execute(input, ctx) {
3312
+ const {
3313
+ action,
3314
+ key,
3315
+ value,
3316
+ scope = "local"
3317
+ } = input ?? {};
3318
+ if (!action) {
3319
+ return { content: "Error: action is required", isError: true };
3320
+ }
3321
+ const filePath = scopePath(ctx.cwd, scope);
3322
+ let data = {};
3323
+ try {
3324
+ const raw = await readFile2(filePath, "utf-8");
3325
+ data = JSON.parse(raw);
3326
+ } catch {
3327
+ data = {};
3328
+ }
3329
+ if (action === "list") {
3330
+ return { content: JSON.stringify(data, null, 2) };
3331
+ }
3332
+ if (!key) {
3333
+ return { content: "Error: key is required for get/set", isError: true };
3334
+ }
3335
+ if (action === "get") {
3336
+ const out = getByPath(data, key);
3337
+ return { content: out === undefined ? "undefined" : JSON.stringify(out, null, 2) };
3338
+ }
3339
+ setByPath(data, key, value);
3340
+ try {
3341
+ await mkdir3(dirname3(filePath), { recursive: true });
3342
+ await writeFile3(filePath, JSON.stringify(data, null, 2) + `
3343
+ `, "utf-8");
3344
+ return { content: `Updated ${key} in ${filePath}` };
3345
+ } catch (err) {
3346
+ const message = err instanceof Error ? err.message : String(err);
3347
+ return { content: `Error writing config: ${message}`, isError: true };
3348
+ }
3349
+ }
3350
+ };
3351
+
3352
+ // src/tools/exit-plan-mode.ts
3353
+ var ExitPlanModeTool = {
3354
+ name: "ExitPlanMode",
3355
+ description: "Exit plan mode and resume normal execution permissions.",
3356
+ inputSchema: {
3357
+ type: "object",
3358
+ properties: {}
3359
+ },
3360
+ async execute() {
3361
+ return {
3362
+ content: "Exiting plan mode.",
3363
+ metadata: {
3364
+ setPermissionMode: "default"
3365
+ }
3366
+ };
3367
+ }
3368
+ };
2629
3369
  // src/tools/index.ts
2630
3370
  var ALL_TOOLS = {
2631
3371
  Bash: BashTool,
@@ -2633,7 +3373,14 @@ var ALL_TOOLS = {
2633
3373
  Write: WriteTool,
2634
3374
  Edit: EditTool,
2635
3375
  Glob: GlobTool,
2636
- Grep: GrepTool
3376
+ Grep: GrepTool,
3377
+ NotebookEdit: NotebookEditTool,
3378
+ WebFetch: WebFetchTool,
3379
+ WebSearch: WebSearchTool,
3380
+ AskUserQuestion: AskUserQuestionTool,
3381
+ TodoWrite: TodoWriteTool,
3382
+ Config: ConfigTool,
3383
+ ExitPlanMode: ExitPlanModeTool
2637
3384
  };
2638
3385
  function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
2639
3386
  const registry = new ToolRegistry;
@@ -2648,6 +3395,156 @@ function buildToolRegistry(toolNames, allowedTools, disallowedTools) {
2648
3395
  return registry;
2649
3396
  }
2650
3397
 
3398
+ // src/utils/session-store.ts
3399
+ import { readFileSync as readFileSync3, appendFileSync, mkdirSync as mkdirSync2, readdirSync, statSync } from "fs";
3400
+ import { join as join5 } from "path";
3401
+ import { homedir as homedir3 } from "os";
3402
+ function safeStringify(value) {
3403
+ try {
3404
+ return JSON.stringify(value);
3405
+ } catch {
3406
+ return String(value);
3407
+ }
3408
+ }
3409
+ function sanitizeCwd(cwd) {
3410
+ return cwd.replace(/[/.]/g, "-");
3411
+ }
3412
+ function sessionsDir(cwd) {
3413
+ return join5(homedir3(), ".claude", "projects", sanitizeCwd(cwd));
3414
+ }
3415
+ function ensureDir(dir) {
3416
+ mkdirSync2(dir, { recursive: true });
3417
+ }
3418
+ function logMessage(dir, sessionId, entry) {
3419
+ ensureDir(dir);
3420
+ const filePath = join5(dir, `${sessionId}.jsonl`);
3421
+ appendFileSync(filePath, JSON.stringify(entry) + `
3422
+ `);
3423
+ }
3424
+ function createSessionLogger(cwd, sessionId, model) {
3425
+ const dir = sessionsDir(cwd);
3426
+ let lastUuid = null;
3427
+ return (role, content, parentUuid) => {
3428
+ const entryUuid = uuid();
3429
+ let normalizedContent = content;
3430
+ if (role === "user" && typeof content === "string") {
3431
+ normalizedContent = [{ type: "text", text: content }];
3432
+ }
3433
+ const entry = {
3434
+ type: role,
3435
+ uuid: entryUuid,
3436
+ parentUuid: parentUuid ?? lastUuid,
3437
+ sessionId,
3438
+ timestamp: new Date().toISOString(),
3439
+ cwd,
3440
+ isSidechain: false,
3441
+ userType: "external",
3442
+ message: {
3443
+ role,
3444
+ content: normalizedContent,
3445
+ ...role === "assistant" && model ? { model } : {}
3446
+ },
3447
+ ...role === "user" ? { permissionMode: "default" } : {}
3448
+ };
3449
+ logMessage(dir, sessionId, entry);
3450
+ lastUuid = entryUuid;
3451
+ return entryUuid;
3452
+ };
3453
+ }
3454
+ function findLatestSession(cwd) {
3455
+ const dir = sessionsDir(cwd);
3456
+ try {
3457
+ const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl")).map((f) => {
3458
+ const filePath = join5(dir, f);
3459
+ try {
3460
+ return { name: f, mtime: statSync(filePath).mtimeMs };
3461
+ } catch {
3462
+ return null;
3463
+ }
3464
+ }).filter((f) => f !== null).sort((a, b) => b.mtime - a.mtime);
3465
+ if (files.length === 0)
3466
+ return null;
3467
+ return files[0].name.replace(/\.jsonl$/, "");
3468
+ } catch {
3469
+ return null;
3470
+ }
3471
+ }
3472
+ function loadSessionMessages(cwd, sessionId, resumeSessionAt) {
3473
+ const dir = sessionsDir(cwd);
3474
+ const filePath = join5(dir, `${sessionId}.jsonl`);
3475
+ let lines;
3476
+ try {
3477
+ lines = readFileSync3(filePath, "utf-8").trim().split(`
3478
+ `).filter(Boolean);
3479
+ } catch {
3480
+ return [];
3481
+ }
3482
+ const messages = [];
3483
+ let reachedResumePoint = false;
3484
+ for (const line of lines) {
3485
+ if (resumeSessionAt && reachedResumePoint)
3486
+ break;
3487
+ try {
3488
+ const entry = JSON.parse(line);
3489
+ if (entry.type !== "user" && entry.type !== "assistant")
3490
+ continue;
3491
+ if (entry.isMeta === true)
3492
+ continue;
3493
+ const message = entry.message;
3494
+ if (!message)
3495
+ continue;
3496
+ const role = entry.type === "user" ? "user" : "assistant";
3497
+ let content;
3498
+ if (typeof message.content === "string") {
3499
+ content = message.content;
3500
+ } else if (Array.isArray(message.content)) {
3501
+ const normalizedBlocks = [];
3502
+ for (const c of message.content) {
3503
+ if (!c || typeof c !== "object")
3504
+ continue;
3505
+ const block = c;
3506
+ if (typeof block.type !== "string")
3507
+ continue;
3508
+ if (block.type === "text" && typeof block.text === "string") {
3509
+ normalizedBlocks.push({ type: "text", text: block.text });
3510
+ continue;
3511
+ }
3512
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
3513
+ normalizedBlocks.push({
3514
+ type: "tool_use",
3515
+ id: block.id,
3516
+ name: block.name,
3517
+ input: block.input ?? {}
3518
+ });
3519
+ continue;
3520
+ }
3521
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
3522
+ normalizedBlocks.push({
3523
+ type: "tool_result",
3524
+ tool_use_id: block.tool_use_id,
3525
+ content: typeof block.content === "string" ? block.content : safeStringify(block.content),
3526
+ is_error: typeof block.is_error === "boolean" ? block.is_error : undefined
3527
+ });
3528
+ continue;
3529
+ }
3530
+ normalizedBlocks.push({
3531
+ type: "text",
3532
+ text: `[session:${block.type}] ${safeStringify(block)}`
3533
+ });
3534
+ }
3535
+ content = normalizedBlocks;
3536
+ } else {
3537
+ continue;
3538
+ }
3539
+ messages.push({ role, content });
3540
+ if (resumeSessionAt && entry.uuid === resumeSessionAt) {
3541
+ reachedResumePoint = true;
3542
+ }
3543
+ } catch {}
3544
+ }
3545
+ return messages;
3546
+ }
3547
+
2651
3548
  // src/agents/tools.ts
2652
3549
  function createTaskTool(ctx) {
2653
3550
  return {
@@ -2668,6 +3565,15 @@ function createTaskTool(ctx) {
2668
3565
  type: "string",
2669
3566
  description: "The type of agent to use. Must match a registered agent definition."
2670
3567
  },
3568
+ model: {
3569
+ type: "string",
3570
+ enum: ["sonnet", "opus", "haiku"],
3571
+ description: "Optional model family hint for this subagent."
3572
+ },
3573
+ resume: {
3574
+ type: "string",
3575
+ description: "Optional session ID to resume this subagent from."
3576
+ },
2671
3577
  run_in_background: {
2672
3578
  type: "boolean",
2673
3579
  description: "If true, run the task in the background and return a task ID."
@@ -2675,16 +3581,35 @@ function createTaskTool(ctx) {
2675
3581
  max_turns: {
2676
3582
  type: "number",
2677
3583
  description: "Maximum number of turns for the subagent."
3584
+ },
3585
+ name: {
3586
+ type: "string",
3587
+ description: "Optional display name for the spawned subagent."
3588
+ },
3589
+ team_name: {
3590
+ type: "string",
3591
+ description: "Optional team name context for this subagent."
3592
+ },
3593
+ mode: {
3594
+ type: "string",
3595
+ enum: ["acceptEdits", "bypassPermissions", "default", "delegate", "dontAsk", "plan"],
3596
+ description: "Permission mode hint for the spawned subagent."
2678
3597
  }
2679
3598
  },
2680
- required: ["prompt", "subagent_type"]
3599
+ required: ["description", "prompt", "subagent_type"]
2681
3600
  },
2682
3601
  async execute(input, toolCtx) {
2683
3602
  const {
3603
+ description,
2684
3604
  prompt,
2685
3605
  subagent_type,
3606
+ model: requestedModel,
3607
+ resume,
2686
3608
  run_in_background,
2687
- max_turns
3609
+ max_turns,
3610
+ name,
3611
+ team_name,
3612
+ mode
2688
3613
  } = input;
2689
3614
  const agentDef = ctx.agents[subagent_type];
2690
3615
  if (!agentDef) {
@@ -2698,28 +3623,53 @@ function createTaskTool(ctx) {
2698
3623
  await ctx.parentHooks.fire("SubagentStart", { event: "SubagentStart", agent_type: subagent_type, session_id: toolCtx.sessionId }, undefined, { signal: toolCtx.signal });
2699
3624
  }
2700
3625
  const provider = agentDef.provider ? getProvider(agentDef.provider) : ctx.parentProvider;
2701
- const model = agentDef.model ?? ctx.parentModel;
2702
- let subTools;
2703
- if (agentDef.tools) {
2704
- subTools = buildToolRegistry(agentDef.tools);
2705
- } else {
2706
- subTools = buildToolRegistry(resolveToolNames("coding"));
2707
- }
3626
+ const modelAliases = {
3627
+ sonnet: "claude-sonnet-4-5-20250929",
3628
+ opus: "claude-opus-4-5-20251101",
3629
+ haiku: "claude-haiku-4-5-20251001"
3630
+ };
3631
+ const model = requestedModel ? modelAliases[requestedModel] : agentDef.model ?? ctx.parentModel;
3632
+ const baseTools = agentDef.tools ?? resolveToolNames({ type: "preset", preset: "claude_code" });
3633
+ const subTools = buildToolRegistry(baseTools, undefined, agentDef.disallowedTools);
2708
3634
  const maxTurns = max_turns ?? agentDef.maxTurns ?? 10;
2709
- const sessionId = uuid();
3635
+ const sessionId = resume ?? uuid();
3636
+ const previousMessages = resume ? loadSessionMessages(ctx.parentCwd, resume) : undefined;
2710
3637
  const abortController = new AbortController;
2711
3638
  if (toolCtx.signal) {
2712
3639
  toolCtx.signal.addEventListener("abort", () => abortController.abort(), { once: true });
2713
3640
  }
2714
- const systemPrompt = `${agentDef.prompt}
3641
+ const systemPromptParts = [
3642
+ agentDef.prompt,
3643
+ `You are a subagent of type "${subagent_type}". ${agentDef.description}`,
3644
+ `Task summary: ${description}`
3645
+ ];
3646
+ if (agentDef.criticalSystemReminder_EXPERIMENTAL) {
3647
+ systemPromptParts.push(`Critical reminder: ${agentDef.criticalSystemReminder_EXPERIMENTAL}`);
3648
+ }
3649
+ if (agentDef.skills && agentDef.skills.length > 0) {
3650
+ systemPromptParts.push(`Available skills:
3651
+ ${agentDef.skills.map((s) => `- ${s}`).join(`
3652
+ `)}`);
3653
+ }
3654
+ if (name) {
3655
+ systemPromptParts.push(`Subagent name: ${name}`);
3656
+ }
3657
+ if (team_name) {
3658
+ systemPromptParts.push(`Team context: ${team_name}`);
3659
+ }
3660
+ if (mode) {
3661
+ systemPromptParts.push(`Permission mode hint: ${mode}`);
3662
+ }
3663
+ const systemPrompt = systemPromptParts.join(`
2715
3664
 
2716
- You are a subagent of type "${subagent_type}". ${agentDef.description}`;
3665
+ `);
2717
3666
  const runAgent = async () => {
2718
3667
  const messages = [];
2719
3668
  let resultText = "";
2720
3669
  for await (const msg of agentLoop(prompt, {
2721
3670
  provider,
2722
3671
  model,
3672
+ modelState: { current: model },
2723
3673
  systemPrompt,
2724
3674
  tools: subTools,
2725
3675
  permissions: ctx.parentPermissions,
@@ -2727,18 +3677,23 @@ You are a subagent of type "${subagent_type}". ${agentDef.description}`;
2727
3677
  sessionId,
2728
3678
  maxTurns,
2729
3679
  maxBudgetUsd: 5,
2730
- includeStreamEvents: false,
3680
+ includePartialMessages: false,
2731
3681
  signal: abortController.signal,
2732
3682
  env: ctx.parentEnv,
2733
3683
  debug: ctx.parentDebug,
2734
- hooks: ctx.parentHooks
3684
+ hooks: ctx.parentHooks,
3685
+ previousMessages
2735
3686
  })) {
2736
3687
  messages.push(msg);
2737
- if (msg.type === "text") {
2738
- resultText += msg.text;
3688
+ if (msg.type === "assistant") {
3689
+ for (const block of msg.message.content) {
3690
+ if (block.type === "text") {
3691
+ resultText += block.text;
3692
+ }
3693
+ }
2739
3694
  }
2740
3695
  if (msg.type === "result" && msg.subtype === "success") {
2741
- resultText = msg.text ?? resultText;
3696
+ resultText = msg.result || resultText;
2742
3697
  }
2743
3698
  }
2744
3699
  return resultText || "Subagent completed with no text output.";