c0de-agent 0.0.0 → 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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/cli/commands/acp.d.ts +19 -0
- package/dist/cli/commands/acp.js +50 -0
- package/dist/cli/commands/chat.d.ts +13 -0
- package/dist/cli/commands/chat.js +26 -0
- package/dist/cli/commands/config.d.ts +17 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/init.d.ts +9 -0
- package/dist/cli/commands/init.js +14 -0
- package/dist/cli/commands/plugin.d.ts +11 -0
- package/dist/cli/commands/plugin.js +31 -0
- package/dist/cli/commands/serve.d.ts +18 -0
- package/dist/cli/commands/serve.js +35 -0
- package/dist/cli/commands/update.d.ts +24 -0
- package/dist/cli/commands/update.js +40 -0
- package/dist/cli/deps.d.ts +19 -0
- package/dist/cli/deps.js +57 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +149 -0
- package/dist/cli/modes/acp.d.ts +18 -0
- package/dist/cli/modes/acp.js +57 -0
- package/dist/cli/modes/print.d.ts +16 -0
- package/dist/cli/modes/print.js +30 -0
- package/dist/cli/parser.d.ts +19 -0
- package/dist/cli/parser.js +28 -0
- package/dist/cli/utils/output.d.ts +11 -0
- package/dist/cli/utils/output.js +23 -0
- package/dist/cli/utils/prompt.d.ts +12 -0
- package/dist/cli/utils/prompt.js +18 -0
- package/dist/core/agent.d.ts +12 -0
- package/dist/core/agent.js +95 -0
- package/dist/core/agents/builtin.d.ts +6 -0
- package/dist/core/agents/builtin.js +76 -0
- package/dist/core/agents/discovery.d.ts +6 -0
- package/dist/core/agents/discovery.js +96 -0
- package/dist/core/agents/index.d.ts +6 -0
- package/dist/core/agents/index.js +4 -0
- package/dist/core/agents/parallel.d.ts +13 -0
- package/dist/core/agents/parallel.js +37 -0
- package/dist/core/agents/registry.d.ts +4 -0
- package/dist/core/agents/registry.js +23 -0
- package/dist/core/agents/types.d.ts +37 -0
- package/dist/core/agents/types.js +1 -0
- package/dist/core/compact.d.ts +11 -0
- package/dist/core/compact.js +31 -0
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +84 -0
- package/dist/core/context.d.ts +23 -0
- package/dist/core/context.js +93 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +11 -0
- package/dist/core/loop.d.ts +25 -0
- package/dist/core/loop.js +651 -0
- package/dist/core/metrics.d.ts +27 -0
- package/dist/core/metrics.js +94 -0
- package/dist/core/prompt-registry.d.ts +21 -0
- package/dist/core/prompt-registry.js +200 -0
- package/dist/core/prompt.d.ts +11 -0
- package/dist/core/prompt.js +13 -0
- package/dist/core/secret.d.ts +12 -0
- package/dist/core/secret.js +51 -0
- package/dist/core/slash.d.ts +15 -0
- package/dist/core/slash.js +114 -0
- package/dist/core/steering.d.ts +8 -0
- package/dist/core/steering.js +15 -0
- package/dist/core/title.d.ts +42 -0
- package/dist/core/title.js +115 -0
- package/dist/core/tool-exec.d.ts +20 -0
- package/dist/core/tool-exec.js +64 -0
- package/dist/core/types.d.ts +95 -0
- package/dist/core/types.js +1 -0
- package/dist/core/worktree.d.ts +24 -0
- package/dist/core/worktree.js +68 -0
- package/dist/dap/index.d.ts +6 -0
- package/dist/dap/index.js +4 -0
- package/dist/dap/protocol.d.ts +40 -0
- package/dist/dap/protocol.js +132 -0
- package/dist/dap/session.d.ts +23 -0
- package/dist/dap/session.js +130 -0
- package/dist/dap/transport.d.ts +8 -0
- package/dist/dap/transport.js +30 -0
- package/dist/dap/types.d.ts +50 -0
- package/dist/dap/types.js +2 -0
- package/dist/db/client.d.ts +22 -0
- package/dist/db/client.js +39 -0
- package/dist/db/index.d.ts +6 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.d.ts +9 -0
- package/dist/db/migrate.js +15 -0
- package/dist/db/schema.d.ts +965 -0
- package/dist/db/schema.js +112 -0
- package/dist/db/types.d.ts +11 -0
- package/dist/db/types.js +1 -0
- package/dist/llm/index.d.ts +13 -0
- package/dist/llm/index.js +8 -0
- package/dist/llm/protocols/openai-compat.d.ts +139 -0
- package/dist/llm/protocols/openai-compat.js +214 -0
- package/dist/llm/protocols/utils/index.d.ts +2 -0
- package/dist/llm/protocols/utils/index.js +2 -0
- package/dist/llm/protocols/utils/lifecycle.d.ts +23 -0
- package/dist/llm/protocols/utils/lifecycle.js +46 -0
- package/dist/llm/protocols/utils/tool-stream.d.ts +46 -0
- package/dist/llm/protocols/utils/tool-stream.js +94 -0
- package/dist/llm/provider-error.d.ts +5 -0
- package/dist/llm/provider-error.js +33 -0
- package/dist/llm/provider.d.ts +32 -0
- package/dist/llm/provider.js +191 -0
- package/dist/llm/registry.d.ts +44 -0
- package/dist/llm/registry.js +107 -0
- package/dist/llm/retry.d.ts +37 -0
- package/dist/llm/retry.js +82 -0
- package/dist/llm/routing.d.ts +36 -0
- package/dist/llm/routing.js +53 -0
- package/dist/llm/schema/errors.d.ts +88 -0
- package/dist/llm/schema/errors.js +33 -0
- package/dist/llm/schema/events.d.ts +135 -0
- package/dist/llm/schema/events.js +35 -0
- package/dist/llm/schema/ids.d.ts +22 -0
- package/dist/llm/schema/ids.js +3 -0
- package/dist/llm/schema/index.d.ts +5 -0
- package/dist/llm/schema/index.js +5 -0
- package/dist/llm/schema/messages.d.ts +97 -0
- package/dist/llm/schema/messages.js +18 -0
- package/dist/llm/schema/options.d.ts +54 -0
- package/dist/llm/schema/options.js +54 -0
- package/dist/llm/token.d.ts +2 -0
- package/dist/llm/token.js +13 -0
- package/dist/llm/transport.d.ts +31 -0
- package/dist/llm/transport.js +165 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +3 -0
- package/dist/plugins/builtin.d.ts +6 -0
- package/dist/plugins/builtin.js +54 -0
- package/dist/plugins/hooks.d.ts +3 -0
- package/dist/plugins/hooks.js +79 -0
- package/dist/plugins/index.d.ts +9 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/init.d.ts +23 -0
- package/dist/plugins/init.js +30 -0
- package/dist/plugins/lifecycle.d.ts +6 -0
- package/dist/plugins/lifecycle.js +74 -0
- package/dist/plugins/loader.d.ts +16 -0
- package/dist/plugins/loader.js +68 -0
- package/dist/plugins/logger.d.ts +4 -0
- package/dist/plugins/logger.js +31 -0
- package/dist/plugins/registry.d.ts +7 -0
- package/dist/plugins/registry.js +24 -0
- package/dist/plugins/types.d.ts +113 -0
- package/dist/plugins/types.js +1 -0
- package/dist/project/detect.d.ts +4 -0
- package/dist/project/detect.js +131 -0
- package/dist/project/index.d.ts +4 -0
- package/dist/project/index.js +2 -0
- package/dist/project/project.d.ts +23 -0
- package/dist/project/project.js +87 -0
- package/dist/project/resolve.d.ts +8 -0
- package/dist/project/resolve.js +81 -0
- package/dist/server/agent-manager.d.ts +32 -0
- package/dist/server/agent-manager.js +54 -0
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.js +75 -0
- package/dist/server/context.d.ts +20 -0
- package/dist/server/context.js +47 -0
- package/dist/server/dev.d.ts +11 -0
- package/dist/server/dev.js +83 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +7 -0
- package/dist/server/middleware/auth.d.ts +15 -0
- package/dist/server/middleware/auth.js +29 -0
- package/dist/server/middleware/cors.d.ts +16 -0
- package/dist/server/middleware/cors.js +44 -0
- package/dist/server/middleware/error.d.ts +15 -0
- package/dist/server/middleware/error.js +11 -0
- package/dist/server/permission/interactive.d.ts +24 -0
- package/dist/server/permission/interactive.js +56 -0
- package/dist/server/permission/store.d.ts +30 -0
- package/dist/server/permission/store.js +47 -0
- package/dist/server/routes/agent.d.ts +4 -0
- package/dist/server/routes/agent.js +19 -0
- package/dist/server/routes/catalog.d.ts +42 -0
- package/dist/server/routes/catalog.js +194 -0
- package/dist/server/routes/chat.d.ts +8 -0
- package/dist/server/routes/chat.js +282 -0
- package/dist/server/routes/commands.d.ts +5 -0
- package/dist/server/routes/commands.js +17 -0
- package/dist/server/routes/config.d.ts +4 -0
- package/dist/server/routes/config.js +27 -0
- package/dist/server/routes/files.d.ts +4 -0
- package/dist/server/routes/files.js +183 -0
- package/dist/server/routes/filesystem.d.ts +29 -0
- package/dist/server/routes/filesystem.js +108 -0
- package/dist/server/routes/health.d.ts +3 -0
- package/dist/server/routes/health.js +13 -0
- package/dist/server/routes/permissions.d.ts +5 -0
- package/dist/server/routes/permissions.js +23 -0
- package/dist/server/routes/project.d.ts +4 -0
- package/dist/server/routes/project.js +51 -0
- package/dist/server/routes/provider.d.ts +16 -0
- package/dist/server/routes/provider.js +66 -0
- package/dist/server/routes/session.d.ts +4 -0
- package/dist/server/routes/session.js +130 -0
- package/dist/server/routes/tool.d.ts +4 -0
- package/dist/server/routes/tool.js +28 -0
- package/dist/server/routes/update.d.ts +14 -0
- package/dist/server/routes/update.js +49 -0
- package/dist/server/server.d.ts +43 -0
- package/dist/server/server.js +197 -0
- package/dist/server/types.d.ts +73 -0
- package/dist/server/types.js +2 -0
- package/dist/server/util/safe-path.d.ts +7 -0
- package/dist/server/util/safe-path.js +7 -0
- package/dist/session/archive.d.ts +15 -0
- package/dist/session/archive.js +129 -0
- package/dist/session/branch.d.ts +9 -0
- package/dist/session/branch.js +63 -0
- package/dist/session/compaction.d.ts +27 -0
- package/dist/session/compaction.js +192 -0
- package/dist/session/context.d.ts +16 -0
- package/dist/session/context.js +173 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.js +10 -0
- package/dist/session/message.d.ts +22 -0
- package/dist/session/message.js +145 -0
- package/dist/session/session.d.ts +37 -0
- package/dist/session/session.js +144 -0
- package/dist/session/snapshot.d.ts +11 -0
- package/dist/session/snapshot.js +60 -0
- package/dist/session/squash.d.ts +9 -0
- package/dist/session/squash.js +88 -0
- package/dist/session/token.d.ts +10 -0
- package/dist/session/token.js +33 -0
- package/dist/session/types.d.ts +122 -0
- package/dist/session/types.js +1 -0
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/types/agent.d.ts +202 -0
- package/dist/shared/types/agent.js +1 -0
- package/dist/shared/types/base.d.ts +32 -0
- package/dist/shared/types/base.js +1 -0
- package/dist/shared/types/config.d.ts +104 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/index.d.ts +6 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/llm.d.ts +122 -0
- package/dist/shared/types/llm.js +1 -0
- package/dist/shared/types/message.d.ts +71 -0
- package/dist/shared/types/message.js +1 -0
- package/dist/shared/types/tool.d.ts +134 -0
- package/dist/shared/types/tool.js +1 -0
- package/dist/shared/utils/path.d.ts +15 -0
- package/dist/shared/utils/path.js +23 -0
- package/dist/shared/utils.d.ts +5 -0
- package/dist/shared/utils.js +10 -0
- package/dist/tools/builtin/bash.d.ts +12 -0
- package/dist/tools/builtin/bash.js +121 -0
- package/dist/tools/builtin/dap.d.ts +23 -0
- package/dist/tools/builtin/dap.js +231 -0
- package/dist/tools/builtin/edit.d.ts +12 -0
- package/dist/tools/builtin/edit.js +151 -0
- package/dist/tools/builtin/glob.d.ts +11 -0
- package/dist/tools/builtin/glob.js +120 -0
- package/dist/tools/builtin/grep.d.ts +6 -0
- package/dist/tools/builtin/grep.js +141 -0
- package/dist/tools/builtin/read.d.ts +6 -0
- package/dist/tools/builtin/read.js +73 -0
- package/dist/tools/builtin/resolvers.d.ts +12 -0
- package/dist/tools/builtin/resolvers.js +79 -0
- package/dist/tools/builtin/task.d.ts +9 -0
- package/dist/tools/builtin/task.js +100 -0
- package/dist/tools/builtin/write.d.ts +6 -0
- package/dist/tools/builtin/write.js +36 -0
- package/dist/tools/builtin/yield.d.ts +9 -0
- package/dist/tools/builtin/yield.js +40 -0
- package/dist/tools/executor.d.ts +9 -0
- package/dist/tools/executor.js +54 -0
- package/dist/tools/hashline/index.d.ts +2 -0
- package/dist/tools/hashline/index.js +3 -0
- package/dist/tools/hashline/patch.d.ts +48 -0
- package/dist/tools/hashline/patch.js +201 -0
- package/dist/tools/index.d.ts +30 -0
- package/dist/tools/index.js +55 -0
- package/dist/tools/permission.d.ts +13 -0
- package/dist/tools/permission.js +39 -0
- package/dist/tools/registry.d.ts +15 -0
- package/dist/tools/registry.js +54 -0
- package/dist/tools/resolver.d.ts +15 -0
- package/dist/tools/resolver.js +47 -0
- package/dist/tools/truncate.d.ts +8 -0
- package/dist/tools/truncate.js +50 -0
- package/dist/tools/types.d.ts +101 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/validate.d.ts +8 -0
- package/dist/tools/validate.js +130 -0
- package/dist/tools/websearch/fetch.d.ts +11 -0
- package/dist/tools/websearch/fetch.js +21 -0
- package/dist/tools/websearch/index.d.ts +19 -0
- package/dist/tools/websearch/index.js +78 -0
- package/dist/tools/websearch/providers/brave.d.ts +5 -0
- package/dist/tools/websearch/providers/brave.js +53 -0
- package/dist/tools/websearch/providers/duckduckgo.d.ts +3 -0
- package/dist/tools/websearch/providers/duckduckgo.js +75 -0
- package/dist/tools/websearch/providers/tavily.d.ts +11 -0
- package/dist/tools/websearch/providers/tavily.js +89 -0
- package/dist/tools/websearch/types.d.ts +51 -0
- package/dist/tools/websearch/types.js +18 -0
- package/dist/tools/websearch/websearch.d.ts +10 -0
- package/dist/tools/websearch/websearch.js +48 -0
- package/dist/update/index.d.ts +10 -0
- package/dist/update/index.js +6 -0
- package/dist/update/ipc.d.ts +15 -0
- package/dist/update/ipc.js +41 -0
- package/dist/update/scheduler.d.ts +34 -0
- package/dist/update/scheduler.js +57 -0
- package/dist/update/snapshot.d.ts +39 -0
- package/dist/update/snapshot.js +99 -0
- package/dist/update/updater.d.ts +48 -0
- package/dist/update/updater.js +83 -0
- package/dist/update/version.d.ts +18 -0
- package/dist/update/version.js +49 -0
- package/drizzle/0000_magical_nightshade.sql +55 -0
- package/drizzle/0001_chemical_doctor_faustus.sql +13 -0
- package/drizzle/0002_amusing_micromacro.sql +12 -0
- package/drizzle/0003_regular_purifiers.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +448 -0
- package/drizzle/meta/0001_snapshot.json +539 -0
- package/drizzle/meta/0002_snapshot.json +633 -0
- package/drizzle/meta/0003_snapshot.json +645 -0
- package/drizzle/meta/_journal.json +34 -0
- package/package.json +100 -8
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { JSONSchema } from '../shared/types/base.js';
|
|
2
|
+
import type { ChatTool } from '../shared/types/llm.js';
|
|
3
|
+
import type { ToolContext, ToolDef, ToolExecutor, ToolMode, ToolPermission, ToolResult } from '../shared/types/tool.js';
|
|
4
|
+
/** Mutable tool registry. Stores both eager tool definitions and lazy factories. */
|
|
5
|
+
type ToolRegistry = {
|
|
6
|
+
tools: Map<string, ToolDef>;
|
|
7
|
+
factories: Map<string, ToolFactory>;
|
|
8
|
+
};
|
|
9
|
+
/** Context passed to tool factories for lazy tool construction. */
|
|
10
|
+
type ToolFactoryContext = {
|
|
11
|
+
config: Record<string, unknown>;
|
|
12
|
+
cwd: string;
|
|
13
|
+
};
|
|
14
|
+
/** Factory function for lazy tool loading. Returns null if tool is unavailable. */
|
|
15
|
+
type ToolFactory = (ctx: ToolFactoryContext) => ToolDef | null;
|
|
16
|
+
/** Result of a permission check. */
|
|
17
|
+
type PermissionResult = {
|
|
18
|
+
_tag: 'allow';
|
|
19
|
+
} | {
|
|
20
|
+
_tag: 'deny';
|
|
21
|
+
reason: string;
|
|
22
|
+
} | {
|
|
23
|
+
_tag: 'ask';
|
|
24
|
+
reason: string;
|
|
25
|
+
toolCallId: string;
|
|
26
|
+
};
|
|
27
|
+
/** Permission checker interface. The executor calls `check` before running a tool. */
|
|
28
|
+
type PermissionChecker = {
|
|
29
|
+
check: (tool: ToolDef, input: unknown, ctx: ToolContext) => Promise<PermissionResult>;
|
|
30
|
+
confirm: (toolCallId: string, approved: boolean) => void;
|
|
31
|
+
};
|
|
32
|
+
/** Result of JSON Schema validation. */
|
|
33
|
+
type ValidationResult = {
|
|
34
|
+
valid: true;
|
|
35
|
+
} | {
|
|
36
|
+
valid: false;
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
39
|
+
/** Options for output truncation. */
|
|
40
|
+
type TruncateOptions = {
|
|
41
|
+
maxLines: number;
|
|
42
|
+
maxChars: number;
|
|
43
|
+
headLines: number;
|
|
44
|
+
tailLines: number;
|
|
45
|
+
};
|
|
46
|
+
/** Result of output truncation. */
|
|
47
|
+
type TruncateResult = {
|
|
48
|
+
output: string;
|
|
49
|
+
truncated: boolean;
|
|
50
|
+
totalLines: number;
|
|
51
|
+
totalChars: number;
|
|
52
|
+
};
|
|
53
|
+
/** Input for the read tool. */
|
|
54
|
+
type ReadInput = {
|
|
55
|
+
path: string;
|
|
56
|
+
offset?: number;
|
|
57
|
+
limit?: number;
|
|
58
|
+
};
|
|
59
|
+
/** Input for the write tool. */
|
|
60
|
+
type WriteInput = {
|
|
61
|
+
path: string;
|
|
62
|
+
content: string;
|
|
63
|
+
};
|
|
64
|
+
/** Input for the edit tool. Two modes (spec §16.4):
|
|
65
|
+
* - diff: search/replace via oldText/newText
|
|
66
|
+
* - hashline: content-hash-anchored patch via `patch` */
|
|
67
|
+
type EditInput = {
|
|
68
|
+
path: string;
|
|
69
|
+
oldText: string;
|
|
70
|
+
newText: string;
|
|
71
|
+
} | {
|
|
72
|
+
path: string;
|
|
73
|
+
patch: string;
|
|
74
|
+
};
|
|
75
|
+
/** Input for the glob tool. */
|
|
76
|
+
type GlobInput = {
|
|
77
|
+
pattern: string;
|
|
78
|
+
path?: string;
|
|
79
|
+
};
|
|
80
|
+
/** Input for the grep tool. */
|
|
81
|
+
type GrepInput = {
|
|
82
|
+
pattern: string;
|
|
83
|
+
path?: string;
|
|
84
|
+
caseSensitive?: boolean;
|
|
85
|
+
maxResults?: number;
|
|
86
|
+
};
|
|
87
|
+
/** Input for the bash tool (sync mode). */
|
|
88
|
+
type BashInput = {
|
|
89
|
+
command: string;
|
|
90
|
+
cwd?: string;
|
|
91
|
+
timeout?: number;
|
|
92
|
+
env?: Record<string, string>;
|
|
93
|
+
};
|
|
94
|
+
/** A single grep match. */
|
|
95
|
+
type GrepMatch = {
|
|
96
|
+
file: string;
|
|
97
|
+
line: number;
|
|
98
|
+
text: string;
|
|
99
|
+
match: string;
|
|
100
|
+
};
|
|
101
|
+
export type { BashInput, ChatTool, EditInput, GlobInput, GrepInput, GrepMatch, JSONSchema, PermissionChecker, PermissionResult, ReadInput, ToolContext, ToolDef, ToolExecutor, ToolFactory, ToolFactoryContext, ToolMode, ToolPermission, ToolRegistry, ToolResult, TruncateOptions, TruncateResult, ValidationResult, WriteInput, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JSONSchema } from '../shared/types/base.js';
|
|
2
|
+
import type { ValidationResult } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Validate a value against a JSON Schema (draft-07 subset).
|
|
5
|
+
* Supports: type, required, properties, items, enum, additionalProperties, anyOf, oneOf.
|
|
6
|
+
* No external dependency — lightweight custom implementation.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateInput(schema: JSONSchema, value: unknown): ValidationResult;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a value against a JSON Schema (draft-07 subset).
|
|
3
|
+
* Supports: type, required, properties, items, enum, additionalProperties, anyOf, oneOf.
|
|
4
|
+
* No external dependency — lightweight custom implementation.
|
|
5
|
+
*/
|
|
6
|
+
export function validateInput(schema, value) {
|
|
7
|
+
const error = validateNode(schema, value, '');
|
|
8
|
+
if (error)
|
|
9
|
+
return { valid: false, error };
|
|
10
|
+
return { valid: true };
|
|
11
|
+
}
|
|
12
|
+
function validateNode(schema, value, path) {
|
|
13
|
+
// Empty schema accepts anything
|
|
14
|
+
if (Object.keys(schema).length === 0)
|
|
15
|
+
return null;
|
|
16
|
+
// anyOf: at least one must pass
|
|
17
|
+
if (schema.anyOf) {
|
|
18
|
+
const passed = schema.anyOf.some((s) => validateNode(s, value, path) === null);
|
|
19
|
+
if (!passed)
|
|
20
|
+
return `${path || 'value'}: does not match anyOf schemas`;
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
// oneOf: exactly one must pass
|
|
24
|
+
if (schema.oneOf) {
|
|
25
|
+
const count = schema.oneOf.filter((s) => validateNode(s, value, path) === null).length;
|
|
26
|
+
if (count !== 1)
|
|
27
|
+
return `${path || 'value'}: must match exactly one oneOf schema (matched ${count})`;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
// enum
|
|
31
|
+
if (schema.enum !== undefined) {
|
|
32
|
+
if (!schema.enum.includes(value)) {
|
|
33
|
+
return `${path || 'value'}: must be one of ${JSON.stringify(schema.enum)}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// type check
|
|
37
|
+
if (schema.type) {
|
|
38
|
+
const typeError = checkType(schema.type, value, path);
|
|
39
|
+
if (typeError)
|
|
40
|
+
return typeError;
|
|
41
|
+
}
|
|
42
|
+
// object validation
|
|
43
|
+
if (schema.type === 'object' &&
|
|
44
|
+
typeof value === 'object' &&
|
|
45
|
+
value !== null &&
|
|
46
|
+
!Array.isArray(value)) {
|
|
47
|
+
const obj = value;
|
|
48
|
+
// required fields
|
|
49
|
+
if (schema.required) {
|
|
50
|
+
for (const field of schema.required) {
|
|
51
|
+
if (!(field in obj)) {
|
|
52
|
+
return `${path ? `${path}.` : ''}${field}: missing required field`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// properties
|
|
57
|
+
if (schema.properties) {
|
|
58
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
59
|
+
if (key in obj) {
|
|
60
|
+
const err = validateNode(propSchema, obj[key], path ? `${path}.${key}` : key);
|
|
61
|
+
if (err)
|
|
62
|
+
return err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// additionalProperties
|
|
67
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
68
|
+
const knownKeys = new Set(Object.keys(schema.properties));
|
|
69
|
+
for (const key of Object.keys(obj)) {
|
|
70
|
+
if (!knownKeys.has(key)) {
|
|
71
|
+
return `${path ? `${path}.` : ''}${key}: additional property not allowed`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// array validation
|
|
77
|
+
if (schema.type === 'array' && Array.isArray(value)) {
|
|
78
|
+
if (schema.items) {
|
|
79
|
+
const itemSchema = Array.isArray(schema.items) ? schema.items : [schema.items];
|
|
80
|
+
for (let i = 0; i < value.length; i++) {
|
|
81
|
+
const s = itemSchema[i] ?? itemSchema[0];
|
|
82
|
+
if (s) {
|
|
83
|
+
const err = validateNode(s, value[i], `${path}[${i}]`);
|
|
84
|
+
if (err)
|
|
85
|
+
return err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
function checkType(type, value, path) {
|
|
93
|
+
const label = path || 'value';
|
|
94
|
+
switch (type) {
|
|
95
|
+
case 'string':
|
|
96
|
+
if (typeof value !== 'string')
|
|
97
|
+
return `${label}: expected string, got ${typeof value}`;
|
|
98
|
+
break;
|
|
99
|
+
case 'number':
|
|
100
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
101
|
+
return `${label}: expected number, got ${typeof value}`;
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case 'integer':
|
|
105
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
106
|
+
return `${label}: expected integer, got ${typeof value}`;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case 'boolean':
|
|
110
|
+
if (typeof value !== 'boolean')
|
|
111
|
+
return `${label}: expected boolean, got ${typeof value}`;
|
|
112
|
+
break;
|
|
113
|
+
case 'object':
|
|
114
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
115
|
+
return `${label}: expected object`;
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
case 'array':
|
|
119
|
+
if (!Array.isArray(value))
|
|
120
|
+
return `${label}: expected array, got ${typeof value}`;
|
|
121
|
+
break;
|
|
122
|
+
case 'null':
|
|
123
|
+
if (value !== null)
|
|
124
|
+
return `${label}: expected null, got ${typeof value}`;
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 读 HTTPS_PROXY/HTTP_PROXY 环境变量,有则返回带 dispatcher 的 fetch,
|
|
3
|
+
* 无则返回全局 fetch。仅影响本模块,不调用 setGlobalDispatcher(避免污染全局)。
|
|
4
|
+
*
|
|
5
|
+
* AGENTS.md 规定代理端口 7890;Node 内置 fetch 不自动走系统代理,故需包装。
|
|
6
|
+
*
|
|
7
|
+
* 注意:必须用 npm 包 undici 自带的 `fetch`(与 ProxyAgent 同版本)发起请求,
|
|
8
|
+
* 而非 Node 全局 fetch——Node 捆绑的 undici 与 npm 安装的 undici 版本不同,
|
|
9
|
+
* 跨版本传 Dispatcher 会被拒绝(UND_ERR_INVALID_ARG)。详见设计文档 §6。
|
|
10
|
+
*/
|
|
11
|
+
export declare function createFetch(): typeof fetch;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ProxyAgent, fetch as undiciFetch } from 'undici';
|
|
2
|
+
/**
|
|
3
|
+
* 读 HTTPS_PROXY/HTTP_PROXY 环境变量,有则返回带 dispatcher 的 fetch,
|
|
4
|
+
* 无则返回全局 fetch。仅影响本模块,不调用 setGlobalDispatcher(避免污染全局)。
|
|
5
|
+
*
|
|
6
|
+
* AGENTS.md 规定代理端口 7890;Node 内置 fetch 不自动走系统代理,故需包装。
|
|
7
|
+
*
|
|
8
|
+
* 注意:必须用 npm 包 undici 自带的 `fetch`(与 ProxyAgent 同版本)发起请求,
|
|
9
|
+
* 而非 Node 全局 fetch——Node 捆绑的 undici 与 npm 安装的 undici 版本不同,
|
|
10
|
+
* 跨版本传 Dispatcher 会被拒绝(UND_ERR_INVALID_ARG)。详见设计文档 §6。
|
|
11
|
+
*/
|
|
12
|
+
export function createFetch() {
|
|
13
|
+
const proxy = process.env.HTTPS_PROXY ||
|
|
14
|
+
process.env.https_proxy ||
|
|
15
|
+
process.env.HTTP_PROXY ||
|
|
16
|
+
process.env.http_proxy;
|
|
17
|
+
if (!proxy)
|
|
18
|
+
return fetch;
|
|
19
|
+
const dispatcher = new ProxyAgent(proxy);
|
|
20
|
+
return ((url, init) => undiciFetch(url, { ...init, dispatcher }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { WebSearchConfig } from '../../shared/types/config.js';
|
|
2
|
+
import type { Recency, WebSearchProvider, WebSearchProviderId, WebSearchResponse } from './types.js';
|
|
3
|
+
import { clampNumResults, DEFAULT_NUM_RESULTS } from './types.js';
|
|
4
|
+
/** 按 preference + key 可用性解析目标 provider。auto 优先级:tavily > brave > duckduckgo。 */
|
|
5
|
+
export declare function resolveProvider(preference: 'auto' | WebSearchProviderId, keys: {
|
|
6
|
+
tavily?: string;
|
|
7
|
+
brave?: string;
|
|
8
|
+
}): WebSearchProvider;
|
|
9
|
+
/** 将统一响应格式化为 LLM 友好的纯文本。 */
|
|
10
|
+
export declare function formatForLLM(response: WebSearchResponse): string;
|
|
11
|
+
/** 测试钩子:覆盖默认 fetch(undici ProxyAgent 包装版)。 */
|
|
12
|
+
export declare function setFetchOverride(f: typeof fetch | undefined): void;
|
|
13
|
+
/** 工具入口:解析 provider → search → 返回 WebSearchResponse。 */
|
|
14
|
+
export declare function runWebSearch(input: {
|
|
15
|
+
query: string;
|
|
16
|
+
numResults?: number;
|
|
17
|
+
recency?: Recency;
|
|
18
|
+
}, config: WebSearchConfig, abort: AbortSignal): Promise<WebSearchResponse>;
|
|
19
|
+
export { clampNumResults, DEFAULT_NUM_RESULTS };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createFetch } from './fetch.js';
|
|
2
|
+
import { braveProvider } from './providers/brave.js';
|
|
3
|
+
import { duckduckgoProvider } from './providers/duckduckgo.js';
|
|
4
|
+
import { tavilyProvider } from './providers/tavily.js';
|
|
5
|
+
import { clampNumResults, DEFAULT_NUM_RESULTS } from './types.js';
|
|
6
|
+
const PROVIDERS = {
|
|
7
|
+
duckduckgo: duckduckgoProvider,
|
|
8
|
+
tavily: tavilyProvider,
|
|
9
|
+
brave: braveProvider,
|
|
10
|
+
};
|
|
11
|
+
/** 环境变量名 → config key 字段映射。环境变量优先于 config。 */
|
|
12
|
+
function resolveKeys(config) {
|
|
13
|
+
return {
|
|
14
|
+
tavily: process.env.TAVILY_API_KEY ?? config.tavilyApiKey,
|
|
15
|
+
brave: process.env.BRAVE_API_KEY ?? config.braveApiKey,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** 按 preference + key 可用性解析目标 provider。auto 优先级:tavily > brave > duckduckgo。 */
|
|
19
|
+
export function resolveProvider(preference, keys) {
|
|
20
|
+
if (preference === 'auto') {
|
|
21
|
+
if (tavilyProvider.isAvailable(keys.tavily))
|
|
22
|
+
return tavilyProvider;
|
|
23
|
+
if (braveProvider.isAvailable(keys.brave))
|
|
24
|
+
return braveProvider;
|
|
25
|
+
return duckduckgoProvider;
|
|
26
|
+
}
|
|
27
|
+
const provider = PROVIDERS[preference];
|
|
28
|
+
if (!provider)
|
|
29
|
+
throw new Error(`Unknown websearch provider: ${preference}`);
|
|
30
|
+
if (!provider.isAvailable(preference === 'tavily' ? keys.tavily : keys.brave)) {
|
|
31
|
+
throw new Error(`${preference} provider requires an API key (set ${preference === 'tavily' ? 'TAVILY_API_KEY' : 'BRAVE_API_KEY'} or config).`);
|
|
32
|
+
}
|
|
33
|
+
return provider;
|
|
34
|
+
}
|
|
35
|
+
const SNIPPET_MAX = 240;
|
|
36
|
+
function truncate(text, max) {
|
|
37
|
+
return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 1))}…`;
|
|
38
|
+
}
|
|
39
|
+
/** 将统一响应格式化为 LLM 友好的纯文本。 */
|
|
40
|
+
export function formatForLLM(response) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (response.answer?.trim()) {
|
|
43
|
+
parts.push(response.answer.trim());
|
|
44
|
+
}
|
|
45
|
+
if (response.sources.length > 0) {
|
|
46
|
+
parts.push(`## Sources (${response.sources.length})`);
|
|
47
|
+
for (const [i, src] of response.sources.entries()) {
|
|
48
|
+
parts.push(`[${i + 1}] ${src.title}`);
|
|
49
|
+
parts.push(` ${src.url}`);
|
|
50
|
+
if (src.snippet)
|
|
51
|
+
parts.push(` ${truncate(src.snippet, SNIPPET_MAX)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parts.join('\n');
|
|
55
|
+
}
|
|
56
|
+
/** 模块级 fetch 覆盖(测试注入)。undefined 时用 createFetch()。 */
|
|
57
|
+
let fetchOverride;
|
|
58
|
+
/** 测试钩子:覆盖默认 fetch(undici ProxyAgent 包装版)。 */
|
|
59
|
+
export function setFetchOverride(f) {
|
|
60
|
+
fetchOverride = f;
|
|
61
|
+
}
|
|
62
|
+
function currentFetch() {
|
|
63
|
+
return fetchOverride ?? createFetch();
|
|
64
|
+
}
|
|
65
|
+
/** 工具入口:解析 provider → search → 返回 WebSearchResponse。 */
|
|
66
|
+
export async function runWebSearch(input, config, abort) {
|
|
67
|
+
const keys = resolveKeys(config);
|
|
68
|
+
const provider = resolveProvider(config.provider, keys);
|
|
69
|
+
return provider.search({
|
|
70
|
+
query: input.query,
|
|
71
|
+
limit: clampNumResults(input.numResults),
|
|
72
|
+
recency: input.recency,
|
|
73
|
+
signal: abort,
|
|
74
|
+
apiKey: provider.id === 'tavily' ? keys.tavily : provider.id === 'brave' ? keys.brave : undefined,
|
|
75
|
+
fetchImpl: currentFetch(),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export { clampNumResults, DEFAULT_NUM_RESULTS };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Recency, WebSearchProvider } from '../types.js';
|
|
2
|
+
/** 导出供测试:recency → Brave freshness 代码映射。 */
|
|
3
|
+
export declare function recencyToFreshness(recency: Recency | undefined): string | undefined;
|
|
4
|
+
/** Brave 后端(独立索引,需 key)。 */
|
|
5
|
+
export declare const braveProvider: WebSearchProvider;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { DEFAULT_NUM_RESULTS } from '../types.js';
|
|
2
|
+
const BRAVE_SEARCH_URL = 'https://api.search.brave.com/res/v1/web/search';
|
|
3
|
+
/** 导出供测试:recency → Brave freshness 代码映射。 */
|
|
4
|
+
export function recencyToFreshness(recency) {
|
|
5
|
+
switch (recency) {
|
|
6
|
+
case 'day':
|
|
7
|
+
return 'pd';
|
|
8
|
+
case 'week':
|
|
9
|
+
return 'pw';
|
|
10
|
+
case 'month':
|
|
11
|
+
return 'pm';
|
|
12
|
+
case 'year':
|
|
13
|
+
return 'py';
|
|
14
|
+
default:
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Brave 后端(独立索引,需 key)。 */
|
|
19
|
+
export const braveProvider = {
|
|
20
|
+
id: 'brave',
|
|
21
|
+
isAvailable: (apiKey) => Boolean(apiKey && apiKey.length > 0),
|
|
22
|
+
search: async (params) => {
|
|
23
|
+
if (!params.apiKey)
|
|
24
|
+
throw new Error('Brave requires an API key (BRAVE_API_KEY or config).');
|
|
25
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
26
|
+
const count = params.limit ?? DEFAULT_NUM_RESULTS;
|
|
27
|
+
const query = new URLSearchParams({ q: params.query, count: String(count) });
|
|
28
|
+
const freshness = recencyToFreshness(params.recency);
|
|
29
|
+
if (freshness)
|
|
30
|
+
query.set('freshness', freshness);
|
|
31
|
+
const response = await fetchImpl(`${BRAVE_SEARCH_URL}?${query.toString()}`, {
|
|
32
|
+
method: 'GET',
|
|
33
|
+
headers: {
|
|
34
|
+
'X-Subscription-Token': params.apiKey,
|
|
35
|
+
Accept: 'application/json',
|
|
36
|
+
'Accept-Encoding': 'gzip',
|
|
37
|
+
},
|
|
38
|
+
signal: params.signal,
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const text = await response.text().catch(() => response.statusText);
|
|
42
|
+
throw new Error(`Brave API error (${response.status}): ${text}`);
|
|
43
|
+
}
|
|
44
|
+
const data = (await response.json());
|
|
45
|
+
const sources = [];
|
|
46
|
+
for (const r of data.web?.results ?? []) {
|
|
47
|
+
if (!r.url)
|
|
48
|
+
continue;
|
|
49
|
+
sources.push({ title: r.title ?? r.url, url: r.url, snippet: r.description ?? undefined });
|
|
50
|
+
}
|
|
51
|
+
return { provider: 'brave', sources };
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const DUCKDUCKGO_SEARCH_URL = 'https://api.duckduckgo.com/';
|
|
2
|
+
const AGENT_TOKEN = 'c0de-agent';
|
|
3
|
+
function cleanText(value) {
|
|
4
|
+
const cleaned = value
|
|
5
|
+
?.replace(/<[^>]*>/g, ' ')
|
|
6
|
+
.replace(/ /gi, ' ')
|
|
7
|
+
.replace(/&/gi, '&')
|
|
8
|
+
.replace(/</gi, '<')
|
|
9
|
+
.replace(/>/gi, '>')
|
|
10
|
+
.replace(/"/gi, '"')
|
|
11
|
+
.replace(/'/gi, "'")
|
|
12
|
+
.replace(/\s+/g, ' ')
|
|
13
|
+
.trim();
|
|
14
|
+
return cleaned ? cleaned : undefined;
|
|
15
|
+
}
|
|
16
|
+
function addSource(sources, source) {
|
|
17
|
+
if (!source.url || sources.some((s) => s.url === source.url))
|
|
18
|
+
return;
|
|
19
|
+
sources.push(source);
|
|
20
|
+
}
|
|
21
|
+
function addTopicSource(sources, topic) {
|
|
22
|
+
const url = topic.FirstURL?.trim();
|
|
23
|
+
if (!url)
|
|
24
|
+
return;
|
|
25
|
+
const text = cleanText(topic.Text);
|
|
26
|
+
addSource(sources, { title: text ?? url, url, snippet: text });
|
|
27
|
+
}
|
|
28
|
+
function collectTopicSources(sources, topics) {
|
|
29
|
+
if (!topics)
|
|
30
|
+
return;
|
|
31
|
+
for (const topic of topics) {
|
|
32
|
+
addTopicSource(sources, topic);
|
|
33
|
+
collectTopicSources(sources, topic.Topics);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function callDuckDuckGoSearch(params) {
|
|
37
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
38
|
+
const query = new URLSearchParams({
|
|
39
|
+
q: params.query,
|
|
40
|
+
format: 'json',
|
|
41
|
+
no_redirect: '1',
|
|
42
|
+
no_html: '1',
|
|
43
|
+
skip_disambig: '1',
|
|
44
|
+
t: AGENT_TOKEN,
|
|
45
|
+
});
|
|
46
|
+
const response = await fetchImpl(`${DUCKDUCKGO_SEARCH_URL}?${query.toString()}`, {
|
|
47
|
+
method: 'GET',
|
|
48
|
+
signal: params.signal,
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const text = await response.text().catch(() => response.statusText);
|
|
52
|
+
throw new Error(`DuckDuckGo API error (${response.status}): ${text}`);
|
|
53
|
+
}
|
|
54
|
+
return (await response.json());
|
|
55
|
+
}
|
|
56
|
+
/** DuckDuckGo Instant Answer 后端(零 key,恒可用)。recency 不支持,忽略。 */
|
|
57
|
+
export const duckduckgoProvider = {
|
|
58
|
+
id: 'duckduckgo',
|
|
59
|
+
isAvailable: () => true,
|
|
60
|
+
search: async (params) => {
|
|
61
|
+
const data = await callDuckDuckGoSearch(params);
|
|
62
|
+
const answer = cleanText(data.AbstractText) ?? cleanText(data.Answer) ?? cleanText(data.Definition);
|
|
63
|
+
const sources = [];
|
|
64
|
+
// AbstractURL 作为首个来源(若无 RelatedTopics 填充)
|
|
65
|
+
if (data.AbstractURL) {
|
|
66
|
+
addSource(sources, {
|
|
67
|
+
title: cleanText(data.Heading) ?? data.AbstractURL,
|
|
68
|
+
url: data.AbstractURL,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
collectTopicSources(sources, data.Results);
|
|
72
|
+
collectTopicSources(sources, data.RelatedTopics);
|
|
73
|
+
return { provider: 'duckduckgo', answer, sources };
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Recency, WebSearchProvider } from '../types.js';
|
|
2
|
+
interface TavilySearchParams {
|
|
3
|
+
query: string;
|
|
4
|
+
numResults?: number;
|
|
5
|
+
recency?: Recency;
|
|
6
|
+
}
|
|
7
|
+
/** 导出供测试:构造 Tavily 请求 body。始终用 general topic(避免技术查询窄化为新闻)。 */
|
|
8
|
+
export declare function buildRequestBody(params: TavilySearchParams): Record<string, unknown>;
|
|
9
|
+
/** Tavily 后端(AI 优化搜索,需 key,返回合成 answer)。 */
|
|
10
|
+
export declare const tavilyProvider: WebSearchProvider;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS, MIN_NUM_RESULTS } from '../types.js';
|
|
2
|
+
const TAVILY_SEARCH_URL = 'https://api.tavily.com/search';
|
|
3
|
+
function clamp(n) {
|
|
4
|
+
if (n === undefined || Number.isNaN(n))
|
|
5
|
+
return DEFAULT_NUM_RESULTS;
|
|
6
|
+
return Math.min(MAX_NUM_RESULTS, Math.max(MIN_NUM_RESULTS, Math.trunc(n)));
|
|
7
|
+
}
|
|
8
|
+
/** 导出供测试:构造 Tavily 请求 body。始终用 general topic(避免技术查询窄化为新闻)。 */
|
|
9
|
+
export function buildRequestBody(params) {
|
|
10
|
+
const body = {
|
|
11
|
+
query: params.query,
|
|
12
|
+
search_depth: 'basic',
|
|
13
|
+
max_results: clamp(params.numResults),
|
|
14
|
+
include_answer: 'advanced',
|
|
15
|
+
include_raw_content: false,
|
|
16
|
+
};
|
|
17
|
+
if (params.recency)
|
|
18
|
+
body.time_range = params.recency;
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
function safeParse(text) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(text);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function asRecord(value) {
|
|
30
|
+
return typeof value === 'object' && value !== null ? value : null;
|
|
31
|
+
}
|
|
32
|
+
function getErrorMessage(value) {
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const t = value.trim();
|
|
35
|
+
return t.length > 0 ? t : null;
|
|
36
|
+
}
|
|
37
|
+
const rec = asRecord(value);
|
|
38
|
+
if (!rec)
|
|
39
|
+
return null;
|
|
40
|
+
for (const key of ['detail', 'error', 'message']) {
|
|
41
|
+
const m = getErrorMessage(rec[key]);
|
|
42
|
+
if (m)
|
|
43
|
+
return m;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
async function callTavilySearch(apiKey, params) {
|
|
48
|
+
const fetchImpl = params.fetchImpl ?? fetch;
|
|
49
|
+
const response = await fetchImpl(TAVILY_SEARCH_URL, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'content-type': 'application/json',
|
|
53
|
+
Authorization: `Bearer ${apiKey}`,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify(buildRequestBody({
|
|
56
|
+
query: params.query,
|
|
57
|
+
numResults: params.limit,
|
|
58
|
+
recency: params.recency,
|
|
59
|
+
})),
|
|
60
|
+
signal: params.signal,
|
|
61
|
+
});
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
const raw = await response.text().catch(() => response.statusText);
|
|
64
|
+
const msg = getErrorMessage(safeParse(raw)) ?? (raw.trim() || response.statusText);
|
|
65
|
+
throw new Error(`Tavily API error (${response.status}): ${msg}`);
|
|
66
|
+
}
|
|
67
|
+
return (await response.json());
|
|
68
|
+
}
|
|
69
|
+
/** Tavily 后端(AI 优化搜索,需 key,返回合成 answer)。 */
|
|
70
|
+
export const tavilyProvider = {
|
|
71
|
+
id: 'tavily',
|
|
72
|
+
isAvailable: (apiKey) => Boolean(apiKey && apiKey.length > 0),
|
|
73
|
+
search: async (params) => {
|
|
74
|
+
if (!params.apiKey)
|
|
75
|
+
throw new Error('Tavily requires an API key (TAVILY_API_KEY or config).');
|
|
76
|
+
const data = await callTavilySearch(params.apiKey, params);
|
|
77
|
+
const sources = [];
|
|
78
|
+
for (const r of data.results ?? []) {
|
|
79
|
+
if (!r.url)
|
|
80
|
+
continue;
|
|
81
|
+
sources.push({ title: r.title ?? r.url, url: r.url, snippet: r.content ?? undefined });
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
provider: 'tavily',
|
|
85
|
+
answer: data.answer?.trim() || undefined,
|
|
86
|
+
sources: sources.slice(0, clamp(params.limit)),
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* websearch 工具的统一类型、常量与工具函数。
|
|
3
|
+
*
|
|
4
|
+
* 来源:oh-my-pi(packages/coding-agent/src/web/search/)多后端 strategy 架构,
|
|
5
|
+
* 裁剪适配 c0de-agent 的 data+functions 范式(无 class)。
|
|
6
|
+
* 详见 docs/superpowers/specs/2026-06-30-websearch-tool-design.md §2-3。
|
|
7
|
+
*/
|
|
8
|
+
/** 后端标识。strategy 接口预留,未来可加 'exa' | 'searxng' | … */
|
|
9
|
+
type WebSearchProviderId = 'duckduckgo' | 'tavily' | 'brave';
|
|
10
|
+
/** 时效过滤窗口。各后端映射为各自 API 参数。 */
|
|
11
|
+
type Recency = 'day' | 'week' | 'month' | 'year';
|
|
12
|
+
/** 单条搜索来源(所有后端统一)。 */
|
|
13
|
+
type WebSearchSource = {
|
|
14
|
+
title: string;
|
|
15
|
+
url: string;
|
|
16
|
+
snippet?: string;
|
|
17
|
+
};
|
|
18
|
+
/** 统一搜索参数。 */
|
|
19
|
+
type WebSearchParams = {
|
|
20
|
+
query: string;
|
|
21
|
+
limit?: number;
|
|
22
|
+
recency?: Recency;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** Tavily/Brave 的 key(DuckDuckGo 不用)。 */
|
|
25
|
+
apiKey?: string;
|
|
26
|
+
/** 测试注入;默认用 createFetch()。 */
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
};
|
|
29
|
+
/** 统一响应(所有后端统一)。 */
|
|
30
|
+
type WebSearchResponse = {
|
|
31
|
+
provider: WebSearchProviderId;
|
|
32
|
+
/** Tavily 返回合成答案;DuckDuckGo/Brave 通常无。 */
|
|
33
|
+
answer?: string;
|
|
34
|
+
sources: WebSearchSource[];
|
|
35
|
+
};
|
|
36
|
+
/** 后端 strategy 接口(data + functions,非 class)。 */
|
|
37
|
+
type WebSearchProvider = {
|
|
38
|
+
id: WebSearchProviderId;
|
|
39
|
+
/** 是否当前可用(DuckDuckGo 恒真;Tavily/Brave 需 key)。驱动 'auto' 选择。 */
|
|
40
|
+
isAvailable: (apiKey?: string) => boolean;
|
|
41
|
+
/** 执行搜索。 */
|
|
42
|
+
search: (params: WebSearchParams) => Promise<WebSearchResponse>;
|
|
43
|
+
};
|
|
44
|
+
/** 工具参数校验常量。 */
|
|
45
|
+
declare const DEFAULT_NUM_RESULTS = 8;
|
|
46
|
+
declare const MAX_NUM_RESULTS = 20;
|
|
47
|
+
declare const MIN_NUM_RESULTS = 1;
|
|
48
|
+
/** clamp 结果数到 [MIN, MAX];undefined/NaN 回退到 fallback。 */
|
|
49
|
+
declare function clampNumResults(n: number | undefined, fallback?: number): number;
|
|
50
|
+
export type { Recency, WebSearchParams, WebSearchProvider, WebSearchProviderId, WebSearchResponse, WebSearchSource, };
|
|
51
|
+
export { clampNumResults, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS, MIN_NUM_RESULTS };
|