@circuitwall/jarela 0.14.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js +6 -1
- package/.next/standalone/.next/server/app/api/v1/agents/[id]/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/agents/route.js +6 -1
- package/.next/standalone/.next/server/app/api/v1/agents/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +9 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js +9 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js +36 -29
- package/.next/standalone/.next/server/app/api/v1/builtin-tools/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/events/route.js +7 -1
- package/.next/standalone/.next/server/app/api/v1/events/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +3 -3
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +4 -4
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/health/route.js +7 -1
- package/.next/standalone/.next/server/app/api/v1/health/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js +9 -1
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/[name]/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js +9 -1
- package/.next/standalone/.next/server/app/api/v1/mcp-servers/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/models/route.js +6 -1
- package/.next/standalone/.next/server/app/api/v1/models/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +7 -1
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js +14 -7
- package/.next/standalone/.next/server/app/api/v1/pending-actions/[id]/approve/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +28 -0
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/providers/route.js +7 -1
- package/.next/standalone/.next/server/app/api/v1/providers/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +16 -2
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +8 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/route.js +6 -1
- package/.next/standalone/.next/server/app/api/v1/threads/route.js.map +1 -1
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +10 -3
- package/.next/standalone/.next/server/app/api/v1/tools/route.js.map +1 -1
- package/.next/standalone/.next/server/app/index.html +2 -2
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page.js +56 -0
- package/.next/standalone/.next/server/app/page.js.map +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup.segment.rsc +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
- package/.next/standalone/.next/server/chunks/1683.js +2 -2
- package/.next/standalone/.next/server/chunks/2082.js +122 -13
- package/.next/standalone/.next/server/chunks/2082.js.map +1 -1
- package/.next/standalone/.next/server/chunks/210.js +3 -3
- package/.next/standalone/.next/server/chunks/210.js.map +1 -1
- package/.next/standalone/.next/server/chunks/239.js +1902 -1487
- package/.next/standalone/.next/server/chunks/239.js.map +1 -1
- package/.next/standalone/.next/server/chunks/2447.js +9 -1
- package/.next/standalone/.next/server/chunks/2447.js.map +1 -1
- package/.next/standalone/.next/server/chunks/423.js +125 -16
- package/.next/standalone/.next/server/chunks/423.js.map +1 -1
- package/.next/standalone/.next/server/chunks/4631.js +36 -29
- package/.next/standalone/.next/server/chunks/4631.js.map +1 -1
- package/.next/standalone/.next/server/chunks/5937.js +3 -2
- package/.next/standalone/.next/server/chunks/5937.js.map +1 -1
- package/.next/standalone/.next/server/chunks/{947.js → 8866.js} +11321 -10883
- package/.next/standalone/.next/server/chunks/8866.js.map +1 -0
- package/.next/standalone/.next/server/chunks/9032.js +3 -3
- package/.next/standalone/.next/server/chunks/9032.js.map +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +122 -13
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/proxy.js.map +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/app/{page-473b39ec30c7f569.js → page-a7cae65f235e2942.js} +57 -1
- package/.next/standalone/.next/static/chunks/app/page-a7cae65f235e2942.js.map +1 -0
- package/.next/standalone/.next/static/css/{6f8b1a84bcbcd467.css → e57bdbbbb5a05779.css} +2 -2
- package/.next/standalone/.next/static/css/e57bdbbbb5a05779.css.map +1 -0
- package/.next/standalone/package.json +9 -1
- package/CHANGELOG.md +90 -0
- package/README.md +30 -2
- package/api/types.ts +8 -0
- package/app/api/v1/agents/[id]/route.ts +7 -0
- package/app/api/v1/agents/route.ts +7 -0
- package/app/api/v1/events/route.ts +8 -0
- package/app/api/v1/extensions/route.ts +2 -2
- package/app/api/v1/extensions/tools/[name]/secrets/route.ts +3 -3
- package/app/api/v1/health/route.ts +8 -0
- package/app/api/v1/models/route.ts +7 -0
- package/app/api/v1/page-capture/route.ts +8 -0
- package/app/api/v1/providers/route.ts +8 -0
- package/app/api/v1/threads/[thread_id]/route.ts +8 -0
- package/app/api/v1/threads/[thread_id]/run/route.ts +9 -0
- package/app/api/v1/threads/route.ts +7 -0
- package/app/api/v1/tools/route.ts +9 -0
- package/components/chat/ContextUsageBar.tsx +44 -0
- package/lib/agents/llm.ts +25 -2
- package/lib/agents/run-thread.ts +13 -1
- package/lib/agents/stream-collector.ts +9 -1
- package/lib/api/serializers.test.ts +15 -0
- package/lib/api/serializers.ts +8 -0
- package/lib/db/migrations.ts +15 -0
- package/lib/health/runner.test.ts +24 -2
- package/lib/mcp/registry.ts +14 -6
- package/lib/providers/anthropic.test.ts +95 -0
- package/lib/providers/anthropic.ts +106 -10
- package/lib/providers/jarela-chat-model.ts +9 -1
- package/lib/providers/known-context-windows.ts +21 -0
- package/lib/providers/types.ts +21 -1
- package/lib/stores/message-usage.test.ts +34 -0
- package/lib/stores/message-usage.ts +15 -3
- package/lib/stores/pricing.test.ts +52 -0
- package/lib/stores/pricing.ts +26 -1
- package/lib/tools/builtins.ts +4 -0
- package/lib/tools/extension-surfaces.test.ts +79 -0
- package/lib/tools/extension-surfaces.ts +153 -0
- package/lib/tools/index.ts +27 -8
- package/lib/tools/list-tools.test.ts +76 -0
- package/lib/tools/list-tools.ts +84 -0
- package/lib/tools/mcp-servers-info.test.ts +73 -0
- package/lib/tools/mcp-servers-info.ts +71 -0
- package/lib/tools/providers-info.test.ts +73 -0
- package/lib/tools/providers-info.ts +106 -0
- package/lib/tools/registry.ts +36 -25
- package/lib/tools/types.ts +13 -0
- package/package.json +9 -1
- package/.next/standalone/.next/server/chunks/947.js.map +0 -1
- package/.next/standalone/.next/static/chunks/app/page-473b39ec30c7f569.js.map +0 -1
- package/.next/standalone/.next/static/css/6f8b1a84bcbcd467.css.map +0 -1
- /package/.next/standalone/.next/static/{T0p2VVPsJPj44rwbmjaFb → d_vhp-lJqfdjRFpnLVIqZ}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{T0p2VVPsJPj44rwbmjaFb → d_vhp-lJqfdjRFpnLVIqZ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Meta-introspection: a hand-curated catalog of every place a third party
|
|
2
|
+
// (or the agent itself, on behalf of the user) can extend or customize the
|
|
3
|
+
// app. Read-only. The tool returns a structured map that points to the
|
|
4
|
+
// registration entrypoint, the integration guide section, and an example
|
|
5
|
+
// file for each surface — so the agent can answer "how do I add an X?"
|
|
6
|
+
// or "what can be customized" without needing the docs in its context.
|
|
7
|
+
//
|
|
8
|
+
// SOURCE OF TRUTH: this file. When a new extension surface lands, add an
|
|
9
|
+
// entry below — it's a single source for both the agent runtime and the
|
|
10
|
+
// `docs/EXTENDING.md` table-of-contents anchor.
|
|
11
|
+
|
|
12
|
+
import { tool } from "@langchain/core/tools";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
import { registerTools } from "./registry";
|
|
15
|
+
|
|
16
|
+
interface ExtensionSurface {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
registration_entrypoint: string;
|
|
21
|
+
doc_section: string;
|
|
22
|
+
example_path?: string;
|
|
23
|
+
introspection_tool?: string;
|
|
24
|
+
related_adrs: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SURFACES: readonly ExtensionSurface[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "llm_provider_builtin",
|
|
30
|
+
name: "Built-in LLM provider",
|
|
31
|
+
summary:
|
|
32
|
+
"Add a new in-tree adapter (Anthropic, OpenAI, Gemini, … pattern). " +
|
|
33
|
+
"Implements the ModelProvider interface and is registered via the static " +
|
|
34
|
+
"BUILTINS map in lib/providers/index.ts.",
|
|
35
|
+
registration_entrypoint: "lib/providers/index.ts (BUILTINS map)",
|
|
36
|
+
doc_section: "docs/EXTENDING.md#adding-an-llm-provider-built-in",
|
|
37
|
+
example_path: "lib/providers/anthropic.ts",
|
|
38
|
+
introspection_tool: "list_providers",
|
|
39
|
+
related_adrs: ["ADR-0013"],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "llm_provider_external",
|
|
43
|
+
name: "External LLM provider plugin",
|
|
44
|
+
summary:
|
|
45
|
+
"Drop a `.cjs` plugin into ~/.jarela/providers/ that exports an object " +
|
|
46
|
+
"matching ModelProvider. Hot-loaded per call (no rebuild).",
|
|
47
|
+
registration_entrypoint: "~/.jarela/providers/<name>.cjs",
|
|
48
|
+
doc_section: "docs/EXTENDING.md#adding-an-llm-provider-external",
|
|
49
|
+
example_path: "lib/providers/template-external.cjs.example",
|
|
50
|
+
introspection_tool: "list_providers",
|
|
51
|
+
related_adrs: ["ADR-0013"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "builtin_tool",
|
|
55
|
+
name: "Built-in tool",
|
|
56
|
+
summary:
|
|
57
|
+
"Add a tool callable by the agent. Implement with @langchain/core/tools, " +
|
|
58
|
+
"register with `registerTools(category, capability, [tool])`, and add a " +
|
|
59
|
+
"side-effect import in lib/tools/builtins.ts. Capability gating is " +
|
|
60
|
+
"read | write | execute.",
|
|
61
|
+
registration_entrypoint: "lib/tools/<name>.ts (registerTools call) + lib/tools/builtins.ts",
|
|
62
|
+
doc_section: "docs/EXTENDING.md#adding-a-built-in-tool",
|
|
63
|
+
example_path: "lib/tools/template.ts",
|
|
64
|
+
introspection_tool: "list_tools",
|
|
65
|
+
related_adrs: ["ADR-0038"],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "mcp_server",
|
|
69
|
+
name: "MCP server",
|
|
70
|
+
summary:
|
|
71
|
+
"Connect a Model Context Protocol server (stdio or http transport). " +
|
|
72
|
+
"Configured via the UI (mcp_servers DB table) or programmatically through " +
|
|
73
|
+
"lib/stores/mcp-servers.ts. Tools auto-merge into the same pool as " +
|
|
74
|
+
"built-ins. Online discovery via the MCP registry is also supported.",
|
|
75
|
+
registration_entrypoint: "lib/stores/mcp-servers.ts (upsertMcpServer)",
|
|
76
|
+
doc_section: "docs/EXTENDING.md#adding-an-mcp-server",
|
|
77
|
+
introspection_tool: "list_mcp_servers",
|
|
78
|
+
related_adrs: ["ADR-0014"],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "agent_harness",
|
|
82
|
+
name: "Agent harness",
|
|
83
|
+
summary:
|
|
84
|
+
"Compose system-prompt sections that shape an agent's behaviour " +
|
|
85
|
+
"(plan-first, self-config, capability-listing, etc.). Built-in presets " +
|
|
86
|
+
"live in lib/agents/harness/presets.ts; custom harnesses are stored in " +
|
|
87
|
+
"the memory_store under the `app-settings` namespace.",
|
|
88
|
+
registration_entrypoint: "lib/agents/harness/presets.ts (built-in) or memory_store (custom)",
|
|
89
|
+
doc_section: "docs/EXTENDING.md#adding-a-custom-harness",
|
|
90
|
+
related_adrs: ["ADR-0036"],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "integration_manifest",
|
|
94
|
+
name: "Integration manifest",
|
|
95
|
+
summary:
|
|
96
|
+
"Add a new agent-led setup recipe — prerequisites, ordered steps, " +
|
|
97
|
+
"troubleshooting hints. The agent narrates the recipe and proposes " +
|
|
98
|
+
"the corresponding config changes through propose_config_change.",
|
|
99
|
+
registration_entrypoint: "lib/integrations/registry.ts",
|
|
100
|
+
doc_section: "docs/EXTENDING.md#adding-an-integration-manifest",
|
|
101
|
+
introspection_tool: "list_integrations",
|
|
102
|
+
related_adrs: ["ADR-0010"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "brand_overlay",
|
|
106
|
+
name: "Brand / app identity",
|
|
107
|
+
summary:
|
|
108
|
+
"Customize the app's name, description, and issue URL via environment " +
|
|
109
|
+
"variables. No code change required. Used by downstream packages that " +
|
|
110
|
+
"consume @circuitwall/jarela as an npm dep (e.g. brand overlays).",
|
|
111
|
+
registration_entrypoint: "NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_APP_DESCRIPTION, NEXT_PUBLIC_APP_ISSUE_URL",
|
|
112
|
+
doc_section: "docs/EXTENDING.md#branding-the-app",
|
|
113
|
+
related_adrs: ["ADR-0005"],
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
export const describeExtensionSurfacesTool = tool(
|
|
118
|
+
async () => {
|
|
119
|
+
return JSON.stringify({
|
|
120
|
+
surfaces: SURFACES,
|
|
121
|
+
count: SURFACES.length,
|
|
122
|
+
guide_path: "docs/EXTENDING.md",
|
|
123
|
+
contract_paths: [
|
|
124
|
+
"lib/providers/types.ts (ModelProvider interface)",
|
|
125
|
+
"lib/tools/types.ts (OpenAITool, ToolContext, InvokeMessage)",
|
|
126
|
+
"lib/tools/registry.ts (registerTools, ToolCategory, Capability)",
|
|
127
|
+
"lib/mcp/registry.ts (RegistryEntry, applyVariables)",
|
|
128
|
+
],
|
|
129
|
+
notes: [
|
|
130
|
+
"Each surface's introspection_tool, when set, lets you enumerate what's " +
|
|
131
|
+
"currently registered. Call those when the user asks 'what's available' " +
|
|
132
|
+
"rather than describing the type from memory.",
|
|
133
|
+
"A surface can have either a static example_path (for external plugins) or " +
|
|
134
|
+
"no example (for surfaces wired entirely through the UI / DB). When in " +
|
|
135
|
+
"doubt, the doc_section under EXTENDING.md is the canonical walk-through.",
|
|
136
|
+
],
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: "describe_extension_surfaces",
|
|
141
|
+
description:
|
|
142
|
+
"Return the curated catalog of every extension surface in this app — " +
|
|
143
|
+
"providers, tools, MCP servers, harnesses, integration manifests, brand " +
|
|
144
|
+
"overlay. Each entry has a registration entrypoint, an EXTENDING.md " +
|
|
145
|
+
"section anchor, and (when applicable) the introspection tool that lists " +
|
|
146
|
+
"what's currently registered. Read-only. Call this when the user asks " +
|
|
147
|
+
"'how do I add an X?' or 'what can be customized?' so you can guide " +
|
|
148
|
+
"them with the right files and docs.",
|
|
149
|
+
schema: z.object({}),
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
registerTools("Config", "read", [describeExtensionSurfacesTool]);
|
package/lib/tools/index.ts
CHANGED
|
@@ -47,13 +47,32 @@ export {
|
|
|
47
47
|
type ToolGroup,
|
|
48
48
|
} from "./registry";
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
// Live accessors — DO NOT snapshot at module-load time. Some tool modules
|
|
51
|
+
// import back from this file (for `getAllToolsAsync` etc.) and would create
|
|
52
|
+
// a circular import cycle: their `registerTools(...)` call runs AFTER this
|
|
53
|
+
// module finishes evaluating, so any captured snapshot here would miss them
|
|
54
|
+
// and they'd silently disappear from the agent's tool pool. Live calls dodge
|
|
55
|
+
// the problem — by the time anyone INVOKES `getAllTools()` / etc., every
|
|
56
|
+
// builtin has registered.
|
|
57
|
+
function allBuiltins(): StructuredToolInterface[] {
|
|
58
|
+
return registeredTools();
|
|
59
|
+
}
|
|
60
|
+
function builtinNames(): ReadonlySet<string> {
|
|
61
|
+
return registeredNames();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Backwards-compatible export. Callers that import this Set get its current
|
|
65
|
+
// contents at the moment they read it (a fresh Set each access). Internal
|
|
66
|
+
// code prefers `builtinNames()` directly; this stays for external consumers
|
|
67
|
+
// like the extensions API route.
|
|
68
|
+
export function getBuiltinToolNames(): ReadonlySet<string> {
|
|
69
|
+
return builtinNames();
|
|
70
|
+
}
|
|
52
71
|
|
|
53
72
|
// Per-call recompute so files dropped in $JARELA_TOOLS_DIR are picked up
|
|
54
73
|
// without restart. loadExternalTools cache-busts require() per file.
|
|
55
74
|
function loadExternal() {
|
|
56
|
-
return loadExternalTools(
|
|
75
|
+
return loadExternalTools(builtinNames());
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
export type ToolSource = "builtin" | "external" | "mcp";
|
|
@@ -63,7 +82,7 @@ export type ToolSource = "builtin" | "external" | "mcp";
|
|
|
63
82
|
// neither a registered built-in nor an external (JARELA_TOOLS_DIR) tool —
|
|
64
83
|
// matches today's behavior where MCP tools are everything else.
|
|
65
84
|
export function getToolSource(name: string): ToolSource {
|
|
66
|
-
if (
|
|
85
|
+
if (builtinNames().has(name)) return "builtin";
|
|
67
86
|
if (loadExternal().tools.some((t) => t.name === name)) return "external";
|
|
68
87
|
return "mcp";
|
|
69
88
|
}
|
|
@@ -120,7 +139,7 @@ function applyCategoryToggles(tools: StructuredToolInterface[]): StructuredToolI
|
|
|
120
139
|
// and any code path that can't await.
|
|
121
140
|
export function getAllTools(policy?: ToolPolicy): StructuredToolInterface[] {
|
|
122
141
|
return applyPolicy(
|
|
123
|
-
[...applyCategoryToggles(
|
|
142
|
+
[...applyCategoryToggles(allBuiltins()), ...loadExternal().tools],
|
|
124
143
|
policy,
|
|
125
144
|
);
|
|
126
145
|
}
|
|
@@ -137,7 +156,7 @@ export async function getAllToolsAsync(policy?: ToolPolicy): Promise<StructuredT
|
|
|
137
156
|
console.error("[tools] MCP load failed, continuing with built-ins only:", err);
|
|
138
157
|
}
|
|
139
158
|
return applyPolicy(
|
|
140
|
-
[...applyCategoryToggles(
|
|
159
|
+
[...applyCategoryToggles(allBuiltins()), ...loadExternal().tools, ...mcpTools],
|
|
141
160
|
policy,
|
|
142
161
|
);
|
|
143
162
|
}
|
|
@@ -161,7 +180,7 @@ export async function executeTool(
|
|
|
161
180
|
args: Record<string, unknown>,
|
|
162
181
|
context: ToolContext = {},
|
|
163
182
|
): Promise<unknown> {
|
|
164
|
-
let t =
|
|
183
|
+
let t = allBuiltins().find((x) => x.name === name);
|
|
165
184
|
if (t) {
|
|
166
185
|
const cat = registeredCategory(name);
|
|
167
186
|
if (cat && disabledCategories().has(cat)) {
|
|
@@ -204,7 +223,7 @@ export function initTools(): InitToolsSummary {
|
|
|
204
223
|
const toolsDir = getToolsDir();
|
|
205
224
|
const result = loadExternal();
|
|
206
225
|
const summary: InitToolsSummary = {
|
|
207
|
-
builtinCount:
|
|
226
|
+
builtinCount: allBuiltins().length,
|
|
208
227
|
externalCount: result.tools.length,
|
|
209
228
|
errors: result.errors,
|
|
210
229
|
toolsDir,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-list-tools-"));
|
|
7
|
+
process.env.HOME = tmpRoot;
|
|
8
|
+
process.env.USERPROFILE = tmpRoot;
|
|
9
|
+
process.env.JARELA_DB_DIR = join(tmpRoot, ".jarela-dbdir");
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const { listToolsTool } = await import("./list-tools");
|
|
15
|
+
|
|
16
|
+
interface Result {
|
|
17
|
+
tools: Array<{
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
category: string;
|
|
21
|
+
capability: string;
|
|
22
|
+
source: string;
|
|
23
|
+
group: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
counts: {
|
|
26
|
+
total: number;
|
|
27
|
+
by_source: Record<string, number>;
|
|
28
|
+
by_capability: Record<string, number>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parse(s: string): Result {
|
|
33
|
+
return JSON.parse(s) as Result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("list_tools", () => {
|
|
37
|
+
it("returns every built-in tool with category, capability, and source", async () => {
|
|
38
|
+
const out = parse(await listToolsTool.invoke({}));
|
|
39
|
+
expect(out.tools.length).toBeGreaterThan(0);
|
|
40
|
+
expect(out.counts.total).toBe(out.tools.length);
|
|
41
|
+
|
|
42
|
+
// Every entry has the documented shape.
|
|
43
|
+
for (const t of out.tools) {
|
|
44
|
+
expect(typeof t.name).toBe("string");
|
|
45
|
+
expect(typeof t.description).toBe("string");
|
|
46
|
+
expect(["read", "write", "execute"]).toContain(t.capability);
|
|
47
|
+
expect(["builtin", "external", "mcp"]).toContain(t.source);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Sanity: list_tools itself is registered as builtin/Config/read.
|
|
51
|
+
const self = out.tools.find((t) => t.name === "list_tools");
|
|
52
|
+
expect(self).toBeDefined();
|
|
53
|
+
expect(self?.source).toBe("builtin");
|
|
54
|
+
expect(self?.category).toBe("Config");
|
|
55
|
+
expect(self?.capability).toBe("read");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("filters by capability", async () => {
|
|
59
|
+
const out = parse(await listToolsTool.invoke({ capability: "read" }));
|
|
60
|
+
expect(out.tools.every((t) => t.capability === "read")).toBe(true);
|
|
61
|
+
expect(out.counts.total).toBe(out.tools.length);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("filters by source", async () => {
|
|
65
|
+
const out = parse(await listToolsTool.invoke({ source: "builtin" }));
|
|
66
|
+
expect(out.tools.every((t) => t.source === "builtin")).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns empty list (not error) when filters match nothing", async () => {
|
|
70
|
+
const out = parse(await listToolsTool.invoke({ source: "external", capability: "read" }));
|
|
71
|
+
// The clean-tmpdir HOME has no JARELA_TOOLS_DIR, so external is empty
|
|
72
|
+
// regardless of capability — verifying empty-result is a normal shape.
|
|
73
|
+
expect(out.tools).toEqual([]);
|
|
74
|
+
expect(out.counts.total).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Read-only introspection tool — lets the agent enumerate every tool it has
|
|
2
|
+
// access to right now (built-in + external + MCP), so it can answer
|
|
3
|
+
// "what's in my toolbox / is X available" without the user having to
|
|
4
|
+
// describe the project's tool surface in the prompt.
|
|
5
|
+
|
|
6
|
+
import { tool } from "@langchain/core/tools";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import {
|
|
9
|
+
getAllToolsAsync,
|
|
10
|
+
getToolCategory,
|
|
11
|
+
getToolCapability,
|
|
12
|
+
getToolGroup,
|
|
13
|
+
getToolSource,
|
|
14
|
+
type ToolSource,
|
|
15
|
+
} from "./index";
|
|
16
|
+
import type { Capability, ToolCategory } from "./registry";
|
|
17
|
+
import { registerTools } from "./registry";
|
|
18
|
+
|
|
19
|
+
interface ToolSummary {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
category: ToolCategory;
|
|
23
|
+
capability: Capability;
|
|
24
|
+
source: ToolSource;
|
|
25
|
+
group: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const listToolsTool = tool(
|
|
29
|
+
async ({ category, capability, source }) => {
|
|
30
|
+
const all = await getAllToolsAsync();
|
|
31
|
+
const summaries: ToolSummary[] = all.map((t) => ({
|
|
32
|
+
name: t.name,
|
|
33
|
+
description: typeof t.description === "string" ? t.description : "",
|
|
34
|
+
category: getToolCategory(t.name),
|
|
35
|
+
capability: getToolCapability(t.name),
|
|
36
|
+
source: getToolSource(t.name),
|
|
37
|
+
group: getToolGroup(t.name),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const filtered = summaries.filter((s) =>
|
|
41
|
+
(!category || s.category === category) &&
|
|
42
|
+
(!capability || s.capability === capability) &&
|
|
43
|
+
(!source || s.source === source),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const counts = {
|
|
47
|
+
total: filtered.length,
|
|
48
|
+
by_source: { builtin: 0, external: 0, mcp: 0 } as Record<ToolSource, number>,
|
|
49
|
+
by_capability: { read: 0, write: 0, execute: 0 } as Record<Capability, number>,
|
|
50
|
+
};
|
|
51
|
+
for (const s of filtered) {
|
|
52
|
+
counts.by_source[s.source]++;
|
|
53
|
+
counts.by_capability[s.capability]++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return JSON.stringify({ tools: filtered, counts });
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "list_tools",
|
|
60
|
+
description:
|
|
61
|
+
"List every tool currently available to this agent — built-in tools, " +
|
|
62
|
+
"external (~/.jarela/providers JS plugins), and MCP server tools — with " +
|
|
63
|
+
"category, capability (read/write/execute), and source. Read-only. " +
|
|
64
|
+
"Use this when the user asks 'what can you do?', when picking between " +
|
|
65
|
+
"tools for a task, or when troubleshooting whether a specific tool is " +
|
|
66
|
+
"registered. Optional filters narrow by category, capability, or source.",
|
|
67
|
+
schema: z.object({
|
|
68
|
+
category: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Optional category filter (e.g. 'Files', 'Mail', 'GitHub', 'MCP')"),
|
|
72
|
+
capability: z
|
|
73
|
+
.enum(["read", "write", "execute"])
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("Optional capability filter — the safety class of the tool"),
|
|
76
|
+
source: z
|
|
77
|
+
.enum(["builtin", "external", "mcp"])
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("Optional source filter — where the tool came from"),
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
registerTools("Config", "read", [listToolsTool]);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-mcp-info-"));
|
|
7
|
+
process.env.HOME = tmpRoot;
|
|
8
|
+
process.env.USERPROFILE = tmpRoot;
|
|
9
|
+
process.env.JARELA_DB_DIR = join(tmpRoot, ".jarela-dbdir");
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const { listMcpServersTool } = await import("./mcp-servers-info");
|
|
15
|
+
const { upsertMcpServer } = await import("@/lib/stores/mcp-servers");
|
|
16
|
+
|
|
17
|
+
interface Result {
|
|
18
|
+
servers: Array<{
|
|
19
|
+
name: string;
|
|
20
|
+
transport: string;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
last_error: string | null;
|
|
23
|
+
tool_count: number;
|
|
24
|
+
}>;
|
|
25
|
+
count: number;
|
|
26
|
+
enabled_count: number;
|
|
27
|
+
total_mcp_tool_count: number;
|
|
28
|
+
notes: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parse(s: string): Result {
|
|
32
|
+
return JSON.parse(s) as Result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("list_mcp_servers", () => {
|
|
36
|
+
it("returns empty list with zero counts when no servers are configured", async () => {
|
|
37
|
+
const out = parse(await listMcpServersTool.invoke({}));
|
|
38
|
+
expect(out.servers).toEqual([]);
|
|
39
|
+
expect(out.count).toBe(0);
|
|
40
|
+
expect(out.enabled_count).toBe(0);
|
|
41
|
+
expect(out.notes.length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("surfaces configured servers with enabled state and transport", async () => {
|
|
45
|
+
upsertMcpServer({
|
|
46
|
+
name: "fake-stdio-server",
|
|
47
|
+
transport: "stdio",
|
|
48
|
+
spec: { command: "echo", args: ["hello"] },
|
|
49
|
+
enabled: false,
|
|
50
|
+
});
|
|
51
|
+
upsertMcpServer({
|
|
52
|
+
name: "fake-http-server",
|
|
53
|
+
transport: "http",
|
|
54
|
+
spec: { url: "http://localhost:9999" },
|
|
55
|
+
enabled: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const out = parse(await listMcpServersTool.invoke({}));
|
|
59
|
+
const names = out.servers.map((s) => s.name).sort();
|
|
60
|
+
expect(names).toEqual(["fake-http-server", "fake-stdio-server"]);
|
|
61
|
+
|
|
62
|
+
const stdioRow = out.servers.find((s) => s.name === "fake-stdio-server");
|
|
63
|
+
expect(stdioRow?.transport).toBe("stdio");
|
|
64
|
+
expect(stdioRow?.enabled).toBe(false);
|
|
65
|
+
|
|
66
|
+
const httpRow = out.servers.find((s) => s.name === "fake-http-server");
|
|
67
|
+
expect(httpRow?.transport).toBe("http");
|
|
68
|
+
expect(httpRow?.enabled).toBe(true);
|
|
69
|
+
|
|
70
|
+
expect(out.enabled_count).toBe(1);
|
|
71
|
+
expect(out.count).toBe(2);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Read-only introspection of configured MCP servers — names, transports,
|
|
2
|
+
// enabled/disabled state, last connection error, and the count of tools each
|
|
3
|
+
// server contributes to the live tool pool. Lets the agent answer "why is
|
|
4
|
+
// my Jira tool unavailable?" without bouncing the user through the UI.
|
|
5
|
+
|
|
6
|
+
import { tool } from "@langchain/core/tools";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { listMcpServers } from "@/lib/stores/mcp-servers";
|
|
9
|
+
import { getAllToolsAsync } from "./index";
|
|
10
|
+
import { getToolSource } from "./index";
|
|
11
|
+
import { registerTools } from "./registry";
|
|
12
|
+
|
|
13
|
+
interface McpServerSummary {
|
|
14
|
+
name: string;
|
|
15
|
+
transport: "stdio" | "http";
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
last_error: string | null;
|
|
18
|
+
tool_count: number;
|
|
19
|
+
created_at: string;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const listMcpServersTool = tool(
|
|
24
|
+
async () => {
|
|
25
|
+
const rows = listMcpServers();
|
|
26
|
+
const allTools = await getAllToolsAsync();
|
|
27
|
+
// MCP tools don't carry their server name as a structured field — for
|
|
28
|
+
// the per-server count we count every tool whose source is "mcp", and
|
|
29
|
+
// we surface the total separately. Per-server attribution would require
|
|
30
|
+
// a deeper hook into MultiServerMCPClient's tool-namespace metadata,
|
|
31
|
+
// which is a bigger change than this introspection step warrants today.
|
|
32
|
+
const totalMcp = allTools.filter((t) => getToolSource(t.name) === "mcp").length;
|
|
33
|
+
|
|
34
|
+
const servers: McpServerSummary[] = rows.map((r) => ({
|
|
35
|
+
name: r.name,
|
|
36
|
+
transport: r.transport,
|
|
37
|
+
enabled: r.enabled === 1,
|
|
38
|
+
last_error: r.last_error,
|
|
39
|
+
// Per-server tool count not yet attributable; surface total on every
|
|
40
|
+
// row for now and document the limitation in `notes` below.
|
|
41
|
+
tool_count: r.enabled === 1 ? totalMcp : 0,
|
|
42
|
+
created_at: r.created_at,
|
|
43
|
+
updated_at: r.updated_at,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
servers,
|
|
48
|
+
count: servers.length,
|
|
49
|
+
enabled_count: servers.filter((s) => s.enabled).length,
|
|
50
|
+
total_mcp_tool_count: totalMcp,
|
|
51
|
+
notes: [
|
|
52
|
+
"tool_count is an aggregate across all enabled MCP servers — per-server " +
|
|
53
|
+
"attribution requires deeper namespace tracking that's not implemented yet.",
|
|
54
|
+
"last_error shows the most recent connection error if any; null means " +
|
|
55
|
+
"the server has never failed since last config change.",
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "list_mcp_servers",
|
|
61
|
+
description:
|
|
62
|
+
"List every configured MCP server (stdio or http transport) with its " +
|
|
63
|
+
"enabled state, last connection error, and the aggregate count of MCP " +
|
|
64
|
+
"tools currently in the agent's pool. Read-only. Use this when " +
|
|
65
|
+
"diagnosing 'my <X> tool isn't working' — check enabled and last_error " +
|
|
66
|
+
"before assuming the tool itself is broken.",
|
|
67
|
+
schema: z.object({}),
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
registerTools("Config", "read", [listMcpServersTool]);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-providers-info-"));
|
|
7
|
+
process.env.HOME = tmpRoot;
|
|
8
|
+
process.env.USERPROFILE = tmpRoot;
|
|
9
|
+
process.env.JARELA_DB_DIR = join(tmpRoot, ".jarela-dbdir");
|
|
10
|
+
afterAll(() => {
|
|
11
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const { listProvidersTool, describeProviderTool } = await import("./providers-info");
|
|
15
|
+
|
|
16
|
+
function parse<T = unknown>(s: string): T {
|
|
17
|
+
return JSON.parse(s) as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("list_providers", () => {
|
|
21
|
+
it("returns all built-in providers with source label", async () => {
|
|
22
|
+
const out = parse<{
|
|
23
|
+
providers: Array<{ name: string; source: string }>;
|
|
24
|
+
count: number;
|
|
25
|
+
builtin_count: number;
|
|
26
|
+
external_count: number;
|
|
27
|
+
}>(await listProvidersTool.invoke({}));
|
|
28
|
+
|
|
29
|
+
const names = out.providers.map((p) => p.name);
|
|
30
|
+
expect(names).toContain("anthropic");
|
|
31
|
+
expect(names).toContain("openai");
|
|
32
|
+
expect(names).toContain("gemini");
|
|
33
|
+
expect(out.providers.every((p) => p.source === "builtin" || p.source === "external")).toBe(true);
|
|
34
|
+
expect(out.count).toBe(out.providers.length);
|
|
35
|
+
expect(out.builtin_count + out.external_count).toBe(out.count);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("describe_provider", () => {
|
|
40
|
+
it("returns capability flags and known models for anthropic", async () => {
|
|
41
|
+
const out = parse<{
|
|
42
|
+
name: string;
|
|
43
|
+
source: string;
|
|
44
|
+
capabilities: Record<string, boolean>;
|
|
45
|
+
known_models: Array<{ model_id: string; context_length: number }>;
|
|
46
|
+
}>(await describeProviderTool.invoke({ name: "anthropic" }));
|
|
47
|
+
|
|
48
|
+
expect(out.name).toBe("anthropic");
|
|
49
|
+
expect(out.source).toBe("builtin");
|
|
50
|
+
expect(out.capabilities.chat).toBe(true);
|
|
51
|
+
expect(out.capabilities.invoke).toBe(true);
|
|
52
|
+
expect(out.capabilities.stream_invoke).toBe(true);
|
|
53
|
+
// Anthropic adapter does not implement embed today.
|
|
54
|
+
expect(out.capabilities.embed).toBe(false);
|
|
55
|
+
expect(out.known_models.length).toBeGreaterThan(0);
|
|
56
|
+
expect(out.known_models.some((m) => m.model_id.startsWith("claude-"))).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns an error object (not throw) for unknown provider", async () => {
|
|
60
|
+
const out = parse<{ error?: string; hint?: string }>(
|
|
61
|
+
await describeProviderTool.invoke({ name: "nonexistent-provider-xyz" }),
|
|
62
|
+
);
|
|
63
|
+
expect(out.error).toBeDefined();
|
|
64
|
+
expect(out.hint).toMatch(/list_providers/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns an empty known_models list for providers without static catalog", async () => {
|
|
68
|
+
const out = parse<{ known_models: unknown[] }>(
|
|
69
|
+
await describeProviderTool.invoke({ name: "langchain" }),
|
|
70
|
+
);
|
|
71
|
+
expect(out.known_models).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|