crewly 1.11.5 → 1.12.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/config/skills/agent/onboarding/synthesize-hierarchy/SKILL.md +65 -0
- package/config/skills/agent/onboarding/synthesize-hierarchy/execute.sh +61 -0
- package/config/skills/agent/web-search/SKILL.md +70 -0
- package/config/skills/agent/web-search/execute.sh +170 -0
- package/config/skills/agent/web-search/skill.json +23 -0
- package/dist/backend/backend/src/constants.d.ts +34 -1
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +34 -1
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts +22 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js +58 -0
- package/dist/backend/backend/src/controllers/cloud/cloud.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js +3 -1
- package/dist/backend/backend/src/controllers/cloud/cloud.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts +27 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js +108 -0
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts +6 -2
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js +9 -3
- package/dist/backend/backend/src/controllers/orchestrator-onboarding/orchestrator-onboarding.routes.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +36 -2
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts +18 -0
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js +24 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/crewly-agent-external-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/backend/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/backend/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/backend/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/backup/backup.types.js +13 -0
- package/dist/backend/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts +29 -2
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js +97 -13
- package/dist/backend/backend/src/services/cloud/cloud-sync.service.js.map +1 -1
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts +102 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js +164 -0
- package/dist/backend/backend/src/services/cloud/mobile-api-relay.service.js.map +1 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts +21 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/fission/fission-guard.service.js +30 -0
- package/dist/backend/backend/src/services/fission/fission-guard.service.js.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts +4 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js +8 -0
- package/dist/backend/backend/src/services/intent-task/intent-classifier.rules.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts +79 -58
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js +140 -65
- package/dist/backend/backend/src/services/orchestrator/onboarding/materialize-team.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts +117 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.d.ts.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js +189 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding/synthesize-hierarchy.js.map +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js +1 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode-loader.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js +2 -0
- package/dist/backend/backend/src/services/orchestrator/onboarding-mode.skill-allowlist.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js +17 -1
- package/dist/backend/backend/src/services/orchestrator/prompts/onboarding-mode.prompt.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts +50 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js +71 -0
- package/dist/backend/backend/src/services/reconciler/reconcile-rules.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts +18 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +75 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts +115 -0
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js +189 -3
- package/dist/backend/backend/src/services/session/pty/pty-session-backend.js.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts +28 -0
- package/dist/backend/backend/src/services/session/pty/pty-session.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js +61 -1
- package/dist/backend/backend/src/services/session/pty/pty-session.js.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/template/template.service.js +67 -2
- package/dist/backend/backend/src/services/template/template.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts +19 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/cascade-request-status.js +39 -2
- package/dist/backend/backend/src/services/v3/cascade-request-status.js.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts +41 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/escalation-router.service.js +169 -0
- package/dist/backend/backend/src/services/v3/escalation-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts +4 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js +21 -0
- package/dist/backend/backend/src/services/v3/request-cascade.subscriber.js.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/intent-task.types.js +8 -0
- package/dist/backend/backend/src/types/intent-task.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts +1 -1
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js +1 -0
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +34 -1
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +34 -1
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts +70 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.d.ts.map +1 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js +427 -0
- package/dist/cli/backend/src/controllers/cloud/cloud-google-auth.controller.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +90 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js +309 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts +75 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js +134 -0
- package/dist/cli/backend/src/services/backup/backup-cloud.client.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts +78 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js +358 -0
- package/dist/cli/backend/src/services/backup/backup-restore.service.js.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts +163 -0
- package/dist/cli/backend/src/services/backup/backup.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/backup/backup.types.js +13 -0
- package/dist/cli/backend/src/services/backup/backup.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts +410 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js +863 -0
- package/dist/cli/backend/src/services/cloud/cloud-client.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts +292 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js +1093 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.service.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts +328 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js +171 -0
- package/dist/cli/backend/src/services/cloud/cloud-sync.types.js.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts +89 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js +148 -0
- package/dist/cli/backend/src/services/cloud/device-identity.service.js.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts +86 -0
- package/dist/cli/backend/src/services/user/user-identity.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js +190 -0
- package/dist/cli/backend/src/services/user/user-identity.service.js.map +1 -0
- package/dist/cli/cli/src/commands/backup.d.ts +31 -0
- package/dist/cli/cli/src/commands/backup.d.ts.map +1 -0
- package/dist/cli/cli/src/commands/backup.js +280 -0
- package/dist/cli/cli/src/commands/backup.js.map +1 -0
- package/dist/cli/cli/src/index.js +10 -0
- package/dist/cli/cli/src/index.js.map +1 -1
- package/package.json +9 -3
- package/packages/crewly-agent/README.md +27 -0
- package/packages/crewly-agent/bin/crewly-agent +33 -0
- package/packages/crewly-agent/package.json +39 -0
- package/packages/crewly-agent/src/cli.ts +168 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.test.ts +2355 -0
- package/packages/crewly-agent/src/runtime/agent-runner.service.ts +1827 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.test.ts +153 -0
- package/packages/crewly-agent/src/runtime/agent-stream.service.ts +225 -0
- package/packages/crewly-agent/src/runtime/agent-worker.test.ts +171 -0
- package/packages/crewly-agent/src/runtime/agent-worker.ts +193 -0
- package/packages/crewly-agent/src/runtime/api-client.ts +143 -0
- package/packages/crewly-agent/src/runtime/approval-queue.service.ts +307 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.test.ts +208 -0
- package/packages/crewly-agent/src/runtime/audit-log.service.ts +332 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.test.ts +178 -0
- package/packages/crewly-agent/src/runtime/audit-trail.service.ts +151 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.test.ts +274 -0
- package/packages/crewly-agent/src/runtime/auditor-tools.ts +311 -0
- package/packages/crewly-agent/src/runtime/cloud-config.ts +67 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.test.ts +165 -0
- package/packages/crewly-agent/src/runtime/deepseek-sse-transform.ts +168 -0
- package/packages/crewly-agent/src/runtime/env-isolation.service.ts +246 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.test.ts +280 -0
- package/packages/crewly-agent/src/runtime/in-process-log-buffer.ts +317 -0
- package/packages/crewly-agent/src/runtime/index.ts +38 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.test.ts +352 -0
- package/packages/crewly-agent/src/runtime/mcp-tool-bridge.ts +244 -0
- package/packages/crewly-agent/src/runtime/model-manager.test.ts +326 -0
- package/packages/crewly-agent/src/runtime/model-manager.ts +363 -0
- package/packages/crewly-agent/src/runtime/output-filter.service.ts +175 -0
- package/packages/crewly-agent/src/runtime/prompt-guard.service.ts +303 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.test.ts +228 -0
- package/packages/crewly-agent/src/runtime/rate-limiter.ts +353 -0
- package/packages/crewly-agent/src/runtime/tool-registry.test.ts +2510 -0
- package/packages/crewly-agent/src/runtime/tool-registry.ts +2104 -0
- package/packages/crewly-agent/src/runtime/types.test.ts +519 -0
- package/packages/crewly-agent/src/runtime/types.ts +637 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.test.ts +131 -0
- package/packages/crewly-agent/src/runtime/web-search.tool.ts +140 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the web_search tool.
|
|
3
|
+
*
|
|
4
|
+
* @module services/agent/crewly-agent/web-search.tool.test
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
import { createWebSearchTool, formatAsMarkdown } from './web-search.tool.js';
|
|
9
|
+
import { CloudNotLoggedInError, type CloudConfig } from './cloud-config.js';
|
|
10
|
+
|
|
11
|
+
const config: CloudConfig = {
|
|
12
|
+
cloudUrl: 'https://api.crewlyai.com',
|
|
13
|
+
token: 'fake-jwt',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
17
|
+
return new Response(JSON.stringify(body), {
|
|
18
|
+
status,
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('formatAsMarkdown', () => {
|
|
24
|
+
it('returns just the answer when there are no sources', () => {
|
|
25
|
+
expect(formatAsMarkdown('hello\n', [])).toBe('hello');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('appends a numbered Sources list', () => {
|
|
29
|
+
const out = formatAsMarkdown('Some answer.', [
|
|
30
|
+
{ title: 'Title A', url: 'https://a.com', snippet: '' },
|
|
31
|
+
{ title: 'Title B', url: 'https://b.com', snippet: '' },
|
|
32
|
+
]);
|
|
33
|
+
expect(out).toBe(
|
|
34
|
+
'Some answer.\n\nSources:\n[1] Title A — https://a.com\n[2] Title B — https://b.com',
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('falls back to url when title is missing', () => {
|
|
39
|
+
const out = formatAsMarkdown('a', [{ title: '', url: 'https://x.com', snippet: '' }]);
|
|
40
|
+
expect(out.endsWith('[1] https://x.com — https://x.com')).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('createWebSearchTool', () => {
|
|
45
|
+
it('returns CloudNotLoggedInError as a structured failure (does not throw)', async () => {
|
|
46
|
+
const tool = createWebSearchTool({
|
|
47
|
+
loadConfig: () => Promise.reject(new CloudNotLoggedInError()),
|
|
48
|
+
});
|
|
49
|
+
const result = (await tool.execute({ query: 'hi' })) as { success: boolean; error?: string };
|
|
50
|
+
expect(result.success).toBe(false);
|
|
51
|
+
expect(result.error).toMatch(/Crewly Cloud is not connected/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('sends the query + bearer token to the configured endpoint', async () => {
|
|
55
|
+
const fetchSpy = vi.fn().mockResolvedValue(
|
|
56
|
+
jsonResponse({
|
|
57
|
+
success: true,
|
|
58
|
+
answer: 'answer text',
|
|
59
|
+
sources: [{ title: 'A', url: 'https://a.com', snippet: '' }],
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
const tool = createWebSearchTool({
|
|
63
|
+
loadConfig: () => Promise.resolve(config),
|
|
64
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = (await tool.execute({ query: 'what is X', max_results: 3 })) as {
|
|
68
|
+
success: boolean;
|
|
69
|
+
result?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
73
|
+
const [url, init] = fetchSpy.mock.calls[0]!;
|
|
74
|
+
expect(url).toBe('https://api.crewlyai.com/api/v1/search');
|
|
75
|
+
expect(init.method).toBe('POST');
|
|
76
|
+
expect(init.headers.Authorization).toBe('Bearer fake-jwt');
|
|
77
|
+
expect(JSON.parse(init.body)).toEqual({ query: 'what is X', max_results: 3 });
|
|
78
|
+
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
expect(result.result).toContain('answer text');
|
|
81
|
+
expect(result.result).toContain('[1] A — https://a.com');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('omits max_results from body when not provided', async () => {
|
|
85
|
+
const fetchSpy = vi.fn().mockResolvedValue(
|
|
86
|
+
jsonResponse({ success: true, answer: 'ok', sources: [] }),
|
|
87
|
+
);
|
|
88
|
+
const tool = createWebSearchTool({
|
|
89
|
+
loadConfig: () => Promise.resolve(config),
|
|
90
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
91
|
+
});
|
|
92
|
+
await tool.execute({ query: 'hi' });
|
|
93
|
+
expect(JSON.parse(fetchSpy.mock.calls[0]![1].body)).toEqual({ query: 'hi' });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('returns a structured error on non-2xx response', async () => {
|
|
97
|
+
const fetchSpy = vi.fn().mockResolvedValue(
|
|
98
|
+
jsonResponse({ success: false, error: 'bad query' }, 400),
|
|
99
|
+
);
|
|
100
|
+
const tool = createWebSearchTool({
|
|
101
|
+
loadConfig: () => Promise.resolve(config),
|
|
102
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
103
|
+
});
|
|
104
|
+
const result = (await tool.execute({ query: 'hi' })) as { success: boolean; error?: string };
|
|
105
|
+
expect(result.success).toBe(false);
|
|
106
|
+
expect(result.error).toMatch(/Search backend returned 400.*bad query/);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns a structured error on fetch failure', async () => {
|
|
110
|
+
const tool = createWebSearchTool({
|
|
111
|
+
loadConfig: () => Promise.resolve(config),
|
|
112
|
+
fetchImpl: (() => Promise.reject(new Error('network down'))) as unknown as typeof fetch,
|
|
113
|
+
});
|
|
114
|
+
const result = (await tool.execute({ query: 'hi' })) as { success: boolean; error?: string };
|
|
115
|
+
expect(result.success).toBe(false);
|
|
116
|
+
expect(result.error).toMatch(/network down/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('reports when the backend returns success:false', async () => {
|
|
120
|
+
const fetchSpy = vi.fn().mockResolvedValue(
|
|
121
|
+
jsonResponse({ success: false, error: 'quota exhausted' }, 200),
|
|
122
|
+
);
|
|
123
|
+
const tool = createWebSearchTool({
|
|
124
|
+
loadConfig: () => Promise.resolve(config),
|
|
125
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
126
|
+
});
|
|
127
|
+
const result = (await tool.execute({ query: 'hi' })) as { success: boolean; error?: string };
|
|
128
|
+
expect(result.success).toBe(false);
|
|
129
|
+
expect(result.error).toMatch(/quota exhausted/);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `web_search` tool — Crewly cloud-backed web search.
|
|
3
|
+
*
|
|
4
|
+
* Models like DeepSeek that lack a built-in search capability call this tool
|
|
5
|
+
* to delegate the search to the Crewly cloud service, which hits Gemini +
|
|
6
|
+
* Google Search and returns a synthesized answer with cited sources. The
|
|
7
|
+
* tool formats the response as markdown with a numbered Sources footer so
|
|
8
|
+
* the model can naturally inline citations like [1][2].
|
|
9
|
+
*
|
|
10
|
+
* Claude Code / Gemini CLI / Codex runtimes have their own search and won't
|
|
11
|
+
* be wired through this — this tool only ships in the Crewly agent runtime.
|
|
12
|
+
*
|
|
13
|
+
* @module services/agent/crewly-agent/web-search.tool
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import type { ToolDefinition } from './types.js';
|
|
18
|
+
import { loadCloudConfig, CloudNotLoggedInError, type CloudConfig } from './cloud-config.js';
|
|
19
|
+
|
|
20
|
+
const SEARCH_PATH = '/api/v1/search';
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
22
|
+
|
|
23
|
+
interface SearchSource {
|
|
24
|
+
title: string;
|
|
25
|
+
url: string;
|
|
26
|
+
snippet: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SearchResponse {
|
|
30
|
+
success: boolean;
|
|
31
|
+
answer?: string;
|
|
32
|
+
sources?: SearchSource[];
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Injectable IO for tests. */
|
|
37
|
+
export interface WebSearchDeps {
|
|
38
|
+
loadConfig?: () => Promise<CloudConfig>;
|
|
39
|
+
fetchImpl?: typeof fetch;
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createWebSearchTool(deps: WebSearchDeps = {}): ToolDefinition {
|
|
44
|
+
const loadConfig = deps.loadConfig ?? (() => loadCloudConfig());
|
|
45
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
46
|
+
const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
description:
|
|
50
|
+
'Search the web. Returns a synthesized answer plus a numbered list of cited sources. Use it whenever the user asks about current events, third-party documentation, package versions, or anything that may have changed since training. Inline-cite sources as [1][2] in your reply.',
|
|
51
|
+
inputSchema: z.object({
|
|
52
|
+
query: z.string().min(1).describe('The search query in natural language.'),
|
|
53
|
+
max_results: z
|
|
54
|
+
.number()
|
|
55
|
+
.int()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(10)
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Maximum number of cited sources to return (1-10, default 5).'),
|
|
60
|
+
}),
|
|
61
|
+
execute: async (args) => {
|
|
62
|
+
const { query, max_results } = args as { query: string; max_results?: number };
|
|
63
|
+
|
|
64
|
+
let config: CloudConfig;
|
|
65
|
+
try {
|
|
66
|
+
config = await loadConfig();
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof CloudNotLoggedInError) {
|
|
69
|
+
return { success: false, error: err.message };
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
76
|
+
let resp: Response;
|
|
77
|
+
try {
|
|
78
|
+
resp = await fetchImpl(`${config.cloudUrl}${SEARCH_PATH}`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
Authorization: `Bearer ${config.token}`,
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(
|
|
85
|
+
max_results !== undefined ? { query, max_results } : { query },
|
|
86
|
+
),
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
92
|
+
return { success: false, error: `Search request failed: ${message}` };
|
|
93
|
+
}
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
|
|
96
|
+
if (!resp.ok) {
|
|
97
|
+
let body: SearchResponse | null = null;
|
|
98
|
+
try {
|
|
99
|
+
body = (await resp.json()) as SearchResponse;
|
|
100
|
+
} catch {
|
|
101
|
+
/* not JSON — fall through */
|
|
102
|
+
}
|
|
103
|
+
const detail = body?.error || resp.statusText;
|
|
104
|
+
return { success: false, error: `Search backend returned ${resp.status}: ${detail}` };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const body = (await resp.json()) as SearchResponse;
|
|
108
|
+
if (!body.success || typeof body.answer !== 'string') {
|
|
109
|
+
return { success: false, error: body.error || 'Search returned no answer.' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sources = Array.isArray(body.sources) ? body.sources : [];
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
result: formatAsMarkdown(body.answer, sources),
|
|
116
|
+
sourceCount: sources.length,
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Format `answer` + `sources` as a single markdown block ready to drop into
|
|
124
|
+
* the model's context: synthesized answer followed by a numbered Sources
|
|
125
|
+
* section. The model can inline [1][2] back-references.
|
|
126
|
+
*
|
|
127
|
+
* Exported for unit testing.
|
|
128
|
+
*/
|
|
129
|
+
export function formatAsMarkdown(answer: string, sources: readonly SearchSource[]): string {
|
|
130
|
+
if (sources.length === 0) return answer.trim();
|
|
131
|
+
|
|
132
|
+
const numbered = sources
|
|
133
|
+
.map((s, i) => {
|
|
134
|
+
const title = s.title?.trim() || s.url;
|
|
135
|
+
return `[${i + 1}] ${title} — ${s.url}`;
|
|
136
|
+
})
|
|
137
|
+
.join('\n');
|
|
138
|
+
|
|
139
|
+
return `${answer.trim()}\n\nSources:\n${numbered}`;
|
|
140
|
+
}
|