hoomanjs 1.0.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/.github/screenshot.png +0 -0
- package/.github/workflows/build-publish.yml +49 -0
- package/LICENSE +21 -0
- package/README.md +399 -0
- package/docker-compose.yml +13 -0
- package/package.json +78 -0
- package/src/acp/acp-agent.ts +803 -0
- package/src/acp/approvals.ts +147 -0
- package/src/acp/index.ts +1 -0
- package/src/acp/meta/system-prompt.ts +44 -0
- package/src/acp/meta/user-id.ts +44 -0
- package/src/acp/prompt-invoke.ts +149 -0
- package/src/acp/sessions/config-options.ts +56 -0
- package/src/acp/sessions/replay.ts +131 -0
- package/src/acp/sessions/store.ts +158 -0
- package/src/acp/sessions/title.ts +22 -0
- package/src/acp/utils/paths.ts +5 -0
- package/src/acp/utils/tool-kind.ts +38 -0
- package/src/acp/utils/tool-locations.ts +46 -0
- package/src/acp/utils/tool-result-content.ts +27 -0
- package/src/chat/app.tsx +428 -0
- package/src/chat/approvals.ts +96 -0
- package/src/chat/components/ApprovalPrompt.tsx +25 -0
- package/src/chat/components/ChatMessage.tsx +47 -0
- package/src/chat/components/Composer.tsx +39 -0
- package/src/chat/components/EmptyChatBanner.tsx +26 -0
- package/src/chat/components/ReasoningStrip.tsx +30 -0
- package/src/chat/components/Spinner.tsx +34 -0
- package/src/chat/components/StatusBar.tsx +65 -0
- package/src/chat/components/ThinkingStatus.tsx +128 -0
- package/src/chat/components/ToolEvent.tsx +34 -0
- package/src/chat/components/Transcript.tsx +34 -0
- package/src/chat/components/ascii-logo.ts +11 -0
- package/src/chat/components/shared.ts +70 -0
- package/src/chat/index.tsx +42 -0
- package/src/chat/types.ts +21 -0
- package/src/cli.ts +146 -0
- package/src/configure/app.tsx +911 -0
- package/src/configure/components/BusyScreen.tsx +22 -0
- package/src/configure/components/HomeScreen.tsx +43 -0
- package/src/configure/components/MenuScreen.tsx +44 -0
- package/src/configure/components/PromptForm.tsx +40 -0
- package/src/configure/components/SelectMenuItem.tsx +30 -0
- package/src/configure/index.tsx +43 -0
- package/src/configure/open-in-editor.ts +133 -0
- package/src/configure/types.ts +45 -0
- package/src/configure/utils.ts +113 -0
- package/src/core/agent/index.ts +76 -0
- package/src/core/config.ts +157 -0
- package/src/core/index.ts +54 -0
- package/src/core/mcp/config.ts +80 -0
- package/src/core/mcp/index.ts +13 -0
- package/src/core/mcp/manager.ts +109 -0
- package/src/core/mcp/prefixed-mcp-tool.ts +45 -0
- package/src/core/mcp/tools.ts +92 -0
- package/src/core/mcp/types.ts +37 -0
- package/src/core/memory/index.ts +17 -0
- package/src/core/memory/ltm/embed.ts +67 -0
- package/src/core/memory/ltm/index.ts +18 -0
- package/src/core/memory/ltm/store.ts +376 -0
- package/src/core/memory/ltm/tools.ts +146 -0
- package/src/core/memory/ltm/types.ts +111 -0
- package/src/core/memory/ltm/utils.ts +218 -0
- package/src/core/memory/stm/index.ts +17 -0
- package/src/core/models/anthropic.ts +53 -0
- package/src/core/models/bedrock.ts +54 -0
- package/src/core/models/google.ts +51 -0
- package/src/core/models/index.ts +16 -0
- package/src/core/models/ollama/index.ts +13 -0
- package/src/core/models/ollama/strands-ollama.ts +439 -0
- package/src/core/models/openai.ts +12 -0
- package/src/core/prompts/index.ts +23 -0
- package/src/core/prompts/skills.ts +66 -0
- package/src/core/prompts/static/fetch.md +33 -0
- package/src/core/prompts/static/filesystem.md +38 -0
- package/src/core/prompts/static/identity.md +22 -0
- package/src/core/prompts/static/ltm.md +39 -0
- package/src/core/prompts/static/memory.md +39 -0
- package/src/core/prompts/static/shell.md +34 -0
- package/src/core/prompts/static/skills.md +19 -0
- package/src/core/prompts/static/thinking.md +27 -0
- package/src/core/prompts/system.ts +109 -0
- package/src/core/skills/index.ts +2 -0
- package/src/core/skills/registry.ts +239 -0
- package/src/core/skills/tools.ts +80 -0
- package/src/core/toolkit.ts +13 -0
- package/src/core/tools/fetch.ts +288 -0
- package/src/core/tools/filesystem.ts +747 -0
- package/src/core/tools/index.ts +5 -0
- package/src/core/tools/shell.ts +426 -0
- package/src/core/tools/thinking.ts +184 -0
- package/src/core/tools/time.ts +121 -0
- package/src/core/utils/cwd-context.ts +11 -0
- package/src/core/utils/paths.ts +28 -0
- package/src/exec/approvals.ts +85 -0
- package/tsconfig.json +30 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
/** LLM backend; extend as you add providers. */
|
|
5
|
+
export enum LlmProvider {
|
|
6
|
+
Anthropic = "anthropic",
|
|
7
|
+
Google = "google",
|
|
8
|
+
OpenAI = "openai",
|
|
9
|
+
Ollama = "ollama",
|
|
10
|
+
Bedrock = "bedrock",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LlmSchema = z.object({
|
|
14
|
+
provider: z.nativeEnum(LlmProvider),
|
|
15
|
+
model: z.string().min(1),
|
|
16
|
+
params: z.record(z.string(), z.any()).default({}),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Partial compaction overrides from JSON; missing fields use defaults below. */
|
|
20
|
+
const CompactionPartialSchema = z.object({
|
|
21
|
+
/** Fraction of context to target after compaction (e.g. 0.75 = keep ~75% budget). */
|
|
22
|
+
ratio: z.number().min(0).max(1).optional(),
|
|
23
|
+
/** Minimum number of recent turns / message groups to preserve verbatim when compacting. */
|
|
24
|
+
keep: z.number().int().nonnegative().optional(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const DEFAULT_COMPACTION = { ratio: 0.75, keep: 5 } as const;
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CHROMA = {
|
|
30
|
+
url: "http://127.0.0.1:8000",
|
|
31
|
+
collection: { memory: "memory" },
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const ChromaPartialSchema = z.object({
|
|
35
|
+
url: z.string().min(1).optional(),
|
|
36
|
+
collection: z
|
|
37
|
+
.object({
|
|
38
|
+
memory: z.string().min(1).optional(),
|
|
39
|
+
})
|
|
40
|
+
.optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const LtmPartialSchema = z.object({
|
|
44
|
+
enabled: z.boolean().optional(),
|
|
45
|
+
chroma: ChromaPartialSchema.optional(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const ToolsPartialSchema = z.object({
|
|
49
|
+
allowed: z.array(z.string().min(1)).default([]),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const ConfigSchema = z.object({
|
|
53
|
+
name: z.string().min(1),
|
|
54
|
+
llm: LlmSchema,
|
|
55
|
+
tools: ToolsPartialSchema.default({ allowed: [] }),
|
|
56
|
+
ltm: LtmPartialSchema.nullish().transform((ltm) => ({
|
|
57
|
+
enabled: ltm?.enabled ?? false,
|
|
58
|
+
chroma: {
|
|
59
|
+
url: ltm?.chroma?.url ?? DEFAULT_CHROMA.url,
|
|
60
|
+
collection: {
|
|
61
|
+
memory:
|
|
62
|
+
ltm?.chroma?.collection?.memory ?? DEFAULT_CHROMA.collection.memory,
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
})),
|
|
66
|
+
compaction: CompactionPartialSchema.nullish().transform((c) => ({
|
|
67
|
+
ratio: c?.ratio ?? DEFAULT_COMPACTION.ratio,
|
|
68
|
+
keep: c?.keep ?? DEFAULT_COMPACTION.keep,
|
|
69
|
+
})),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type ConfigData = z.infer<typeof ConfigSchema>;
|
|
73
|
+
export type LlmConfig = z.infer<typeof LlmSchema>;
|
|
74
|
+
export type CompactionConfig = ConfigData["compaction"];
|
|
75
|
+
export type LtmConfig = ConfigData["ltm"];
|
|
76
|
+
export type ToolsConfig = ConfigData["tools"];
|
|
77
|
+
|
|
78
|
+
const defaultConfigData = (): ConfigData => ({
|
|
79
|
+
name: "Hooman",
|
|
80
|
+
llm: {
|
|
81
|
+
provider: LlmProvider.Ollama,
|
|
82
|
+
model: "gemma4:e4b",
|
|
83
|
+
params: {},
|
|
84
|
+
},
|
|
85
|
+
tools: {
|
|
86
|
+
allowed: [],
|
|
87
|
+
},
|
|
88
|
+
ltm: {
|
|
89
|
+
enabled: false,
|
|
90
|
+
chroma: {
|
|
91
|
+
url: "http://127.0.0.1:8000",
|
|
92
|
+
collection: { memory: "memory" },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
compaction: {
|
|
96
|
+
ratio: 0.75,
|
|
97
|
+
keep: 5,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
export class Config {
|
|
102
|
+
private data!: ConfigData;
|
|
103
|
+
private readonly path: string;
|
|
104
|
+
|
|
105
|
+
public constructor(path: string) {
|
|
106
|
+
this.path = path;
|
|
107
|
+
this.reload();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get name(): string {
|
|
111
|
+
return this.data.name;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get llm(): LlmConfig {
|
|
115
|
+
return this.data.llm;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
get tools(): ToolsConfig {
|
|
119
|
+
return {
|
|
120
|
+
...this.data.tools,
|
|
121
|
+
allowed: [...this.data.tools.allowed],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
get compaction(): CompactionConfig {
|
|
126
|
+
return this.data.compaction;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get ltm(): LtmConfig {
|
|
130
|
+
return this.data.ltm;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private readJson(): unknown {
|
|
134
|
+
if (!existsSync(this.path)) {
|
|
135
|
+
return defaultConfigData();
|
|
136
|
+
}
|
|
137
|
+
return JSON.parse(readFileSync(this.path, "utf8"));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public reload(): void {
|
|
141
|
+
const wasMissing = !existsSync(this.path);
|
|
142
|
+
this.data = ConfigSchema.parse(this.readJson());
|
|
143
|
+
if (wasMissing) {
|
|
144
|
+
this.persist();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
public persist(): void {
|
|
149
|
+
writeFileSync(this.path, JSON.stringify(this.data, null, 2), "utf8");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
public update(partial: Partial<ConfigData>): void {
|
|
153
|
+
const updated = { ...this.data, ...partial };
|
|
154
|
+
this.data = ConfigSchema.parse(updated);
|
|
155
|
+
this.persist();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Agent } from "@strands-agents/sdk";
|
|
2
|
+
import { Config } from "./config.ts";
|
|
3
|
+
import { create as createAgent } from "./agent/index.ts";
|
|
4
|
+
import {
|
|
5
|
+
createMcpConfig,
|
|
6
|
+
createMcpManager,
|
|
7
|
+
type Config as McpServersConfig,
|
|
8
|
+
type Manager as McpConnectionManager,
|
|
9
|
+
} from "./mcp/index.ts";
|
|
10
|
+
import { createSkillsRegistry } from "./skills/index.ts";
|
|
11
|
+
import type { Registry } from "./skills/index.ts";
|
|
12
|
+
import { system as createSystemPrompt } from "./prompts/index.ts";
|
|
13
|
+
import type { Toolkit } from "./toolkit.ts";
|
|
14
|
+
import {
|
|
15
|
+
basePath,
|
|
16
|
+
configJsonPath,
|
|
17
|
+
instructionsMdPath,
|
|
18
|
+
mcpJsonPath,
|
|
19
|
+
} from "./utils/paths.ts";
|
|
20
|
+
|
|
21
|
+
export async function bootstrap(
|
|
22
|
+
meta: {
|
|
23
|
+
userId?: string;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
systemPrompt?: string;
|
|
26
|
+
toolkit?: Toolkit;
|
|
27
|
+
},
|
|
28
|
+
print: boolean = false,
|
|
29
|
+
): Promise<{
|
|
30
|
+
config: Config;
|
|
31
|
+
agent: Agent;
|
|
32
|
+
mcp: { config: McpServersConfig; manager: McpConnectionManager };
|
|
33
|
+
registry: Registry;
|
|
34
|
+
}> {
|
|
35
|
+
const config = new Config(configJsonPath());
|
|
36
|
+
const mcpConfig = createMcpConfig(mcpJsonPath());
|
|
37
|
+
const mcpManager = createMcpManager(mcpConfig);
|
|
38
|
+
const mcp = { config: mcpConfig, manager: mcpManager };
|
|
39
|
+
const registry = createSkillsRegistry(basePath());
|
|
40
|
+
const toolkit = meta.toolkit ?? "max";
|
|
41
|
+
const system = await createSystemPrompt(
|
|
42
|
+
instructionsMdPath(),
|
|
43
|
+
config,
|
|
44
|
+
toolkit,
|
|
45
|
+
);
|
|
46
|
+
const sessionId = meta?.sessionId ?? crypto.randomUUID();
|
|
47
|
+
const agent = await createAgent(config, system, registry, mcp, print, {
|
|
48
|
+
userId: meta?.userId ?? sessionId,
|
|
49
|
+
sessionId,
|
|
50
|
+
systemPrompt: meta?.systemPrompt,
|
|
51
|
+
toolkit,
|
|
52
|
+
});
|
|
53
|
+
return { config, agent, mcp, registry };
|
|
54
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { McpTransportSchema, type McpTransport } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
const McpServersFileSchema = z.object({
|
|
6
|
+
mcpServers: z.record(z.string().min(1), McpTransportSchema).default({}),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export type McpServersFile = z.infer<typeof McpServersFileSchema>;
|
|
10
|
+
|
|
11
|
+
/** One named MCP transport from the config file. */
|
|
12
|
+
export type NamedMcpTransport = { name: string; transport: McpTransport };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read/write `{"mcpServers": { "<name>": <transport> }}` on disk.
|
|
16
|
+
*/
|
|
17
|
+
export class Config {
|
|
18
|
+
private readonly path: string;
|
|
19
|
+
private servers: Record<string, McpTransport>;
|
|
20
|
+
|
|
21
|
+
public constructor(path: string) {
|
|
22
|
+
this.path = path;
|
|
23
|
+
this.servers = {};
|
|
24
|
+
this.reload();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private readJson(): McpServersFile {
|
|
28
|
+
if (!existsSync(this.path)) {
|
|
29
|
+
return { mcpServers: {} };
|
|
30
|
+
}
|
|
31
|
+
const raw = readFileSync(this.path, "utf8");
|
|
32
|
+
return JSON.parse(raw) as McpServersFile;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Reload servers from disk (overwrites unsaved in-memory changes). */
|
|
36
|
+
public reload(): void {
|
|
37
|
+
const parsed = McpServersFileSchema.parse(this.readJson());
|
|
38
|
+
this.servers = { ...parsed.mcpServers };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private persist(): void {
|
|
42
|
+
const payload: McpServersFile = { mcpServers: { ...this.servers } };
|
|
43
|
+
writeFileSync(this.path, JSON.stringify(payload, null, 2), "utf8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** All configured servers, stable sort by name. */
|
|
47
|
+
public list(): NamedMcpTransport[] {
|
|
48
|
+
return Object.entries(this.servers)
|
|
49
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
50
|
+
.map(([name, transport]) => ({ name, transport }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public get(name: string): McpTransport | undefined {
|
|
54
|
+
return this.servers[name];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public add(name: string, transport: McpTransport): void {
|
|
58
|
+
if (Object.hasOwn(this.servers, name)) {
|
|
59
|
+
throw new Error(`MCP server "${name}" already exists`);
|
|
60
|
+
}
|
|
61
|
+
this.servers[name] = transport;
|
|
62
|
+
this.persist();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public update(name: string, transport: McpTransport): void {
|
|
66
|
+
if (!Object.hasOwn(this.servers, name)) {
|
|
67
|
+
throw new Error(`MCP server "${name}" not found`);
|
|
68
|
+
}
|
|
69
|
+
this.servers[name] = transport;
|
|
70
|
+
this.persist();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public remove(name: string): void {
|
|
74
|
+
if (!Object.hasOwn(this.servers, name)) {
|
|
75
|
+
throw new Error(`MCP server "${name}" not found`);
|
|
76
|
+
}
|
|
77
|
+
delete this.servers[name];
|
|
78
|
+
this.persist();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Config } from "./config.ts";
|
|
2
|
+
import { Manager } from "./manager.ts";
|
|
3
|
+
|
|
4
|
+
export { Config, Manager };
|
|
5
|
+
export { createMcpTools } from "./tools.ts";
|
|
6
|
+
|
|
7
|
+
export function createMcpConfig(path: string): Config {
|
|
8
|
+
return new Config(path);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createMcpManager(config: Config): Manager {
|
|
12
|
+
return new Manager(config);
|
|
13
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { McpClient, type Tool } from "@strands-agents/sdk";
|
|
2
|
+
import { PrefixedMcpTool } from "./prefixed-mcp-tool.ts";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
6
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
7
|
+
import { Config } from "./config.ts";
|
|
8
|
+
import type { McpTransport } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
function transportFor(spec: McpTransport): Transport {
|
|
11
|
+
switch (spec.type) {
|
|
12
|
+
case "stdio":
|
|
13
|
+
return new StdioClientTransport({
|
|
14
|
+
command: spec.command,
|
|
15
|
+
args: spec.args,
|
|
16
|
+
env: spec.env,
|
|
17
|
+
cwd: spec.cwd,
|
|
18
|
+
stderr: "ignore",
|
|
19
|
+
});
|
|
20
|
+
case "streamable-http": {
|
|
21
|
+
const headers = spec.headers;
|
|
22
|
+
return new StreamableHTTPClientTransport(new URL(spec.url), {
|
|
23
|
+
requestInit: headers ? { headers } : undefined,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
case "sse": {
|
|
27
|
+
const headers = spec.headers;
|
|
28
|
+
return new SSEClientTransport(new URL(spec.url), {
|
|
29
|
+
requestInit: headers ? { headers } : undefined,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
default: {
|
|
33
|
+
const _exhaustive: never = spec;
|
|
34
|
+
return _exhaustive;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Holds one {@link McpClient} per named entry in {@link Config}. Call {@link reload}
|
|
41
|
+
* after changing the file on disk (or construct and then {@link reload} once).
|
|
42
|
+
*/
|
|
43
|
+
export class Manager {
|
|
44
|
+
private instances: Map<string, McpClient> | null = null;
|
|
45
|
+
|
|
46
|
+
public constructor(private readonly config: Config) {}
|
|
47
|
+
|
|
48
|
+
/** Lazily builds clients from the current in-memory config (reloads file first). */
|
|
49
|
+
get clients(): ReadonlyMap<string, McpClient> {
|
|
50
|
+
if (this.instances === null) {
|
|
51
|
+
this.reload();
|
|
52
|
+
}
|
|
53
|
+
return this.instances!;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Rereads the config file, replaces all clients, and best-effort disconnects
|
|
58
|
+
* previous clients (stdio subprocesses, HTTP sessions).
|
|
59
|
+
*/
|
|
60
|
+
public reload(): void {
|
|
61
|
+
this.config.reload();
|
|
62
|
+
const previous = this.instances;
|
|
63
|
+
const next = new Map<string, McpClient>();
|
|
64
|
+
for (const { name, transport } of this.config.list()) {
|
|
65
|
+
next.set(
|
|
66
|
+
name,
|
|
67
|
+
new McpClient({
|
|
68
|
+
transport: transportFor(transport),
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
this.instances = next;
|
|
73
|
+
if (previous?.size) {
|
|
74
|
+
for (const client of previous.values()) {
|
|
75
|
+
void client.disconnect().catch(() => {});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async disconnect(): Promise<void> {
|
|
81
|
+
const toClose = this.instances;
|
|
82
|
+
this.instances = null;
|
|
83
|
+
if (!toClose?.size) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
await Promise.all(
|
|
87
|
+
[...toClose.values()].map((c) => c.disconnect().catch(() => undefined)),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Lists tools from every configured MCP client with names prefixed by a
|
|
93
|
+
* slugified server config key (see {@link PrefixedMcpTool}).
|
|
94
|
+
*/
|
|
95
|
+
public async listPrefixedTools(): Promise<Tool[]> {
|
|
96
|
+
if (this.instances === null) {
|
|
97
|
+
this.reload();
|
|
98
|
+
}
|
|
99
|
+
const map = this.instances!;
|
|
100
|
+
const batches = await Promise.all(
|
|
101
|
+
[...map.entries()].map(async ([serverKey, client]) =>
|
|
102
|
+
client
|
|
103
|
+
.listTools()
|
|
104
|
+
.then((tools) => tools.map((t) => new PrefixedMcpTool(serverKey, t))),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
return batches.flat();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Tool,
|
|
3
|
+
type ToolContext,
|
|
4
|
+
type ToolSpec,
|
|
5
|
+
type ToolStreamGenerator,
|
|
6
|
+
} from "@strands-agents/sdk";
|
|
7
|
+
import slugify from "slugify";
|
|
8
|
+
|
|
9
|
+
function mcpServerPrefix(serverKey: string): string {
|
|
10
|
+
const slug = slugify(serverKey, {
|
|
11
|
+
replacement: "_",
|
|
12
|
+
lower: true,
|
|
13
|
+
strict: true,
|
|
14
|
+
});
|
|
15
|
+
return slug.length > 0 ? slug : "mcp";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Exposes MCP server tools to the model as `slugifiedServerKey__originalName`
|
|
20
|
+
* while delegating execution to the stock SDK tool (correct wire name).
|
|
21
|
+
*/
|
|
22
|
+
export class PrefixedMcpTool extends Tool {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
toolSpec: ToolSpec;
|
|
26
|
+
|
|
27
|
+
public constructor(
|
|
28
|
+
private readonly serverKey: string,
|
|
29
|
+
private readonly inner: Tool,
|
|
30
|
+
) {
|
|
31
|
+
super();
|
|
32
|
+
const prefix = mcpServerPrefix(serverKey);
|
|
33
|
+
this.name = `${prefix}__${inner.name}`;
|
|
34
|
+
this.description = `${inner.description} (MCP server: ${serverKey})`;
|
|
35
|
+
this.toolSpec = {
|
|
36
|
+
...inner.toolSpec,
|
|
37
|
+
name: this.name,
|
|
38
|
+
description: this.description,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
stream(toolContext: ToolContext): ToolStreamGenerator {
|
|
43
|
+
return this.inner.stream(toolContext);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { tool } from "@strands-agents/sdk";
|
|
2
|
+
import type { JSONValue } from "@strands-agents/sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { Config } from "./config.ts";
|
|
5
|
+
import { McpTransportSchema } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
function toJsonValue(value: unknown): JSONValue {
|
|
8
|
+
return JSON.parse(JSON.stringify(value)) as JSONValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const NamedTransportSchema = z.object({
|
|
12
|
+
name: z.string().min(1).describe("Unique MCP server name."),
|
|
13
|
+
transport: McpTransportSchema.describe(
|
|
14
|
+
"Transport configuration for the MCP server.",
|
|
15
|
+
),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function createMcpTools(config: Config) {
|
|
19
|
+
return [
|
|
20
|
+
tool({
|
|
21
|
+
name: "list_mcp_servers",
|
|
22
|
+
description:
|
|
23
|
+
"List configured MCP servers from the local MCP config file.",
|
|
24
|
+
inputSchema: z.object({}),
|
|
25
|
+
callback: async () => {
|
|
26
|
+
const servers = config.list();
|
|
27
|
+
return toJsonValue({
|
|
28
|
+
count: servers.length,
|
|
29
|
+
servers,
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
tool({
|
|
34
|
+
name: "get_mcp_server",
|
|
35
|
+
description: "Get a configured MCP server by name.",
|
|
36
|
+
inputSchema: z.object({
|
|
37
|
+
name: z.string().min(1).describe("MCP server name."),
|
|
38
|
+
}),
|
|
39
|
+
callback: async (input) => {
|
|
40
|
+
const transport = config.get(input.name);
|
|
41
|
+
return toJsonValue({
|
|
42
|
+
name: input.name,
|
|
43
|
+
found: transport !== undefined,
|
|
44
|
+
transport: transport ?? null,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
tool({
|
|
49
|
+
name: "add_mcp_server",
|
|
50
|
+
description:
|
|
51
|
+
"Add a new MCP server configuration to the local MCP config file.",
|
|
52
|
+
inputSchema: NamedTransportSchema,
|
|
53
|
+
callback: async (input) => {
|
|
54
|
+
config.add(input.name, input.transport);
|
|
55
|
+
return toJsonValue({
|
|
56
|
+
added: true,
|
|
57
|
+
name: input.name,
|
|
58
|
+
transport: input.transport,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
tool({
|
|
63
|
+
name: "update_mcp_server",
|
|
64
|
+
description:
|
|
65
|
+
"Update an existing MCP server configuration in the local MCP config file.",
|
|
66
|
+
inputSchema: NamedTransportSchema,
|
|
67
|
+
callback: async (input) => {
|
|
68
|
+
config.update(input.name, input.transport);
|
|
69
|
+
return toJsonValue({
|
|
70
|
+
updated: true,
|
|
71
|
+
name: input.name,
|
|
72
|
+
transport: input.transport,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
tool({
|
|
77
|
+
name: "delete_mcp_server",
|
|
78
|
+
description:
|
|
79
|
+
"Delete an MCP server configuration from the local MCP config file.",
|
|
80
|
+
inputSchema: z.object({
|
|
81
|
+
name: z.string().min(1).describe("MCP server name to remove."),
|
|
82
|
+
}),
|
|
83
|
+
callback: async (input) => {
|
|
84
|
+
config.remove(input.name);
|
|
85
|
+
return toJsonValue({
|
|
86
|
+
deleted: true,
|
|
87
|
+
name: input.name,
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const headersOrEnvVarsSchema = z.record(z.string(), z.string()).optional();
|
|
4
|
+
|
|
5
|
+
/** MCP stdio server: subprocess with JSON-RPC over stdin/stdout. */
|
|
6
|
+
export const StdioSchema = z.object({
|
|
7
|
+
type: z.literal("stdio"),
|
|
8
|
+
command: z.string().min(1),
|
|
9
|
+
args: z.array(z.string()).optional(),
|
|
10
|
+
env: headersOrEnvVarsSchema,
|
|
11
|
+
cwd: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** MCP remote server over streamable HTTP (preferred over legacy SSE). */
|
|
15
|
+
export const StreamableHttpSchema = z.object({
|
|
16
|
+
type: z.literal("streamable-http"),
|
|
17
|
+
url: z.url(),
|
|
18
|
+
headers: headersOrEnvVarsSchema,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/** MCP remote server over SSE + POST messages (legacy transport). */
|
|
22
|
+
export const SseSchema = z.object({
|
|
23
|
+
type: z.literal("sse"),
|
|
24
|
+
url: z.url(),
|
|
25
|
+
headers: headersOrEnvVarsSchema,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const McpTransportSchema = z.discriminatedUnion("type", [
|
|
29
|
+
StdioSchema,
|
|
30
|
+
StreamableHttpSchema,
|
|
31
|
+
SseSchema,
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
export type Stdio = z.infer<typeof StdioSchema>;
|
|
35
|
+
export type StreamableHttp = z.infer<typeof StreamableHttpSchema>;
|
|
36
|
+
export type Sse = z.infer<typeof SseSchema>;
|
|
37
|
+
export type McpTransport = z.infer<typeof McpTransportSchema>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { create as createShortTermMemory } from "./stm";
|
|
2
|
+
export {
|
|
3
|
+
createLongTermMemoryStore,
|
|
4
|
+
createLongTermMemoryTools,
|
|
5
|
+
LongTermMemoryStore,
|
|
6
|
+
} from "./ltm";
|
|
7
|
+
export type {
|
|
8
|
+
ArchiveMemoryInput,
|
|
9
|
+
LongTermMemoryOptions,
|
|
10
|
+
LongTermMemoryScope,
|
|
11
|
+
MemorySource,
|
|
12
|
+
SearchMemoryInput,
|
|
13
|
+
SearchMemoryResult,
|
|
14
|
+
StoreMemoryInput,
|
|
15
|
+
StoreMemoryResult,
|
|
16
|
+
UpdateMemoryInput,
|
|
17
|
+
} from "./ltm";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { pipeline } from "@huggingface/transformers";
|
|
2
|
+
import type { FeatureExtractionPipeline } from "@huggingface/transformers";
|
|
3
|
+
import type { EmbeddingFunction } from "chromadb";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MODEL =
|
|
6
|
+
process.env.HF_EMBEDDING_MODEL ?? "Xenova/bge-small-en-v1.5";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Chroma {@link EmbeddingFunction} backed by `@huggingface/transformers`
|
|
10
|
+
* `feature-extraction` (mean-pooled, L2-normalized sentence embeddings).
|
|
11
|
+
*
|
|
12
|
+
* The pipeline is loaded lazily on first {@link HFEmbedding.generate} call so
|
|
13
|
+
* constructors stay synchronous and startup stays fast.
|
|
14
|
+
*/
|
|
15
|
+
export class HFEmbedding implements EmbeddingFunction {
|
|
16
|
+
private readonly modelId: string;
|
|
17
|
+
private pipePromise: Promise<FeatureExtractionPipeline> | null = null;
|
|
18
|
+
|
|
19
|
+
constructor(modelId: string = DEFAULT_MODEL) {
|
|
20
|
+
this.modelId = modelId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get name(): string {
|
|
24
|
+
return `hf-transformers:${this.modelId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async getPipeline(): Promise<FeatureExtractionPipeline> {
|
|
28
|
+
if (!this.pipePromise) {
|
|
29
|
+
this.pipePromise = pipeline(
|
|
30
|
+
"feature-extraction",
|
|
31
|
+
this.modelId,
|
|
32
|
+
) as Promise<FeatureExtractionPipeline>;
|
|
33
|
+
}
|
|
34
|
+
return this.pipePromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async generate(texts: string[]): Promise<number[][]> {
|
|
38
|
+
if (texts.length === 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const pipe = await this.getPipeline();
|
|
42
|
+
const tensor = await pipe(texts, {
|
|
43
|
+
pooling: "mean",
|
|
44
|
+
normalize: true,
|
|
45
|
+
});
|
|
46
|
+
const raw = tensor.tolist() as unknown;
|
|
47
|
+
|
|
48
|
+
if (texts.length === 1) {
|
|
49
|
+
if (Array.isArray(raw) && raw.every((x) => typeof x === "number")) {
|
|
50
|
+
return [raw as number[]];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
Array.isArray(raw) &&
|
|
56
|
+
raw.length > 0 &&
|
|
57
|
+
Array.isArray(raw[0]) &&
|
|
58
|
+
(raw[0] as unknown[]).every((x) => typeof x === "number")
|
|
59
|
+
) {
|
|
60
|
+
return raw as number[][];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw new Error(
|
|
64
|
+
"Unexpected embedding tensor shape from feature-extraction pipeline",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
LongTermMemoryStore,
|
|
3
|
+
create as createLongTermMemoryStore,
|
|
4
|
+
} from "./store.ts";
|
|
5
|
+
|
|
6
|
+
export { create as createLongTermMemoryTools } from "./tools.ts";
|
|
7
|
+
|
|
8
|
+
export type {
|
|
9
|
+
ArchiveMemoryInput,
|
|
10
|
+
LongTermMemoryOptions,
|
|
11
|
+
LongTermMemoryScope,
|
|
12
|
+
MemorySource,
|
|
13
|
+
SearchMemoryInput,
|
|
14
|
+
SearchMemoryResult,
|
|
15
|
+
StoreMemoryInput,
|
|
16
|
+
StoreMemoryResult,
|
|
17
|
+
UpdateMemoryInput,
|
|
18
|
+
} from "./types.ts";
|