@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
mock,
|
|
7
|
+
spyOn,
|
|
8
|
+
test,
|
|
9
|
+
} from "bun:test";
|
|
2
10
|
|
|
3
11
|
import { setConfig } from "../../../__tests__/helpers/set-config.js";
|
|
4
12
|
|
|
@@ -101,6 +109,32 @@ describe("executeTinyfishFetch", () => {
|
|
|
101
109
|
);
|
|
102
110
|
});
|
|
103
111
|
|
|
112
|
+
test("an empty upstream title is no title", async () => {
|
|
113
|
+
spyOn(globalThis, "fetch").mockResolvedValue(
|
|
114
|
+
fetchResponse({
|
|
115
|
+
results: [
|
|
116
|
+
{
|
|
117
|
+
url: "https://example.com/",
|
|
118
|
+
final_url: "https://example.com/",
|
|
119
|
+
title: " ",
|
|
120
|
+
text: "# Example",
|
|
121
|
+
format: "markdown",
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
errors: [],
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const result = await executeTinyfishFetch(
|
|
129
|
+
{ url: "https://example.com" },
|
|
130
|
+
{ apiKey: "tf_test" },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(result.isError).toBe(false);
|
|
134
|
+
expect(result.activityMetadata?.webFetch?.title).toBeUndefined();
|
|
135
|
+
expect(result.content).not.toContain("Title:");
|
|
136
|
+
});
|
|
137
|
+
|
|
104
138
|
test("uses a custom API base and applies character windows", async () => {
|
|
105
139
|
seedWebFetch("tinyfish", "https://fetch.example.com/api/");
|
|
106
140
|
let capturedUrl = "";
|
|
@@ -12,6 +12,7 @@ let mockKeenableSecureKey: string | undefined;
|
|
|
12
12
|
let mockFastcrwSecureKey: string | undefined;
|
|
13
13
|
let mockSearxngSecureKey: string | undefined;
|
|
14
14
|
let mockTinyfishSecureKey: string | undefined;
|
|
15
|
+
let mockExaSecureKey: string | undefined;
|
|
15
16
|
let mockManagedSearchProxyResult: any;
|
|
16
17
|
let mockManagedSearchAvailable = true;
|
|
17
18
|
let mockManagedSearchProxyCalls: Array<{
|
|
@@ -65,6 +66,9 @@ mock.module("../../../security/secure-keys.js", () => ({
|
|
|
65
66
|
if (provider === "tinyfish") {
|
|
66
67
|
return mockTinyfishSecureKey;
|
|
67
68
|
}
|
|
69
|
+
if (provider === "exa") {
|
|
70
|
+
return mockExaSecureKey;
|
|
71
|
+
}
|
|
68
72
|
return undefined;
|
|
69
73
|
},
|
|
70
74
|
}));
|
|
@@ -111,6 +115,7 @@ describe("web_search tool", () => {
|
|
|
111
115
|
mockFastcrwSecureKey = undefined;
|
|
112
116
|
mockSearxngSecureKey = undefined;
|
|
113
117
|
mockTinyfishSecureKey = undefined;
|
|
118
|
+
mockExaSecureKey = undefined;
|
|
114
119
|
mockManagedSearchProxyCalls = [];
|
|
115
120
|
mockManagedSearchAvailable = true;
|
|
116
121
|
mockManagedSearchProxyResult = {
|
|
@@ -1345,6 +1350,97 @@ describe("web_search tool", () => {
|
|
|
1345
1350
|
},
|
|
1346
1351
|
);
|
|
1347
1352
|
|
|
1353
|
+
// ---- Exa provider ------------------------------------------------------
|
|
1354
|
+
|
|
1355
|
+
test("Exa search posts the recommended request and maps freshness", async () => {
|
|
1356
|
+
seedWebSearch("your-own", "exa");
|
|
1357
|
+
mockExaSecureKey = "exa_test";
|
|
1358
|
+
let capturedUrl = "";
|
|
1359
|
+
let capturedInit: RequestInit | undefined;
|
|
1360
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
1361
|
+
capturedUrl = url;
|
|
1362
|
+
capturedInit = init;
|
|
1363
|
+
return new Response(
|
|
1364
|
+
JSON.stringify({
|
|
1365
|
+
requestId: "r1",
|
|
1366
|
+
results: [
|
|
1367
|
+
{
|
|
1368
|
+
title: "Exa Result",
|
|
1369
|
+
url: "https://example.com/exa",
|
|
1370
|
+
publishedDate: "2026-09-18T00:00:00.000Z",
|
|
1371
|
+
highlights: ["Fresh from Exa"],
|
|
1372
|
+
},
|
|
1373
|
+
],
|
|
1374
|
+
}),
|
|
1375
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1376
|
+
);
|
|
1377
|
+
}) as any;
|
|
1378
|
+
|
|
1379
|
+
const result = await execute({
|
|
1380
|
+
query: "fresh tools",
|
|
1381
|
+
freshness: "pw",
|
|
1382
|
+
count: 5,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
expect(result.isError).toBe(false);
|
|
1386
|
+
expect(capturedUrl).toBe("https://api.exa.ai/search");
|
|
1387
|
+
expect(capturedInit?.method).toBe("POST");
|
|
1388
|
+
const headers = new Headers(capturedInit?.headers);
|
|
1389
|
+
expect(headers.get("x-api-key")).toBe("exa_test");
|
|
1390
|
+
const body = JSON.parse(String(capturedInit?.body));
|
|
1391
|
+
expect(body.query).toBe("fresh tools");
|
|
1392
|
+
expect(body.type).toBe("auto");
|
|
1393
|
+
expect(body.numResults).toBe(5);
|
|
1394
|
+
expect(body.contents.highlights).toBe(true);
|
|
1395
|
+
expect(typeof body.startPublishedDate).toBe("string");
|
|
1396
|
+
const delta = Date.now() - Date.parse(body.startPublishedDate);
|
|
1397
|
+
expect(delta).toBeGreaterThan(7 * 24 * 60 * 60 * 1000 - 60_000);
|
|
1398
|
+
expect(delta).toBeLessThan(7 * 24 * 60 * 60 * 1000 + 60_000);
|
|
1399
|
+
expect(result.content).toContain("Exa Result");
|
|
1400
|
+
expect(result.content).toContain("Fresh from Exa");
|
|
1401
|
+
expect(result.activityMetadata?.webSearch?.provider).toBe("exa");
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
test("Exa omits the date filter without freshness and trims to count", async () => {
|
|
1405
|
+
seedWebSearch("your-own", "exa");
|
|
1406
|
+
mockExaSecureKey = "exa_test";
|
|
1407
|
+
let capturedInit: RequestInit | undefined;
|
|
1408
|
+
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
|
1409
|
+
capturedInit = init;
|
|
1410
|
+
return new Response(
|
|
1411
|
+
JSON.stringify({
|
|
1412
|
+
results: [
|
|
1413
|
+
{ title: "A", url: "https://a.example.com", highlights: ["a"] },
|
|
1414
|
+
{ title: "B", url: "https://b.example.com", highlights: ["b"] },
|
|
1415
|
+
{ title: "C", url: "https://c.example.com", highlights: ["c"] },
|
|
1416
|
+
],
|
|
1417
|
+
}),
|
|
1418
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
1419
|
+
);
|
|
1420
|
+
}) as any;
|
|
1421
|
+
|
|
1422
|
+
const result = await execute({ query: "test", count: 2 });
|
|
1423
|
+
|
|
1424
|
+
const body = JSON.parse(String(capturedInit?.body));
|
|
1425
|
+
expect(body.startPublishedDate).toBeUndefined();
|
|
1426
|
+
expect(result.content).toContain("A");
|
|
1427
|
+
expect(result.content).toContain("B");
|
|
1428
|
+
expect(result.content).not.toContain("https://c.example.com");
|
|
1429
|
+
expect(result.activityMetadata?.webSearch?.resultCount).toBe(2);
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
test.each([401, 402, 403])("Exa handles %d access error", async (status) => {
|
|
1433
|
+
seedWebSearch("your-own", "exa");
|
|
1434
|
+
mockExaSecureKey = "exa_test";
|
|
1435
|
+
globalThis.fetch = (async () =>
|
|
1436
|
+
new Response(JSON.stringify({ error: "denied" }), { status })) as any;
|
|
1437
|
+
|
|
1438
|
+
const result = await execute({ query: "test" });
|
|
1439
|
+
|
|
1440
|
+
expect(result.isError).toBe(true);
|
|
1441
|
+
expect(result.activityMetadata?.webSearch?.provider).toBe("exa");
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1348
1444
|
// ---- Provider fallback --------------------------------------------------
|
|
1349
1445
|
|
|
1350
1446
|
test("falls back from perplexity to brave when perplexity has no key", async () => {
|
|
@@ -58,6 +58,15 @@ const MAX_TIMEOUT_SECONDS = 60;
|
|
|
58
58
|
const DEFAULT_MAX_CHARS = 12_000;
|
|
59
59
|
const MAX_MAX_CHARS = 40_000;
|
|
60
60
|
const MAX_DOWNLOAD_BYTES = 2_000_000;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The metadata this assistant writes. `startIndexPastEnd` is optional on the
|
|
64
|
+
* wire because older records lack it; every writer here sets it, which is how
|
|
65
|
+
* a reader tells this metadata carries every reader-facing warning.
|
|
66
|
+
*/
|
|
67
|
+
type WrittenWebFetchMetadata = WebFetchMetadata & {
|
|
68
|
+
startIndexPastEnd: boolean;
|
|
69
|
+
};
|
|
61
70
|
const MAX_REDIRECTS = 10;
|
|
62
71
|
|
|
63
72
|
const TEXT_LIKE_CONTENT_TYPES = [
|
|
@@ -672,7 +681,8 @@ export async function executeWebFetch(
|
|
|
672
681
|
redirectCount: meta.redirectCount ?? 0,
|
|
673
682
|
durationMs: Date.now() - startedAt,
|
|
674
683
|
errorMessage,
|
|
675
|
-
|
|
684
|
+
startIndexPastEnd: false,
|
|
685
|
+
} satisfies WrittenWebFetchMetadata,
|
|
676
686
|
},
|
|
677
687
|
};
|
|
678
688
|
};
|
|
@@ -1023,7 +1033,7 @@ export async function executeWebFetch(
|
|
|
1023
1033
|
const truncated = body.truncated || safeEnd < processed.length;
|
|
1024
1034
|
const parsedTitle = html ? parseHtmlTitle(body.text) : undefined;
|
|
1025
1035
|
const finalDomain = extractDomain(currentUrl.href);
|
|
1026
|
-
const meta:
|
|
1036
|
+
const meta: WrittenWebFetchMetadata = {
|
|
1027
1037
|
url: safeRequestedUrl,
|
|
1028
1038
|
finalUrl: sanitizeUrlForOutput(currentUrl),
|
|
1029
1039
|
provider: "default",
|
|
@@ -1038,6 +1048,9 @@ export async function executeWebFetch(
|
|
|
1038
1048
|
redirectCount,
|
|
1039
1049
|
durationMs: Date.now() - startedAt,
|
|
1040
1050
|
mayRequireJavaScript: mayRequireJavaScript || undefined,
|
|
1051
|
+
// Past the end of the page only when the whole page was downloaded: past
|
|
1052
|
+
// a capped prefix, the page may still have content at that offset.
|
|
1053
|
+
startIndexPastEnd: !body.truncated && startIndex > processed.length,
|
|
1041
1054
|
};
|
|
1042
1055
|
|
|
1043
1056
|
if (!response.ok) {
|
|
@@ -1223,7 +1236,8 @@ function hostedScrapeErrorResult(
|
|
|
1223
1236
|
redirectCount: 0,
|
|
1224
1237
|
durationMs: Date.now() - startedAt,
|
|
1225
1238
|
errorMessage,
|
|
1226
|
-
|
|
1239
|
+
startIndexPastEnd: false,
|
|
1240
|
+
} satisfies WrittenWebFetchMetadata,
|
|
1227
1241
|
},
|
|
1228
1242
|
};
|
|
1229
1243
|
}
|
|
@@ -1394,6 +1408,8 @@ export async function executeFirecrawlCompatScrape(
|
|
|
1394
1408
|
);
|
|
1395
1409
|
}
|
|
1396
1410
|
|
|
1411
|
+
// A provider can pass an empty upstream title through; empty is no title.
|
|
1412
|
+
const title = scrapeMeta.title?.trim() || undefined;
|
|
1397
1413
|
const content = formatWebFetchOutput({
|
|
1398
1414
|
requestedUrl: safeRequestedUrl,
|
|
1399
1415
|
finalUrl,
|
|
@@ -1405,7 +1421,7 @@ export async function executeFirecrawlCompatScrape(
|
|
|
1405
1421
|
startIndex: safeStart,
|
|
1406
1422
|
endIndex: safeEnd,
|
|
1407
1423
|
content: sliced,
|
|
1408
|
-
title
|
|
1424
|
+
title,
|
|
1409
1425
|
description: scrapeMeta.description,
|
|
1410
1426
|
notices,
|
|
1411
1427
|
raw: false,
|
|
@@ -1413,7 +1429,7 @@ export async function executeFirecrawlCompatScrape(
|
|
|
1413
1429
|
});
|
|
1414
1430
|
|
|
1415
1431
|
const finalDomain = extractDomain(finalUrl);
|
|
1416
|
-
const meta:
|
|
1432
|
+
const meta: WrittenWebFetchMetadata = {
|
|
1417
1433
|
url: safeRequestedUrl,
|
|
1418
1434
|
finalUrl,
|
|
1419
1435
|
provider: options.provider,
|
|
@@ -1422,11 +1438,13 @@ export async function executeFirecrawlCompatScrape(
|
|
|
1422
1438
|
byteCount: bytesRead,
|
|
1423
1439
|
charCount: sliced.length,
|
|
1424
1440
|
truncated: safeEnd < processed.length,
|
|
1425
|
-
title
|
|
1441
|
+
title,
|
|
1426
1442
|
domain: finalDomain,
|
|
1427
1443
|
faviconUrl: faviconUrlForDomain(finalDomain),
|
|
1428
1444
|
redirectCount: 0,
|
|
1429
1445
|
durationMs: Date.now() - startedAt,
|
|
1446
|
+
startIndexPastEnd: startIndex > processed.length,
|
|
1447
|
+
providerWarning: warning || undefined,
|
|
1430
1448
|
};
|
|
1431
1449
|
|
|
1432
1450
|
return {
|
|
@@ -1670,6 +1688,8 @@ export async function executeTinyfishFetch(
|
|
|
1670
1688
|
);
|
|
1671
1689
|
}
|
|
1672
1690
|
|
|
1691
|
+
// A provider can pass an empty upstream title through; empty is no title.
|
|
1692
|
+
const title = page.title?.trim() || undefined;
|
|
1673
1693
|
const content = formatWebFetchOutput({
|
|
1674
1694
|
requestedUrl: safeRequestedUrl,
|
|
1675
1695
|
finalUrl,
|
|
@@ -1681,14 +1701,14 @@ export async function executeTinyfishFetch(
|
|
|
1681
1701
|
startIndex: safeStart,
|
|
1682
1702
|
endIndex: safeEnd,
|
|
1683
1703
|
content: sliced,
|
|
1684
|
-
title
|
|
1704
|
+
title,
|
|
1685
1705
|
description: page.description ?? undefined,
|
|
1686
1706
|
notices,
|
|
1687
1707
|
raw: false,
|
|
1688
1708
|
markdown: true,
|
|
1689
1709
|
});
|
|
1690
1710
|
const finalDomain = extractDomain(finalUrl);
|
|
1691
|
-
const metadata:
|
|
1711
|
+
const metadata: WrittenWebFetchMetadata = {
|
|
1692
1712
|
url: safeRequestedUrl,
|
|
1693
1713
|
finalUrl,
|
|
1694
1714
|
provider: "tinyfish",
|
|
@@ -1697,11 +1717,12 @@ export async function executeTinyfishFetch(
|
|
|
1697
1717
|
byteCount: bytesRead,
|
|
1698
1718
|
charCount: sliced.length,
|
|
1699
1719
|
truncated: safeEnd < processed.length,
|
|
1700
|
-
title
|
|
1720
|
+
title,
|
|
1701
1721
|
domain: finalDomain,
|
|
1702
1722
|
faviconUrl: faviconUrlForDomain(finalDomain),
|
|
1703
1723
|
redirectCount: finalUrl === safeRequestedUrl ? 0 : 1,
|
|
1704
1724
|
durationMs: Date.now() - startedAt,
|
|
1725
|
+
startIndexPastEnd: startIndex > processed.length,
|
|
1705
1726
|
};
|
|
1706
1727
|
return {
|
|
1707
1728
|
content,
|
|
@@ -45,6 +45,7 @@ const TAVILY_API_URL = "https://api.tavily.com/search";
|
|
|
45
45
|
const FIRECRAWL_API_URL = "https://api.firecrawl.dev/v2/search";
|
|
46
46
|
const FASTCRW_SEARCH_PATH = "/v1/search";
|
|
47
47
|
const TINYFISH_DEFAULT_SEARCH_API_BASE = "https://api.search.tinyfish.ai";
|
|
48
|
+
const EXA_API_URL = "https://api.exa.ai/search";
|
|
48
49
|
// Keenable is keyless by default: the public path needs no key (rate-limited);
|
|
49
50
|
// a key switches to the authenticated path and lifts the cap.
|
|
50
51
|
const KEENABLE_API_BASE_URL = "https://api.keenable.ai";
|
|
@@ -59,7 +60,8 @@ type WebSearchProvider =
|
|
|
59
60
|
| "keenable"
|
|
60
61
|
| "fastcrw"
|
|
61
62
|
| "searxng"
|
|
62
|
-
| "tinyfish"
|
|
63
|
+
| "tinyfish"
|
|
64
|
+
| "exa";
|
|
63
65
|
|
|
64
66
|
/**
|
|
65
67
|
* Arguments passed to every {@link WebSearchAdapter}. The full superset is
|
|
@@ -197,6 +199,20 @@ interface TinyfishSearchResponse {
|
|
|
197
199
|
page?: number;
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
interface ExaSearchResult {
|
|
203
|
+
id?: string;
|
|
204
|
+
title?: string | null;
|
|
205
|
+
url?: string;
|
|
206
|
+
publishedDate?: string | null;
|
|
207
|
+
author?: string | null;
|
|
208
|
+
highlights?: string[];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface ExaSearchResponse {
|
|
212
|
+
requestId?: string;
|
|
213
|
+
results?: ExaSearchResult[];
|
|
214
|
+
}
|
|
215
|
+
|
|
200
216
|
const SEARXNG_MISSING_INSTANCE_MESSAGE =
|
|
201
217
|
"SearXNG needs an instance URL. Set API Base in Settings under Web Search.";
|
|
202
218
|
const SEARXNG_JSON_DISABLED_MESSAGE =
|
|
@@ -716,6 +732,84 @@ function buildTinyfishMetadata(
|
|
|
716
732
|
};
|
|
717
733
|
}
|
|
718
734
|
|
|
735
|
+
function formatExaResults(data: ExaSearchResponse, query: string): string {
|
|
736
|
+
const results = data.results ?? [];
|
|
737
|
+
if (results.length === 0) {
|
|
738
|
+
return `No results found for "${query}".`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const lines: string[] = [`Web search results for "${query}":\n`];
|
|
742
|
+
for (let i = 0; i < results.length; i++) {
|
|
743
|
+
const result = results[i];
|
|
744
|
+
const title =
|
|
745
|
+
result.title?.trim() || result.url?.trim() || "Untitled result";
|
|
746
|
+
lines.push(`${i + 1}. ${title}`);
|
|
747
|
+
if (result.url) {
|
|
748
|
+
lines.push(` URL: ${result.url}`);
|
|
749
|
+
}
|
|
750
|
+
const snippet = result.highlights?.join(" ").trim();
|
|
751
|
+
if (snippet) {
|
|
752
|
+
lines.push(` ${snippet}`);
|
|
753
|
+
}
|
|
754
|
+
if (result.publishedDate) {
|
|
755
|
+
lines.push(` Published: ${result.publishedDate}`);
|
|
756
|
+
}
|
|
757
|
+
lines.push("");
|
|
758
|
+
}
|
|
759
|
+
return lines.join("\n");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function buildExaMetadata(
|
|
763
|
+
data: ExaSearchResponse,
|
|
764
|
+
query: string,
|
|
765
|
+
durationMs: number,
|
|
766
|
+
): WebSearchMetadata {
|
|
767
|
+
const results = data.results ?? [];
|
|
768
|
+
const items: WebSearchResultItem[] = results.map((result, index) => {
|
|
769
|
+
const url = result.url ?? "";
|
|
770
|
+
const domain = extractDomain(url);
|
|
771
|
+
return {
|
|
772
|
+
rank: index + 1,
|
|
773
|
+
title: result.title?.trim() || url.trim() || "Untitled result",
|
|
774
|
+
url,
|
|
775
|
+
domain,
|
|
776
|
+
faviconUrl: faviconUrlForDomain(domain),
|
|
777
|
+
snippet: result.highlights?.join(" ").trim() || undefined,
|
|
778
|
+
};
|
|
779
|
+
});
|
|
780
|
+
return {
|
|
781
|
+
query,
|
|
782
|
+
provider: "exa",
|
|
783
|
+
resultCount: items.length,
|
|
784
|
+
durationMs,
|
|
785
|
+
results: items,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function exaStartPublishedDateForFreshness(
|
|
790
|
+
freshness: string | undefined,
|
|
791
|
+
now: number = Date.now(),
|
|
792
|
+
): string | undefined {
|
|
793
|
+
let days: number;
|
|
794
|
+
switch (freshness) {
|
|
795
|
+
case "pd":
|
|
796
|
+
days = 1;
|
|
797
|
+
break;
|
|
798
|
+
case "pw":
|
|
799
|
+
days = 7;
|
|
800
|
+
break;
|
|
801
|
+
case "pm":
|
|
802
|
+
days = 30;
|
|
803
|
+
break;
|
|
804
|
+
case "py":
|
|
805
|
+
days = 365;
|
|
806
|
+
break;
|
|
807
|
+
default:
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
return new Date(now - days * 24 * 60 * 60 * 1000).toISOString();
|
|
811
|
+
}
|
|
812
|
+
|
|
719
813
|
function tinyfishRecencyMinutesForFreshness(
|
|
720
814
|
freshness: string | undefined,
|
|
721
815
|
): number | undefined {
|
|
@@ -1801,6 +1895,134 @@ async function executeTinyfishSearch(
|
|
|
1801
1895
|
);
|
|
1802
1896
|
}
|
|
1803
1897
|
|
|
1898
|
+
async function executeExaSearch(
|
|
1899
|
+
query: string,
|
|
1900
|
+
count: number,
|
|
1901
|
+
freshness: string | undefined,
|
|
1902
|
+
apiKey: string,
|
|
1903
|
+
signal?: AbortSignal,
|
|
1904
|
+
): Promise<ToolExecutionResult> {
|
|
1905
|
+
const startedAt = Date.now();
|
|
1906
|
+
const requestBody: Record<string, unknown> = {
|
|
1907
|
+
query,
|
|
1908
|
+
type: "auto",
|
|
1909
|
+
numResults: count,
|
|
1910
|
+
contents: { highlights: true },
|
|
1911
|
+
};
|
|
1912
|
+
const startPublishedDate = exaStartPublishedDateForFreshness(freshness);
|
|
1913
|
+
if (startPublishedDate !== undefined) {
|
|
1914
|
+
requestBody.startPublishedDate = startPublishedDate;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
const headers = {
|
|
1918
|
+
"Content-Type": "application/json",
|
|
1919
|
+
Accept: "application/json",
|
|
1920
|
+
"x-api-key": apiKey.trim(),
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
for (let attempt = 0; attempt <= DEFAULT_MAX_RETRIES; attempt++) {
|
|
1924
|
+
let response: Response;
|
|
1925
|
+
try {
|
|
1926
|
+
response = await fetch(EXA_API_URL, {
|
|
1927
|
+
method: "POST",
|
|
1928
|
+
headers,
|
|
1929
|
+
body: JSON.stringify(requestBody),
|
|
1930
|
+
signal,
|
|
1931
|
+
});
|
|
1932
|
+
} catch (err) {
|
|
1933
|
+
return networkFailureResult(query, "exa", startedAt, err, signal);
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const bodyText = await response.text();
|
|
1937
|
+
if (response.ok) {
|
|
1938
|
+
let data: ExaSearchResponse;
|
|
1939
|
+
try {
|
|
1940
|
+
data = JSON.parse(bodyText) as ExaSearchResponse;
|
|
1941
|
+
} catch {
|
|
1942
|
+
return errorResult(
|
|
1943
|
+
query,
|
|
1944
|
+
"exa",
|
|
1945
|
+
startedAt,
|
|
1946
|
+
"Exa Search returned an invalid JSON payload.",
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
if (data.results && data.results.length > count) {
|
|
1950
|
+
data.results = data.results.slice(0, count);
|
|
1951
|
+
}
|
|
1952
|
+
const durationMs = Date.now() - startedAt;
|
|
1953
|
+
return {
|
|
1954
|
+
content:
|
|
1955
|
+
wrapUntrustedContent(formatExaResults(data, query), {
|
|
1956
|
+
source: "search",
|
|
1957
|
+
sourceDetail: "exa",
|
|
1958
|
+
}) + CITATION_INSTRUCTION,
|
|
1959
|
+
isError: false,
|
|
1960
|
+
activityMetadata: {
|
|
1961
|
+
webSearch: buildExaMetadata(data, query, durationMs),
|
|
1962
|
+
},
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
if (response.status === 401) {
|
|
1967
|
+
return errorResult(
|
|
1968
|
+
query,
|
|
1969
|
+
"exa",
|
|
1970
|
+
startedAt,
|
|
1971
|
+
"Invalid or expired Exa API key",
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
if (response.status === 402) {
|
|
1975
|
+
return errorResult(
|
|
1976
|
+
query,
|
|
1977
|
+
"exa",
|
|
1978
|
+
startedAt,
|
|
1979
|
+
"Exa account has no remaining credits.",
|
|
1980
|
+
);
|
|
1981
|
+
}
|
|
1982
|
+
if (response.status === 403) {
|
|
1983
|
+
return errorResult(
|
|
1984
|
+
query,
|
|
1985
|
+
"exa",
|
|
1986
|
+
startedAt,
|
|
1987
|
+
"Exa Search request was forbidden by the upstream service.",
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
if (response.status === 429 && attempt < DEFAULT_MAX_RETRIES) {
|
|
1992
|
+
const delayMs = getHttpRetryDelay(
|
|
1993
|
+
response,
|
|
1994
|
+
attempt,
|
|
1995
|
+
DEFAULT_BASE_DELAY_MS,
|
|
1996
|
+
);
|
|
1997
|
+
log.warn(
|
|
1998
|
+
{ attempt: attempt + 1, delayMs },
|
|
1999
|
+
"Exa Search rate limited, retrying",
|
|
2000
|
+
);
|
|
2001
|
+
await sleep(delayMs);
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
log.warn({ status: response.status }, "Exa Search API error");
|
|
2006
|
+
return backendFailureResult(
|
|
2007
|
+
query,
|
|
2008
|
+
"exa",
|
|
2009
|
+
startedAt,
|
|
2010
|
+
{ statusCode: response.status, error: rawBodyDetail(bodyText) },
|
|
2011
|
+
response.status === 429
|
|
2012
|
+
? "Exa Search rate limit exceeded after retries. Try again shortly."
|
|
2013
|
+
: `Exa Search API returned status ${response.status}`,
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
return backendFailureResult(
|
|
2018
|
+
query,
|
|
2019
|
+
"exa",
|
|
2020
|
+
startedAt,
|
|
2021
|
+
{ statusCode: 429 },
|
|
2022
|
+
"Exa Search rate limit exceeded after retries. Try again shortly.",
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1804
2026
|
// ----------------------------------------------------------------------------
|
|
1805
2027
|
// Adapter registry
|
|
1806
2028
|
//
|
|
@@ -1884,6 +2106,14 @@ const tinyfishSearchAdapter: WebSearchAdapter = {
|
|
|
1884
2106
|
executeTinyfishSearch(query, count, freshness, apiKey, signal),
|
|
1885
2107
|
};
|
|
1886
2108
|
|
|
2109
|
+
const exaSearchAdapter: WebSearchAdapter = {
|
|
2110
|
+
id: "exa",
|
|
2111
|
+
providerKeyName: "exa",
|
|
2112
|
+
fallbackOrder: 9,
|
|
2113
|
+
execute: ({ query, count, freshness, apiKey, signal }) =>
|
|
2114
|
+
executeExaSearch(query, count, freshness, apiKey, signal),
|
|
2115
|
+
};
|
|
2116
|
+
|
|
1887
2117
|
/**
|
|
1888
2118
|
* All built-in web-search adapters keyed by provider id. The
|
|
1889
2119
|
* `Record<WebSearchProvider, ...>` shape forces TypeScript to flag any
|
|
@@ -1898,6 +2128,7 @@ const WEB_SEARCH_ADAPTERS: Record<WebSearchProvider, WebSearchAdapter> = {
|
|
|
1898
2128
|
fastcrw: fastcrwSearchAdapter,
|
|
1899
2129
|
searxng: searxngSearchAdapter,
|
|
1900
2130
|
tinyfish: tinyfishSearchAdapter,
|
|
2131
|
+
exa: exaSearchAdapter,
|
|
1901
2132
|
};
|
|
1902
2133
|
|
|
1903
2134
|
/**
|
|
@@ -1929,7 +2160,7 @@ export const webSearchTool = {
|
|
|
1929
2160
|
count: {
|
|
1930
2161
|
type: "number",
|
|
1931
2162
|
description:
|
|
1932
|
-
"Number of results to return (1-20, default 10). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, and
|
|
2163
|
+
"Number of results to return (1-20, default 10). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, TinyFish, and Exa providers.",
|
|
1933
2164
|
},
|
|
1934
2165
|
offset: {
|
|
1935
2166
|
type: "number",
|
|
@@ -1939,7 +2170,7 @@ export const webSearchTool = {
|
|
|
1939
2170
|
freshness: {
|
|
1940
2171
|
type: "string",
|
|
1941
2172
|
description:
|
|
1942
|
-
'Filter by recency: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, and
|
|
2173
|
+
'Filter by recency: "pd" (past day), "pw" (past week), "pm" (past month), "py" (past year). Used with Brave, Tavily, Firecrawl, Keenable, fastCRW, SearXNG, TinyFish, and Exa providers. SearXNG maps day/month/year and omits week.',
|
|
1943
2174
|
},
|
|
1944
2175
|
},
|
|
1945
2176
|
required: ["query"],
|
|
@@ -2055,7 +2286,7 @@ export const webSearchTool = {
|
|
|
2055
2286
|
query,
|
|
2056
2287
|
provider,
|
|
2057
2288
|
startedAt,
|
|
2058
|
-
"No web search API key configured. Set it via `keys set perplexity <key>`, `keys set brave <key>`, `keys set tavily <key>`, `keys set firecrawl <key>`, or `keys set fastcrw <key>`, or `keys set tinyfish <key>`, or configure it from the Settings page under API Keys. Or switch the web-search provider to Keenable, which works without a key, or SearXNG with your instance URL.",
|
|
2289
|
+
"No web search API key configured. Set it via `keys set perplexity <key>`, `keys set brave <key>`, `keys set tavily <key>`, `keys set firecrawl <key>`, or `keys set fastcrw <key>`, or `keys set tinyfish <key>`, or `keys set exa <key>`, or configure it from the Settings page under API Keys. Or switch the web-search provider to Keenable, which works without a key, or SearXNG with your instance URL.",
|
|
2059
2290
|
);
|
|
2060
2291
|
}
|
|
2061
2292
|
}
|
|
@@ -178,7 +178,7 @@ export function augmentSkillExecuteError(
|
|
|
178
178
|
export const skillExecuteTool = {
|
|
179
179
|
name: "skill_execute",
|
|
180
180
|
description:
|
|
181
|
-
"Execute a tool provided by a loaded skill.
|
|
181
|
+
"Execute a tool provided by a loaded skill. When skill_load is available, call it for the owning skill before first use, even if a capability card says it is available. Use the current instructions and input schema over remembered arguments. Use this instead of calling skill tools directly. For browser automation, use the `assistant browser` CLI commands instead.",
|
|
182
182
|
category: "skills",
|
|
183
183
|
executionTarget: "sandbox",
|
|
184
184
|
defaultRiskLevel: RiskLevel.Low,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getWorkspaceDirDisplay } from "../../util/platform.js";
|
|
2
|
+
|
|
3
|
+
/** Preserve nested fields and alternatives when disclosing a skill's input. */
|
|
4
|
+
export function formatSkillInputSchema(schema: object): string {
|
|
5
|
+
const workspaceDir = getWorkspaceDirDisplay();
|
|
6
|
+
const json = JSON.stringify(
|
|
7
|
+
schema,
|
|
8
|
+
(_key, value: unknown) =>
|
|
9
|
+
typeof value === "string"
|
|
10
|
+
? value.replaceAll("{workspaceDir}", workspaceDir)
|
|
11
|
+
: value,
|
|
12
|
+
2,
|
|
13
|
+
);
|
|
14
|
+
return `Input schema for \`skill_execute.input\`:\n\`\`\`json\n${json}\n\`\`\``;
|
|
15
|
+
}
|