@vellumai/assistant 0.6.0 → 0.6.2
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +42 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +539 -4
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +138 -172
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +257 -100
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +63 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +16 -29
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +4 -27
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +6 -29
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +56 -0
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +25 -5
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +11 -2
- package/src/cli.ts +1 -120
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
- package/src/config/bundled-skills/gmail/SKILL.md +13 -8
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +85 -3
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +179 -65
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +30 -8
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +359 -308
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +44 -17
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +19 -3
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +5 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +70 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +220 -158
- package/src/daemon/message-types/conversations.ts +29 -6
- package/src/daemon/message-types/messages.ts +9 -2
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +18 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +87 -10
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/daemon/transport-hints.ts +33 -0
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +151 -117
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +66 -6
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/embedding-local.ts +1 -1
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -17
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/group-crud.ts +25 -9
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +15 -4
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/prompter.ts +0 -2
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +76 -162
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +30 -9
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +270 -44
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +41 -278
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -18
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/secret-detection-handler.ts +0 -1
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/skills/sandbox-runner.ts +3 -6
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -3
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +85 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -75,6 +75,7 @@ function injectSubagent(
|
|
|
75
75
|
}
|
|
76
76
|
>;
|
|
77
77
|
parentToChildren: Map<string, Set<string>>;
|
|
78
|
+
labelIndex: Map<string, string>;
|
|
78
79
|
};
|
|
79
80
|
const state: SubagentState = {
|
|
80
81
|
config: {
|
|
@@ -108,6 +109,14 @@ function injectSubagent(
|
|
|
108
109
|
internals.parentToChildren.set(parentConversationId, new Set());
|
|
109
110
|
}
|
|
110
111
|
internals.parentToChildren.get(parentConversationId)!.add(subagentId);
|
|
112
|
+
|
|
113
|
+
// Populate label index so label-based lookups work in tests.
|
|
114
|
+
const label = state.config.label;
|
|
115
|
+
internals.labelIndex.set(
|
|
116
|
+
`${parentConversationId}:${label.toLowerCase().trim()}`,
|
|
117
|
+
subagentId,
|
|
118
|
+
);
|
|
119
|
+
|
|
111
120
|
return state;
|
|
112
121
|
}
|
|
113
122
|
|
|
@@ -135,25 +144,29 @@ describe("Subagent tool definitions", () => {
|
|
|
135
144
|
test("abort tool has correct definition", () => {
|
|
136
145
|
const def = findTool("subagent_abort");
|
|
137
146
|
expect(def).toBeDefined();
|
|
138
|
-
expect(def.input_schema.required).toEqual([
|
|
147
|
+
expect(def.input_schema.required).toEqual([]);
|
|
148
|
+
expect(def.input_schema.properties.label).toBeDefined();
|
|
139
149
|
});
|
|
140
150
|
|
|
141
151
|
test("message tool has correct definition", () => {
|
|
142
152
|
const def = findTool("subagent_message");
|
|
143
153
|
expect(def).toBeDefined();
|
|
144
|
-
expect(def.input_schema.required).toEqual(["
|
|
154
|
+
expect(def.input_schema.required).toEqual(["content"]);
|
|
155
|
+
expect(def.input_schema.properties.label).toBeDefined();
|
|
145
156
|
});
|
|
146
157
|
|
|
147
158
|
test("read tool has correct definition", () => {
|
|
148
159
|
const def = findTool("subagent_read");
|
|
149
160
|
expect(def).toBeDefined();
|
|
150
|
-
expect(def.input_schema.required).toEqual([
|
|
161
|
+
expect(def.input_schema.required).toEqual([]);
|
|
162
|
+
expect(def.input_schema.properties.label).toBeDefined();
|
|
151
163
|
});
|
|
152
164
|
|
|
153
165
|
test("status tool has correct definition", () => {
|
|
154
166
|
const def = findTool("subagent_status");
|
|
155
167
|
expect(def).toBeDefined();
|
|
156
168
|
expect(def.input_schema.required).toEqual([]);
|
|
169
|
+
expect(def.input_schema.properties.label).toBeDefined();
|
|
157
170
|
});
|
|
158
171
|
});
|
|
159
172
|
|
|
@@ -247,7 +260,7 @@ describe("Subagent tool execute validation", () => {
|
|
|
247
260
|
expect(result.content).toContain("required");
|
|
248
261
|
});
|
|
249
262
|
|
|
250
|
-
test("message returns error when missing subagent_id", async () => {
|
|
263
|
+
test("message returns error when missing subagent_id and label", async () => {
|
|
251
264
|
const result = await executeSubagentMessage(
|
|
252
265
|
{ content: "hello" },
|
|
253
266
|
makeContext("sess-1"),
|
|
@@ -790,6 +803,111 @@ describe("Subagent read tool", () => {
|
|
|
790
803
|
}
|
|
791
804
|
});
|
|
792
805
|
|
|
806
|
+
test("read with last_n: 1 returns only the last message", async () => {
|
|
807
|
+
const manager = getSubagentManager();
|
|
808
|
+
const subagentId = "read-last-n-1";
|
|
809
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
810
|
+
|
|
811
|
+
mockGetMessages = (convId: string) => {
|
|
812
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
813
|
+
return [
|
|
814
|
+
{ role: "assistant", content: "First message" },
|
|
815
|
+
{ role: "assistant", content: "Second message" },
|
|
816
|
+
{ role: "assistant", content: "Third message" },
|
|
817
|
+
];
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
const result = await executeSubagentRead(
|
|
822
|
+
{ subagent_id: subagentId, last_n: 1 },
|
|
823
|
+
makeContext(ownerConversation),
|
|
824
|
+
);
|
|
825
|
+
expect(result.isError).toBe(false);
|
|
826
|
+
expect(result.content).toBe("Third message");
|
|
827
|
+
} finally {
|
|
828
|
+
mockGetMessages = () => null;
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
test("read with last_n: 2 returns last 2 messages joined", async () => {
|
|
833
|
+
const manager = getSubagentManager();
|
|
834
|
+
const subagentId = "read-last-n-2";
|
|
835
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
836
|
+
|
|
837
|
+
mockGetMessages = (convId: string) => {
|
|
838
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
839
|
+
return [
|
|
840
|
+
{ role: "assistant", content: "First message" },
|
|
841
|
+
{ role: "assistant", content: "Second message" },
|
|
842
|
+
{ role: "assistant", content: "Third message" },
|
|
843
|
+
];
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
const result = await executeSubagentRead(
|
|
848
|
+
{ subagent_id: subagentId, last_n: 2 },
|
|
849
|
+
makeContext(ownerConversation),
|
|
850
|
+
);
|
|
851
|
+
expect(result.isError).toBe(false);
|
|
852
|
+
expect(result.content).toBe("Second message\n\nThird message");
|
|
853
|
+
} finally {
|
|
854
|
+
mockGetMessages = () => null;
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("read with last_n omitted returns all messages", async () => {
|
|
859
|
+
const manager = getSubagentManager();
|
|
860
|
+
const subagentId = "read-last-n-omit";
|
|
861
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
862
|
+
|
|
863
|
+
mockGetMessages = (convId: string) => {
|
|
864
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
865
|
+
return [
|
|
866
|
+
{ role: "assistant", content: "First message" },
|
|
867
|
+
{ role: "assistant", content: "Second message" },
|
|
868
|
+
{ role: "assistant", content: "Third message" },
|
|
869
|
+
];
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
try {
|
|
873
|
+
const result = await executeSubagentRead(
|
|
874
|
+
{ subagent_id: subagentId },
|
|
875
|
+
makeContext(ownerConversation),
|
|
876
|
+
);
|
|
877
|
+
expect(result.isError).toBe(false);
|
|
878
|
+
expect(result.content).toBe(
|
|
879
|
+
"First message\n\nSecond message\n\nThird message",
|
|
880
|
+
);
|
|
881
|
+
} finally {
|
|
882
|
+
mockGetMessages = () => null;
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
test("read with last_n larger than available returns all messages", async () => {
|
|
887
|
+
const manager = getSubagentManager();
|
|
888
|
+
const subagentId = "read-last-n-large";
|
|
889
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
890
|
+
|
|
891
|
+
mockGetMessages = (convId: string) => {
|
|
892
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
893
|
+
return [
|
|
894
|
+
{ role: "assistant", content: "First message" },
|
|
895
|
+
{ role: "assistant", content: "Second message" },
|
|
896
|
+
];
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
const result = await executeSubagentRead(
|
|
901
|
+
{ subagent_id: subagentId, last_n: 100 },
|
|
902
|
+
makeContext(ownerConversation),
|
|
903
|
+
);
|
|
904
|
+
expect(result.isError).toBe(false);
|
|
905
|
+
expect(result.content).toBe("First message\n\nSecond message");
|
|
906
|
+
} finally {
|
|
907
|
+
mockGetMessages = () => null;
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
793
911
|
test("read concatenates multiple assistant messages", async () => {
|
|
794
912
|
const manager = getSubagentManager();
|
|
795
913
|
const subagentId = "read-multi-1";
|
|
@@ -822,6 +940,113 @@ describe("Subagent read tool", () => {
|
|
|
822
940
|
mockGetMessages = () => null;
|
|
823
941
|
}
|
|
824
942
|
});
|
|
943
|
+
|
|
944
|
+
test("read with last_n: 1 returns only the last message", async () => {
|
|
945
|
+
const manager = getSubagentManager();
|
|
946
|
+
const subagentId = "read-last-n-1";
|
|
947
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
948
|
+
|
|
949
|
+
mockGetMessages = (convId: string) => {
|
|
950
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
951
|
+
return [
|
|
952
|
+
{ role: "assistant", content: "First response" },
|
|
953
|
+
{ role: "user", content: "Follow up" },
|
|
954
|
+
{ role: "assistant", content: "Second response" },
|
|
955
|
+
{ role: "assistant", content: "Third response" },
|
|
956
|
+
];
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
try {
|
|
960
|
+
const result = await executeSubagentRead(
|
|
961
|
+
{ subagent_id: subagentId, last_n: 1 },
|
|
962
|
+
makeContext(ownerConversation),
|
|
963
|
+
);
|
|
964
|
+
expect(result.isError).toBe(false);
|
|
965
|
+
expect(result.content).toBe("Third response");
|
|
966
|
+
} finally {
|
|
967
|
+
mockGetMessages = () => null;
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
test("read with last_n: 2 returns last two messages", async () => {
|
|
972
|
+
const manager = getSubagentManager();
|
|
973
|
+
const subagentId = "read-last-n-2";
|
|
974
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
975
|
+
|
|
976
|
+
mockGetMessages = (convId: string) => {
|
|
977
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
978
|
+
return [
|
|
979
|
+
{ role: "assistant", content: "First response" },
|
|
980
|
+
{ role: "user", content: "Follow up" },
|
|
981
|
+
{ role: "assistant", content: "Second response" },
|
|
982
|
+
{ role: "assistant", content: "Third response" },
|
|
983
|
+
];
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
const result = await executeSubagentRead(
|
|
988
|
+
{ subagent_id: subagentId, last_n: 2 },
|
|
989
|
+
makeContext(ownerConversation),
|
|
990
|
+
);
|
|
991
|
+
expect(result.isError).toBe(false);
|
|
992
|
+
expect(result.content).toBe("Second response\n\nThird response");
|
|
993
|
+
} finally {
|
|
994
|
+
mockGetMessages = () => null;
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test("read without last_n returns all messages", async () => {
|
|
999
|
+
const manager = getSubagentManager();
|
|
1000
|
+
const subagentId = "read-no-last-n-1";
|
|
1001
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
1002
|
+
|
|
1003
|
+
mockGetMessages = (convId: string) => {
|
|
1004
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
1005
|
+
return [
|
|
1006
|
+
{ role: "assistant", content: "First response" },
|
|
1007
|
+
{ role: "assistant", content: "Second response" },
|
|
1008
|
+
{ role: "assistant", content: "Third response" },
|
|
1009
|
+
];
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
const result = await executeSubagentRead(
|
|
1014
|
+
{ subagent_id: subagentId },
|
|
1015
|
+
makeContext(ownerConversation),
|
|
1016
|
+
);
|
|
1017
|
+
expect(result.isError).toBe(false);
|
|
1018
|
+
expect(result.content).toBe(
|
|
1019
|
+
"First response\n\nSecond response\n\nThird response",
|
|
1020
|
+
);
|
|
1021
|
+
} finally {
|
|
1022
|
+
mockGetMessages = () => null;
|
|
1023
|
+
}
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test("read with last_n larger than available messages returns all", async () => {
|
|
1027
|
+
const manager = getSubagentManager();
|
|
1028
|
+
const subagentId = "read-last-n-big-1";
|
|
1029
|
+
injectSubagent(manager, subagentId, ownerConversation, "completed");
|
|
1030
|
+
|
|
1031
|
+
mockGetMessages = (convId: string) => {
|
|
1032
|
+
if (convId !== `conv-${subagentId}`) return null;
|
|
1033
|
+
return [
|
|
1034
|
+
{ role: "assistant", content: "First response" },
|
|
1035
|
+
{ role: "assistant", content: "Second response" },
|
|
1036
|
+
];
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
try {
|
|
1040
|
+
const result = await executeSubagentRead(
|
|
1041
|
+
{ subagent_id: subagentId, last_n: 100 },
|
|
1042
|
+
makeContext(ownerConversation),
|
|
1043
|
+
);
|
|
1044
|
+
expect(result.isError).toBe(false);
|
|
1045
|
+
expect(result.content).toBe("First response\n\nSecond response");
|
|
1046
|
+
} finally {
|
|
1047
|
+
mockGetMessages = () => null;
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
825
1050
|
});
|
|
826
1051
|
|
|
827
1052
|
// ── Abort success path details ──────────────────────────────────────
|
|
@@ -869,3 +1094,238 @@ describe("Subagent abort success responses", () => {
|
|
|
869
1094
|
expect(result.content).toContain("Could not abort");
|
|
870
1095
|
});
|
|
871
1096
|
});
|
|
1097
|
+
|
|
1098
|
+
// ── Label-based subagent lookup ────────────────────────────────────
|
|
1099
|
+
|
|
1100
|
+
describe("Label-based subagent lookup", () => {
|
|
1101
|
+
const parentConversation = "label-test-sess";
|
|
1102
|
+
const subagentId = "label-sub-1";
|
|
1103
|
+
|
|
1104
|
+
// Inject a subagent with a specific label for the test suite.
|
|
1105
|
+
const manager = getSubagentManager();
|
|
1106
|
+
injectSubagent(manager, subagentId, parentConversation, "running", {
|
|
1107
|
+
config: {
|
|
1108
|
+
id: subagentId,
|
|
1109
|
+
parentConversationId: parentConversation,
|
|
1110
|
+
label: "Research task",
|
|
1111
|
+
objective: "research something",
|
|
1112
|
+
},
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
test("subagent_status with label returns status", async () => {
|
|
1116
|
+
const result = await executeSubagentStatus(
|
|
1117
|
+
{ label: "Research task" },
|
|
1118
|
+
makeContext(parentConversation),
|
|
1119
|
+
);
|
|
1120
|
+
expect(result.isError).toBe(false);
|
|
1121
|
+
const parsed = JSON.parse(result.content);
|
|
1122
|
+
expect(parsed.subagentId).toBe(subagentId);
|
|
1123
|
+
expect(parsed.label).toBe("Research task");
|
|
1124
|
+
expect(parsed.status).toBe("running");
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
test("subagent_status with lowercase label (case-insensitive)", async () => {
|
|
1128
|
+
const result = await executeSubagentStatus(
|
|
1129
|
+
{ label: "research task" },
|
|
1130
|
+
makeContext(parentConversation),
|
|
1131
|
+
);
|
|
1132
|
+
expect(result.isError).toBe(false);
|
|
1133
|
+
const parsed = JSON.parse(result.content);
|
|
1134
|
+
expect(parsed.subagentId).toBe(subagentId);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
test("subagent_status with nonexistent label returns error", async () => {
|
|
1138
|
+
const result = await executeSubagentStatus(
|
|
1139
|
+
{ label: "nonexistent" },
|
|
1140
|
+
makeContext(parentConversation),
|
|
1141
|
+
);
|
|
1142
|
+
expect(result.isError).toBe(true);
|
|
1143
|
+
expect(result.content).toContain("No subagent found");
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
test("subagent_message with label succeeds", async () => {
|
|
1147
|
+
const result = await executeSubagentMessage(
|
|
1148
|
+
{ label: "Research task", content: "hello" },
|
|
1149
|
+
makeContext(parentConversation),
|
|
1150
|
+
);
|
|
1151
|
+
expect(result.isError).toBe(false);
|
|
1152
|
+
const parsed = JSON.parse(result.content);
|
|
1153
|
+
expect(parsed.subagentId).toBe(subagentId);
|
|
1154
|
+
expect(parsed.message).toContain("Message sent");
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("subagent_read with label on completed subagent returns output", async () => {
|
|
1158
|
+
// Inject a completed subagent for the read test.
|
|
1159
|
+
const readSubId = "label-read-sub-1";
|
|
1160
|
+
injectSubagent(manager, readSubId, parentConversation, "completed", {
|
|
1161
|
+
config: {
|
|
1162
|
+
id: readSubId,
|
|
1163
|
+
parentConversationId: parentConversation,
|
|
1164
|
+
label: "Read task",
|
|
1165
|
+
objective: "read something",
|
|
1166
|
+
},
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
mockGetMessages = (convId: string) => {
|
|
1170
|
+
if (convId !== `conv-${readSubId}`) return null;
|
|
1171
|
+
return [
|
|
1172
|
+
{
|
|
1173
|
+
role: "assistant",
|
|
1174
|
+
content: JSON.stringify([
|
|
1175
|
+
{ type: "text", text: "Research findings here" },
|
|
1176
|
+
]),
|
|
1177
|
+
},
|
|
1178
|
+
];
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
const result = await executeSubagentRead(
|
|
1183
|
+
{ label: "Read task" },
|
|
1184
|
+
makeContext(parentConversation),
|
|
1185
|
+
);
|
|
1186
|
+
expect(result.isError).toBe(false);
|
|
1187
|
+
expect(result.content).toContain("Research findings here");
|
|
1188
|
+
} finally {
|
|
1189
|
+
mockGetMessages = () => null;
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
// ── Label collision & dispose guard ─────────────────────────────────
|
|
1195
|
+
|
|
1196
|
+
describe("Label collision and dispose guard", () => {
|
|
1197
|
+
test("disposing second subagent with same label keeps first reachable by label", () => {
|
|
1198
|
+
const manager = getSubagentManager();
|
|
1199
|
+
const parentConversation = "label-collision-sess";
|
|
1200
|
+
const firstId = "collision-sub-1";
|
|
1201
|
+
const secondId = "collision-sub-2";
|
|
1202
|
+
const sharedLabel = "Shared Worker";
|
|
1203
|
+
|
|
1204
|
+
// Inject two subagents with the same label — second overwrites label index.
|
|
1205
|
+
injectSubagent(manager, firstId, parentConversation, "running", {
|
|
1206
|
+
config: {
|
|
1207
|
+
id: firstId,
|
|
1208
|
+
parentConversationId: parentConversation,
|
|
1209
|
+
label: sharedLabel,
|
|
1210
|
+
objective: "first task",
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
injectSubagent(manager, secondId, parentConversation, "completed", {
|
|
1214
|
+
config: {
|
|
1215
|
+
id: secondId,
|
|
1216
|
+
parentConversationId: parentConversation,
|
|
1217
|
+
label: sharedLabel,
|
|
1218
|
+
objective: "second task",
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
// Label should currently resolve to the second subagent.
|
|
1223
|
+
expect(manager.getByLabel(sharedLabel, parentConversation)?.config.id).toBe(
|
|
1224
|
+
secondId,
|
|
1225
|
+
);
|
|
1226
|
+
|
|
1227
|
+
// Dispose the FIRST subagent — its label was already overwritten,
|
|
1228
|
+
// so the label index entry (pointing to second) must survive.
|
|
1229
|
+
manager.dispose(firstId);
|
|
1230
|
+
|
|
1231
|
+
const afterDispose = manager.getByLabel(sharedLabel, parentConversation);
|
|
1232
|
+
expect(afterDispose).toBeDefined();
|
|
1233
|
+
expect(afterDispose!.config.id).toBe(secondId);
|
|
1234
|
+
|
|
1235
|
+
// The second subagent should still be directly accessible too.
|
|
1236
|
+
expect(manager.getState(secondId)).toBeDefined();
|
|
1237
|
+
// And the first should be gone.
|
|
1238
|
+
expect(manager.getState(firstId)).toBeUndefined();
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
// ── Role-based spawn ──────────────────────────────────────────────
|
|
1243
|
+
|
|
1244
|
+
describe("Subagent role-based spawn", () => {
|
|
1245
|
+
test("spawn with role 'researcher' passes role to manager", async () => {
|
|
1246
|
+
const manager = getSubagentManager();
|
|
1247
|
+
const originalSpawn = manager.spawn.bind(manager);
|
|
1248
|
+
let capturedConfig: Record<string, unknown> | undefined;
|
|
1249
|
+
|
|
1250
|
+
manager.spawn = async (config: Record<string, unknown>) => {
|
|
1251
|
+
capturedConfig = config;
|
|
1252
|
+
return "role-researcher-id";
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
try {
|
|
1256
|
+
const result = await executeSubagentSpawn(
|
|
1257
|
+
{
|
|
1258
|
+
label: "Research task",
|
|
1259
|
+
objective: "Find pricing data",
|
|
1260
|
+
role: "researcher",
|
|
1261
|
+
},
|
|
1262
|
+
makeContext("sess-role-1", { sendToClient: () => {} }),
|
|
1263
|
+
);
|
|
1264
|
+
expect(result.isError).toBe(false);
|
|
1265
|
+
const parsed = JSON.parse(result.content);
|
|
1266
|
+
expect(parsed.subagentId).toBe("role-researcher-id");
|
|
1267
|
+
expect(capturedConfig).toBeDefined();
|
|
1268
|
+
expect(capturedConfig!.role).toBe("researcher");
|
|
1269
|
+
} finally {
|
|
1270
|
+
manager.spawn = originalSpawn;
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
test("spawn without role defaults to general (backwards compat)", async () => {
|
|
1275
|
+
const manager = getSubagentManager();
|
|
1276
|
+
const originalSpawn = manager.spawn.bind(manager);
|
|
1277
|
+
let capturedConfig: Record<string, unknown> | undefined;
|
|
1278
|
+
|
|
1279
|
+
manager.spawn = async (config: Record<string, unknown>) => {
|
|
1280
|
+
capturedConfig = config;
|
|
1281
|
+
return "role-default-id";
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
const result = await executeSubagentSpawn(
|
|
1286
|
+
{ label: "General task", objective: "Do something" },
|
|
1287
|
+
makeContext("sess-role-2", { sendToClient: () => {} }),
|
|
1288
|
+
);
|
|
1289
|
+
expect(result.isError).toBe(false);
|
|
1290
|
+
const parsed = JSON.parse(result.content);
|
|
1291
|
+
expect(parsed.subagentId).toBe("role-default-id");
|
|
1292
|
+
expect(capturedConfig).toBeDefined();
|
|
1293
|
+
// When role is not specified, it should not be present in config
|
|
1294
|
+
expect(capturedConfig!.role).toBeUndefined();
|
|
1295
|
+
} finally {
|
|
1296
|
+
manager.spawn = originalSpawn;
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
test("spawn with invalid role returns clear error message", async () => {
|
|
1301
|
+
const result = await executeSubagentSpawn(
|
|
1302
|
+
{
|
|
1303
|
+
label: "Bad role task",
|
|
1304
|
+
objective: "Should fail",
|
|
1305
|
+
role: "nonexistent-role",
|
|
1306
|
+
},
|
|
1307
|
+
makeContext("sess-role-invalid", { sendToClient: () => {} }),
|
|
1308
|
+
);
|
|
1309
|
+
expect(result.isError).toBe(true);
|
|
1310
|
+
expect(result.content).toContain("Invalid subagent role");
|
|
1311
|
+
expect(result.content).toContain("nonexistent-role");
|
|
1312
|
+
expect(result.content).toContain("Must be one of");
|
|
1313
|
+
expect(result.content).toContain("general");
|
|
1314
|
+
expect(result.content).toContain("researcher");
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
test("spawn tool definition includes role property", () => {
|
|
1318
|
+
const def = findTool("subagent_spawn");
|
|
1319
|
+
expect(def).toBeDefined();
|
|
1320
|
+
expect(def.input_schema.properties.role).toBeDefined();
|
|
1321
|
+
expect(def.input_schema.properties.role.type).toBe("string");
|
|
1322
|
+
expect(def.input_schema.properties.role.enum).toEqual([
|
|
1323
|
+
"general",
|
|
1324
|
+
"researcher",
|
|
1325
|
+
"coder",
|
|
1326
|
+
"planner",
|
|
1327
|
+
]);
|
|
1328
|
+
// role is not required
|
|
1329
|
+
expect(def.input_schema.required).not.toContain("role");
|
|
1330
|
+
});
|
|
1331
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the "Action Confirmation Mode" system prompt injection.
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - Prompt includes the section when `permission-controls-v2` flag is enabled
|
|
6
|
+
* AND `askBeforeActing` is `true`.
|
|
7
|
+
* - Prompt excludes the section when the flag is disabled.
|
|
8
|
+
* - Prompt excludes the section when `askBeforeActing` is `false`.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync } from "node:fs";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
// Mock platform to use a temp directory
|
|
14
|
+
const TEST_DIR = process.env.VELLUM_WORKSPACE_DIR!;
|
|
15
|
+
|
|
16
|
+
const noopLogger: Record<string, unknown> = new Proxy(
|
|
17
|
+
{} as Record<string, unknown>,
|
|
18
|
+
{
|
|
19
|
+
get: (_target, prop) => (prop === "child" ? () => noopLogger : () => {}),
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
24
|
+
const realLogger = require("../util/logger.js");
|
|
25
|
+
mock.module("../util/logger.js", () => ({
|
|
26
|
+
...realLogger,
|
|
27
|
+
getLogger: () => noopLogger,
|
|
28
|
+
getCliLogger: () => noopLogger,
|
|
29
|
+
truncateForLog: (v: string) => v,
|
|
30
|
+
initLogger: () => {},
|
|
31
|
+
pruneOldLogFiles: () => 0,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mock.module("../config/loader.js", () => ({
|
|
35
|
+
getConfig: () => ({
|
|
36
|
+
ui: {},
|
|
37
|
+
services: {
|
|
38
|
+
inference: {
|
|
39
|
+
mode: "your-own",
|
|
40
|
+
provider: "anthropic",
|
|
41
|
+
model: "claude-opus-4-6",
|
|
42
|
+
},
|
|
43
|
+
"image-generation": {
|
|
44
|
+
mode: "your-own",
|
|
45
|
+
provider: "gemini",
|
|
46
|
+
model: "gemini-3.1-flash-image-preview",
|
|
47
|
+
},
|
|
48
|
+
"web-search": { mode: "your-own", provider: "inference-provider-native" },
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
loadConfig: () => ({}),
|
|
52
|
+
loadRawConfig: () => ({}),
|
|
53
|
+
saveConfig: () => {},
|
|
54
|
+
saveRawConfig: () => {},
|
|
55
|
+
invalidateConfigCache: () => {},
|
|
56
|
+
getNestedValue: () => undefined,
|
|
57
|
+
setNestedValue: () => {},
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
61
|
+
const realUserReference = require("../prompts/user-reference.js");
|
|
62
|
+
mock.module("../prompts/user-reference.js", () => ({
|
|
63
|
+
...realUserReference,
|
|
64
|
+
resolveUserReference: () => "John",
|
|
65
|
+
resolveUserPronouns: () => null,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Controllable mocks for feature flags and permission mode
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
let flagEnabled = false;
|
|
73
|
+
let askBeforeActing = true;
|
|
74
|
+
|
|
75
|
+
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
76
|
+
isAssistantFeatureFlagEnabled: (key: string) => {
|
|
77
|
+
if (key === "permission-controls-v2") return flagEnabled;
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
_setOverridesForTesting: () => {},
|
|
81
|
+
clearFeatureFlagOverridesCache: () => {},
|
|
82
|
+
getAssistantFeatureFlagDefaults: () => ({}),
|
|
83
|
+
}));
|
|
84
|
+
|
|
85
|
+
mock.module("../permissions/permission-mode-store.js", () => ({
|
|
86
|
+
getMode: () => ({ askBeforeActing, hostAccess: false }),
|
|
87
|
+
initPermissionModeStore: () => {},
|
|
88
|
+
setAskBeforeActing: () => {},
|
|
89
|
+
setHostAccess: () => {},
|
|
90
|
+
onModeChanged: () => () => {},
|
|
91
|
+
resetForTesting: () => {},
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// Import after mocks
|
|
95
|
+
const { buildSystemPrompt } = await import("../prompts/system-prompt.js");
|
|
96
|
+
|
|
97
|
+
const ACTION_CONFIRMATION_HEADING = "## Action Confirmation Mode";
|
|
98
|
+
|
|
99
|
+
describe("Action Confirmation Mode system prompt injection", () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
102
|
+
flagEnabled = false;
|
|
103
|
+
askBeforeActing = true;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
flagEnabled = false;
|
|
108
|
+
askBeforeActing = true;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("includes section when flag enabled and askBeforeActing is true", () => {
|
|
112
|
+
flagEnabled = true;
|
|
113
|
+
askBeforeActing = true;
|
|
114
|
+
const result = buildSystemPrompt();
|
|
115
|
+
expect(result).toContain(ACTION_CONFIRMATION_HEADING);
|
|
116
|
+
expect(result).toContain('"Ask before acting" mode');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("excludes section when flag is disabled", () => {
|
|
120
|
+
flagEnabled = false;
|
|
121
|
+
askBeforeActing = true;
|
|
122
|
+
const result = buildSystemPrompt();
|
|
123
|
+
expect(result).not.toContain(ACTION_CONFIRMATION_HEADING);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("excludes section when askBeforeActing is false", () => {
|
|
127
|
+
flagEnabled = true;
|
|
128
|
+
askBeforeActing = false;
|
|
129
|
+
const result = buildSystemPrompt();
|
|
130
|
+
expect(result).not.toContain(ACTION_CONFIRMATION_HEADING);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("excludes section when both flag disabled and askBeforeActing false", () => {
|
|
134
|
+
flagEnabled = false;
|
|
135
|
+
askBeforeActing = false;
|
|
136
|
+
const result = buildSystemPrompt();
|
|
137
|
+
expect(result).not.toContain(ACTION_CONFIRMATION_HEADING);
|
|
138
|
+
});
|
|
139
|
+
});
|