@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
|
@@ -42,6 +42,7 @@ mock.module("../../util/logger.js", () => ({
|
|
|
42
42
|
// the a2a.enabled flag. We use the real config system backed by initializeDb's
|
|
43
43
|
// workspace directory.
|
|
44
44
|
|
|
45
|
+
import { seedContactChannel } from "../../__tests__/helpers/seed-contact-channel.js";
|
|
45
46
|
import {
|
|
46
47
|
invalidateConfigCache,
|
|
47
48
|
loadRawConfig,
|
|
@@ -80,6 +81,15 @@ const originalFetch = globalThis.fetch;
|
|
|
80
81
|
// Helpers
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
83
|
|
|
84
|
+
/** Read a channel's local ACL columns directly to assert the gateway dual-write. */
|
|
85
|
+
function aclColumns(
|
|
86
|
+
channelId: string,
|
|
87
|
+
): { status: string; policy: string } | null {
|
|
88
|
+
return getSqlite()
|
|
89
|
+
.query("SELECT status, policy FROM contact_channels WHERE id = ?")
|
|
90
|
+
.get(channelId) as { status: string; policy: string } | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
function resetTables(): void {
|
|
84
94
|
const sqlite = getSqlite();
|
|
85
95
|
sqlite.run("DELETE FROM a2a_tasks");
|
|
@@ -164,17 +174,16 @@ describe("e2e: trusted contact setup", () => {
|
|
|
164
174
|
{
|
|
165
175
|
type: "a2a",
|
|
166
176
|
address: "assistant-b",
|
|
167
|
-
status: "active",
|
|
168
|
-
policy: "allow",
|
|
169
177
|
},
|
|
170
178
|
],
|
|
171
179
|
});
|
|
172
180
|
|
|
181
|
+
// upsertContact persists the a2a channel identity; the gateway owns the ACL
|
|
182
|
+
// status verdict.
|
|
173
183
|
const contact = findContactByAddress("a2a", "assistant-b");
|
|
174
184
|
expect(contact).not.toBeNull();
|
|
175
185
|
expect(contact!.channels.some((ch) => ch.type === "a2a")).toBe(true);
|
|
176
186
|
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
|
|
177
|
-
expect(a2aChannel!.status).toBe("active");
|
|
178
187
|
expect(a2aChannel!.address).toBe("assistant-b");
|
|
179
188
|
});
|
|
180
189
|
});
|
|
@@ -355,21 +364,13 @@ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
|
|
|
355
364
|
});
|
|
356
365
|
|
|
357
366
|
test("trusted contact exists with active a2a channel — ACL passes", async () => {
|
|
358
|
-
const { upsertContact } = await import("../../contacts/contact-store.js");
|
|
359
|
-
|
|
360
367
|
// Pre-create a trusted contact for the sender
|
|
361
|
-
|
|
368
|
+
seedContactChannel({
|
|
369
|
+
sourceChannel: "a2a",
|
|
370
|
+
externalUserId: "trusted-assistant",
|
|
362
371
|
displayName: "Trusted Bot",
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
channels: [
|
|
366
|
-
{
|
|
367
|
-
type: "a2a",
|
|
368
|
-
address: "trusted-assistant",
|
|
369
|
-
status: "active",
|
|
370
|
-
policy: "allow",
|
|
371
|
-
},
|
|
372
|
-
],
|
|
372
|
+
status: "active",
|
|
373
|
+
policy: "allow",
|
|
373
374
|
});
|
|
374
375
|
|
|
375
376
|
// Verify the contact exists (the ACL check the runtime performs)
|
|
@@ -378,8 +379,9 @@ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
|
|
|
378
379
|
|
|
379
380
|
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
|
|
380
381
|
expect(a2aChannel).toBeTruthy();
|
|
381
|
-
|
|
382
|
-
expect(
|
|
382
|
+
const acl = aclColumns(a2aChannel!.id);
|
|
383
|
+
expect(acl!.status).toBe("active");
|
|
384
|
+
expect(acl!.policy).toBe("allow");
|
|
383
385
|
|
|
384
386
|
// A task from this sender would pass the ACL check
|
|
385
387
|
const msg = makeRequestMessage();
|
|
@@ -391,28 +393,21 @@ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
|
|
|
391
393
|
});
|
|
392
394
|
|
|
393
395
|
test("contact exists but channel is blocked — ACL would reject", async () => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
396
|
+
seedContactChannel({
|
|
397
|
+
sourceChannel: "a2a",
|
|
398
|
+
externalUserId: "blocked-assistant",
|
|
397
399
|
displayName: "Blocked Bot",
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
channels: [
|
|
401
|
-
{
|
|
402
|
-
type: "a2a",
|
|
403
|
-
address: "blocked-assistant",
|
|
404
|
-
status: "blocked",
|
|
405
|
-
policy: "deny",
|
|
406
|
-
},
|
|
407
|
-
],
|
|
400
|
+
status: "blocked",
|
|
401
|
+
policy: "deny",
|
|
408
402
|
});
|
|
409
403
|
|
|
410
404
|
const contact = findContactByAddress("a2a", "blocked-assistant");
|
|
411
405
|
expect(contact).not.toBeNull();
|
|
412
406
|
|
|
413
407
|
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
|
|
414
|
-
|
|
415
|
-
expect(
|
|
408
|
+
const acl = aclColumns(a2aChannel!.id);
|
|
409
|
+
expect(acl!.status).toBe("blocked");
|
|
410
|
+
expect(acl!.policy).toBe("deny");
|
|
416
411
|
});
|
|
417
412
|
});
|
|
418
413
|
|
|
@@ -507,26 +502,19 @@ describe("e2e: full A2A round-trip", () => {
|
|
|
507
502
|
setConfigEnabled(true);
|
|
508
503
|
|
|
509
504
|
// Step 1: Create trusted contact for Assistant B (platform-mediated)
|
|
510
|
-
|
|
505
|
+
seedContactChannel({
|
|
506
|
+
sourceChannel: "a2a",
|
|
507
|
+
externalUserId: "assistant-b",
|
|
511
508
|
displayName: "Assistant B",
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
channels: [
|
|
515
|
-
{
|
|
516
|
-
type: "a2a",
|
|
517
|
-
address: "assistant-b",
|
|
518
|
-
status: "active",
|
|
519
|
-
policy: "allow",
|
|
520
|
-
},
|
|
521
|
-
],
|
|
509
|
+
status: "active",
|
|
510
|
+
policy: "allow",
|
|
522
511
|
});
|
|
523
512
|
|
|
524
513
|
// Step 2: Verify trusted contact was created
|
|
525
514
|
const contact = findContactByAddress("a2a", "assistant-b");
|
|
526
515
|
expect(contact).not.toBeNull();
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
516
|
+
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a")!;
|
|
517
|
+
expect(aclColumns(a2aChannel.id)!.status).toBe("active");
|
|
530
518
|
|
|
531
519
|
// Step 3: Simulate inbound A2A message from B (as if B sent us a request)
|
|
532
520
|
const inboundMsg = makeRequestMessage({
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Verifies the agent loop's exclusive-tool dispatch: when a tool the loop is
|
|
3
|
-
* told is exclusive
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* told is exclusive appears in a multi-call turn, only that tool runs and the
|
|
4
|
+
* siblings are deferred un-run with a benign result — so the model incorporates
|
|
5
|
+
* the exclusive tool's output before acting on anything else. Drives the REAL
|
|
6
|
+
* loop, mocking only the provider boundary.
|
|
7
7
|
*/
|
|
8
8
|
import { describe, expect, test } from "bun:test";
|
|
9
9
|
|
|
@@ -55,7 +55,7 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
55
55
|
test("runs the exclusive tool alone and defers sibling calls un-run", async () => {
|
|
56
56
|
const { provider } = createMockProvider([
|
|
57
57
|
toolUseTurn([
|
|
58
|
-
{ id: "call-
|
|
58
|
+
{ id: "call-exclusive", name: "exclusive_tool" },
|
|
59
59
|
{ id: "call-edit", name: "write_file" },
|
|
60
60
|
]),
|
|
61
61
|
endTurn("done"),
|
|
@@ -67,7 +67,11 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
67
67
|
systemPrompt: "sys",
|
|
68
68
|
conversationId: "excl-1",
|
|
69
69
|
tools: [
|
|
70
|
-
{
|
|
70
|
+
{
|
|
71
|
+
name: "exclusive_tool",
|
|
72
|
+
description: "",
|
|
73
|
+
input_schema: { type: "object" },
|
|
74
|
+
},
|
|
71
75
|
{
|
|
72
76
|
name: "write_file",
|
|
73
77
|
description: "",
|
|
@@ -78,7 +82,7 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
78
82
|
executed.push(name);
|
|
79
83
|
return { content: `ran ${name}`, isError: false };
|
|
80
84
|
},
|
|
81
|
-
isExclusiveTool: (name) => name === "
|
|
85
|
+
isExclusiveTool: (name) => name === "exclusive_tool",
|
|
82
86
|
});
|
|
83
87
|
|
|
84
88
|
const { history } = await loop.run({
|
|
@@ -87,19 +91,19 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
87
91
|
});
|
|
88
92
|
|
|
89
93
|
// Only the exclusive tool actually executed.
|
|
90
|
-
expect(executed).toEqual(["
|
|
94
|
+
expect(executed).toEqual(["exclusive_tool"]);
|
|
91
95
|
|
|
92
96
|
const results = toolResults(history);
|
|
93
|
-
const
|
|
94
|
-
(b) => b.tool_use_id === "call-
|
|
97
|
+
const exclusiveResult = results.find(
|
|
98
|
+
(b) => b.tool_use_id === "call-exclusive",
|
|
95
99
|
)!;
|
|
96
100
|
const editResult = results.find((b) => b.tool_use_id === "call-edit")!;
|
|
97
101
|
|
|
98
|
-
// The
|
|
99
|
-
// can re-issue it after reading the guidance.
|
|
100
|
-
expect(
|
|
102
|
+
// The exclusive tool ran; the sibling came back un-run (not an error) so the
|
|
103
|
+
// model can re-issue it after reading the guidance.
|
|
104
|
+
expect(exclusiveResult.content).toBe("ran exclusive_tool");
|
|
101
105
|
expect(editResult.content).toContain("not run");
|
|
102
|
-
expect(editResult.content).toContain("
|
|
106
|
+
expect(editResult.content).toContain("exclusive_tool");
|
|
103
107
|
expect(editResult.is_error).toBe(false);
|
|
104
108
|
});
|
|
105
109
|
|
|
@@ -133,7 +137,7 @@ describe("AgentLoop — exclusive tool deferral", () => {
|
|
|
133
137
|
executed.push(name);
|
|
134
138
|
return { content: `ran ${name}`, isError: false };
|
|
135
139
|
},
|
|
136
|
-
isExclusiveTool: (name) => name === "
|
|
140
|
+
isExclusiveTool: (name) => name === "exclusive_tool",
|
|
137
141
|
});
|
|
138
142
|
|
|
139
143
|
const { history } = await loop.run({
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the agent loop's provider-native web-search gate (used by the
|
|
3
|
+
* tool-less advisor consult): when `enableNativeWebSearch` is set, the loop
|
|
4
|
+
* appends a `web_search`-named SERVER tool to the outbound request and forces
|
|
5
|
+
* `tool_choice: auto` — but ONLY when the provider/model the call ACTUALLY
|
|
6
|
+
* routes to reports native-search support. A non-native target gets nothing,
|
|
7
|
+
* and the consult stays tool-less (no client `web_search` tool surfaced).
|
|
8
|
+
*
|
|
9
|
+
* The gate prefers the routing-aware `supportsNativeWebSearchFor(options)` (the
|
|
10
|
+
* routed (provider, model)'s capability) over the construction-time
|
|
11
|
+
* `supportsNativeWebSearch` snapshot. The advisor's `advisorProfile` can route
|
|
12
|
+
* `subagentSpawn` to a provider/model whose native-search support DIFFERS from
|
|
13
|
+
* the default, so the routed capability — not the default's — must drive the
|
|
14
|
+
* decision in both directions. Drives the REAL loop, mocking only the provider
|
|
15
|
+
* boundary.
|
|
16
|
+
*/
|
|
17
|
+
import { describe, expect, test } from "bun:test";
|
|
18
|
+
|
|
19
|
+
import { createMockProvider } from "../__tests__/helpers/mock-provider.js";
|
|
20
|
+
import type {
|
|
21
|
+
Provider,
|
|
22
|
+
ProviderResponse,
|
|
23
|
+
SendMessageOptions,
|
|
24
|
+
} from "../providers/types.js";
|
|
25
|
+
import { AgentLoop } from "./loop.js";
|
|
26
|
+
|
|
27
|
+
const endTurn = (text: string): ProviderResponse => ({
|
|
28
|
+
content: [{ type: "text", text }],
|
|
29
|
+
model: "mock-model",
|
|
30
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
31
|
+
stopReason: "end_turn",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const baseRun = {
|
|
35
|
+
requestId: "req-web",
|
|
36
|
+
onEvent: () => {},
|
|
37
|
+
callSite: "subagentSpawn" as const,
|
|
38
|
+
trust: { sourceChannel: "vellum" as const, trustClass: "unknown" as const },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const userMessages = [
|
|
42
|
+
{
|
|
43
|
+
role: "user" as const,
|
|
44
|
+
content: [{ type: "text" as const, text: "advise" }],
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/** Build a tool-less loop (mirrors the advisor consult) with the given flag. */
|
|
49
|
+
function buildAdvisorLoop(
|
|
50
|
+
provider: Provider,
|
|
51
|
+
enableNativeWebSearch: boolean,
|
|
52
|
+
): AgentLoop {
|
|
53
|
+
return new AgentLoop({
|
|
54
|
+
provider,
|
|
55
|
+
systemPrompt: "advisor system",
|
|
56
|
+
conversationId: "advisor-1",
|
|
57
|
+
// Tool-less for client tools — exactly the advisor role's empty allowlist.
|
|
58
|
+
config: { enableNativeWebSearch },
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("AgentLoop — provider-native web search gate", () => {
|
|
63
|
+
test("attaches the native web_search server tool when the provider supports it", async () => {
|
|
64
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
65
|
+
(
|
|
66
|
+
provider as { supportsNativeWebSearch?: boolean }
|
|
67
|
+
).supportsNativeWebSearch = true;
|
|
68
|
+
|
|
69
|
+
const loop = buildAdvisorLoop(provider, true);
|
|
70
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
71
|
+
|
|
72
|
+
expect(calls).toHaveLength(1);
|
|
73
|
+
const sent = calls[0];
|
|
74
|
+
// The native web_search SERVER tool is the only tool surfaced.
|
|
75
|
+
expect(sent.tools?.map((t) => t.name)).toEqual(["web_search"]);
|
|
76
|
+
// tool_choice is forced to auto so the model may invoke the search.
|
|
77
|
+
expect(sent.options?.config?.tool_choice).toEqual({ type: "auto" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("attaches nothing on a non-native provider (consult stays tool-less)", async () => {
|
|
81
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
82
|
+
// supportsNativeWebSearch is absent (falsy) — a non-native provider.
|
|
83
|
+
|
|
84
|
+
const loop = buildAdvisorLoop(provider, true);
|
|
85
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
86
|
+
|
|
87
|
+
expect(calls).toHaveLength(1);
|
|
88
|
+
const sent = calls[0];
|
|
89
|
+
// No web_search tool surfaced — no client tool the one-shot consult can't run.
|
|
90
|
+
expect(sent.tools).toBeUndefined();
|
|
91
|
+
// No tool_choice forced when nothing is attached.
|
|
92
|
+
expect(sent.options?.config?.tool_choice).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("attaches nothing when the flag is off, even on a native provider", async () => {
|
|
96
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
97
|
+
(
|
|
98
|
+
provider as { supportsNativeWebSearch?: boolean }
|
|
99
|
+
).supportsNativeWebSearch = true;
|
|
100
|
+
|
|
101
|
+
const loop = buildAdvisorLoop(provider, false);
|
|
102
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
103
|
+
|
|
104
|
+
expect(calls).toHaveLength(1);
|
|
105
|
+
const sent = calls[0];
|
|
106
|
+
expect(sent.tools).toBeUndefined();
|
|
107
|
+
expect(sent.options?.config?.tool_choice).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("does not duplicate web_search when a tool of that name is already present", async () => {
|
|
111
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
112
|
+
(
|
|
113
|
+
provider as { supportsNativeWebSearch?: boolean }
|
|
114
|
+
).supportsNativeWebSearch = true;
|
|
115
|
+
|
|
116
|
+
// A loop that already exposes a `web_search` client tool (e.g. researcher
|
|
117
|
+
// role). The gate must not append a second `web_search` entry.
|
|
118
|
+
const loop = new AgentLoop({
|
|
119
|
+
provider,
|
|
120
|
+
systemPrompt: "sys",
|
|
121
|
+
conversationId: "advisor-dup",
|
|
122
|
+
config: { enableNativeWebSearch: true },
|
|
123
|
+
tools: [
|
|
124
|
+
{
|
|
125
|
+
name: "web_search",
|
|
126
|
+
description: "",
|
|
127
|
+
input_schema: { type: "object" },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
132
|
+
|
|
133
|
+
const sent = calls[0];
|
|
134
|
+
expect(sent.tools?.filter((t) => t.name === "web_search")).toHaveLength(1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── Routed capability drives the decision (not the static default) ────────
|
|
138
|
+
|
|
139
|
+
test("false positive: static flag is native but the ROUTED target is not — attaches nothing", async () => {
|
|
140
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
141
|
+
// The construction-time default supports native search…
|
|
142
|
+
(
|
|
143
|
+
provider as { supportsNativeWebSearch?: boolean }
|
|
144
|
+
).supportsNativeWebSearch = true;
|
|
145
|
+
// …but the advisorProfile routes `subagentSpawn` to a provider/model that
|
|
146
|
+
// does NOT. The routing-aware probe wins, so no unexecutable client tool is
|
|
147
|
+
// surfaced to the otherwise tool-less advisor.
|
|
148
|
+
(
|
|
149
|
+
provider as {
|
|
150
|
+
supportsNativeWebSearchFor?: (o?: SendMessageOptions) => boolean;
|
|
151
|
+
}
|
|
152
|
+
).supportsNativeWebSearchFor = () => false;
|
|
153
|
+
|
|
154
|
+
const loop = buildAdvisorLoop(provider, true);
|
|
155
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
156
|
+
|
|
157
|
+
const sent = calls[0];
|
|
158
|
+
expect(sent.tools).toBeUndefined();
|
|
159
|
+
expect(sent.options?.config?.tool_choice).toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("false negative: static flag is non-native but the ROUTED target is — attaches the tool", async () => {
|
|
163
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
164
|
+
// The construction-time default lacks native search (flag absent/falsy)…
|
|
165
|
+
// …but the advisorProfile routes to a provider/model that has it.
|
|
166
|
+
(
|
|
167
|
+
provider as {
|
|
168
|
+
supportsNativeWebSearchFor?: (o?: SendMessageOptions) => boolean;
|
|
169
|
+
}
|
|
170
|
+
).supportsNativeWebSearchFor = () => true;
|
|
171
|
+
|
|
172
|
+
const loop = buildAdvisorLoop(provider, true);
|
|
173
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
174
|
+
|
|
175
|
+
const sent = calls[0];
|
|
176
|
+
expect(sent.tools?.map((t) => t.name)).toEqual(["web_search"]);
|
|
177
|
+
expect(sent.options?.config?.tool_choice).toEqual({ type: "auto" });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("the routing probe receives the loop's callSite", async () => {
|
|
181
|
+
const { provider, calls } = createMockProvider([endTurn("guidance")]);
|
|
182
|
+
const probeOptions: (SendMessageOptions | undefined)[] = [];
|
|
183
|
+
(
|
|
184
|
+
provider as {
|
|
185
|
+
supportsNativeWebSearchFor?: (o?: SendMessageOptions) => boolean;
|
|
186
|
+
}
|
|
187
|
+
).supportsNativeWebSearchFor = (o) => {
|
|
188
|
+
probeOptions.push(o);
|
|
189
|
+
return true;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const loop = buildAdvisorLoop(provider, true);
|
|
193
|
+
await loop.run({ ...baseRun, messages: userMessages });
|
|
194
|
+
|
|
195
|
+
expect(calls).toHaveLength(1);
|
|
196
|
+
// The probe is resolved against the same callSite the dispatch uses
|
|
197
|
+
// (`subagentSpawn` per `baseRun`), so the routed (provider, model) matches.
|
|
198
|
+
expect(probeOptions[0]?.config?.callSite).toBe("subagentSpawn");
|
|
199
|
+
});
|
|
200
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -95,6 +95,74 @@ export interface AgentLoopConfig {
|
|
|
95
95
|
minTurnIntervalMs?: number;
|
|
96
96
|
/** Override the default prompt cache TTL sent to the provider (e.g. "5m" for short-lived subagents). */
|
|
97
97
|
cacheTtl?: "5m" | "1h";
|
|
98
|
+
/**
|
|
99
|
+
* Give every LLM call provider-native (server-side) web search, gated on the
|
|
100
|
+
* native-search capability of the (provider, model) the call routes to —
|
|
101
|
+
* {@link Provider.supportsNativeWebSearchFor} when the provider exposes it,
|
|
102
|
+
* else the static {@link Provider.supportsNativeWebSearch} flag.
|
|
103
|
+
* When both are true, the loop appends a `web_search`-named tool to the
|
|
104
|
+
* outbound request — which Anthropic/OpenAI substitute for their server-side
|
|
105
|
+
* search tool, running the search inline and returning results without a
|
|
106
|
+
* client tool round-trip — and forces `tool_choice: auto` so the model may
|
|
107
|
+
* call it. Non-native providers get nothing.
|
|
108
|
+
*
|
|
109
|
+
* This is a SERVER tool the provider runs itself, distinct from the client
|
|
110
|
+
* tool list (`tools` / `resolveTools`): it is never executed by
|
|
111
|
+
* {@link AgentLoopConstructorOptions.toolExecutor} and does not require any
|
|
112
|
+
* client-tool allowlist entry. Used by the tool-less advisor consult to
|
|
113
|
+
* ground its guidance with live web access while staying one-shot for client
|
|
114
|
+
* tools. Defaults to false — existing behavior.
|
|
115
|
+
*/
|
|
116
|
+
enableNativeWebSearch?: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The `web_search`-named tool the loop appends when
|
|
121
|
+
* {@link AgentLoopConfig.enableNativeWebSearch} is set on a native provider.
|
|
122
|
+
* Anthropic/OpenAI intercept a tool with this name and substitute their own
|
|
123
|
+
* server-side web search (run inline, no client execution), so the exact
|
|
124
|
+
* `input_schema` is informational — the provider supplies the real schema.
|
|
125
|
+
*/
|
|
126
|
+
const NATIVE_WEB_SEARCH_TOOL: ToolDefinition = {
|
|
127
|
+
name: "web_search",
|
|
128
|
+
description:
|
|
129
|
+
"Search the web for current information to ground your response.",
|
|
130
|
+
input_schema: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
query: { type: "string", description: "The search query." },
|
|
134
|
+
},
|
|
135
|
+
required: ["query"],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build the minimal `SendMessageOptions` a routing-aware provider needs to
|
|
141
|
+
* report the native web-search capability of the (provider, model) THIS turn
|
|
142
|
+
* routes to. Mirrors the call-site fields the loop plumbs onto the actual send
|
|
143
|
+
* (`callSite` + `overrideProfile`/`forceOverrideProfile` + per-conversation
|
|
144
|
+
* `selectionSeed`) so the capability probe and the dispatch resolve the same
|
|
145
|
+
* arm. Returns `undefined` when there is no `callSite` (the legacy
|
|
146
|
+
* default-provider path); `selectionSeed` is omitted for standalone loops with
|
|
147
|
+
* no conversation id, matching the dispatch path's own guard.
|
|
148
|
+
*/
|
|
149
|
+
function buildNativeWebSearchProbeOptions(
|
|
150
|
+
callSite: LLMCallSite | undefined,
|
|
151
|
+
overrideProfile: string | undefined,
|
|
152
|
+
forceOverrideProfile: boolean,
|
|
153
|
+
conversationId: string | undefined,
|
|
154
|
+
): SendMessageOptions | undefined {
|
|
155
|
+
if (!callSite) return undefined;
|
|
156
|
+
return {
|
|
157
|
+
config: {
|
|
158
|
+
callSite,
|
|
159
|
+
...(overrideProfile ? { overrideProfile } : {}),
|
|
160
|
+
...(overrideProfile && forceOverrideProfile
|
|
161
|
+
? { forceOverrideProfile: true }
|
|
162
|
+
: {}),
|
|
163
|
+
...(conversationId ? { selectionSeed: conversationId } : {}),
|
|
164
|
+
},
|
|
165
|
+
};
|
|
98
166
|
}
|
|
99
167
|
|
|
100
168
|
export interface CheckpointInfo {
|
|
@@ -1240,10 +1308,44 @@ export class AgentLoop {
|
|
|
1240
1308
|
|
|
1241
1309
|
// Resolve tools for this turn: use the dynamic resolver if provided,
|
|
1242
1310
|
// otherwise fall back to the static tool list.
|
|
1243
|
-
const
|
|
1311
|
+
const resolvedTools = this.resolveTools
|
|
1244
1312
|
? this.resolveTools(history)
|
|
1245
1313
|
: this.tools;
|
|
1246
1314
|
|
|
1315
|
+
// Provider-native web search: append a `web_search`-named tool that the
|
|
1316
|
+
// provider substitutes for its server-side search (run inline, no client
|
|
1317
|
+
// execution), gated STRICTLY on the capability of the provider/model
|
|
1318
|
+
// this call ACTUALLY routes to so a non-native provider never sees an
|
|
1319
|
+
// unexecutable client tool. The advisor consult's `advisorProfile` can
|
|
1320
|
+
// route `subagentSpawn` to a provider/model whose native-search support
|
|
1321
|
+
// differs from the construction-time default, so the gate resolves the
|
|
1322
|
+
// routed target (callSite + overrideProfile) via
|
|
1323
|
+
// `supportsNativeWebSearchFor` rather than the static
|
|
1324
|
+
// `this.provider.supportsNativeWebSearch` snapshot; providers without
|
|
1325
|
+
// the routing-aware probe fall back to the static flag. This is a SERVER
|
|
1326
|
+
// tool — it bypasses the client allowlist and the tool executor — so the
|
|
1327
|
+
// tool-less advisor consult can ground its guidance with live web access
|
|
1328
|
+
// while staying one-shot for client tools. Skip when a `web_search` tool
|
|
1329
|
+
// is already present so we never duplicate the name.
|
|
1330
|
+
const supportsRoutedNativeWebSearch = this.provider
|
|
1331
|
+
.supportsNativeWebSearchFor
|
|
1332
|
+
? this.provider.supportsNativeWebSearchFor(
|
|
1333
|
+
buildNativeWebSearchProbeOptions(
|
|
1334
|
+
callSite,
|
|
1335
|
+
resolveEffectiveOverrideProfile(),
|
|
1336
|
+
forceOverrideProfile,
|
|
1337
|
+
this.conversationId,
|
|
1338
|
+
),
|
|
1339
|
+
)
|
|
1340
|
+
: this.provider.supportsNativeWebSearch === true;
|
|
1341
|
+
const attachNativeWebSearch =
|
|
1342
|
+
this.config.enableNativeWebSearch === true &&
|
|
1343
|
+
supportsRoutedNativeWebSearch &&
|
|
1344
|
+
!resolvedTools.some((t) => t.name === NATIVE_WEB_SEARCH_TOOL.name);
|
|
1345
|
+
const currentTools = attachNativeWebSearch
|
|
1346
|
+
? [...resolvedTools, NATIVE_WEB_SEARCH_TOOL]
|
|
1347
|
+
: resolvedTools;
|
|
1348
|
+
|
|
1247
1349
|
// Field precedence (highest wins):
|
|
1248
1350
|
// 1. Per-run explicit (`runModel`)
|
|
1249
1351
|
// 2. Call-site resolved values (filled by
|
|
@@ -1286,6 +1388,11 @@ export class AgentLoop {
|
|
|
1286
1388
|
|
|
1287
1389
|
if (this.config.toolChoice) {
|
|
1288
1390
|
providerConfig.tool_choice = this.config.toolChoice;
|
|
1391
|
+
} else if (attachNativeWebSearch) {
|
|
1392
|
+
// The native web-search tool is the only tool on this turn (the
|
|
1393
|
+
// advisor consult is otherwise tool-less). Let the model decide
|
|
1394
|
+
// whether to search rather than forcing it.
|
|
1395
|
+
providerConfig.tool_choice = { type: "auto" };
|
|
1289
1396
|
}
|
|
1290
1397
|
|
|
1291
1398
|
if (this.config.cacheTtl) {
|
|
@@ -283,6 +283,13 @@ const SlackMessageLinkSchema = z.object({
|
|
|
283
283
|
webUrl: z.string().optional(),
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
const SlackReactionSchema = z.object({
|
|
287
|
+
emoji: z.string(),
|
|
288
|
+
op: z.enum(["added", "removed"]),
|
|
289
|
+
actorDisplayName: z.string().optional(),
|
|
290
|
+
targetChannelTs: z.string(),
|
|
291
|
+
});
|
|
292
|
+
|
|
286
293
|
/** Slack provenance for a history row that originated from a Slack channel. */
|
|
287
294
|
export const ConversationSlackMessageSchema = z.object({
|
|
288
295
|
channelId: z.string(),
|
|
@@ -297,6 +304,8 @@ export const ConversationSlackMessageSchema = z.object({
|
|
|
297
304
|
.optional(),
|
|
298
305
|
messageLink: SlackMessageLinkSchema.optional(),
|
|
299
306
|
threadLink: SlackMessageLinkSchema.optional(),
|
|
307
|
+
eventKind: z.enum(["message", "reaction"]).optional(),
|
|
308
|
+
reaction: SlackReactionSchema.optional(),
|
|
300
309
|
});
|
|
301
310
|
export type ConversationSlackMessage = z.infer<
|
|
302
311
|
typeof ConversationSlackMessageSchema
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { answerCall } from "../calls/call-domain.js";
|
|
15
15
|
import { findContactChannel } from "../contacts/contact-store.js";
|
|
16
|
-
import {
|
|
16
|
+
import { activateMemberChannel } from "../contacts/member-write-relay.js";
|
|
17
17
|
import { findConversation } from "../daemon/conversation-registry.js";
|
|
18
18
|
import {
|
|
19
19
|
type CanonicalGuardianRequest,
|
|
@@ -592,19 +592,31 @@ const accessRequestResolver: GuardianRequestResolver = {
|
|
|
592
592
|
// a verification session. The caller is already on the line and the
|
|
593
593
|
// relay server's in-call wait loop will detect the approved status.
|
|
594
594
|
if (channel === "phone") {
|
|
595
|
+
let activation: Awaited<ReturnType<typeof activateMemberChannel>>;
|
|
595
596
|
try {
|
|
596
|
-
|
|
597
|
+
// Gateway-first activation: the gateway owns the ACL verdict, the local
|
|
598
|
+
// mirror persists the caller's contact/channel identity.
|
|
599
|
+
activation = await activateMemberChannel({
|
|
597
600
|
sourceChannel: "phone",
|
|
598
601
|
externalUserId: requesterExternalUserId,
|
|
599
602
|
externalChatId: requesterChatId,
|
|
600
|
-
status: "active",
|
|
601
|
-
policy: "allow",
|
|
602
603
|
});
|
|
603
604
|
} catch (err) {
|
|
604
605
|
log.error(
|
|
605
606
|
{ err, requesterExternalUserId },
|
|
606
607
|
"Access request resolver: failed to activate voice caller as trusted contact",
|
|
607
608
|
);
|
|
609
|
+
return { ok: false, reason: "voice_activation_failed" };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Fail-closed: a refused activation did not land on the gateway source of
|
|
613
|
+
// truth, so the caller is not actually trusted — do not report success.
|
|
614
|
+
if (activation.status === "refused") {
|
|
615
|
+
log.error(
|
|
616
|
+
{ requesterExternalUserId },
|
|
617
|
+
"Access request resolver: gateway refused voice caller activation",
|
|
618
|
+
);
|
|
619
|
+
return { ok: false, reason: "voice_activation_refused" };
|
|
608
620
|
}
|
|
609
621
|
|
|
610
622
|
log.info(
|