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.
- package/README.md +126 -198
- package/dist/agent-loop.d.ts +21 -3
- package/dist/agent-loop.d.ts.map +1 -1
- package/dist/agent-loop.js +279 -90
- package/dist/agents/index.js +1079 -124
- package/dist/agents/tools.d.ts.map +1 -1
- package/dist/agents/tools.js +1079 -124
- package/dist/agents/types.d.ts +4 -0
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/api.d.ts +8 -5
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1663 -430
- package/dist/hooks.d.ts +19 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +27 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1671 -431
- package/dist/mcp/client.d.ts +8 -1
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +134 -13
- package/dist/mcp/index.js +134 -13
- package/dist/mcp/types.d.ts +21 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +7 -3
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +41 -2
- package/dist/providers/openai.d.ts +6 -0
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +36 -6
- package/dist/providers/registry.js +76 -8
- package/dist/providers/types.d.ts +4 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/query.d.ts +21 -2
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +69 -1
- package/dist/skills/index.js +23 -1
- package/dist/skills/skills.d.ts +16 -0
- package/dist/skills/skills.d.ts.map +1 -1
- package/dist/skills/skills.js +23 -1
- package/dist/tools/ask-user-question.d.ts +7 -0
- package/dist/tools/ask-user-question.d.ts.map +1 -0
- package/dist/tools/ask-user-question.js +48 -0
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +47 -2
- package/dist/tools/config.d.ts +7 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +114 -0
- package/dist/tools/exit-plan-mode.d.ts +7 -0
- package/dist/tools/exit-plan-mode.d.ts.map +1 -0
- package/dist/tools/exit-plan-mode.js +34 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +506 -9
- package/dist/tools/notebook-edit.d.ts +7 -0
- package/dist/tools/notebook-edit.d.ts.map +1 -0
- package/dist/tools/notebook-edit.js +83 -0
- package/dist/tools/presets.d.ts +2 -1
- package/dist/tools/presets.d.ts.map +1 -1
- package/dist/tools/presets.js +22 -4
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +12 -1
- package/dist/tools/registry.d.ts +2 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +10 -0
- package/dist/tools/todo-write.d.ts +7 -0
- package/dist/tools/todo-write.d.ts.map +1 -0
- package/dist/tools/todo-write.js +69 -0
- package/dist/tools/web-fetch.d.ts +6 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +85 -0
- package/dist/tools/web-search.d.ts +7 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +78 -0
- package/dist/types.d.ts +344 -42
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/session-store.d.ts +1 -1
- package/dist/utils/session-store.d.ts.map +1 -1
- package/dist/utils/session-store.js +49 -2
- package/dist/utils/system-prompt.d.ts +2 -0
- package/dist/utils/system-prompt.d.ts.map +1 -1
- package/dist/utils/system-prompt.js +33 -4
- package/package.json +3 -2
package/dist/agents/index.js
CHANGED
|
@@ -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
|
-
|
|
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: "
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
574
|
-
yield {
|
|
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 (
|
|
579
|
-
yield {
|
|
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
|
-
|
|
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(
|
|
753
|
+
const turnCost = provider.calculateCost(activeModel, turnUsage);
|
|
601
754
|
costUsd += turnCost;
|
|
602
|
-
if (!modelUsage[
|
|
603
|
-
modelUsage[
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
modelUsage[
|
|
612
|
-
modelUsage[
|
|
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
|
-
|
|
634
|
-
|
|
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", {
|
|
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", {
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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", {
|
|
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", {
|
|
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.
|
|
2128
|
-
if (
|
|
2129
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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: {
|
|
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(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/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
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
}
|
|
2706
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
2738
|
-
|
|
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.
|
|
3696
|
+
resultText = msg.result || resultText;
|
|
2742
3697
|
}
|
|
2743
3698
|
}
|
|
2744
3699
|
return resultText || "Subagent completed with no text output.";
|