@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c
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/docs/architecture/memory.md +9 -1
- package/docs/architecture/scheduling.md +13 -5
- package/docs/desktop-browser-cli.md +3 -2
- package/docs/guardian-request-flow.md +1 -1
- 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/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/openapi.yaml +152 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +98 -1
- package/src/__tests__/background-tool-registry.test.ts +39 -0
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
- package/src/__tests__/conversation-agent-loop.test.ts +529 -1
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-pairing.test.ts +69 -0
- package/src/__tests__/conversation-queue.test.ts +257 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
- package/src/__tests__/gemini-provider.test.ts +9 -0
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/introduction-card-resolver.test.ts +53 -8
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/non-member-access-request.test.ts +48 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
- package/src/__tests__/notification-platform-adapter.test.ts +97 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
- package/src/__tests__/scheduler-result-notification.test.ts +521 -7
- package/src/__tests__/scheduler-wake.test.ts +1 -0
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
- package/src/__tests__/subagent-manager-notify.test.ts +48 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
- package/src/agent/loop.ts +23 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/generation-handoff.ts +4 -0
- package/src/api/events/message-complete.ts +4 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +11 -0
- package/src/api/requests/guardian-actions.test.ts +45 -0
- package/src/api/requests/guardian-actions.ts +20 -0
- package/src/api/responses/conversation-message.ts +4 -0
- package/src/api/responses/home.ts +27 -0
- package/src/approvals/guardian-decision-primitive.ts +6 -4
- package/src/approvals/guardian-request-resolvers.ts +35 -30
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +12 -4
- package/src/calls/voice-escalation-judge.ts +2 -2
- package/src/calls/voice-leg-coordinator.ts +23 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +63 -20
- package/src/calls/voice-triage-escalate.ts +82 -27
- package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
- package/src/cli/commands/conversations.help.ts +7 -2
- package/src/cli/commands/conversations.ts +36 -30
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/bundled-marketplace.json +35 -0
- package/src/cli/lib/bundled-plugin-packages.json +21 -0
- package/src/cli/lib/plugin-catalog-cache.ts +17 -2
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/profile-order.test.ts +19 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
- package/src/config/call-site-defaults.ts +6 -0
- package/src/config/default-profile-catalog.ts +30 -1
- package/src/config/default-profile-names.ts +36 -2
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-order.ts +7 -1
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +21 -5
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
- package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
- package/src/daemon/activity-trail.test.ts +206 -0
- package/src/daemon/activity-trail.ts +190 -0
- package/src/daemon/auto-profile-router.ts +431 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +157 -32
- package/src/daemon/conversation-error.ts +19 -0
- package/src/daemon/conversation-lifecycle.ts +70 -7
- package/src/daemon/conversation-media-retry.ts +1 -2
- package/src/daemon/conversation-messaging.ts +31 -2
- package/src/daemon/conversation-process.ts +107 -16
- package/src/daemon/conversation-queue-manager.ts +7 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/conversation-turn-finalize.ts +14 -0
- package/src/daemon/conversation.ts +25 -6
- package/src/daemon/event-loop-watchdog.test.ts +201 -2
- package/src/daemon/event-loop-watchdog.ts +182 -6
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/wake-conversation-ops.ts +4 -2
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- 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 +25 -19
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/discord/withdraw.ts +4 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/messaging/providers/slack/withdraw.ts +5 -5
- package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
- package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
- package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
- package/src/monitoring/__tests__/process-usage.test.ts +243 -0
- package/src/monitoring/proc-wait-state.ts +366 -0
- package/src/monitoring/process-memory.ts +4 -3
- package/src/monitoring/process-usage.ts +215 -0
- package/src/monitoring/resource-sample-types.ts +8 -0
- package/src/monitoring/resource-sampler.ts +17 -2
- package/src/monitoring/stall-capture.ts +12 -0
- package/src/notifications/README.md +88 -9
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
- package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
- package/src/notifications/__tests__/broadcaster.test.ts +292 -1
- package/src/notifications/__tests__/completion-policy.test.ts +243 -0
- package/src/notifications/__tests__/decision-engine.test.ts +51 -4
- package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
- package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
- package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
- package/src/notifications/adapters/macos.ts +24 -12
- package/src/notifications/adapters/platform.ts +16 -5
- package/src/notifications/assistant-reply-producer.ts +96 -71
- package/src/notifications/background-result-producer.ts +369 -0
- package/src/notifications/broadcaster.ts +28 -8
- package/src/notifications/completion-policy.ts +115 -0
- package/src/notifications/completion-work.ts +46 -0
- package/src/notifications/conversation-pairing.ts +32 -13
- package/src/notifications/decision-engine.ts +18 -24
- package/src/notifications/emit-signal.ts +16 -1
- package/src/notifications/events-store.ts +4 -0
- package/src/notifications/has-pending-background-work.ts +91 -0
- package/src/notifications/home-feed-mirror.ts +44 -0
- package/src/notifications/home-feed-side-effect.ts +54 -28
- package/src/notifications/resolve-visible-in-source.ts +56 -2
- package/src/notifications/result-output.ts +154 -0
- package/src/notifications/schedule-result-producer.ts +6 -187
- package/src/notifications/types.ts +2 -0
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
- package/src/persistence/conversation-crud.ts +26 -0
- package/src/persistence/conversation-types.ts +15 -37
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
- 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/plugins/defaults/memory/AGENTS.md +5 -3
- 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/__tests__/turn-retrieval.test.ts +21 -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/hooks/user-prompt-submit.ts +7 -3
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/injector.ts +4 -4
- package/src/plugins/types.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +4 -0
- package/src/providers/gemini/client.ts +7 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/inference/profile-probe.ts +1 -0
- package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
- package/src/providers/openai/api-error-normalization.ts +4 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +7 -0
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/__tests__/agent-wake.test.ts +308 -0
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake-queue.ts +52 -0
- package/src/runtime/agent-wake.ts +111 -18
- package/src/runtime/channel-approval-types.ts +6 -38
- package/src/runtime/channel-approvals.ts +4 -5
- package/src/runtime/http-server.ts +4 -1
- package/src/runtime/introduction-policy.test.ts +17 -1
- package/src/runtime/introduction-policy.ts +25 -7
- package/src/runtime/migrations/vbundle-builder.ts +9 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/auto-profile-routes.ts +83 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -3
- package/src/runtime/routes/conversation-routes.ts +9 -0
- package/src/runtime/routes/guardian-action-routes.ts +8 -23
- package/src/runtime/routes/host-cu-routes.ts +3 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +5 -2
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/migration-routes.ts +8 -2
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/user-route-dispatcher.ts +1 -2
- package/src/runtime/web-presence.ts +3 -4
- package/src/schedule/schedule-store.ts +10 -0
- package/src/schedule/scheduler.ts +92 -24
- package/src/subagent/manager.ts +32 -12
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/tools/background-tool-registry.ts +30 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +7 -0
- 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/load.ts +13 -17
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/terminal/shell-launch.test.ts +27 -1
- package/src/tools/terminal/shell.ts +4 -0
- package/src/tools/workspace-tools/loader.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/abort-reasons.ts +2 -0
- package/src/util/errors.ts +1 -0
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/provider-error-patterns.ts +11 -0
- package/src/util/single-flight.test.ts +6 -0
- package/src/util/single-flight.ts +2 -0
- package/src/util/worker-pipe.test.ts +56 -10
- package/src/util/worker-pipe.ts +9 -4
- package/src/workspace/git-service.ts +6 -1
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -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
|
}
|
package/src/tools/skills/load.ts
CHANGED
|
@@ -157,6 +157,7 @@ export const skillLoadTool = {
|
|
|
157
157
|
async execute(
|
|
158
158
|
input: Record<string, unknown>,
|
|
159
159
|
context: ToolContext,
|
|
160
|
+
options?: { readOnly?: boolean },
|
|
160
161
|
): Promise<ToolExecutionResult> {
|
|
161
162
|
const selector = input.skill;
|
|
162
163
|
if (typeof selector !== "string" || selector.trim().length === 0) {
|
|
@@ -171,13 +172,17 @@ export const skillLoadTool = {
|
|
|
171
172
|
// command execution. Instructions are still returned so the assistant can
|
|
172
173
|
// load the system-storage-cleanup skill (and any already-installed skill).
|
|
173
174
|
const cleanupMode = context.diskPressureCleanupModeActive === true;
|
|
175
|
+
const readOnly = cleanupMode || options?.readOnly === true;
|
|
176
|
+
const inlineCommandStub = cleanupMode
|
|
177
|
+
? INLINE_COMMAND_CLEANUP_STUB
|
|
178
|
+
: "[inline command not executed]";
|
|
174
179
|
|
|
175
180
|
let loaded = loadSkillBySelector(selector);
|
|
176
181
|
|
|
177
182
|
// Auto-install from catalog if the skill isn't found locally
|
|
178
183
|
if (
|
|
179
184
|
!loaded.skill &&
|
|
180
|
-
!
|
|
185
|
+
!readOnly &&
|
|
181
186
|
(loaded.errorCode === "not_found" || loaded.errorCode === "empty_catalog")
|
|
182
187
|
) {
|
|
183
188
|
try {
|
|
@@ -273,9 +278,8 @@ export const skillLoadTool = {
|
|
|
273
278
|
break;
|
|
274
279
|
}
|
|
275
280
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
if (cleanupMode) {
|
|
281
|
+
// Read-only loads leave missing includes advisory ("not loaded").
|
|
282
|
+
if (readOnly) {
|
|
279
283
|
break;
|
|
280
284
|
}
|
|
281
285
|
|
|
@@ -349,17 +353,11 @@ export const skillLoadTool = {
|
|
|
349
353
|
const hasInlineCommands =
|
|
350
354
|
skill.inlineCommandExpansions && skill.inlineCommandExpansions.length > 0;
|
|
351
355
|
|
|
352
|
-
if (hasInlineCommands &&
|
|
353
|
-
|
|
354
|
-
// Strip inline command tokens instead of rendering them; the rest of the
|
|
355
|
-
// instructions are still returned.
|
|
356
|
-
body = body.replace(
|
|
357
|
-
INLINE_COMMAND_TOKEN_PATTERN,
|
|
358
|
-
INLINE_COMMAND_CLEANUP_STUB,
|
|
359
|
-
);
|
|
356
|
+
if (hasInlineCommands && readOnly) {
|
|
357
|
+
body = body.replace(INLINE_COMMAND_TOKEN_PATTERN, inlineCommandStub);
|
|
360
358
|
log.info(
|
|
361
359
|
{ skillId: skill.id },
|
|
362
|
-
"Skipped inline command expansion during
|
|
360
|
+
"Skipped inline command expansion during read-only skill load",
|
|
363
361
|
);
|
|
364
362
|
} else if (hasInlineCommands) {
|
|
365
363
|
if (skill.source === "extra" || skill.source === "plugin") {
|
|
@@ -451,12 +449,10 @@ export const skillLoadTool = {
|
|
|
451
449
|
childLoaded.skill.inlineCommandExpansions &&
|
|
452
450
|
childLoaded.skill.inlineCommandExpansions.length > 0;
|
|
453
451
|
|
|
454
|
-
if (childHasInlineCommands &&
|
|
455
|
-
// No shell execution under the disk-pressure lock — strip the
|
|
456
|
-
// child's inline command tokens rather than rendering them.
|
|
452
|
+
if (childHasInlineCommands && readOnly) {
|
|
457
453
|
childBody = childBody.replace(
|
|
458
454
|
INLINE_COMMAND_TOKEN_PATTERN,
|
|
459
|
-
|
|
455
|
+
inlineCommandStub,
|
|
460
456
|
);
|
|
461
457
|
} else if (childHasInlineCommands) {
|
|
462
458
|
if (
|