@vellumai/assistant 0.10.3 → 0.10.4-staging.1
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/openapi.yaml +73 -56
- package/package.json +1 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
- package/src/__tests__/assistant-stream-state.test.ts +3 -76
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
- package/src/__tests__/channel-approval-routes.test.ts +21 -26
- package/src/__tests__/channel-delivery-store.test.ts +28 -0
- package/src/__tests__/channel-guardian.test.ts +82 -32
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
- package/src/__tests__/channel-reply-delivery.test.ts +6 -2
- package/src/__tests__/compaction-ledger-store.test.ts +128 -0
- package/src/__tests__/config-loader-backfill.test.ts +148 -0
- package/src/__tests__/consult-deadline.test.ts +60 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
- package/src/__tests__/contact-store-user-file.test.ts +7 -10
- package/src/__tests__/contacts-relay-reads.test.ts +6 -9
- package/src/__tests__/contacts-write.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
- package/src/__tests__/conversation-agent-loop.test.ts +98 -7
- package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-fork-crud.test.ts +354 -24
- package/src/__tests__/conversation-title-service.test.ts +222 -201
- package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
- package/src/__tests__/delete-propagation.test.ts +5 -3
- package/src/__tests__/dm-backfill.test.ts +6 -4
- package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
- package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
- package/src/__tests__/guardian-dispatch.test.ts +50 -5
- package/src/__tests__/guardian-routing-state.test.ts +6 -10
- package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
- package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
- package/src/__tests__/helpers/mock-logger.ts +1 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
- package/src/__tests__/invite-redemption-service.test.ts +273 -53
- package/src/__tests__/invite-routes-http.test.ts +34 -0
- package/src/__tests__/invite-service-ipc.test.ts +65 -2
- package/src/__tests__/list-messages-page-latest.test.ts +173 -4
- package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
- package/src/__tests__/non-member-access-request.test.ts +15 -13
- package/src/__tests__/onboarding-persona-write.test.ts +52 -22
- package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
- package/src/__tests__/persona-resolver.test.ts +75 -45
- package/src/__tests__/plugin-bootstrap.test.ts +13 -5
- package/src/__tests__/plugin-disabled-state.test.ts +190 -0
- package/src/__tests__/provider-usage-tracking.test.ts +1 -1
- package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
- package/src/__tests__/reaction-persistence.test.ts +51 -4
- package/src/__tests__/relay-server.test.ts +88 -31
- package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
- package/src/__tests__/settings-routes.test.ts +32 -0
- package/src/__tests__/slack-block-formatting.test.ts +1 -38
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
- package/src/__tests__/stt-hints.test.ts +6 -3
- package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
- package/src/__tests__/subagent-role-registry.test.ts +17 -4
- package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
- package/src/__tests__/subagent-tools.test.ts +398 -3
- package/src/__tests__/thread-backfill.test.ts +3 -3
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
- package/src/__tests__/tool-start-timestamp.test.ts +4 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
- package/src/__tests__/trusted-contact-verification.test.ts +79 -54
- package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
- package/src/__tests__/voice-invite-redemption.test.ts +183 -20
- package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
- package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
- package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
- package/src/agent/loop-exclusive-tool.test.ts +19 -15
- package/src/agent/loop-native-web-search.test.ts +200 -0
- package/src/agent/loop.ts +108 -1
- package/src/api/responses/conversation-message.ts +9 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -4
- package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
- package/src/calls/guardian-dispatch.ts +14 -11
- package/src/calls/inbound-trust-reader.ts +7 -1
- package/src/calls/relay-access-wait.ts +6 -6
- package/src/calls/relay-server.ts +22 -2
- package/src/calls/relay-setup-router.ts +10 -10
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/contacts.ts +10 -7
- package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
- package/src/cli/commands/memory/worker.ts +97 -30
- package/src/cli/commands/plugins.ts +3 -146
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
- package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
- package/src/cli/lib/publish-plugin.ts +231 -1
- package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
- package/src/config/bundled-skills/subagent/SKILL.md +16 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
- package/src/config/call-site-defaults.ts +0 -6
- package/src/config/llm-resolver.ts +0 -3
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/heartbeat.ts +2 -5
- package/src/config/schemas/llm.ts +3 -12
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/seed-inference-profiles.ts +76 -35
- package/src/config/sync-gated-profiles.ts +0 -3
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
- package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
- package/src/contacts/contact-store.ts +27 -237
- package/src/contacts/contacts-write.ts +18 -58
- package/src/contacts/gateway-channel-read.ts +51 -0
- package/src/contacts/member-write-relay.ts +25 -31
- package/src/contacts/types.ts +3 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
- package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
- package/src/daemon/conversation-agent-loop.ts +68 -61
- package/src/daemon/conversation-error.ts +7 -10
- package/src/daemon/conversation-tool-setup.ts +0 -10
- package/src/daemon/conversation.ts +10 -0
- package/src/daemon/external-plugins-bootstrap.ts +8 -2
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
- package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
- package/src/daemon/handlers/config-channels.ts +14 -29
- package/src/daemon/lifecycle.ts +16 -4
- package/src/daemon/message-types/surfaces.ts +2 -0
- package/src/heartbeat/heartbeat-service.ts +5 -0
- package/src/home/relationship-state-writer.ts +5 -0
- package/src/memory/__tests__/embedding-cache.test.ts +136 -0
- package/src/memory/compaction-ledger-store.ts +107 -0
- package/src/memory/conversation-crud.ts +136 -61
- package/src/memory/conversation-title-service.ts +173 -24
- package/src/memory/embedding-backend.ts +8 -1
- package/src/memory/embedding-cache.ts +139 -0
- package/src/memory/jobs-worker.ts +75 -29
- package/src/memory/memory-retrospective-job.ts +5 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
- package/src/memory/migrations/302-create-compaction-events.ts +107 -0
- package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
- package/src/memory/schema/contacts.ts +6 -2
- package/src/memory/schema/conversations.ts +39 -0
- package/src/memory/steps.ts +1090 -367
- package/src/memory/worker-control.ts +104 -18
- package/src/memory/worker-process.ts +17 -0
- package/src/messaging/channel-binding-metadata.ts +31 -0
- package/src/messaging/channel-binding-schema.ts +51 -0
- package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
- package/src/messaging/providers/a2a/deliver.ts +5 -1
- package/src/messaging/providers/a2a/transport.ts +10 -0
- package/src/messaging/providers/callback-routing.ts +48 -0
- package/src/messaging/providers/channel-transport.ts +55 -0
- package/src/messaging/providers/index.ts +65 -241
- package/src/messaging/providers/slack/binding-metadata.ts +62 -0
- package/src/messaging/providers/slack/transport.ts +92 -0
- package/src/messaging/providers/telegram-bot/transport.ts +51 -0
- package/src/messaging/providers/whatsapp/transport.ts +38 -0
- package/src/notifications/__tests__/broadcaster.test.ts +0 -8
- package/src/notifications/__tests__/connected-channels.test.ts +8 -36
- package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
- package/src/notifications/destination-resolver.ts +7 -23
- package/src/notifications/emit-signal.ts +5 -11
- package/src/plugins/defaults/index.ts +0 -35
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
- package/src/plugins/disabled-state.ts +31 -0
- package/src/plugins/registry.ts +55 -12
- package/src/prompts/persona-resolver.ts +43 -11
- package/src/providers/call-site-routing.ts +41 -0
- package/src/providers/provider-send-message.ts +6 -0
- package/src/providers/ratelimit.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/providers/retry.ts +6 -0
- package/src/providers/types.ts +13 -0
- package/src/providers/usage-tracking.ts +6 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
- package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
- package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
- package/src/runtime/access-request-helper.ts +1 -2
- package/src/runtime/actor-trust-resolver.ts +44 -17
- package/src/runtime/anchored-guardian.test.ts +7 -54
- package/src/runtime/anchored-guardian.ts +4 -53
- package/src/runtime/assistant-stream-state.ts +12 -74
- package/src/runtime/channel-reply-delivery.ts +3 -8
- package/src/runtime/guardian-vellum-migration.ts +18 -16
- package/src/runtime/invite-redemption-service.ts +25 -10
- package/src/runtime/local-actor-identity.test.ts +108 -0
- package/src/runtime/local-actor-identity.ts +27 -20
- package/src/runtime/member-verdict-cache.ts +0 -0
- package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
- package/src/runtime/routes/contact-routes.ts +40 -25
- package/src/runtime/routes/conversation-list-routes.ts +1 -29
- package/src/runtime/routes/conversation-routes.ts +27 -7
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
- package/src/runtime/routes/settings-routes.ts +8 -3
- package/src/runtime/services/conversation-serializer.ts +6 -49
- package/src/runtime/slack-block-formatting.ts +0 -15
- package/src/runtime/trust-verdict-consumer.ts +36 -41
- package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
- package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
- package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
- package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
- package/src/subagent/index.ts +1 -1
- package/src/subagent/manager.ts +245 -33
- package/src/subagent/types.ts +8 -1
- package/src/tools/registry.ts +10 -3
- package/src/tools/subagent/consult-deadline.ts +49 -0
- package/src/tools/subagent/spawn.ts +234 -5
- package/src/util/logger.ts +9 -0
- package/src/util/platform.ts +14 -0
- package/src/workspace/migrations/031-drop-user-md.ts +232 -148
- package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
- package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
- package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
- package/src/plugins/defaults/advisor/config.ts +0 -21
- package/src/plugins/defaults/advisor/consult.ts +0 -197
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/advisor/package.json +0 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
|
@@ -2,18 +2,16 @@
|
|
|
2
2
|
* Regression: the SSE subscribe path must resolve the actor principal from the
|
|
3
3
|
* SAME guardian source as the send/result routes.
|
|
4
4
|
*
|
|
5
|
-
* The send/result routes resolve the actor principal via the async
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* targeted result submissions 403.
|
|
5
|
+
* The send/result routes resolve the actor principal via the async
|
|
6
|
+
* `findLocalGuardianPrincipalId`. The SSE eager-subscribe path cannot await and
|
|
7
|
+
* uses the sync `findLocalGuardianPrincipalIdFromStore`. Both read the
|
|
8
|
+
* gateway-owned guardian binding — async via the cached IPC read, sync via the
|
|
9
|
+
* IO-free cache snapshot — so the event hub registers the SSE client under the
|
|
10
|
+
* SAME principal the turn/result paths use; otherwise targeted result
|
|
11
|
+
* submissions 403.
|
|
13
12
|
*
|
|
14
|
-
* These tests pin the invariant by priming the gateway-delivery cache
|
|
15
|
-
*
|
|
16
|
-
* resolvers agree; and that a cold cache falls back to the local store.
|
|
13
|
+
* These tests pin the invariant by priming the gateway-delivery cache and
|
|
14
|
+
* asserting both resolvers agree; and that a cold cache yields no principal.
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
17
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -30,17 +28,6 @@ mock.module("../ipc/gateway-client.js", () => ({
|
|
|
30
28
|
resetPersistentClient: () => {},
|
|
31
29
|
}));
|
|
32
30
|
|
|
33
|
-
// ── Local store mock (the stale fallback source) ─────────────────────────────
|
|
34
|
-
|
|
35
|
-
let storePrincipalId: string | undefined;
|
|
36
|
-
|
|
37
|
-
mock.module("../contacts/contact-store.js", () => ({
|
|
38
|
-
findGuardianForChannel: (channelType: string) =>
|
|
39
|
-
storePrincipalId && channelType === "vellum"
|
|
40
|
-
? { contact: { principalId: storePrincipalId }, channel: {} }
|
|
41
|
-
: null,
|
|
42
|
-
}));
|
|
43
|
-
|
|
44
31
|
import {
|
|
45
32
|
__resetGuardianDeliveryCacheForTest,
|
|
46
33
|
getGuardianDelivery,
|
|
@@ -62,31 +49,21 @@ describe("SSE actor principal resolves from the same guardian source as send/res
|
|
|
62
49
|
beforeEach(() => {
|
|
63
50
|
__resetGuardianDeliveryCacheForTest();
|
|
64
51
|
ipcGuardians = [];
|
|
65
|
-
storePrincipalId = undefined;
|
|
66
52
|
});
|
|
67
53
|
|
|
68
|
-
test("warm gateway cache: sync (SSE) and async (send/result) resolve the SAME principal
|
|
69
|
-
// Gateway binding is canonical; local store row is stale (different id).
|
|
54
|
+
test("warm gateway cache: sync (SSE) and async (send/result) resolve the SAME principal", async () => {
|
|
70
55
|
ipcGuardians = [gatewayVellumGuardian];
|
|
71
|
-
storePrincipalId = "guardian-stale-local";
|
|
72
56
|
|
|
73
57
|
// Prime the cache the way the async hot paths do.
|
|
74
58
|
const asyncPrincipalId = await findLocalGuardianPrincipalId();
|
|
75
59
|
expect(asyncPrincipalId).toBe("guardian-from-gateway");
|
|
76
60
|
|
|
77
|
-
// SSE's sync resolver reads the same cached gateway snapshot
|
|
78
|
-
//
|
|
61
|
+
// SSE's sync resolver reads the same cached gateway snapshot — so the
|
|
62
|
+
// principals match and host-proxy targeting works.
|
|
79
63
|
expect(findLocalGuardianPrincipalIdFromStore()).toBe(asyncPrincipalId);
|
|
80
64
|
});
|
|
81
65
|
|
|
82
|
-
test("cold cache: sync resolver
|
|
83
|
-
// Nothing primed the cache; only the local store has a binding.
|
|
84
|
-
storePrincipalId = "guardian-stale-local";
|
|
85
|
-
|
|
86
|
-
expect(findLocalGuardianPrincipalIdFromStore()).toBe("guardian-stale-local");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("cold cache with no store binding: sync resolver returns undefined", () => {
|
|
66
|
+
test("cold cache: sync resolver returns undefined", () => {
|
|
90
67
|
expect(findLocalGuardianPrincipalIdFromStore()).toBeUndefined();
|
|
91
68
|
});
|
|
92
69
|
|
|
@@ -318,13 +318,12 @@ function makeContact(displayName: string): ContactWithChannels {
|
|
|
318
318
|
id: `contact-${displayName.toLowerCase()}`,
|
|
319
319
|
displayName,
|
|
320
320
|
notes: null,
|
|
321
|
+
role: "contact",
|
|
321
322
|
lastInteraction: null,
|
|
322
323
|
interactionCount: 0,
|
|
323
324
|
createdAt: now,
|
|
324
325
|
updatedAt: now,
|
|
325
|
-
role: "contact",
|
|
326
326
|
contactType: "human",
|
|
327
|
-
principalId: null,
|
|
328
327
|
userFile: null,
|
|
329
328
|
channels: [],
|
|
330
329
|
};
|
|
@@ -345,7 +344,11 @@ describe("resolveCallHints", () => {
|
|
|
345
344
|
|
|
346
345
|
test("guardian displayName for hints comes from the gateway binding", async () => {
|
|
347
346
|
mockGuardianDelivery = [
|
|
348
|
-
{
|
|
347
|
+
{
|
|
348
|
+
channelType: "phone",
|
|
349
|
+
status: "active",
|
|
350
|
+
displayName: "GatewayGuardian",
|
|
351
|
+
},
|
|
349
352
|
];
|
|
350
353
|
|
|
351
354
|
await resolveCallHints(null, []);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for decoupling message-inheritance from prompt source and role on
|
|
3
|
+
* context-inheriting subagents (forks).
|
|
4
|
+
*
|
|
5
|
+
* A fork normally pins the parent's system prompt verbatim and keeps the
|
|
6
|
+
* `general` role so its KV cache stays aligned with the parent. These tests
|
|
7
|
+
* verify the opt-out paths: a fork may supply its own `systemPromptOverride`
|
|
8
|
+
* (which is used as-is and does NOT set `hasSystemPromptOverride`), and a fork
|
|
9
|
+
* may carry an explicit read-only role (whose tool allowlist is applied). A
|
|
10
|
+
* plain fork (no override, no role) must still behave exactly as before.
|
|
11
|
+
*
|
|
12
|
+
* The harness stubs `Conversation` and the spawn() dependencies so we can call
|
|
13
|
+
* `SubagentManager.spawn()` directly and capture the constructed conversation's
|
|
14
|
+
* system prompt, `hasSystemPromptOverride` flag, and allowed-tools set.
|
|
15
|
+
*/
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
17
|
+
|
|
18
|
+
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
19
|
+
|
|
20
|
+
// ── Captured constructor state ──────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
interface CapturedConversationState {
|
|
23
|
+
systemPrompt: string;
|
|
24
|
+
hasSystemPromptOverride: boolean;
|
|
25
|
+
allowedTools: Set<string> | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const capturedConversations: CapturedConversationState[] = [];
|
|
29
|
+
|
|
30
|
+
// Stub Conversation so spawn() never runs an agent loop — we only inspect how
|
|
31
|
+
// it was constructed and configured.
|
|
32
|
+
class FakeConversation {
|
|
33
|
+
private readonly captured: CapturedConversationState;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
_id: string,
|
|
37
|
+
_provider: unknown,
|
|
38
|
+
systemPrompt: string,
|
|
39
|
+
_sendToClient: (msg: ServerMessage) => void,
|
|
40
|
+
_workingDir: string,
|
|
41
|
+
_options?: unknown,
|
|
42
|
+
) {
|
|
43
|
+
this.captured = {
|
|
44
|
+
systemPrompt,
|
|
45
|
+
hasSystemPromptOverride: false,
|
|
46
|
+
allowedTools: undefined,
|
|
47
|
+
};
|
|
48
|
+
capturedConversations.push(this.captured);
|
|
49
|
+
}
|
|
50
|
+
set hasSystemPromptOverride(value: boolean) {
|
|
51
|
+
this.captured.hasSystemPromptOverride = value;
|
|
52
|
+
}
|
|
53
|
+
get hasSystemPromptOverride(): boolean {
|
|
54
|
+
return this.captured.hasSystemPromptOverride;
|
|
55
|
+
}
|
|
56
|
+
conversationType = "background";
|
|
57
|
+
updateClient() {}
|
|
58
|
+
setIsSubagent() {}
|
|
59
|
+
setTrustContext() {}
|
|
60
|
+
setAuthContext() {}
|
|
61
|
+
getAuthContext() {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
setAssistantId() {}
|
|
65
|
+
setSubagentAllowedTools(tools: Set<string>) {
|
|
66
|
+
this.captured.allowedTools = tools;
|
|
67
|
+
}
|
|
68
|
+
setPreactivatedSkillIds() {}
|
|
69
|
+
injectInheritedContext() {}
|
|
70
|
+
abort() {}
|
|
71
|
+
dispose() {}
|
|
72
|
+
messages = [];
|
|
73
|
+
usageStats = { inputTokens: 0, outputTokens: 0, estimatedCost: 0 };
|
|
74
|
+
sendToClient() {}
|
|
75
|
+
persistUserMessage() {
|
|
76
|
+
return { id: "msg-id", deduplicated: false };
|
|
77
|
+
}
|
|
78
|
+
runAgentLoop() {
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
getCurrentSystemPrompt() {
|
|
82
|
+
return "resolved-parent-prompt";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
mock.module("../daemon/conversation.js", () => ({
|
|
87
|
+
Conversation: FakeConversation,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
mock.module("../memory/conversation-bootstrap.js", () => ({
|
|
91
|
+
bootstrapConversation: () => ({ id: "conv-id" }),
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
// Resolve a stub provider without touching connections/DB.
|
|
95
|
+
const providerStub = { name: "anthropic", sendMessage: async () => ({}) };
|
|
96
|
+
|
|
97
|
+
mock.module("../providers/connection-resolution.js", () => ({
|
|
98
|
+
resolveDefaultProvider: async () => providerStub,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
mock.module("../providers/call-site-routing.js", () => ({
|
|
102
|
+
wrapWithCallSiteRouting: (p: unknown) => p,
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
mock.module("../config/loader.js", () => ({
|
|
106
|
+
getConfig: () => ({
|
|
107
|
+
llm: { default: { provider: "anthropic", model: "claude-opus-4-7" } },
|
|
108
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
109
|
+
}),
|
|
110
|
+
}));
|
|
111
|
+
|
|
112
|
+
mock.module("../config/llm-resolver.js", () => ({
|
|
113
|
+
resolveCallSiteConfig: () => ({ provider: "anthropic", maxTokens: 8192 }),
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
mock.module("../util/logger.js", () => ({
|
|
117
|
+
getLogger: () =>
|
|
118
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
// ── Imports (after mocks) ───────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
import { clearConversations } from "../daemon/conversation-registry.js";
|
|
124
|
+
import { SubagentManager } from "../subagent/manager.js";
|
|
125
|
+
import type { SubagentConfig } from "../subagent/types.js";
|
|
126
|
+
|
|
127
|
+
const PARENT_PROMPT = "You are the parent's system prompt.";
|
|
128
|
+
|
|
129
|
+
type SpawnConfig = Omit<SubagentConfig, "id">;
|
|
130
|
+
|
|
131
|
+
function makeForkSpawnConfig(
|
|
132
|
+
overrides: Partial<SpawnConfig> = {},
|
|
133
|
+
): SpawnConfig {
|
|
134
|
+
return {
|
|
135
|
+
parentConversationId: `parent-${Math.random().toString(36).slice(2)}`,
|
|
136
|
+
label: "test fork",
|
|
137
|
+
objective: "do something",
|
|
138
|
+
fork: true,
|
|
139
|
+
parentSystemPrompt: PARENT_PROMPT,
|
|
140
|
+
...overrides,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe("SubagentManager fork — prompt source and role decoupling", () => {
|
|
145
|
+
let manager: SubagentManager;
|
|
146
|
+
|
|
147
|
+
beforeEach(() => {
|
|
148
|
+
clearConversations();
|
|
149
|
+
capturedConversations.length = 0;
|
|
150
|
+
manager = new SubagentManager();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
afterEach(() => {
|
|
154
|
+
(manager as unknown as { stopSweep: () => void }).stopSweep();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/** Spawn a fork and return the single conversation that spawn() constructed. */
|
|
158
|
+
async function spawnFork(
|
|
159
|
+
overrides: Partial<SpawnConfig> = {},
|
|
160
|
+
): Promise<CapturedConversationState> {
|
|
161
|
+
await manager.spawn(makeForkSpawnConfig(overrides), () => {});
|
|
162
|
+
const created = capturedConversations[0];
|
|
163
|
+
if (!created) throw new Error("Expected a subagent conversation");
|
|
164
|
+
return created;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
test("fork with systemPromptOverride uses that prompt and does not set hasSystemPromptOverride", async () => {
|
|
168
|
+
const overridePrompt = "You are an advisor. Frame the context as advice.";
|
|
169
|
+
const created = await spawnFork({ systemPromptOverride: overridePrompt });
|
|
170
|
+
|
|
171
|
+
expect(created.systemPrompt).toBe(overridePrompt);
|
|
172
|
+
expect(created.hasSystemPromptOverride).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("fork with explicit read-only role applies the role allowlist via setSubagentAllowedTools", async () => {
|
|
176
|
+
const created = await spawnFork({ role: "researcher" });
|
|
177
|
+
|
|
178
|
+
// The researcher role is read-only; its allowlist must be applied even for
|
|
179
|
+
// a fork.
|
|
180
|
+
expect(created.allowedTools).toBeInstanceOf(Set);
|
|
181
|
+
expect(created.allowedTools?.has("web_search")).toBe(true);
|
|
182
|
+
expect(created.allowedTools?.has("file_read")).toBe(true);
|
|
183
|
+
expect(created.allowedTools?.has("bash")).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("plain fork (no override, no role) keeps parent prompt verbatim, general tools, and sets hasSystemPromptOverride", async () => {
|
|
187
|
+
const created = await spawnFork();
|
|
188
|
+
|
|
189
|
+
// Parent prompt verbatim, no tool filter (general role has no allowlist),
|
|
190
|
+
// and the prompt is pinned for KV-cache alignment.
|
|
191
|
+
expect(created.systemPrompt).toBe(PARENT_PROMPT);
|
|
192
|
+
expect(created.allowedTools).toBeUndefined();
|
|
193
|
+
expect(created.hasSystemPromptOverride).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -273,7 +273,7 @@ describe("SubagentManager fork spawn", () => {
|
|
|
273
273
|
expect(memoryPolicy.includeDefaultFallback).toBe(true);
|
|
274
274
|
});
|
|
275
275
|
|
|
276
|
-
test("fork
|
|
276
|
+
test("fork defaults to general role (which has no tool allowlist)", async () => {
|
|
277
277
|
const manager = new SubagentManager();
|
|
278
278
|
const subagentId = "sub-fork-role";
|
|
279
279
|
|
|
@@ -286,14 +286,16 @@ describe("SubagentManager fork spawn", () => {
|
|
|
286
286
|
fakeConversation.injectInheritedContext = () => {};
|
|
287
287
|
fakeConversation.setSubagentAllowedTools = () => {};
|
|
288
288
|
|
|
289
|
-
//
|
|
290
|
-
//
|
|
289
|
+
// A fork that does not request a role defaults to "general", which has
|
|
290
|
+
// `allowedTools: undefined` — so no tool filter is applied. (An explicit
|
|
291
|
+
// non-general role on a fork IS honored; that path is covered in
|
|
292
|
+
// subagent-fork-prompt-role.test.ts.)
|
|
291
293
|
const state = makeState(
|
|
292
294
|
subagentId,
|
|
293
295
|
{ isFork: true },
|
|
294
296
|
{
|
|
295
297
|
fork: true,
|
|
296
|
-
role: "general",
|
|
298
|
+
role: "general",
|
|
297
299
|
parentMessages: FAKE_PARENT_MESSAGES,
|
|
298
300
|
parentSystemPrompt: "Parent system prompt.",
|
|
299
301
|
},
|
|
@@ -303,9 +305,6 @@ describe("SubagentManager fork spawn", () => {
|
|
|
303
305
|
|
|
304
306
|
await asInternals(manager).runSubagent(subagentId, "Do something");
|
|
305
307
|
|
|
306
|
-
// Tool filtering is only applied in spawn(), not runSubagent(), so we
|
|
307
|
-
// verify the logic directly: forks skip setSubagentAllowedTools.
|
|
308
|
-
// For this test, we verify the fork's role is general (which has no allowedTools).
|
|
309
308
|
expect(state.config.role).toBe("general");
|
|
310
309
|
|
|
311
310
|
asInternals(manager).stopSweep();
|
|
@@ -13,6 +13,7 @@ const ALL_ROLES: SubagentRole[] = [
|
|
|
13
13
|
"coder",
|
|
14
14
|
"planner",
|
|
15
15
|
"investigator",
|
|
16
|
+
"advisor",
|
|
16
17
|
];
|
|
17
18
|
|
|
18
19
|
describe("SUBAGENT_ROLE_REGISTRY", () => {
|
|
@@ -32,18 +33,30 @@ describe("SUBAGENT_ROLE_REGISTRY", () => {
|
|
|
32
33
|
expect(SUBAGENT_ROLE_REGISTRY.general.allowedTools).toBeUndefined();
|
|
33
34
|
});
|
|
34
35
|
|
|
35
|
-
test("all
|
|
36
|
+
test("all scoped tool-using roles have allowedTools as a non-empty array", () => {
|
|
36
37
|
for (const role of ALL_ROLES) {
|
|
37
|
-
|
|
38
|
+
// 'general' has no filter (undefined); 'advisor' is tool-less (empty).
|
|
39
|
+
if (role === "general" || role === "advisor") continue;
|
|
38
40
|
const config = SUBAGENT_ROLE_REGISTRY[role];
|
|
39
41
|
expect(Array.isArray(config.allowedTools)).toBe(true);
|
|
40
42
|
expect(config.allowedTools!.length).toBeGreaterThan(0);
|
|
41
43
|
}
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
test(
|
|
46
|
+
test("advisor is tool-less with an empty allowedTools array", () => {
|
|
47
|
+
const config = SUBAGENT_ROLE_REGISTRY.advisor;
|
|
48
|
+
expect(config.allowedTools).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("SubagentRole type includes advisor", () => {
|
|
52
|
+
const advisor: SubagentRole = "advisor";
|
|
53
|
+
expect(SUBAGENT_ROLE_REGISTRY[advisor]).toBeDefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('every role with a non-empty allowlist includes "notify_parent"', () => {
|
|
45
57
|
for (const [_role, config] of Object.entries(SUBAGENT_ROLE_REGISTRY)) {
|
|
46
|
-
|
|
58
|
+
// 'advisor' is tool-less (empty allowlist) and intentionally has none.
|
|
59
|
+
if (config.allowedTools !== undefined && config.allowedTools.length > 0) {
|
|
47
60
|
expect(config.allowedTools).toContain("notify_parent");
|
|
48
61
|
}
|
|
49
62
|
}
|