@vellumai/assistant 0.5.6 → 0.5.7
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/.env.example +16 -2
- package/ARCHITECTURE.md +6 -75
- package/Dockerfile +1 -1
- package/README.md +0 -2
- package/bun.lock +0 -414
- package/docs/architecture/keychain-broker.md +45 -240
- package/docs/architecture/security.md +0 -17
- package/docs/credential-execution-service.md +2 -2
- package/node_modules/@vellumai/ces-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
- package/node_modules/@vellumai/credential-storage/package.json +1 -0
- package/node_modules/@vellumai/egress-proxy/package.json +1 -0
- package/package.json +2 -3
- package/src/__tests__/actor-token-service.test.ts +0 -114
- package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
- package/src/__tests__/browser-skill-endstate.test.ts +6 -5
- package/src/__tests__/btw-routes.test.ts +0 -39
- package/src/__tests__/call-domain.test.ts +0 -128
- package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -5
- package/src/__tests__/channel-readiness-service.test.ts +1 -60
- package/src/__tests__/checker.test.ts +4 -2
- package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +1 -1
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
- package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
- package/src/__tests__/conversation-skill-tools.test.ts +0 -54
- package/src/__tests__/conversation-title-service.test.ts +87 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
- package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
- package/src/__tests__/credential-security-e2e.test.ts +0 -66
- package/src/__tests__/credential-security-invariants.test.ts +4 -45
- package/src/__tests__/credentials-cli.test.ts +78 -0
- package/src/__tests__/db-migration-rollback.test.ts +2015 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
- package/src/__tests__/guardian-routing-state.test.ts +0 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -7
- package/src/__tests__/http-user-message-parity.test.ts +3 -103
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
- package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
- package/src/__tests__/intent-routing.test.ts +0 -13
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
- package/src/__tests__/keychain-broker-client.test.ts +161 -22
- package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
- package/src/__tests__/migration-export-http.test.ts +2 -2
- package/src/__tests__/migration-import-commit-http.test.ts +2 -2
- package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
- package/src/__tests__/migration-validate-http.test.ts +2 -2
- package/src/__tests__/non-member-access-request.test.ts +0 -5
- package/src/__tests__/notification-decision-fallback.test.ts +4 -0
- package/src/__tests__/notification-decision-identity.test.ts +4 -0
- package/src/__tests__/permission-types.test.ts +1 -0
- package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
- package/src/__tests__/qdrant-manager.test.ts +28 -2
- package/src/__tests__/registry.test.ts +0 -6
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
- package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
- package/src/__tests__/secure-keys.test.ts +83 -263
- package/src/__tests__/shell-identity.test.ts +96 -6
- package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
- package/src/__tests__/skill-feature-flags.test.ts +46 -45
- package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
- package/src/__tests__/skill-load-inline-command.test.ts +8 -12
- package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
- package/src/__tests__/skill-load-tool.test.ts +0 -2
- package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
- package/src/__tests__/skills.test.ts +0 -2
- package/src/__tests__/slack-inbound-verification.test.ts +0 -4
- package/src/__tests__/suggestion-routes.test.ts +1 -32
- package/src/__tests__/system-prompt.test.ts +0 -1
- package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
- package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
- package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
- package/src/calls/audio-store.test.ts +97 -0
- package/src/calls/audio-store.ts +205 -0
- package/src/calls/call-controller.ts +85 -7
- package/src/calls/call-domain.ts +3 -0
- package/src/calls/call-store.ts +10 -3
- package/src/calls/fish-audio-client.ts +117 -0
- package/src/calls/relay-server.ts +27 -0
- package/src/calls/twilio-routes.ts +2 -1
- package/src/calls/types.ts +1 -0
- package/src/calls/voice-ingress-preflight.ts +0 -42
- package/src/calls/voice-quality.ts +26 -5
- package/src/calls/voice-session-bridge.ts +6 -12
- package/src/cli/commands/config.ts +1 -4
- package/src/cli/commands/credentials.ts +34 -4
- package/src/cli/commands/oauth/index.ts +7 -0
- package/src/cli/commands/oauth/platform.ts +179 -0
- package/src/cli/commands/platform.ts +3 -3
- package/src/config/assistant-feature-flags.ts +186 -5
- package/src/config/bundled-skills/messaging/SKILL.md +5 -5
- package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
- package/src/config/bundled-skills/settings/TOOLS.json +2 -2
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
- package/src/config/bundled-tool-registry.ts +1 -11
- package/src/config/env-registry.ts +1 -1
- package/src/config/env.ts +8 -14
- package/src/config/feature-flag-registry.json +48 -8
- package/src/config/loader.ts +98 -31
- package/src/config/schema.ts +4 -13
- package/src/config/schemas/calls.ts +13 -0
- package/src/config/schemas/fish-audio.ts +39 -0
- package/src/config/schemas/security.ts +0 -4
- package/src/config/types.ts +0 -1
- package/src/contacts/contact-store.ts +39 -0
- package/src/contacts/types.ts +2 -0
- package/src/credential-execution/approval-bridge.ts +1 -0
- package/src/credential-execution/executable-discovery.ts +28 -4
- package/src/credential-execution/feature-gates.ts +16 -0
- package/src/credential-execution/process-manager.ts +38 -0
- package/src/daemon/assistant-attachments.ts +9 -0
- package/src/daemon/config-watcher.ts +5 -0
- package/src/daemon/conversation-tool-setup.ts +0 -105
- package/src/daemon/conversation.ts +10 -1
- package/src/daemon/handlers/config-vercel.ts +92 -0
- package/src/daemon/handlers/skills.ts +2 -15
- package/src/daemon/install-symlink.ts +195 -0
- package/src/daemon/lifecycle.ts +227 -51
- package/src/daemon/message-types/conversations.ts +3 -4
- package/src/daemon/message-types/diagnostics.ts +3 -22
- package/src/daemon/message-types/messages.ts +0 -2
- package/src/daemon/message-types/upgrades.ts +8 -0
- package/src/daemon/server.ts +30 -92
- package/src/events/domain-events.ts +2 -1
- package/src/inbound/platform-callback-registration.ts +3 -3
- package/src/instrument.ts +8 -5
- package/src/memory/conversation-title-service.ts +50 -1
- package/src/memory/db-init.ts +12 -0
- package/src/memory/items-extractor.ts +15 -1
- package/src/memory/job-handlers/conversation-starters.ts +4 -1
- package/src/memory/jobs-store.ts +30 -5
- package/src/memory/jobs-worker.ts +31 -7
- package/src/memory/migrations/001-job-deferrals.ts +19 -0
- package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
- package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
- package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
- package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
- package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
- package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
- package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
- package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
- package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
- package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
- package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
- package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
- package/src/memory/migrations/116-messages-fts.ts +106 -1
- package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
- package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
- package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
- package/src/memory/migrations/141-rename-verification-table.ts +54 -0
- package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
- package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
- package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
- package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
- package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
- package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
- package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
- package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
- package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
- package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
- package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
- package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
- package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
- package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
- package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/migrations/registry.ts +90 -0
- package/src/memory/migrations/validate-migration-state.ts +137 -11
- package/src/memory/qdrant-circuit-breaker.ts +9 -0
- package/src/memory/qdrant-manager.ts +64 -7
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/contacts.ts +1 -0
- package/src/notifications/decision-engine.ts +4 -1
- package/src/oauth/connection-resolver.ts +6 -4
- package/src/permissions/checker.ts +0 -38
- package/src/permissions/shell-identity.ts +76 -22
- package/src/permissions/types.ts +4 -2
- package/src/platform/client.ts +35 -7
- package/src/prompts/persona-resolver.ts +138 -0
- package/src/prompts/system-prompt.ts +36 -4
- package/src/prompts/templates/users/default.md +1 -0
- package/src/providers/registry.ts +27 -40
- package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
- package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
- package/src/runtime/auth/external-assistant-id.ts +13 -59
- package/src/runtime/auth/route-policy.ts +15 -1
- package/src/runtime/auth/token-service.ts +43 -138
- package/src/runtime/channel-readiness-service.ts +1 -16
- package/src/runtime/http-server.ts +27 -2
- package/src/runtime/middleware/error-handler.ts +1 -9
- package/src/runtime/routes/audio-routes.ts +40 -0
- package/src/runtime/routes/btw-routes.ts +0 -17
- package/src/runtime/routes/conversation-query-routes.ts +63 -1
- package/src/runtime/routes/conversation-routes.ts +4 -44
- package/src/runtime/routes/diagnostics-routes.ts +1 -477
- package/src/runtime/routes/identity-routes.ts +18 -29
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
- package/src/runtime/routes/integrations/vercel.ts +89 -0
- package/src/runtime/routes/log-export-routes.ts +5 -0
- package/src/runtime/routes/memory-item-routes.ts +24 -6
- package/src/runtime/routes/migration-rollback-routes.ts +209 -0
- package/src/runtime/routes/migration-routes.ts +17 -1
- package/src/runtime/routes/notification-routes.ts +58 -0
- package/src/runtime/routes/schedule-routes.ts +65 -0
- package/src/runtime/routes/settings-routes.ts +41 -1
- package/src/runtime/routes/tts-routes.ts +86 -0
- package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
- package/src/runtime/routes/workspace-commit-routes.ts +62 -0
- package/src/runtime/routes/workspace-routes.test.ts +22 -1
- package/src/runtime/routes/workspace-routes.ts +1 -1
- package/src/runtime/routes/workspace-utils.ts +86 -2
- package/src/security/ces-credential-client.ts +59 -22
- package/src/security/ces-rpc-credential-backend.ts +85 -0
- package/src/security/credential-backend.ts +12 -88
- package/src/security/keychain-broker-client.ts +10 -2
- package/src/security/secure-keys.ts +94 -113
- package/src/skills/catalog-install.ts +13 -7
- package/src/telemetry/usage-telemetry-reporter.ts +4 -2
- package/src/tools/calls/call-start.ts +1 -0
- package/src/tools/executor.ts +0 -4
- package/src/tools/network/script-proxy/session-manager.ts +19 -4
- package/src/tools/network/web-fetch.ts +3 -1
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/types.ts +0 -8
- package/src/util/errors.ts +0 -12
- package/src/util/platform.ts +3 -50
- package/src/workspace/git-service.ts +5 -2
- package/src/workspace/migrations/001-avatar-rename.ts +15 -0
- package/src/workspace/migrations/003-seed-device-id.ts +17 -1
- package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
- package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
- package/src/workspace/migrations/006-services-config.ts +49 -0
- package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
- package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
- package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
- package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
- package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
- package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
- package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
- package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +106 -2
- package/src/workspace/migrations/types.ts +4 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
- package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
- package/src/__tests__/diagnostics-export.test.ts +0 -288
- package/src/__tests__/local-gateway-health.test.ts +0 -209
- package/src/__tests__/secret-ingress-handler.test.ts +0 -120
- package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
- package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
- package/src/__tests__/swarm-orchestrator.test.ts +0 -463
- package/src/__tests__/swarm-plan-validator.test.ts +0 -384
- package/src/__tests__/swarm-recursion.test.ts +0 -197
- package/src/__tests__/swarm-router-planner.test.ts +0 -234
- package/src/__tests__/swarm-tool.test.ts +0 -185
- package/src/__tests__/swarm-worker-backend.test.ts +0 -144
- package/src/__tests__/swarm-worker-runner.test.ts +0 -288
- package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
- package/src/commands/cc-command-registry.ts +0 -248
- package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
- package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
- package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
- package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
- package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
- package/src/config/schemas/swarm.ts +0 -82
- package/src/logfire.ts +0 -135
- package/src/runtime/local-gateway-health.ts +0 -275
- package/src/security/secret-ingress.ts +0 -68
- package/src/swarm/backend-claude-code.ts +0 -225
- package/src/swarm/checkpoint.ts +0 -137
- package/src/swarm/graph-utils.ts +0 -53
- package/src/swarm/index.ts +0 -55
- package/src/swarm/limits.ts +0 -66
- package/src/swarm/orchestrator.ts +0 -424
- package/src/swarm/plan-validator.ts +0 -117
- package/src/swarm/router-planner.ts +0 -162
- package/src/swarm/router-prompts.ts +0 -39
- package/src/swarm/synthesizer.ts +0 -81
- package/src/swarm/types.ts +0 -72
- package/src/swarm/worker-backend.ts +0 -131
- package/src/swarm/worker-prompts.ts +0 -80
- package/src/swarm/worker-runner.ts +0 -170
- package/src/tools/claude-code/claude-code.ts +0 -610
- package/src/tools/swarm/delegate.ts +0 -205
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { Provider, ProviderResponse } from "../providers/types.js";
|
|
4
|
-
import { resolveSwarmLimits } from "../swarm/limits.js";
|
|
5
|
-
import {
|
|
6
|
-
generatePlan,
|
|
7
|
-
makeFallbackPlan,
|
|
8
|
-
parsePlanJSON,
|
|
9
|
-
} from "../swarm/router-planner.js";
|
|
10
|
-
|
|
11
|
-
const DEFAULT_LIMITS = resolveSwarmLimits({
|
|
12
|
-
maxWorkers: 3,
|
|
13
|
-
maxTasks: 8,
|
|
14
|
-
maxRetriesPerTask: 1,
|
|
15
|
-
workerTimeoutSec: 900,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
function makeProvider(responseText: string): Provider {
|
|
19
|
-
return {
|
|
20
|
-
name: "test",
|
|
21
|
-
async sendMessage(): Promise<ProviderResponse> {
|
|
22
|
-
return {
|
|
23
|
-
content: [{ type: "text", text: responseText }],
|
|
24
|
-
model: "test-model",
|
|
25
|
-
usage: { inputTokens: 100, outputTokens: 50 },
|
|
26
|
-
stopReason: "end_turn",
|
|
27
|
-
};
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function makeFailingProvider(): Provider {
|
|
33
|
-
return {
|
|
34
|
-
name: "test",
|
|
35
|
-
async sendMessage(): Promise<ProviderResponse> {
|
|
36
|
-
throw new Error("API error");
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe("parsePlanJSON", () => {
|
|
42
|
-
test("parses bare JSON", () => {
|
|
43
|
-
const result = parsePlanJSON(
|
|
44
|
-
'{"tasks":[{"id":"a","role":"coder","objective":"do stuff","dependencies":[]}]}',
|
|
45
|
-
);
|
|
46
|
-
expect(result).not.toBeNull();
|
|
47
|
-
expect(result!.tasks).toHaveLength(1);
|
|
48
|
-
expect(result!.tasks[0].id).toBe("a");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("parses fenced JSON", () => {
|
|
52
|
-
const raw =
|
|
53
|
-
'```json\n{"tasks":[{"id":"a","role":"coder","objective":"do stuff","dependencies":[]}]}\n```';
|
|
54
|
-
const result = parsePlanJSON(raw);
|
|
55
|
-
expect(result).not.toBeNull();
|
|
56
|
-
expect(result!.tasks).toHaveLength(1);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test("parses fenced code block without json tag", () => {
|
|
60
|
-
const raw =
|
|
61
|
-
'```\n{"tasks":[{"id":"a","role":"coder","objective":"do stuff","dependencies":[]}]}\n```';
|
|
62
|
-
const result = parsePlanJSON(raw);
|
|
63
|
-
expect(result).not.toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("returns null for invalid JSON", () => {
|
|
67
|
-
expect(parsePlanJSON("this is not json")).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("returns null for JSON without tasks array", () => {
|
|
71
|
-
expect(parsePlanJSON('{"plan":"something"}')).toBeNull();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("returns null for empty string", () => {
|
|
75
|
-
expect(parsePlanJSON("")).toBeNull();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("finds plan JSON in second fenced block when first is not valid JSON", () => {
|
|
79
|
-
const raw =
|
|
80
|
-
'```\nHere is my thinking about the plan\n```\n\n```json\n{"tasks":[{"id":"a","role":"coder","objective":"do stuff","dependencies":[]}]}\n```';
|
|
81
|
-
const result = parsePlanJSON(raw);
|
|
82
|
-
expect(result).not.toBeNull();
|
|
83
|
-
expect(result!.tasks).toHaveLength(1);
|
|
84
|
-
expect(result!.tasks[0].id).toBe("a");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("finds plan JSON in second fenced block when first has no tasks array", () => {
|
|
88
|
-
const raw =
|
|
89
|
-
'```json\n{"note":"not a plan"}\n```\n\n```json\n{"tasks":[{"id":"b","role":"researcher","objective":"research","dependencies":[]}]}\n```';
|
|
90
|
-
const result = parsePlanJSON(raw);
|
|
91
|
-
expect(result).not.toBeNull();
|
|
92
|
-
expect(result!.tasks[0].id).toBe("b");
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("makeFallbackPlan", () => {
|
|
97
|
-
test("creates single coder task", () => {
|
|
98
|
-
const plan = makeFallbackPlan("Build a feature");
|
|
99
|
-
expect(plan.objective).toBe("Build a feature");
|
|
100
|
-
expect(plan.tasks).toHaveLength(1);
|
|
101
|
-
expect(plan.tasks[0].role).toBe("coder");
|
|
102
|
-
expect(plan.tasks[0].id).toBe("fallback-coder");
|
|
103
|
-
expect(plan.tasks[0].dependencies).toEqual([]);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("generatePlan", () => {
|
|
108
|
-
test("generates a valid plan from LLM response", async () => {
|
|
109
|
-
const responseText = JSON.stringify({
|
|
110
|
-
tasks: [
|
|
111
|
-
{
|
|
112
|
-
id: "research",
|
|
113
|
-
role: "researcher",
|
|
114
|
-
objective: "Find docs",
|
|
115
|
-
dependencies: [],
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
id: "code",
|
|
119
|
-
role: "coder",
|
|
120
|
-
objective: "Implement",
|
|
121
|
-
dependencies: ["research"],
|
|
122
|
-
},
|
|
123
|
-
],
|
|
124
|
-
});
|
|
125
|
-
const plan = await generatePlan({
|
|
126
|
-
objective: "Build feature X",
|
|
127
|
-
provider: makeProvider(responseText),
|
|
128
|
-
modelIntent: "latency-optimized",
|
|
129
|
-
limits: DEFAULT_LIMITS,
|
|
130
|
-
});
|
|
131
|
-
expect(plan.tasks).toHaveLength(2);
|
|
132
|
-
expect(plan.tasks[0].id).toBe("research");
|
|
133
|
-
expect(plan.tasks[1].dependencies).toContain("research");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("falls back on invalid LLM JSON", async () => {
|
|
137
|
-
const plan = await generatePlan({
|
|
138
|
-
objective: "Build feature X",
|
|
139
|
-
provider: makeProvider("Sorry, I cannot help with that."),
|
|
140
|
-
modelIntent: "latency-optimized",
|
|
141
|
-
limits: DEFAULT_LIMITS,
|
|
142
|
-
});
|
|
143
|
-
expect(plan.tasks).toHaveLength(1);
|
|
144
|
-
expect(plan.tasks[0].id).toBe("fallback-coder");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("falls back when provider throws", async () => {
|
|
148
|
-
const plan = await generatePlan({
|
|
149
|
-
objective: "Build feature X",
|
|
150
|
-
provider: makeFailingProvider(),
|
|
151
|
-
modelIntent: "latency-optimized",
|
|
152
|
-
limits: DEFAULT_LIMITS,
|
|
153
|
-
});
|
|
154
|
-
expect(plan.tasks).toHaveLength(1);
|
|
155
|
-
expect(plan.tasks[0].id).toBe("fallback-coder");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("falls back when LLM plan has invalid roles", async () => {
|
|
159
|
-
const responseText = JSON.stringify({
|
|
160
|
-
tasks: [
|
|
161
|
-
{
|
|
162
|
-
id: "hack",
|
|
163
|
-
role: "hacker",
|
|
164
|
-
objective: "Hack stuff",
|
|
165
|
-
dependencies: [],
|
|
166
|
-
},
|
|
167
|
-
],
|
|
168
|
-
});
|
|
169
|
-
const plan = await generatePlan({
|
|
170
|
-
objective: "Build feature X",
|
|
171
|
-
provider: makeProvider(responseText),
|
|
172
|
-
modelIntent: "latency-optimized",
|
|
173
|
-
limits: DEFAULT_LIMITS,
|
|
174
|
-
});
|
|
175
|
-
// Plan validator rejects invalid roles, so generatePlan catches and falls back
|
|
176
|
-
expect(plan.tasks).toHaveLength(1);
|
|
177
|
-
expect(plan.tasks[0].id).toBe("fallback-coder");
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("falls back when LLM plan has cycles", async () => {
|
|
181
|
-
const responseText = JSON.stringify({
|
|
182
|
-
tasks: [
|
|
183
|
-
{ id: "a", role: "coder", objective: "A", dependencies: ["b"] },
|
|
184
|
-
{ id: "b", role: "coder", objective: "B", dependencies: ["a"] },
|
|
185
|
-
],
|
|
186
|
-
});
|
|
187
|
-
const plan = await generatePlan({
|
|
188
|
-
objective: "Build feature X",
|
|
189
|
-
provider: makeProvider(responseText),
|
|
190
|
-
modelIntent: "latency-optimized",
|
|
191
|
-
limits: DEFAULT_LIMITS,
|
|
192
|
-
});
|
|
193
|
-
expect(plan.tasks).toHaveLength(1);
|
|
194
|
-
expect(plan.tasks[0].id).toBe("fallback-coder");
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("truncates plans exceeding maxTasks", async () => {
|
|
198
|
-
const tasks = Array.from({ length: 15 }, (_, i) => ({
|
|
199
|
-
id: `t${i}`,
|
|
200
|
-
role: "coder",
|
|
201
|
-
objective: `Task ${i}`,
|
|
202
|
-
dependencies: [],
|
|
203
|
-
}));
|
|
204
|
-
const plan = await generatePlan({
|
|
205
|
-
objective: "Big feature",
|
|
206
|
-
provider: makeProvider(JSON.stringify({ tasks })),
|
|
207
|
-
modelIntent: "latency-optimized",
|
|
208
|
-
limits: DEFAULT_LIMITS,
|
|
209
|
-
});
|
|
210
|
-
expect(plan.tasks.length).toBeLessThanOrEqual(DEFAULT_LIMITS.maxTasks);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
test("handles response with no text content", async () => {
|
|
214
|
-
const provider: Provider = {
|
|
215
|
-
name: "test",
|
|
216
|
-
async sendMessage(): Promise<ProviderResponse> {
|
|
217
|
-
return {
|
|
218
|
-
content: [],
|
|
219
|
-
model: "test-model",
|
|
220
|
-
usage: { inputTokens: 0, outputTokens: 0 },
|
|
221
|
-
stopReason: "end_turn",
|
|
222
|
-
};
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
const plan = await generatePlan({
|
|
226
|
-
objective: "Build feature",
|
|
227
|
-
provider,
|
|
228
|
-
modelIntent: "latency-optimized",
|
|
229
|
-
limits: DEFAULT_LIMITS,
|
|
230
|
-
});
|
|
231
|
-
expect(plan.tasks).toHaveLength(1);
|
|
232
|
-
expect(plan.tasks[0].id).toBe("fallback-coder");
|
|
233
|
-
});
|
|
234
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Mocks — declared before imports that depend on them
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
mock.module("../util/logger.js", () => ({
|
|
8
|
-
getLogger: () =>
|
|
9
|
-
new Proxy({} as Record<string, unknown>, {
|
|
10
|
-
get: () => () => {},
|
|
11
|
-
}),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
mock.module("../config/loader.js", () => ({
|
|
15
|
-
getConfig: () => ({
|
|
16
|
-
ui: {},
|
|
17
|
-
|
|
18
|
-
provider: "anthropic",
|
|
19
|
-
providerOrder: ["anthropic"],
|
|
20
|
-
swarm: {
|
|
21
|
-
enabled: true,
|
|
22
|
-
maxWorkers: 3,
|
|
23
|
-
maxTasks: 8,
|
|
24
|
-
maxRetriesPerTask: 1,
|
|
25
|
-
workerTimeoutSec: 900,
|
|
26
|
-
roleTimeoutsSec: {},
|
|
27
|
-
plannerModelIntent: "latency-optimized",
|
|
28
|
-
synthesizerModelIntent: "quality-optimized",
|
|
29
|
-
},
|
|
30
|
-
services: {
|
|
31
|
-
inference: {
|
|
32
|
-
mode: "your-own",
|
|
33
|
-
provider: "anthropic",
|
|
34
|
-
model: "claude-opus-4-6",
|
|
35
|
-
},
|
|
36
|
-
"image-generation": {
|
|
37
|
-
mode: "your-own",
|
|
38
|
-
provider: "gemini",
|
|
39
|
-
model: "gemini-3.1-flash-image-preview",
|
|
40
|
-
},
|
|
41
|
-
"web-search": { mode: "your-own", provider: "inference-provider-native" },
|
|
42
|
-
},
|
|
43
|
-
}),
|
|
44
|
-
getSwarmDisabledConfig: () => ({
|
|
45
|
-
provider: "anthropic",
|
|
46
|
-
providerOrder: ["anthropic"],
|
|
47
|
-
swarm: {
|
|
48
|
-
enabled: false,
|
|
49
|
-
maxWorkers: 3,
|
|
50
|
-
maxTasks: 8,
|
|
51
|
-
maxRetriesPerTask: 1,
|
|
52
|
-
workerTimeoutSec: 900,
|
|
53
|
-
roleTimeoutsSec: {},
|
|
54
|
-
plannerModelIntent: "latency-optimized",
|
|
55
|
-
synthesizerModelIntent: "quality-optimized",
|
|
56
|
-
},
|
|
57
|
-
}),
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
// Mock provider registry — returns a mock provider
|
|
61
|
-
const mockProvider = {
|
|
62
|
-
name: "test",
|
|
63
|
-
async sendMessage() {
|
|
64
|
-
return {
|
|
65
|
-
content: [
|
|
66
|
-
{
|
|
67
|
-
type: "text",
|
|
68
|
-
text: '{"tasks":[{"id":"t1","role":"coder","objective":"Do it","dependencies":[]}]}',
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
model: "test",
|
|
72
|
-
usage: { inputTokens: 10, outputTokens: 10 },
|
|
73
|
-
stopReason: "end_turn",
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
mock.module("../security/secure-keys.js", () => ({
|
|
78
|
-
getSecureKeyAsync: async () => "test-api-key",
|
|
79
|
-
getProviderKeyAsync: async () => "test-api-key",
|
|
80
|
-
}));
|
|
81
|
-
|
|
82
|
-
mock.module("../providers/registry.js", () => ({
|
|
83
|
-
getProvider: () => mockProvider,
|
|
84
|
-
getFailoverProvider: () => mockProvider,
|
|
85
|
-
}));
|
|
86
|
-
|
|
87
|
-
// Mock the Agent SDK to prevent real subprocess spawning
|
|
88
|
-
mock.module("@anthropic-ai/claude-agent-sdk", () => ({
|
|
89
|
-
query: () => ({
|
|
90
|
-
async *[Symbol.asyncIterator]() {
|
|
91
|
-
yield {
|
|
92
|
-
type: "result" as const,
|
|
93
|
-
session_id: "test-session",
|
|
94
|
-
subtype: "success" as const,
|
|
95
|
-
result:
|
|
96
|
-
'```json\n{"summary":"Done","artifacts":[],"issues":[],"nextSteps":[]}\n```',
|
|
97
|
-
};
|
|
98
|
-
},
|
|
99
|
-
}),
|
|
100
|
-
}));
|
|
101
|
-
|
|
102
|
-
import {
|
|
103
|
-
_resetSwarmActive,
|
|
104
|
-
swarmDelegateTool,
|
|
105
|
-
} from "../tools/swarm/delegate.js";
|
|
106
|
-
import type { ToolContext } from "../tools/types.js";
|
|
107
|
-
|
|
108
|
-
function makeContext(overrides?: Partial<ToolContext>): ToolContext {
|
|
109
|
-
return {
|
|
110
|
-
conversationId: "test-session",
|
|
111
|
-
workingDir: "/tmp/test",
|
|
112
|
-
trustClass: "guardian",
|
|
113
|
-
onOutput: () => {},
|
|
114
|
-
...overrides,
|
|
115
|
-
} as ToolContext;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
describe("swarm_delegate tool", () => {
|
|
119
|
-
beforeEach(() => {
|
|
120
|
-
_resetSwarmActive();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("getDefinition returns valid schema", () => {
|
|
124
|
-
const def = swarmDelegateTool.getDefinition();
|
|
125
|
-
expect(def.name).toBe("swarm_delegate");
|
|
126
|
-
const props = (def.input_schema as Record<string, unknown>)
|
|
127
|
-
.properties as Record<string, unknown>;
|
|
128
|
-
expect(props.objective).toBeDefined();
|
|
129
|
-
expect(props.context).toBeDefined();
|
|
130
|
-
expect(props.max_workers).toBeDefined();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("executes successfully with a simple objective", async () => {
|
|
134
|
-
const outputs: string[] = [];
|
|
135
|
-
const result = await swarmDelegateTool.execute(
|
|
136
|
-
{ objective: "Build a simple feature" },
|
|
137
|
-
makeContext({ onOutput: (text: string) => outputs.push(text) }),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(result.isError).toBeFalsy();
|
|
141
|
-
expect(result.content).toBeTruthy();
|
|
142
|
-
expect(outputs.length).toBeGreaterThan(0);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
test("blocks nested swarm invocation", async () => {
|
|
146
|
-
// Simulate active swarm by calling _resetSwarmActive then manually setting it
|
|
147
|
-
// We test this by running two sequential calls where the first doesn't finish
|
|
148
|
-
// Actually, we can test by checking the recursion guard directly
|
|
149
|
-
const result1Promise = swarmDelegateTool.execute(
|
|
150
|
-
{ objective: "First task" },
|
|
151
|
-
makeContext(),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
// While first is running, try a second
|
|
155
|
-
// Since the mock backend resolves instantly, we need to be creative
|
|
156
|
-
// Let's just verify the guard works by testing post-execution
|
|
157
|
-
await result1Promise;
|
|
158
|
-
|
|
159
|
-
// After completion, the flag should be reset
|
|
160
|
-
const result2 = await swarmDelegateTool.execute(
|
|
161
|
-
{ objective: "Second task" },
|
|
162
|
-
makeContext(),
|
|
163
|
-
);
|
|
164
|
-
expect(result2.isError).toBeFalsy();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("handles objective with context", async () => {
|
|
168
|
-
const result = await swarmDelegateTool.execute(
|
|
169
|
-
{ objective: "Build feature", context: "This is a React project" },
|
|
170
|
-
makeContext(),
|
|
171
|
-
);
|
|
172
|
-
expect(result.isError).toBeFalsy();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("short-circuits when signal is already aborted", async () => {
|
|
176
|
-
const controller = new AbortController();
|
|
177
|
-
controller.abort();
|
|
178
|
-
const result = await swarmDelegateTool.execute(
|
|
179
|
-
{ objective: "Should not run" },
|
|
180
|
-
makeContext({ signal: controller.signal }),
|
|
181
|
-
);
|
|
182
|
-
expect(result.isError).toBe(true);
|
|
183
|
-
expect(result.content).toBe("Cancelled");
|
|
184
|
-
});
|
|
185
|
-
});
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import type { WorkerProfile } from "../swarm/worker-backend.js";
|
|
4
|
-
import { getProfilePolicy, roleToProfile } from "../swarm/worker-backend.js";
|
|
5
|
-
|
|
6
|
-
describe("roleToProfile", () => {
|
|
7
|
-
test("maps researcher role to researcher profile", () => {
|
|
8
|
-
expect(roleToProfile("researcher")).toBe("researcher");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test("maps coder role to coder profile", () => {
|
|
12
|
-
expect(roleToProfile("coder")).toBe("coder");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test("maps reviewer role to reviewer profile", () => {
|
|
16
|
-
expect(roleToProfile("reviewer")).toBe("reviewer");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("maps router role to general profile", () => {
|
|
20
|
-
expect(roleToProfile("router")).toBe("general");
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("getProfilePolicy", () => {
|
|
25
|
-
const READ_ONLY_TOOLS = [
|
|
26
|
-
"Read",
|
|
27
|
-
"Glob",
|
|
28
|
-
"Grep",
|
|
29
|
-
"WebSearch",
|
|
30
|
-
"WebFetch",
|
|
31
|
-
"LS",
|
|
32
|
-
"Bash(grep *)",
|
|
33
|
-
"Bash(rg *)",
|
|
34
|
-
"Bash(find *)",
|
|
35
|
-
];
|
|
36
|
-
|
|
37
|
-
const WRITE_TOOLS = ["Edit", "Write", "MultiEdit", "NotebookEdit"];
|
|
38
|
-
|
|
39
|
-
describe("general profile", () => {
|
|
40
|
-
const policy = getProfilePolicy("general");
|
|
41
|
-
|
|
42
|
-
test("allows read-only tools", () => {
|
|
43
|
-
for (const tool of READ_ONLY_TOOLS) {
|
|
44
|
-
expect(policy.allow.has(tool)).toBe(true);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test("has no hard denies", () => {
|
|
49
|
-
expect(policy.deny.size).toBe(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("requires approval for write tools and Bash", () => {
|
|
53
|
-
for (const tool of WRITE_TOOLS) {
|
|
54
|
-
expect(policy.approvalRequired.has(tool)).toBe(true);
|
|
55
|
-
}
|
|
56
|
-
expect(policy.approvalRequired.has("Bash")).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe("researcher profile", () => {
|
|
61
|
-
const policy = getProfilePolicy("researcher");
|
|
62
|
-
|
|
63
|
-
test("allows read-only tools", () => {
|
|
64
|
-
for (const tool of READ_ONLY_TOOLS) {
|
|
65
|
-
expect(policy.allow.has(tool)).toBe(true);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("denies write tools and Bash", () => {
|
|
70
|
-
for (const tool of WRITE_TOOLS) {
|
|
71
|
-
expect(policy.deny.has(tool)).toBe(true);
|
|
72
|
-
}
|
|
73
|
-
expect(policy.deny.has("Bash")).toBe(true);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe("coder profile", () => {
|
|
78
|
-
const policy = getProfilePolicy("coder");
|
|
79
|
-
|
|
80
|
-
test("allows read-only tools", () => {
|
|
81
|
-
for (const tool of READ_ONLY_TOOLS) {
|
|
82
|
-
expect(policy.allow.has(tool)).toBe(true);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("has no hard denies", () => {
|
|
87
|
-
expect(policy.deny.size).toBe(0);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("requires approval for write tools and Bash", () => {
|
|
91
|
-
for (const tool of WRITE_TOOLS) {
|
|
92
|
-
expect(policy.approvalRequired.has(tool)).toBe(true);
|
|
93
|
-
}
|
|
94
|
-
expect(policy.approvalRequired.has("Bash")).toBe(true);
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
describe("reviewer profile", () => {
|
|
99
|
-
const policy = getProfilePolicy("reviewer");
|
|
100
|
-
|
|
101
|
-
test("allows read-only tools", () => {
|
|
102
|
-
for (const tool of READ_ONLY_TOOLS) {
|
|
103
|
-
expect(policy.allow.has(tool)).toBe(true);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("denies write tools and Bash", () => {
|
|
108
|
-
for (const tool of WRITE_TOOLS) {
|
|
109
|
-
expect(policy.deny.has(tool)).toBe(true);
|
|
110
|
-
}
|
|
111
|
-
expect(policy.deny.has("Bash")).toBe(true);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("all profiles allow the same read-only tool set", () => {
|
|
116
|
-
const profiles: WorkerProfile[] = [
|
|
117
|
-
"general",
|
|
118
|
-
"researcher",
|
|
119
|
-
"coder",
|
|
120
|
-
"reviewer",
|
|
121
|
-
];
|
|
122
|
-
for (const profile of profiles) {
|
|
123
|
-
const policy = getProfilePolicy(profile);
|
|
124
|
-
for (const tool of READ_ONLY_TOOLS) {
|
|
125
|
-
expect(policy.allow.has(tool)).toBe(true);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("denied tools are never also in allow set", () => {
|
|
131
|
-
const profiles: WorkerProfile[] = [
|
|
132
|
-
"general",
|
|
133
|
-
"researcher",
|
|
134
|
-
"coder",
|
|
135
|
-
"reviewer",
|
|
136
|
-
];
|
|
137
|
-
for (const profile of profiles) {
|
|
138
|
-
const policy = getProfilePolicy(profile);
|
|
139
|
-
for (const tool of policy.deny) {
|
|
140
|
-
expect(policy.allow.has(tool)).toBe(false);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
});
|