hoomanjs 1.16.1 → 1.17.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/package.json +2 -1
- package/src/acp/utils/tool-kind.ts +1 -0
- package/src/configure/app.tsx +201 -62
- package/src/configure/types.ts +3 -0
- package/src/core/agent/index.ts +5 -9
- 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/mcp/manager.ts +34 -25
- package/src/core/prompts/environment.ts +9 -0
- package/src/core/prompts/static/environment.md +4 -2
- package/src/core/prompts/static/web-search.md +38 -0
- package/src/core/prompts/system.ts +3 -0
- package/src/core/tools/index.ts +1 -0
- package/src/core/tools/web-search.ts +278 -0
- package/src/daemon/index.ts +11 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoomanjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.1",
|
|
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/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
|
|
|
@@ -52,15 +53,9 @@ export async function create(
|
|
|
52
53
|
const ltm = config.tools.ltm.enabled
|
|
53
54
|
? createLongTermMemoryStore(config)
|
|
54
55
|
: null;
|
|
55
|
-
const skills =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const prefixed = config.tools.mcp.enabled
|
|
59
|
-
? await mcp.manager.listPrefixedTools()
|
|
60
|
-
: [];
|
|
61
|
-
const append = config.tools.mcp.enabled
|
|
62
|
-
? await mcp.manager.listServerInstructions()
|
|
63
|
-
: [];
|
|
56
|
+
const skills = (await createSkillsPrompt(registry)).content;
|
|
57
|
+
const prefixed = await mcp.manager.listPrefixedTools();
|
|
58
|
+
const append = await mcp.manager.listServerInstructions();
|
|
64
59
|
const prompt = [system.content, meta.systemPrompt, ...append, skills]
|
|
65
60
|
.filter((x) => !!x)
|
|
66
61
|
.join(SECTION_BREAK);
|
|
@@ -73,6 +68,7 @@ export async function create(
|
|
|
73
68
|
...(ltm ? createLongTermMemoryTools(ltm) : []),
|
|
74
69
|
...(config.tools.filesystem.enabled ? createFilesystemTools() : []),
|
|
75
70
|
...(config.tools.shell.enabled ? createShellTools() : []),
|
|
71
|
+
...(config.search.enabled ? createWebSearchTools(config) : []),
|
|
76
72
|
...(config.tools.wiki.enabled ? createWikiTools(config) : []),
|
|
77
73
|
...(config.tools.mcp.enabled ? createMcpTools(mcp.config) : []),
|
|
78
74
|
...(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/mcp/manager.ts
CHANGED
|
@@ -15,16 +15,11 @@ export const HOOMAN_CHANNEL_PERMISSION = "hooman/channel/permission";
|
|
|
15
15
|
const HOOMAN_CHANNEL_PERMISSION_METHOD = `notifications/${HOOMAN_CHANNEL_PERMISSION}`;
|
|
16
16
|
|
|
17
17
|
export type ChannelMessageMeta = {
|
|
18
|
-
|
|
19
|
-
channel: string;
|
|
20
|
-
method: string;
|
|
21
|
-
params: unknown;
|
|
18
|
+
subscription: ChannelSubscription;
|
|
22
19
|
source?: string;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
thread?: string;
|
|
27
|
-
};
|
|
20
|
+
user?: string;
|
|
21
|
+
session?: string;
|
|
22
|
+
thread?: string;
|
|
28
23
|
};
|
|
29
24
|
|
|
30
25
|
export type ChannelMessage = {
|
|
@@ -325,7 +320,7 @@ export class Manager {
|
|
|
325
320
|
params?: unknown;
|
|
326
321
|
}) => {
|
|
327
322
|
const { method, params } = notification;
|
|
328
|
-
const prompt = this.toChannelPrompt(
|
|
323
|
+
const prompt = this.toChannelPrompt(params);
|
|
329
324
|
if (!prompt) {
|
|
330
325
|
return;
|
|
331
326
|
}
|
|
@@ -335,16 +330,11 @@ export class Manager {
|
|
|
335
330
|
prompt,
|
|
336
331
|
attachments,
|
|
337
332
|
meta: {
|
|
338
|
-
server,
|
|
339
|
-
channel,
|
|
340
|
-
method,
|
|
341
|
-
params,
|
|
333
|
+
subscription: { server, channel },
|
|
342
334
|
source: readSourceValue(params),
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
thread: readPathValue(params, thread),
|
|
347
|
-
},
|
|
335
|
+
user: readPathValue(params, user),
|
|
336
|
+
session: readPathValue(params, session),
|
|
337
|
+
thread: readPathValue(params, thread),
|
|
348
338
|
},
|
|
349
339
|
});
|
|
350
340
|
};
|
|
@@ -463,20 +453,39 @@ export class Manager {
|
|
|
463
453
|
}
|
|
464
454
|
}
|
|
465
455
|
|
|
466
|
-
private toChannelPrompt(
|
|
456
|
+
private toChannelPrompt(params?: unknown): string {
|
|
457
|
+
const parts = [];
|
|
458
|
+
|
|
467
459
|
if (
|
|
468
460
|
params &&
|
|
469
461
|
typeof params === "object" &&
|
|
470
462
|
"content" in params &&
|
|
471
463
|
typeof params.content === "string"
|
|
472
464
|
) {
|
|
473
|
-
|
|
465
|
+
parts.push(params.content.trim());
|
|
474
466
|
}
|
|
475
467
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
468
|
+
if (
|
|
469
|
+
params &&
|
|
470
|
+
typeof params === "object" &&
|
|
471
|
+
"attachments" in params &&
|
|
472
|
+
Array.isArray(params.attachments) &&
|
|
473
|
+
params.attachments.length > 0
|
|
474
|
+
) {
|
|
475
|
+
parts.push("User sent attachments.");
|
|
480
476
|
}
|
|
477
|
+
|
|
478
|
+
if (
|
|
479
|
+
params &&
|
|
480
|
+
typeof params === "object" &&
|
|
481
|
+
"event" in params &&
|
|
482
|
+
typeof params.event === "object"
|
|
483
|
+
) {
|
|
484
|
+
parts.push(
|
|
485
|
+
"Raw event data:\n```json\n" + JSON.stringify(params.event) + "\n```",
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return parts.filter(Boolean).join("\n").trim();
|
|
481
490
|
}
|
|
482
491
|
}
|
|
@@ -12,6 +12,8 @@ type EnvironmentPromptContext = {
|
|
|
12
12
|
osVersion: string;
|
|
13
13
|
shell: string;
|
|
14
14
|
isGitRepo: boolean;
|
|
15
|
+
currentDateTime: string;
|
|
16
|
+
timeZone: string;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
function detectPlatform(): string {
|
|
@@ -50,13 +52,20 @@ function detectGitRepo(startDir: string): boolean {
|
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
function detectTimeZone(): string {
|
|
56
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
export function getEnvironmentPromptContext(): EnvironmentPromptContext {
|
|
54
60
|
const cwd = process.cwd();
|
|
61
|
+
const now = new Date();
|
|
55
62
|
return {
|
|
56
63
|
cwd,
|
|
57
64
|
platform: detectPlatform(),
|
|
58
65
|
osVersion: detectOsVersion(),
|
|
59
66
|
shell: detectShell(),
|
|
60
67
|
isGitRepo: detectGitRepo(cwd),
|
|
68
|
+
currentDateTime: now.toString(),
|
|
69
|
+
timeZone: detectTimeZone(),
|
|
61
70
|
};
|
|
62
71
|
}
|
|
@@ -7,9 +7,11 @@ You are running in the following runtime environment:
|
|
|
7
7
|
- Shell: `{{ environment.shell }}`
|
|
8
8
|
- OS version: `{{ environment.osVersion }}`
|
|
9
9
|
- Is git repository: `{{ environment.isGitRepo }}`
|
|
10
|
+
- Current date/time: `{{ environment.currentDateTime }}`
|
|
11
|
+
- Time zone: `{{ environment.timeZone }}`
|
|
10
12
|
|
|
11
13
|
### How To Use This
|
|
12
14
|
|
|
13
15
|
- Use this information to choose correct path handling, shell syntax, and platform-specific behavior
|
|
14
|
-
- Treat this section as runtime context
|
|
15
|
-
- If the task needs
|
|
16
|
+
- Treat this section as runtime context captured when the prompt was built
|
|
17
|
+
- If the task needs precise current time during a later turn, call `get_current_time`
|
|
@@ -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",
|
|
@@ -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
|
+
}
|
package/src/daemon/index.ts
CHANGED
|
@@ -32,23 +32,23 @@ function resolveSessionId(
|
|
|
32
32
|
message: ChannelMessage,
|
|
33
33
|
fallback?: string,
|
|
34
34
|
): string | undefined {
|
|
35
|
-
const raw = message.meta.
|
|
35
|
+
const raw = message.meta.session?.trim() || fallback;
|
|
36
36
|
if (!raw) return undefined;
|
|
37
37
|
// Namespace per `server:channel` so the same chat id coming from two
|
|
38
38
|
// different MCP servers (or two channels on the same server) never collide.
|
|
39
|
-
return `${message.meta.server}:${message.meta.channel}:${raw}`;
|
|
39
|
+
return `${message.meta.subscription.server}:${message.meta.subscription.channel}:${raw}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function resolveUserId(
|
|
43
43
|
message: ChannelMessage,
|
|
44
44
|
session?: string,
|
|
45
45
|
): string | undefined {
|
|
46
|
-
const raw = message.meta.
|
|
46
|
+
const raw = message.meta.user?.trim();
|
|
47
47
|
if (!raw) return session;
|
|
48
48
|
// Same user id across different servers is not the same human, so scope
|
|
49
49
|
// user ids by server. Channel is intentionally omitted so long-term memory
|
|
50
50
|
// can stay consistent for a user across rooms within one server.
|
|
51
|
-
return `${message.meta.server}:${raw}`;
|
|
51
|
+
return `${message.meta.subscription.server}:${raw}`;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function formatSubscriptions(
|
|
@@ -91,7 +91,7 @@ export async function main(options: RunDaemonOptions): Promise<void> {
|
|
|
91
91
|
|
|
92
92
|
const [queue, stop] = await createQueue(
|
|
93
93
|
async (message: ChannelMessage) => {
|
|
94
|
-
const tag = `${message.meta.server}:${message.meta.channel}`;
|
|
94
|
+
const tag = `${message.meta.subscription.server}:${message.meta.subscription.channel}`;
|
|
95
95
|
const session = resolveSessionId(message, options.session);
|
|
96
96
|
const user = resolveUserId(message, session);
|
|
97
97
|
|
|
@@ -103,18 +103,12 @@ export async function main(options: RunDaemonOptions): Promise<void> {
|
|
|
103
103
|
options.agent.appState.set("userId", user);
|
|
104
104
|
options.agent.appState.set("sessionId", session);
|
|
105
105
|
const origin = {
|
|
106
|
-
server: message.meta.server,
|
|
107
|
-
channel: message.meta.channel,
|
|
106
|
+
server: message.meta.subscription.server,
|
|
107
|
+
channel: message.meta.subscription.channel,
|
|
108
108
|
...(message.meta.source ? { source: message.meta.source } : {}),
|
|
109
|
-
...(message.meta.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
...(message.meta.identity.session
|
|
113
|
-
? { session: message.meta.identity.session }
|
|
114
|
-
: {}),
|
|
115
|
-
...(message.meta.identity.thread
|
|
116
|
-
? { thread: message.meta.identity.thread }
|
|
117
|
-
: {}),
|
|
109
|
+
...(message.meta.user ? { user: message.meta.user } : {}),
|
|
110
|
+
...(message.meta.session ? { session: message.meta.session } : {}),
|
|
111
|
+
...(message.meta.thread ? { thread: message.meta.thread } : {}),
|
|
118
112
|
};
|
|
119
113
|
options.agent.appState.set("origin", {
|
|
120
114
|
...origin,
|
|
@@ -143,7 +137,7 @@ export async function main(options: RunDaemonOptions): Promise<void> {
|
|
|
143
137
|
channels,
|
|
144
138
|
(message) => {
|
|
145
139
|
debug(
|
|
146
|
-
`received notification → ${message.meta.server}:${message.meta.channel}`,
|
|
140
|
+
`received notification → ${message.meta.subscription.server}:${message.meta.subscription.channel}`,
|
|
147
141
|
);
|
|
148
142
|
void queue.push(message);
|
|
149
143
|
},
|