hoomanjs 1.16.0 → 1.17.0
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/package.json +2 -1
- package/src/acp/acp-agent.ts +2 -0
- package/src/acp/utils/tool-kind.ts +1 -0
- package/src/cli.ts +3 -3
- package/src/configure/app.tsx +201 -62
- package/src/configure/types.ts +3 -0
- package/src/core/agent/index.ts +2 -0
- package/src/core/agents/definitions.ts +2 -0
- package/src/core/approvals/allowed-tools.ts +2 -0
- package/src/core/config.ts +43 -0
- package/src/core/index.ts +5 -7
- package/src/core/prompts/static/web-search.md +38 -0
- package/src/core/prompts/system.ts +4 -1
- package/src/core/tools/index.ts +1 -0
- package/src/core/tools/web-search.ts +278 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoomanjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Hackable Bun-powered AI agent toolkit for building local CLI, ACP, MCP, and channel-driven workflows.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Vaibhav Pandey",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
61
61
|
"@mozilla/readability": "^0.6.0",
|
|
62
62
|
"@strands-agents/sdk": "^1.0.0-rc.3",
|
|
63
|
+
"@tavily/core": "^0.7.2",
|
|
63
64
|
"chromadb": "^3.4.3",
|
|
64
65
|
"cli-highlight": "^2.1.11",
|
|
65
66
|
"cli-spinners": "^3.4.0",
|
package/src/acp/acp-agent.ts
CHANGED
|
@@ -334,6 +334,7 @@ export class AcpAgent implements AgentContract {
|
|
|
334
334
|
agent,
|
|
335
335
|
mcp: { manager },
|
|
336
336
|
} = await bootstrap(
|
|
337
|
+
"acp",
|
|
337
338
|
{
|
|
338
339
|
userId: bootstrapUserId,
|
|
339
340
|
sessionId,
|
|
@@ -432,6 +433,7 @@ export class AcpAgent implements AgentContract {
|
|
|
432
433
|
agent,
|
|
433
434
|
mcp: { manager },
|
|
434
435
|
} = await bootstrap(
|
|
436
|
+
"acp",
|
|
435
437
|
{
|
|
436
438
|
userId: bootstrapUserId,
|
|
437
439
|
sessionId: params.sessionId,
|
package/src/cli.ts
CHANGED
|
@@ -55,7 +55,7 @@ program
|
|
|
55
55
|
const {
|
|
56
56
|
agent,
|
|
57
57
|
mcp: { manager },
|
|
58
|
-
} = await bootstrap({ sessionId }, true);
|
|
58
|
+
} = await bootstrap("default", { sessionId }, true);
|
|
59
59
|
agent.addHook(
|
|
60
60
|
BeforeToolCallEvent,
|
|
61
61
|
createToolApprovalHandler({ yolo: Boolean(options.yolo) }),
|
|
@@ -86,7 +86,7 @@ program
|
|
|
86
86
|
agent,
|
|
87
87
|
mcp: { manager },
|
|
88
88
|
registry,
|
|
89
|
-
} = await bootstrap({ sessionId }, false);
|
|
89
|
+
} = await bootstrap("default", { sessionId }, false);
|
|
90
90
|
|
|
91
91
|
try {
|
|
92
92
|
await chat({
|
|
@@ -129,10 +129,10 @@ program
|
|
|
129
129
|
agent,
|
|
130
130
|
mcp: { manager },
|
|
131
131
|
} = await bootstrap(
|
|
132
|
+
"daemon",
|
|
132
133
|
{
|
|
133
134
|
sessionId: session,
|
|
134
135
|
userId: session,
|
|
135
|
-
mode: "daemon",
|
|
136
136
|
},
|
|
137
137
|
true,
|
|
138
138
|
);
|
package/src/configure/app.tsx
CHANGED
|
@@ -139,6 +139,7 @@ export function ConfigureApp({
|
|
|
139
139
|
({
|
|
140
140
|
name: config.name,
|
|
141
141
|
llm: config.llm,
|
|
142
|
+
search: config.search,
|
|
142
143
|
prompts: config.prompts,
|
|
143
144
|
tools: config.tools,
|
|
144
145
|
compaction: config.compaction,
|
|
@@ -448,6 +449,81 @@ export function ConfigureApp({
|
|
|
448
449
|
label: `Prompts • ${enabledPrompts}/${totalPrompts} enabled`,
|
|
449
450
|
value: () => setScreen({ kind: "config-prompts" }),
|
|
450
451
|
},
|
|
452
|
+
{
|
|
453
|
+
label: "Tools • configure enabled tools",
|
|
454
|
+
value: () => setScreen({ kind: "config-tools" }),
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
label: `Compaction ratio • ${configData.compaction.ratio}`,
|
|
458
|
+
value: () =>
|
|
459
|
+
promptValue({
|
|
460
|
+
title: "Update compaction ratio",
|
|
461
|
+
label: "Ratio",
|
|
462
|
+
initialValue: String(configData.compaction.ratio),
|
|
463
|
+
onSubmit: async (value) => {
|
|
464
|
+
const ratio = parseNumber(value, "Compaction ratio", {
|
|
465
|
+
min: 0,
|
|
466
|
+
max: 1,
|
|
467
|
+
});
|
|
468
|
+
updateConfig(
|
|
469
|
+
{
|
|
470
|
+
compaction: {
|
|
471
|
+
...config.compaction,
|
|
472
|
+
ratio,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
"Updated compaction ratio.",
|
|
476
|
+
);
|
|
477
|
+
setPrompt(null);
|
|
478
|
+
},
|
|
479
|
+
}),
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
label: `Compaction keep • ${configData.compaction.keep}`,
|
|
483
|
+
value: () =>
|
|
484
|
+
promptValue({
|
|
485
|
+
title: "Update compaction keep",
|
|
486
|
+
label: "Keep",
|
|
487
|
+
initialValue: String(configData.compaction.keep),
|
|
488
|
+
onSubmit: async (value) => {
|
|
489
|
+
const keep = parseNumber(value, "Compaction keep", {
|
|
490
|
+
min: 0,
|
|
491
|
+
integer: true,
|
|
492
|
+
});
|
|
493
|
+
updateConfig(
|
|
494
|
+
{
|
|
495
|
+
compaction: {
|
|
496
|
+
...config.compaction,
|
|
497
|
+
keep,
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
"Updated compaction keep.",
|
|
501
|
+
);
|
|
502
|
+
setPrompt(null);
|
|
503
|
+
},
|
|
504
|
+
}),
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
label: "Back",
|
|
508
|
+
value: () => setScreen({ kind: "home" }),
|
|
509
|
+
},
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<MenuScreen
|
|
514
|
+
title="Configuration"
|
|
515
|
+
description="Edit the same values loaded from ~/.hooman/config.json."
|
|
516
|
+
items={items}
|
|
517
|
+
/>
|
|
518
|
+
);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const renderToolsConfigMenu = () => {
|
|
522
|
+
const items: MenuItem[] = [
|
|
523
|
+
{
|
|
524
|
+
label: `Search tool • ${configData.search.enabled ? "Enabled" : "Disabled"} • ${configData.search.provider}`,
|
|
525
|
+
value: () => setScreen({ kind: "config-search" }),
|
|
526
|
+
},
|
|
451
527
|
{
|
|
452
528
|
label: `Todo tool • ${configData.tools.todo.enabled ? "Enabled" : "Disabled"}`,
|
|
453
529
|
value: () => {
|
|
@@ -462,7 +538,7 @@ export function ConfigureApp({
|
|
|
462
538
|
},
|
|
463
539
|
`Todo tool ${configData.tools.todo.enabled ? "disabled" : "enabled"}.`,
|
|
464
540
|
);
|
|
465
|
-
setScreen({ kind: "config" });
|
|
541
|
+
setScreen({ kind: "config-tools" });
|
|
466
542
|
},
|
|
467
543
|
},
|
|
468
544
|
{
|
|
@@ -479,7 +555,7 @@ export function ConfigureApp({
|
|
|
479
555
|
},
|
|
480
556
|
`Fetch tool ${configData.tools.fetch.enabled ? "disabled" : "enabled"}.`,
|
|
481
557
|
);
|
|
482
|
-
setScreen({ kind: "config" });
|
|
558
|
+
setScreen({ kind: "config-tools" });
|
|
483
559
|
},
|
|
484
560
|
},
|
|
485
561
|
{
|
|
@@ -496,7 +572,7 @@ export function ConfigureApp({
|
|
|
496
572
|
},
|
|
497
573
|
`Filesystem tool ${configData.tools.filesystem.enabled ? "disabled" : "enabled"}.`,
|
|
498
574
|
);
|
|
499
|
-
setScreen({ kind: "config" });
|
|
575
|
+
setScreen({ kind: "config-tools" });
|
|
500
576
|
},
|
|
501
577
|
},
|
|
502
578
|
{
|
|
@@ -513,7 +589,7 @@ export function ConfigureApp({
|
|
|
513
589
|
},
|
|
514
590
|
`Shell tool ${configData.tools.shell.enabled ? "disabled" : "enabled"}.`,
|
|
515
591
|
);
|
|
516
|
-
setScreen({ kind: "config" });
|
|
592
|
+
setScreen({ kind: "config-tools" });
|
|
517
593
|
},
|
|
518
594
|
},
|
|
519
595
|
{
|
|
@@ -530,7 +606,7 @@ export function ConfigureApp({
|
|
|
530
606
|
},
|
|
531
607
|
`Sleep tool ${configData.tools.sleep.enabled ? "disabled" : "enabled"}.`,
|
|
532
608
|
);
|
|
533
|
-
setScreen({ kind: "config" });
|
|
609
|
+
setScreen({ kind: "config-tools" });
|
|
534
610
|
},
|
|
535
611
|
},
|
|
536
612
|
{
|
|
@@ -555,7 +631,7 @@ export function ConfigureApp({
|
|
|
555
631
|
},
|
|
556
632
|
`MCP tools ${configData.tools.mcp.enabled ? "disabled" : "enabled"}.`,
|
|
557
633
|
);
|
|
558
|
-
setScreen({ kind: "config" });
|
|
634
|
+
setScreen({ kind: "config-tools" });
|
|
559
635
|
},
|
|
560
636
|
},
|
|
561
637
|
{
|
|
@@ -572,69 +648,19 @@ export function ConfigureApp({
|
|
|
572
648
|
},
|
|
573
649
|
`Skills tools ${configData.tools.skills.enabled ? "disabled" : "enabled"}.`,
|
|
574
650
|
);
|
|
575
|
-
setScreen({ kind: "config" });
|
|
651
|
+
setScreen({ kind: "config-tools" });
|
|
576
652
|
},
|
|
577
653
|
},
|
|
578
|
-
{
|
|
579
|
-
label: `Compaction ratio • ${configData.compaction.ratio}`,
|
|
580
|
-
value: () =>
|
|
581
|
-
promptValue({
|
|
582
|
-
title: "Update compaction ratio",
|
|
583
|
-
label: "Ratio",
|
|
584
|
-
initialValue: String(configData.compaction.ratio),
|
|
585
|
-
onSubmit: async (value) => {
|
|
586
|
-
const ratio = parseNumber(value, "Compaction ratio", {
|
|
587
|
-
min: 0,
|
|
588
|
-
max: 1,
|
|
589
|
-
});
|
|
590
|
-
updateConfig(
|
|
591
|
-
{
|
|
592
|
-
compaction: {
|
|
593
|
-
...config.compaction,
|
|
594
|
-
ratio,
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
"Updated compaction ratio.",
|
|
598
|
-
);
|
|
599
|
-
setPrompt(null);
|
|
600
|
-
},
|
|
601
|
-
}),
|
|
602
|
-
},
|
|
603
|
-
{
|
|
604
|
-
label: `Compaction keep • ${configData.compaction.keep}`,
|
|
605
|
-
value: () =>
|
|
606
|
-
promptValue({
|
|
607
|
-
title: "Update compaction keep",
|
|
608
|
-
label: "Keep",
|
|
609
|
-
initialValue: String(configData.compaction.keep),
|
|
610
|
-
onSubmit: async (value) => {
|
|
611
|
-
const keep = parseNumber(value, "Compaction keep", {
|
|
612
|
-
min: 0,
|
|
613
|
-
integer: true,
|
|
614
|
-
});
|
|
615
|
-
updateConfig(
|
|
616
|
-
{
|
|
617
|
-
compaction: {
|
|
618
|
-
...config.compaction,
|
|
619
|
-
keep,
|
|
620
|
-
},
|
|
621
|
-
},
|
|
622
|
-
"Updated compaction keep.",
|
|
623
|
-
);
|
|
624
|
-
setPrompt(null);
|
|
625
|
-
},
|
|
626
|
-
}),
|
|
627
|
-
},
|
|
628
654
|
{
|
|
629
655
|
label: "Back",
|
|
630
|
-
value: () => setScreen({ kind: "
|
|
656
|
+
value: () => setScreen({ kind: "config" }),
|
|
631
657
|
},
|
|
632
658
|
];
|
|
633
659
|
|
|
634
660
|
return (
|
|
635
661
|
<MenuScreen
|
|
636
|
-
title="
|
|
637
|
-
description="
|
|
662
|
+
title="Tools"
|
|
663
|
+
description="Enable, disable, and configure built-in tools."
|
|
638
664
|
items={items}
|
|
639
665
|
/>
|
|
640
666
|
);
|
|
@@ -696,7 +722,7 @@ export function ConfigureApp({
|
|
|
696
722
|
}),
|
|
697
723
|
{
|
|
698
724
|
label: "Back",
|
|
699
|
-
value: () => setScreen({ kind: "config" }),
|
|
725
|
+
value: () => setScreen({ kind: "config-tools" }),
|
|
700
726
|
},
|
|
701
727
|
];
|
|
702
728
|
|
|
@@ -709,6 +735,113 @@ export function ConfigureApp({
|
|
|
709
735
|
);
|
|
710
736
|
};
|
|
711
737
|
|
|
738
|
+
const renderSearchProviderMenu = () => {
|
|
739
|
+
const items: MenuItem[] = [
|
|
740
|
+
...(["brave", "tavily"] as const).map((provider) => ({
|
|
741
|
+
label:
|
|
742
|
+
provider === configData.search.provider
|
|
743
|
+
? `${provider} • current`
|
|
744
|
+
: provider,
|
|
745
|
+
value: () => {
|
|
746
|
+
updateConfig(
|
|
747
|
+
{
|
|
748
|
+
search: {
|
|
749
|
+
...config.search,
|
|
750
|
+
provider,
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
`Updated search provider to "${provider}".`,
|
|
754
|
+
);
|
|
755
|
+
setScreen({ kind: "config-search" });
|
|
756
|
+
},
|
|
757
|
+
})),
|
|
758
|
+
{
|
|
759
|
+
label: "Back",
|
|
760
|
+
value: () => setScreen({ kind: "config-search" }),
|
|
761
|
+
},
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
return (
|
|
765
|
+
<MenuScreen
|
|
766
|
+
title="Search Provider"
|
|
767
|
+
description="Pick which web search provider to use."
|
|
768
|
+
items={items}
|
|
769
|
+
/>
|
|
770
|
+
);
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
const renderSearchConfigMenu = () => {
|
|
774
|
+
const activeProvider = configData.search.provider;
|
|
775
|
+
const apiKey =
|
|
776
|
+
activeProvider === "brave"
|
|
777
|
+
? configData.search.brave.apiKey
|
|
778
|
+
: configData.search.tavily.apiKey;
|
|
779
|
+
const redacted = compactJson(
|
|
780
|
+
maskSensitiveParamsForDisplay({ apiKey: apiKey ?? "" }),
|
|
781
|
+
);
|
|
782
|
+
const items: MenuItem[] = [
|
|
783
|
+
{
|
|
784
|
+
label: `Enabled • ${configData.search.enabled ? "On" : "Off"}`,
|
|
785
|
+
value: () => {
|
|
786
|
+
updateConfig(
|
|
787
|
+
{
|
|
788
|
+
search: {
|
|
789
|
+
...config.search,
|
|
790
|
+
enabled: !configData.search.enabled,
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
`Search tool ${configData.search.enabled ? "disabled" : "enabled"}.`,
|
|
794
|
+
);
|
|
795
|
+
setScreen({ kind: "config-search" });
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
label: `Provider • ${configData.search.provider}`,
|
|
800
|
+
value: () => setScreen({ kind: "config-search-provider" }),
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
label: `${activeProvider} API key • ${truncate(redacted, 44)}`,
|
|
804
|
+
value: () =>
|
|
805
|
+
promptValue({
|
|
806
|
+
title: `Update ${activeProvider} API key`,
|
|
807
|
+
label: "API key",
|
|
808
|
+
initialValue: apiKey ?? "",
|
|
809
|
+
onSubmit: async (value) => {
|
|
810
|
+
const nextApiKey = value.trim();
|
|
811
|
+
if (!nextApiKey) {
|
|
812
|
+
throw new Error("API key is required.");
|
|
813
|
+
}
|
|
814
|
+
updateConfig(
|
|
815
|
+
{
|
|
816
|
+
search: {
|
|
817
|
+
...config.search,
|
|
818
|
+
[activeProvider]: {
|
|
819
|
+
...config.search[activeProvider],
|
|
820
|
+
apiKey: nextApiKey,
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
`Updated ${activeProvider} API key.`,
|
|
825
|
+
);
|
|
826
|
+
setPrompt(null);
|
|
827
|
+
},
|
|
828
|
+
}),
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
label: "Back",
|
|
832
|
+
value: () => setScreen({ kind: "config-tools" }),
|
|
833
|
+
},
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
return (
|
|
837
|
+
<MenuScreen
|
|
838
|
+
title="Search"
|
|
839
|
+
description="Configure web search provider and credentials."
|
|
840
|
+
items={items}
|
|
841
|
+
/>
|
|
842
|
+
);
|
|
843
|
+
};
|
|
844
|
+
|
|
712
845
|
const renderLtmConfigMenu = () => {
|
|
713
846
|
const items: MenuItem[] = [
|
|
714
847
|
{
|
|
@@ -793,7 +926,7 @@ export function ConfigureApp({
|
|
|
793
926
|
},
|
|
794
927
|
{
|
|
795
928
|
label: "Back",
|
|
796
|
-
value: () => setScreen({ kind: "config" }),
|
|
929
|
+
value: () => setScreen({ kind: "config-tools" }),
|
|
797
930
|
},
|
|
798
931
|
];
|
|
799
932
|
|
|
@@ -1171,6 +1304,12 @@ export function ConfigureApp({
|
|
|
1171
1304
|
return renderProviderMenu();
|
|
1172
1305
|
case "config-prompts":
|
|
1173
1306
|
return renderPromptsConfigMenu();
|
|
1307
|
+
case "config-tools":
|
|
1308
|
+
return renderToolsConfigMenu();
|
|
1309
|
+
case "config-search":
|
|
1310
|
+
return renderSearchConfigMenu();
|
|
1311
|
+
case "config-search-provider":
|
|
1312
|
+
return renderSearchProviderMenu();
|
|
1174
1313
|
case "config-ltm":
|
|
1175
1314
|
return renderLtmConfigMenu();
|
|
1176
1315
|
case "config-wiki":
|
package/src/configure/types.ts
CHANGED
|
@@ -12,8 +12,11 @@ export type ConfigureAppProps = {
|
|
|
12
12
|
export type Screen =
|
|
13
13
|
| { kind: "home" }
|
|
14
14
|
| { kind: "config" }
|
|
15
|
+
| { kind: "config-tools" }
|
|
15
16
|
| { kind: "config-provider" }
|
|
16
17
|
| { kind: "config-prompts" }
|
|
18
|
+
| { kind: "config-search" }
|
|
19
|
+
| { kind: "config-search-provider" }
|
|
17
20
|
| { kind: "config-ltm" }
|
|
18
21
|
| { kind: "config-wiki" }
|
|
19
22
|
| { kind: "mcp" }
|
package/src/core/agent/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
createThinkingTools,
|
|
29
29
|
createTimeTools,
|
|
30
30
|
createWikiTools,
|
|
31
|
+
createWebSearchTools,
|
|
31
32
|
} from "../tools";
|
|
32
33
|
import { clearTodoState } from "../tools/todo.ts";
|
|
33
34
|
|
|
@@ -73,6 +74,7 @@ export async function create(
|
|
|
73
74
|
...(ltm ? createLongTermMemoryTools(ltm) : []),
|
|
74
75
|
...(config.tools.filesystem.enabled ? createFilesystemTools() : []),
|
|
75
76
|
...(config.tools.shell.enabled ? createShellTools() : []),
|
|
77
|
+
...(config.search.enabled ? createWebSearchTools(config) : []),
|
|
76
78
|
...(config.tools.wiki.enabled ? createWikiTools(config) : []),
|
|
77
79
|
...(config.tools.mcp.enabled ? createMcpTools(mcp.config) : []),
|
|
78
80
|
...(config.tools.skills.enabled ? createSkillsTools(registry) : []),
|
|
@@ -26,6 +26,7 @@ export const BUILTIN_AGENT_CONFIGS: readonly AgentConfig[] = [
|
|
|
26
26
|
"search_files",
|
|
27
27
|
"get_file_info",
|
|
28
28
|
"fetch",
|
|
29
|
+
"web_search",
|
|
29
30
|
"think",
|
|
30
31
|
],
|
|
31
32
|
},
|
|
@@ -40,6 +41,7 @@ export const BUILTIN_AGENT_CONFIGS: readonly AgentConfig[] = [
|
|
|
40
41
|
"directory_tree",
|
|
41
42
|
"search_files",
|
|
42
43
|
"get_file_info",
|
|
44
|
+
"web_search",
|
|
43
45
|
"think",
|
|
44
46
|
],
|
|
45
47
|
},
|
package/src/core/config.ts
CHANGED
|
@@ -92,6 +92,23 @@ const AgentsPartialSchema = z.object({
|
|
|
92
92
|
concurrency: z.number().int().min(1).optional(),
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
+
const SearchProviderSchema = z.enum(["brave", "tavily"]);
|
|
96
|
+
|
|
97
|
+
const SearchPartialSchema = z.object({
|
|
98
|
+
enabled: z.boolean().optional(),
|
|
99
|
+
provider: SearchProviderSchema.optional(),
|
|
100
|
+
brave: z
|
|
101
|
+
.object({
|
|
102
|
+
apiKey: z.string().min(1).optional(),
|
|
103
|
+
})
|
|
104
|
+
.optional(),
|
|
105
|
+
tavily: z
|
|
106
|
+
.object({
|
|
107
|
+
apiKey: z.string().min(1).optional(),
|
|
108
|
+
})
|
|
109
|
+
.optional(),
|
|
110
|
+
});
|
|
111
|
+
|
|
95
112
|
const ToolsPartialSchema = z.object({
|
|
96
113
|
todo: ToolTogglePartialSchema.optional(),
|
|
97
114
|
fetch: ToolTogglePartialSchema.optional(),
|
|
@@ -109,6 +126,7 @@ const ConfigSchema = z
|
|
|
109
126
|
.object({
|
|
110
127
|
name: z.string().min(1),
|
|
111
128
|
llm: LlmSchema,
|
|
129
|
+
search: SearchPartialSchema.nullish(),
|
|
112
130
|
prompts: PromptsPartialSchema.nullish(),
|
|
113
131
|
tools: ToolsPartialSchema.nullish(),
|
|
114
132
|
compaction: CompactionPartialSchema.nullish().transform((c) => ({
|
|
@@ -122,6 +140,16 @@ const ConfigSchema = z
|
|
|
122
140
|
return {
|
|
123
141
|
name: input.name,
|
|
124
142
|
llm: input.llm,
|
|
143
|
+
search: {
|
|
144
|
+
enabled: input.search?.enabled ?? false,
|
|
145
|
+
provider: input.search?.provider ?? "brave",
|
|
146
|
+
brave: {
|
|
147
|
+
apiKey: input.search?.brave?.apiKey,
|
|
148
|
+
},
|
|
149
|
+
tavily: {
|
|
150
|
+
apiKey: input.search?.tavily?.apiKey,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
125
153
|
prompts: {
|
|
126
154
|
behaviour: input.prompts?.behaviour ?? DEFAULT_PROMPTS.behaviour,
|
|
127
155
|
communication:
|
|
@@ -189,6 +217,7 @@ export type CompactionConfig = ConfigData["compaction"];
|
|
|
189
217
|
export type PromptsConfig = ConfigData["prompts"];
|
|
190
218
|
export type LtmConfig = ConfigData["tools"]["ltm"];
|
|
191
219
|
export type WikiConfig = ConfigData["tools"]["wiki"];
|
|
220
|
+
export type SearchConfig = ConfigData["search"];
|
|
192
221
|
export type ToolsConfig = ConfigData["tools"];
|
|
193
222
|
|
|
194
223
|
const defaultConfigData = (): ConfigData => ({
|
|
@@ -198,6 +227,12 @@ const defaultConfigData = (): ConfigData => ({
|
|
|
198
227
|
model: "gemma4:e4b",
|
|
199
228
|
params: {},
|
|
200
229
|
},
|
|
230
|
+
search: {
|
|
231
|
+
enabled: false,
|
|
232
|
+
provider: "brave",
|
|
233
|
+
brave: { apiKey: undefined },
|
|
234
|
+
tavily: { apiKey: undefined },
|
|
235
|
+
},
|
|
201
236
|
prompts: { ...DEFAULT_PROMPTS },
|
|
202
237
|
tools: {
|
|
203
238
|
todo: {
|
|
@@ -263,6 +298,14 @@ export class Config {
|
|
|
263
298
|
return this.data.llm;
|
|
264
299
|
}
|
|
265
300
|
|
|
301
|
+
get search(): SearchConfig {
|
|
302
|
+
return {
|
|
303
|
+
...this.data.search,
|
|
304
|
+
brave: { ...this.data.search.brave },
|
|
305
|
+
tavily: { ...this.data.search.tavily },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
266
309
|
get prompts(): PromptsConfig {
|
|
267
310
|
return { ...this.data.prompts };
|
|
268
311
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -21,16 +21,18 @@ import {
|
|
|
21
21
|
export type BootstrapMeta = {
|
|
22
22
|
userId?: string;
|
|
23
23
|
sessionId?: string;
|
|
24
|
-
mode?: "default" | "daemon";
|
|
25
24
|
acp?: AcpMeta;
|
|
26
25
|
};
|
|
27
26
|
|
|
27
|
+
export type BootstrapMode = "default" | "daemon" | "acp";
|
|
28
|
+
|
|
28
29
|
export type AcpMeta = {
|
|
29
30
|
systemPrompt?: string;
|
|
30
31
|
mcpServers?: NamedMcpTransport[];
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
export async function bootstrap(
|
|
35
|
+
mode: BootstrapMode,
|
|
34
36
|
meta: BootstrapMeta,
|
|
35
37
|
print: boolean = false,
|
|
36
38
|
): Promise<{
|
|
@@ -43,16 +45,12 @@ export async function bootstrap(
|
|
|
43
45
|
const mcpConfig = createMcpConfig(mcpJsonPath());
|
|
44
46
|
const mcpManager = createMcpManager(
|
|
45
47
|
mcpConfig,
|
|
46
|
-
|
|
48
|
+
mode === "acp",
|
|
47
49
|
meta.acp?.mcpServers ?? [],
|
|
48
50
|
);
|
|
49
51
|
const mcp = { config: mcpConfig, manager: mcpManager };
|
|
50
52
|
const registry = createSkillsRegistry(basePath());
|
|
51
|
-
const system = await createSystemPrompt(
|
|
52
|
-
instructionsMdPath(),
|
|
53
|
-
config,
|
|
54
|
-
meta.mode ?? "default",
|
|
55
|
-
);
|
|
53
|
+
const system = await createSystemPrompt(instructionsMdPath(), config, mode);
|
|
56
54
|
const agent = await createAgent(config, system, registry, mcp, print, {
|
|
57
55
|
userId: meta?.userId ?? meta?.sessionId,
|
|
58
56
|
sessionId: meta?.sessionId,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## Web Search
|
|
2
|
+
|
|
3
|
+
You have access to a `web_search` tool for finding relevant webpages and snippets.
|
|
4
|
+
|
|
5
|
+
### When To Use It
|
|
6
|
+
|
|
7
|
+
- Use `web_search` when you need current or external information not available in local context.
|
|
8
|
+
- Prefer it for discovering candidate sources before reading full page content.
|
|
9
|
+
- After identifying promising URLs, use `fetch` to read those pages in detail.
|
|
10
|
+
|
|
11
|
+
### Input Contract
|
|
12
|
+
|
|
13
|
+
Use only these inputs:
|
|
14
|
+
|
|
15
|
+
- `query` (required)
|
|
16
|
+
- `count` (optional)
|
|
17
|
+
- `freshness` (optional: `day`, `week`, `month`, `year`)
|
|
18
|
+
- `start_date` + `end_date` (optional date range, `YYYY-MM-DD`)
|
|
19
|
+
- `country` (optional country code)
|
|
20
|
+
- `safe_search` (optional boolean)
|
|
21
|
+
|
|
22
|
+
Do not invent provider-specific parameters.
|
|
23
|
+
|
|
24
|
+
### Examples
|
|
25
|
+
|
|
26
|
+
- General current-information search:
|
|
27
|
+
- `{"query":"latest TypeScript 6 release notes","count":5}`
|
|
28
|
+
- Recency-filtered search:
|
|
29
|
+
- `{"query":"browser rendering performance updates","freshness":"week","count":5}`
|
|
30
|
+
- Country-targeted search:
|
|
31
|
+
- `{"query":"renewable energy policy updates","country":"DE","count":5}`
|
|
32
|
+
- Search operators inside query:
|
|
33
|
+
- `{"query":"\"climate change\" site:ipcc.ch filetype:pdf -draft","count":5}`
|
|
34
|
+
|
|
35
|
+
### Notes
|
|
36
|
+
|
|
37
|
+
- `web_search` returns result pages and snippets, not full article bodies.
|
|
38
|
+
- For complete page content, call `fetch` on selected result URLs.
|
|
@@ -14,6 +14,7 @@ const STATIC_PROMPT_FILES = [
|
|
|
14
14
|
"thinking.md",
|
|
15
15
|
"filesystem.md",
|
|
16
16
|
"fetch.md",
|
|
17
|
+
"web-search.md",
|
|
17
18
|
"shell.md",
|
|
18
19
|
"sleep.md",
|
|
19
20
|
"daemon.md",
|
|
@@ -30,7 +31,7 @@ const HARNESS_PROMPT_FILES = [
|
|
|
30
31
|
{ key: "guardrails", file: "guardrails.md" },
|
|
31
32
|
] as const;
|
|
32
33
|
|
|
33
|
-
export type SystemMode = "default" | "daemon";
|
|
34
|
+
export type SystemMode = "default" | "daemon" | "acp";
|
|
34
35
|
|
|
35
36
|
const SECTION_BREAK = "\n\n---\n\n";
|
|
36
37
|
|
|
@@ -61,6 +62,8 @@ export class System {
|
|
|
61
62
|
return this.config.tools.ltm.enabled;
|
|
62
63
|
case "fetch.md":
|
|
63
64
|
return this.config.tools.fetch.enabled;
|
|
65
|
+
case "web-search.md":
|
|
66
|
+
return this.config.search.enabled;
|
|
64
67
|
case "todo.md":
|
|
65
68
|
return this.config.tools.todo.enabled;
|
|
66
69
|
case "filesystem.md":
|
package/src/core/tools/index.ts
CHANGED
|
@@ -5,4 +5,5 @@ export { createShellTools } from "./shell.ts";
|
|
|
5
5
|
export { createThinkingTools } from "./thinking.ts";
|
|
6
6
|
export { createTimeTools } from "./time.ts";
|
|
7
7
|
export { createTodoTools } from "./todo.ts";
|
|
8
|
+
export { createWebSearchTools } from "./web-search.ts";
|
|
8
9
|
export { createWikiTools } from "./wiki.ts";
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { tool } from "@strands-agents/sdk";
|
|
2
|
+
import type { JSONValue, ToolContext } from "@strands-agents/sdk";
|
|
3
|
+
import { tavily } from "@tavily/core";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import type { Config } from "../config.ts";
|
|
6
|
+
|
|
7
|
+
const BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
|
8
|
+
const DEFAULT_TIMEOUT_SECONDS = 20;
|
|
9
|
+
const DEFAULT_RESULT_COUNT = 5;
|
|
10
|
+
const MAX_RESULT_COUNT = 20;
|
|
11
|
+
|
|
12
|
+
const FreshnessSchema = z.enum(["day", "week", "month", "year"]);
|
|
13
|
+
|
|
14
|
+
const InputSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
query: z.string().min(1).max(400),
|
|
17
|
+
count: z.coerce
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(MAX_RESULT_COUNT)
|
|
22
|
+
.default(DEFAULT_RESULT_COUNT),
|
|
23
|
+
freshness: FreshnessSchema.optional(),
|
|
24
|
+
start_date: z
|
|
25
|
+
.string()
|
|
26
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
27
|
+
.optional(),
|
|
28
|
+
end_date: z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
31
|
+
.optional(),
|
|
32
|
+
country: z
|
|
33
|
+
.string()
|
|
34
|
+
.regex(/^[a-z]{2}$/i)
|
|
35
|
+
.optional(),
|
|
36
|
+
safe_search: z.boolean().optional(),
|
|
37
|
+
})
|
|
38
|
+
.superRefine((input, context) => {
|
|
39
|
+
const hasStartDate = Boolean(input.start_date);
|
|
40
|
+
const hasEndDate = Boolean(input.end_date);
|
|
41
|
+
if (hasStartDate !== hasEndDate) {
|
|
42
|
+
context.addIssue({
|
|
43
|
+
code: z.ZodIssueCode.custom,
|
|
44
|
+
message: "start_date and end_date must be provided together.",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (hasStartDate && input.freshness) {
|
|
48
|
+
context.addIssue({
|
|
49
|
+
code: z.ZodIssueCode.custom,
|
|
50
|
+
message:
|
|
51
|
+
"Use either freshness or start_date/end_date, not both together.",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
type WebSearchInput = z.infer<typeof InputSchema>;
|
|
57
|
+
|
|
58
|
+
type NormalizedResult = {
|
|
59
|
+
title: string;
|
|
60
|
+
url: string;
|
|
61
|
+
snippet: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type NormalizedOutput = {
|
|
65
|
+
provider: "brave" | "tavily";
|
|
66
|
+
query: string;
|
|
67
|
+
results: NormalizedResult[];
|
|
68
|
+
metadata: {
|
|
69
|
+
count: number;
|
|
70
|
+
freshness: WebSearchInput["freshness"] | null;
|
|
71
|
+
start_date: string | null;
|
|
72
|
+
end_date: string | null;
|
|
73
|
+
country: string | null;
|
|
74
|
+
safe_search: boolean | null;
|
|
75
|
+
returned_results: number;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function toJsonValue(value: unknown): JSONValue {
|
|
80
|
+
return JSON.parse(JSON.stringify(value)) as JSONValue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toBraveFreshness(input: WebSearchInput): string | undefined {
|
|
84
|
+
if (input.start_date && input.end_date) {
|
|
85
|
+
return `${input.start_date}to${input.end_date}`;
|
|
86
|
+
}
|
|
87
|
+
switch (input.freshness) {
|
|
88
|
+
case "day":
|
|
89
|
+
return "pd";
|
|
90
|
+
case "week":
|
|
91
|
+
return "pw";
|
|
92
|
+
case "month":
|
|
93
|
+
return "pm";
|
|
94
|
+
case "year":
|
|
95
|
+
return "py";
|
|
96
|
+
default:
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function tavilyCountryCode(code: string | undefined): string | undefined {
|
|
102
|
+
if (!code) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const display = new Intl.DisplayNames(["en"], { type: "region" }).of(
|
|
107
|
+
code.toUpperCase(),
|
|
108
|
+
);
|
|
109
|
+
if (!display) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return display.toLowerCase();
|
|
113
|
+
} catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cleanString(value: unknown): string {
|
|
119
|
+
return typeof value === "string" ? value.trim() : "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeBraveResults(payload: unknown): NormalizedResult[] {
|
|
123
|
+
const root = payload as {
|
|
124
|
+
web?: { results?: Array<Record<string, unknown>> };
|
|
125
|
+
};
|
|
126
|
+
const results = root.web?.results;
|
|
127
|
+
if (!Array.isArray(results)) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
return results
|
|
131
|
+
.map((item) => ({
|
|
132
|
+
title: cleanString(item.title),
|
|
133
|
+
url: cleanString(item.url),
|
|
134
|
+
snippet: cleanString(item.description),
|
|
135
|
+
}))
|
|
136
|
+
.filter((item) => item.url.length > 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeTavilyResults(payload: unknown): NormalizedResult[] {
|
|
140
|
+
const root = payload as { results?: Array<Record<string, unknown>> };
|
|
141
|
+
if (!Array.isArray(root.results)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
return root.results
|
|
145
|
+
.map((item) => ({
|
|
146
|
+
title: cleanString(item.title),
|
|
147
|
+
url: cleanString(item.url),
|
|
148
|
+
snippet: cleanString(item.content),
|
|
149
|
+
}))
|
|
150
|
+
.filter((item) => item.url.length > 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function normalizedOutput(
|
|
154
|
+
provider: "brave" | "tavily",
|
|
155
|
+
input: WebSearchInput,
|
|
156
|
+
results: NormalizedResult[],
|
|
157
|
+
): NormalizedOutput {
|
|
158
|
+
return {
|
|
159
|
+
provider,
|
|
160
|
+
query: input.query,
|
|
161
|
+
results,
|
|
162
|
+
metadata: {
|
|
163
|
+
count: input.count,
|
|
164
|
+
freshness: input.freshness ?? null,
|
|
165
|
+
start_date: input.start_date ?? null,
|
|
166
|
+
end_date: input.end_date ?? null,
|
|
167
|
+
country: input.country?.toUpperCase() ?? null,
|
|
168
|
+
safe_search: input.safe_search ?? null,
|
|
169
|
+
returned_results: results.length,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function searchBrave(
|
|
175
|
+
input: WebSearchInput,
|
|
176
|
+
apiKey: string,
|
|
177
|
+
signal: AbortSignal,
|
|
178
|
+
): Promise<NormalizedOutput> {
|
|
179
|
+
const url = new URL(BRAVE_ENDPOINT);
|
|
180
|
+
url.searchParams.set("q", input.query);
|
|
181
|
+
url.searchParams.set("count", String(input.count));
|
|
182
|
+
if (input.country) {
|
|
183
|
+
url.searchParams.set("country", input.country.toUpperCase());
|
|
184
|
+
}
|
|
185
|
+
const freshness = toBraveFreshness(input);
|
|
186
|
+
if (freshness) {
|
|
187
|
+
url.searchParams.set("freshness", freshness);
|
|
188
|
+
}
|
|
189
|
+
if (input.safe_search !== undefined) {
|
|
190
|
+
url.searchParams.set("safesearch", input.safe_search ? "strict" : "off");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const response = await fetch(url, {
|
|
194
|
+
method: "GET",
|
|
195
|
+
signal,
|
|
196
|
+
headers: {
|
|
197
|
+
accept: "application/json",
|
|
198
|
+
"x-subscription-token": apiKey,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
const body = await response.text();
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Brave search failed (${response.status} ${response.statusText}): ${body}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const parsed = JSON.parse(body) as unknown;
|
|
208
|
+
return normalizedOutput("brave", input, normalizeBraveResults(parsed));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function searchTavily(
|
|
212
|
+
input: WebSearchInput,
|
|
213
|
+
apiKey: string,
|
|
214
|
+
): Promise<NormalizedOutput> {
|
|
215
|
+
const client = tavily({ apiKey }) as {
|
|
216
|
+
search: (
|
|
217
|
+
query: string,
|
|
218
|
+
options?: Record<string, unknown>,
|
|
219
|
+
) => Promise<unknown>;
|
|
220
|
+
};
|
|
221
|
+
const options: Record<string, unknown> = {
|
|
222
|
+
max_results: input.count,
|
|
223
|
+
};
|
|
224
|
+
if (input.country) {
|
|
225
|
+
const mappedCountry = tavilyCountryCode(input.country);
|
|
226
|
+
if (mappedCountry) {
|
|
227
|
+
options.country = mappedCountry;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (input.start_date && input.end_date) {
|
|
231
|
+
options.start_date = input.start_date;
|
|
232
|
+
options.end_date = input.end_date;
|
|
233
|
+
} else if (input.freshness) {
|
|
234
|
+
options.time_range = input.freshness;
|
|
235
|
+
}
|
|
236
|
+
if (input.safe_search !== undefined) {
|
|
237
|
+
options.safe_search = input.safe_search;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const response = await client.search(input.query, options);
|
|
241
|
+
return normalizedOutput("tavily", input, normalizeTavilyResults(response));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function createWebSearchTools(config: Config) {
|
|
245
|
+
return [
|
|
246
|
+
tool({
|
|
247
|
+
name: "web_search",
|
|
248
|
+
description:
|
|
249
|
+
"Search the web using configured provider and return normalized results.",
|
|
250
|
+
inputSchema: InputSchema,
|
|
251
|
+
callback: async (input, context?: ToolContext) => {
|
|
252
|
+
const timeoutSignal = AbortSignal.timeout(
|
|
253
|
+
DEFAULT_TIMEOUT_SECONDS * 1000,
|
|
254
|
+
);
|
|
255
|
+
const signal = context
|
|
256
|
+
? AbortSignal.any([timeoutSignal, context.agent.cancelSignal])
|
|
257
|
+
: timeoutSignal;
|
|
258
|
+
const provider = config.search.provider;
|
|
259
|
+
if (provider === "brave") {
|
|
260
|
+
const apiKey = config.search.brave.apiKey;
|
|
261
|
+
if (!apiKey) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
"Search provider is brave but search.brave.apiKey is missing.",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return toJsonValue(await searchBrave(input, apiKey, signal));
|
|
267
|
+
}
|
|
268
|
+
const apiKey = config.search.tavily.apiKey;
|
|
269
|
+
if (!apiKey) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
"Search provider is tavily but search.tavily.apiKey is missing.",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return toJsonValue(await searchTavily(input, apiKey));
|
|
275
|
+
},
|
|
276
|
+
}),
|
|
277
|
+
];
|
|
278
|
+
}
|