@vellumai/assistant 0.8.2 → 0.8.3
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/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for A2A config handler.
|
|
3
|
+
*
|
|
4
|
+
* Uses the real DB (via `initializeDb()`) and the test preload which sets
|
|
5
|
+
* `VELLUM_WORKSPACE_DIR` to a per-file temp directory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
mock.module("../../../util/logger.js", () => ({
|
|
11
|
+
getLogger: () =>
|
|
12
|
+
new Proxy({} as Record<string, unknown>, {
|
|
13
|
+
get: () => () => {},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
invalidateConfigCache,
|
|
19
|
+
loadRawConfig,
|
|
20
|
+
saveRawConfig,
|
|
21
|
+
setNestedValue,
|
|
22
|
+
} from "../../../config/loader.js";
|
|
23
|
+
import { getSqlite } from "../../../memory/db-connection.js";
|
|
24
|
+
import { initializeDb } from "../../../memory/db-init.js";
|
|
25
|
+
import { clearA2AConfig, getA2AConfig, setA2AConfig } from "../config-a2a.js";
|
|
26
|
+
|
|
27
|
+
initializeDb();
|
|
28
|
+
|
|
29
|
+
function resetTables(): void {
|
|
30
|
+
const sqlite = getSqlite();
|
|
31
|
+
sqlite.run("DELETE FROM assistant_contact_metadata");
|
|
32
|
+
sqlite.run("DELETE FROM contact_channels");
|
|
33
|
+
sqlite.run("DELETE FROM contacts");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setConfigEnabled(enabled: boolean): void {
|
|
37
|
+
const raw = loadRawConfig();
|
|
38
|
+
setNestedValue(raw, "a2a.enabled", enabled);
|
|
39
|
+
saveRawConfig(raw);
|
|
40
|
+
invalidateConfigCache();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("getA2AConfig", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
resetTables();
|
|
46
|
+
setConfigEnabled(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns enabled: false when a2a is disabled", () => {
|
|
50
|
+
const result = getA2AConfig();
|
|
51
|
+
expect(result.success).toBe(true);
|
|
52
|
+
expect(result.enabled).toBe(false);
|
|
53
|
+
expect(result.activeConnections).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns enabled: true when a2a is enabled", () => {
|
|
57
|
+
setConfigEnabled(true);
|
|
58
|
+
const result = getA2AConfig();
|
|
59
|
+
expect(result.success).toBe(true);
|
|
60
|
+
expect(result.enabled).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("setA2AConfig", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
resetTables();
|
|
67
|
+
setConfigEnabled(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("enables a2a in config", () => {
|
|
71
|
+
const result = setA2AConfig();
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
expect(result.enabled).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("is idempotent", () => {
|
|
77
|
+
setA2AConfig();
|
|
78
|
+
const result = setA2AConfig();
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
expect(result.enabled).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("clearA2AConfig", () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
resetTables();
|
|
87
|
+
setConfigEnabled(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("disables a2a in config", () => {
|
|
91
|
+
const result = clearA2AConfig();
|
|
92
|
+
expect(result.success).toBe(true);
|
|
93
|
+
expect(result.enabled).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A channel configuration handler.
|
|
3
|
+
*
|
|
4
|
+
* - getA2AConfig() — read a2a.enabled, count active a2a contact_channels
|
|
5
|
+
* - setA2AConfig() — set a2a.enabled = true
|
|
6
|
+
* - clearA2AConfig() — set a2a.enabled = false
|
|
7
|
+
* - createA2AInvite() — create a shareable invite token for link-based contact creation
|
|
8
|
+
* - redeemA2AInvite() — receiver-side: create trusted contact from sender identity
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getConfig,
|
|
13
|
+
invalidateConfigCache,
|
|
14
|
+
loadRawConfig,
|
|
15
|
+
saveRawConfig,
|
|
16
|
+
setNestedValue,
|
|
17
|
+
} from "../../config/loader.js";
|
|
18
|
+
import {
|
|
19
|
+
findContactByAddress,
|
|
20
|
+
searchContacts,
|
|
21
|
+
upsertContact,
|
|
22
|
+
} from "../../contacts/contact-store.js";
|
|
23
|
+
import type { VellumAssistantMetadata } from "../../contacts/types.js";
|
|
24
|
+
import { getPublicBaseUrl } from "../../inbound/public-ingress-urls.js";
|
|
25
|
+
import { getDb } from "../../memory/db-connection.js";
|
|
26
|
+
import {
|
|
27
|
+
claimA2AInvite,
|
|
28
|
+
createInvite,
|
|
29
|
+
hashToken,
|
|
30
|
+
} from "../../memory/invite-store.js";
|
|
31
|
+
import { assistantContactMetadata } from "../../memory/schema.js";
|
|
32
|
+
import { getAssistantName } from "../identity-helpers.js";
|
|
33
|
+
// ── Result types ────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface A2AConfigResult {
|
|
36
|
+
success: boolean;
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
activeConnections: number;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CreateA2AInviteResult {
|
|
43
|
+
success: boolean;
|
|
44
|
+
inviteId?: string;
|
|
45
|
+
token?: string;
|
|
46
|
+
expiresAt?: number;
|
|
47
|
+
senderGatewayUrl?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CompleteA2AInviteResult {
|
|
52
|
+
success: boolean;
|
|
53
|
+
sender?: { assistantId: string; displayName: string; gatewayUrl: string };
|
|
54
|
+
error?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RedeemA2AInviteResult {
|
|
58
|
+
success: boolean;
|
|
59
|
+
contactId?: string;
|
|
60
|
+
alreadyConnected?: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Config operations ───────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function getA2AConfig(): A2AConfigResult {
|
|
67
|
+
const config = getConfig();
|
|
68
|
+
const enabled = config.a2a?.enabled ?? false;
|
|
69
|
+
|
|
70
|
+
const contacts = searchContacts({ channelType: "a2a" });
|
|
71
|
+
const activeConnections = contacts.reduce((count, c) => {
|
|
72
|
+
return (
|
|
73
|
+
count +
|
|
74
|
+
c.channels.filter((ch) => ch.type === "a2a" && ch.status === "active")
|
|
75
|
+
.length
|
|
76
|
+
);
|
|
77
|
+
}, 0);
|
|
78
|
+
|
|
79
|
+
return { success: true, enabled, activeConnections };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function setA2AConfig(): A2AConfigResult {
|
|
83
|
+
const raw = loadRawConfig();
|
|
84
|
+
setNestedValue(raw, "a2a.enabled", true);
|
|
85
|
+
saveRawConfig(raw);
|
|
86
|
+
invalidateConfigCache();
|
|
87
|
+
|
|
88
|
+
const result = getA2AConfig();
|
|
89
|
+
return { ...result, success: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function clearA2AConfig(): A2AConfigResult {
|
|
93
|
+
const raw = loadRawConfig();
|
|
94
|
+
setNestedValue(raw, "a2a.enabled", false);
|
|
95
|
+
saveRawConfig(raw);
|
|
96
|
+
invalidateConfigCache();
|
|
97
|
+
|
|
98
|
+
return { success: true, enabled: false, activeConnections: 0 };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── A2A invite creation ────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function createA2AInvite(params: {
|
|
104
|
+
expiresInHours?: number;
|
|
105
|
+
}): CreateA2AInviteResult {
|
|
106
|
+
// 1. Ensure A2A channel is enabled (auto-enable on first invite)
|
|
107
|
+
const config = getA2AConfig();
|
|
108
|
+
if (!config.enabled) {
|
|
109
|
+
setA2AConfig();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Resolve public base URL
|
|
113
|
+
let publicBaseUrl: string;
|
|
114
|
+
try {
|
|
115
|
+
publicBaseUrl = getPublicBaseUrl(getConfig());
|
|
116
|
+
} catch {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error:
|
|
120
|
+
"No public base URL configured. Set ingress.publicBaseUrl in config.",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 3. Create placeholder contact (no channels — will be bound on acceptance)
|
|
125
|
+
const contact = upsertContact({
|
|
126
|
+
displayName: "Pending A2A invite",
|
|
127
|
+
contactType: "assistant",
|
|
128
|
+
role: "contact",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 4. Create the invite
|
|
132
|
+
const expiresInMs = (params.expiresInHours ?? 72) * 60 * 60 * 1000;
|
|
133
|
+
const { invite, rawToken } = createInvite({
|
|
134
|
+
sourceChannel: "a2a",
|
|
135
|
+
contactId: contact.id,
|
|
136
|
+
maxUses: 1,
|
|
137
|
+
expiresInMs,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
inviteId: invite.id,
|
|
143
|
+
token: rawToken,
|
|
144
|
+
expiresAt: invite.expiresAt,
|
|
145
|
+
senderGatewayUrl: publicBaseUrl,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── A2A invite completion (sender side) ───────────────────────────
|
|
150
|
+
|
|
151
|
+
export function completeA2AInvite(params: {
|
|
152
|
+
token: string;
|
|
153
|
+
senderAssistantId: string;
|
|
154
|
+
acceptor: {
|
|
155
|
+
assistantId: string;
|
|
156
|
+
displayName: string;
|
|
157
|
+
gatewayUrl: string;
|
|
158
|
+
};
|
|
159
|
+
}): CompleteA2AInviteResult {
|
|
160
|
+
// Resolve sender identity before any mutations so we fail cleanly
|
|
161
|
+
const displayName = getAssistantName() ?? "Vellum Assistant";
|
|
162
|
+
let gatewayUrl: string;
|
|
163
|
+
try {
|
|
164
|
+
gatewayUrl = getPublicBaseUrl(getConfig());
|
|
165
|
+
} catch {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error:
|
|
169
|
+
"No public base URL configured. Set ingress.publicBaseUrl in config.",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const tokenHash = hashToken(params.token);
|
|
174
|
+
const claimResult = claimA2AInvite({
|
|
175
|
+
tokenHash,
|
|
176
|
+
redeemedByExternalUserId: params.acceptor.assistantId,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!claimResult.claimed || !claimResult.invite) {
|
|
180
|
+
return { success: false, error: claimResult.error };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const invite = claimResult.invite;
|
|
184
|
+
|
|
185
|
+
// Promote the placeholder contact with the acceptor's identity
|
|
186
|
+
upsertContact({
|
|
187
|
+
id: invite.contactId,
|
|
188
|
+
displayName: params.acceptor.displayName,
|
|
189
|
+
contactType: "assistant",
|
|
190
|
+
role: "contact",
|
|
191
|
+
channels: [
|
|
192
|
+
{
|
|
193
|
+
type: "a2a",
|
|
194
|
+
address: params.acceptor.assistantId.toLowerCase(),
|
|
195
|
+
externalUserId: params.acceptor.assistantId,
|
|
196
|
+
status: "active",
|
|
197
|
+
policy: "allow",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Write assistant contact metadata
|
|
203
|
+
const db = getDb();
|
|
204
|
+
const metadataJson = JSON.stringify({
|
|
205
|
+
assistantId: params.acceptor.assistantId,
|
|
206
|
+
gatewayUrl: params.acceptor.gatewayUrl,
|
|
207
|
+
} satisfies VellumAssistantMetadata);
|
|
208
|
+
db.insert(assistantContactMetadata)
|
|
209
|
+
.values({
|
|
210
|
+
contactId: invite.contactId,
|
|
211
|
+
species: "vellum",
|
|
212
|
+
metadata: metadataJson,
|
|
213
|
+
})
|
|
214
|
+
.onConflictDoUpdate({
|
|
215
|
+
target: assistantContactMetadata.contactId,
|
|
216
|
+
set: { species: "vellum", metadata: metadataJson },
|
|
217
|
+
})
|
|
218
|
+
.run();
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
sender: {
|
|
223
|
+
assistantId: params.senderAssistantId,
|
|
224
|
+
displayName,
|
|
225
|
+
gatewayUrl,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── A2A invite redemption (receiver side) ─────────────────────────
|
|
231
|
+
|
|
232
|
+
export function redeemA2AInvite(params: {
|
|
233
|
+
sender: {
|
|
234
|
+
assistantId: string;
|
|
235
|
+
displayName: string;
|
|
236
|
+
gatewayUrl: string;
|
|
237
|
+
};
|
|
238
|
+
}): RedeemA2AInviteResult {
|
|
239
|
+
// 1. Ensure A2A channel is enabled (auto-enable if needed)
|
|
240
|
+
const config = getA2AConfig();
|
|
241
|
+
if (!config.enabled) {
|
|
242
|
+
setA2AConfig();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 2. Check for existing active contact with this sender
|
|
246
|
+
const existing = findContactByAddress("a2a", params.sender.assistantId);
|
|
247
|
+
if (
|
|
248
|
+
existing &&
|
|
249
|
+
existing.channels.some((ch) => ch.type === "a2a" && ch.status === "active")
|
|
250
|
+
) {
|
|
251
|
+
return { success: true, alreadyConnected: true, contactId: existing.id };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// 3. Create the sender as a local trusted contact
|
|
255
|
+
const contact = upsertContact({
|
|
256
|
+
displayName: params.sender.displayName,
|
|
257
|
+
contactType: "assistant",
|
|
258
|
+
role: "contact",
|
|
259
|
+
channels: [
|
|
260
|
+
{
|
|
261
|
+
type: "a2a",
|
|
262
|
+
address: params.sender.assistantId.toLowerCase(),
|
|
263
|
+
externalUserId: params.sender.assistantId,
|
|
264
|
+
status: "active",
|
|
265
|
+
policy: "allow",
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// 4. Write assistant contact metadata
|
|
271
|
+
const db = getDb();
|
|
272
|
+
const metadataJson = JSON.stringify({
|
|
273
|
+
assistantId: params.sender.assistantId,
|
|
274
|
+
gatewayUrl: params.sender.gatewayUrl,
|
|
275
|
+
} satisfies VellumAssistantMetadata);
|
|
276
|
+
db.insert(assistantContactMetadata)
|
|
277
|
+
.values({
|
|
278
|
+
contactId: contact.id,
|
|
279
|
+
species: "vellum",
|
|
280
|
+
metadata: metadataJson,
|
|
281
|
+
})
|
|
282
|
+
.onConflictDoUpdate({
|
|
283
|
+
target: assistantContactMetadata.contactId,
|
|
284
|
+
set: { species: "vellum", metadata: metadataJson },
|
|
285
|
+
})
|
|
286
|
+
.run();
|
|
287
|
+
|
|
288
|
+
return { success: true, contactId: contact.id };
|
|
289
|
+
}
|
|
@@ -141,6 +141,7 @@ export async function regenerateResponse(
|
|
|
141
141
|
const conversation = await getOrCreateConversation(conversationId);
|
|
142
142
|
touchConversation(conversationId);
|
|
143
143
|
conversation.updateClient(broadcastMessage, false);
|
|
144
|
+
getSubagentManager().updateParentSender(conversationId, broadcastMessage);
|
|
144
145
|
const requestId = uuid();
|
|
145
146
|
conversation.traceEmitter.emit("request_received", "Regenerate requested", {
|
|
146
147
|
requestId,
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
* `running`; the rollback path on a failed `start` restores from the
|
|
23
23
|
* current confirmed pointer (not from a per-call snapshot of a sibling
|
|
24
24
|
* optimistic write), so two overlapping starts that both fail cannot
|
|
25
|
-
* leave a phantom lock
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* leave a phantom lock. Each session carries a monotonic `dispatchedAt`
|
|
26
|
+
* counter so out-of-order `running` responses promote in dispatch order:
|
|
27
|
+
* the latest-dispatched start that the host confirms becomes the
|
|
28
|
+
* confirmed baseline, regardless of which response arrived last. The
|
|
29
|
+
* lock is released outright when the owning proxy's `dispose()` fires.
|
|
29
30
|
*
|
|
30
31
|
* `app_control_start` is the only tool that can acquire the lock — the
|
|
31
32
|
* user's medium-risk approval at start time is the consent boundary. All
|
|
@@ -84,8 +85,22 @@ export interface ActiveAppControlSession {
|
|
|
84
85
|
* the `app` of subsequent non-start tool calls.
|
|
85
86
|
*/
|
|
86
87
|
app: string;
|
|
88
|
+
/**
|
|
89
|
+
* Strictly monotonic counter assigned when the session is created (in
|
|
90
|
+
* `request()` for a `start`). Used by {@link promoteStartIfCurrent} to
|
|
91
|
+
* tell which of two confirmations from overlapping starts is newer when
|
|
92
|
+
* host responses arrive out of order. Larger values are newer.
|
|
93
|
+
*/
|
|
94
|
+
dispatchedAt: number;
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Monotonic counter that stamps each `start`'s {@link
|
|
99
|
+
* ActiveAppControlSession.dispatchedAt}. Process-lifetime monotonic; the
|
|
100
|
+
* absolute value is meaningless — only ordering matters.
|
|
101
|
+
*/
|
|
102
|
+
let nextDispatchedAt = 1;
|
|
103
|
+
|
|
89
104
|
/**
|
|
90
105
|
* Currently active session, or `undefined` when no session is held. This
|
|
91
106
|
* is the optimistic value: it is set the moment a `start` is dispatched
|
|
@@ -112,6 +127,13 @@ export function _getActiveAppControlSession():
|
|
|
112
127
|
return activeAppControlSession;
|
|
113
128
|
}
|
|
114
129
|
|
|
130
|
+
/** Test-only helper: read the last host-confirmed session. */
|
|
131
|
+
export function _getConfirmedAppControlSession():
|
|
132
|
+
| ActiveAppControlSession
|
|
133
|
+
| undefined {
|
|
134
|
+
return confirmedAppControlSession;
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
/** Test-only helper: clear both session pointers between test cases. */
|
|
116
138
|
export function _resetActiveAppControlSession(): void {
|
|
117
139
|
activeAppControlSession = undefined;
|
|
@@ -123,11 +145,18 @@ export function _resetActiveAppControlSession(): void {
|
|
|
123
145
|
* round-trip. Useful for tests that exercise non-start tool paths and
|
|
124
146
|
* don't need to verify the start flow itself.
|
|
125
147
|
*/
|
|
126
|
-
export function _setActiveAppControlSession(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
export function _setActiveAppControlSession(session: {
|
|
149
|
+
conversationId: string;
|
|
150
|
+
app: string;
|
|
151
|
+
dispatchedAt?: number;
|
|
152
|
+
}): void {
|
|
153
|
+
const full: ActiveAppControlSession = {
|
|
154
|
+
conversationId: session.conversationId,
|
|
155
|
+
app: session.app,
|
|
156
|
+
dispatchedAt: session.dispatchedAt ?? nextDispatchedAt++,
|
|
157
|
+
};
|
|
158
|
+
activeAppControlSession = full;
|
|
159
|
+
confirmedAppControlSession = full;
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
/**
|
|
@@ -280,6 +309,7 @@ export class HostAppControlProxy extends HostProxyBase<
|
|
|
280
309
|
attemptedSession = {
|
|
281
310
|
conversationId: this.conversationId,
|
|
282
311
|
app: input.app,
|
|
312
|
+
dispatchedAt: nextDispatchedAt++,
|
|
283
313
|
};
|
|
284
314
|
activeAppControlSession = attemptedSession;
|
|
285
315
|
} else {
|
|
@@ -356,15 +386,27 @@ export class HostAppControlProxy extends HostProxyBase<
|
|
|
356
386
|
}
|
|
357
387
|
|
|
358
388
|
/**
|
|
359
|
-
* Promote this start's
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
* The
|
|
366
|
-
*
|
|
367
|
-
*
|
|
389
|
+
* Promote this start's session to the confirmed pointer when the host
|
|
390
|
+
* returns `running`. Two gates:
|
|
391
|
+
*
|
|
392
|
+
* 1. The live optimistic write must still belong to this conversation —
|
|
393
|
+
* if `dispose()` cleared the lock or another conversation acquired
|
|
394
|
+
* it, this confirmation must not resurrect a stale session.
|
|
395
|
+
* 2. The confirming session must be at least as recent as the currently
|
|
396
|
+
* confirmed one, compared via {@link
|
|
397
|
+
* ActiveAppControlSession.dispatchedAt}. The dispatch counter is
|
|
398
|
+
* assigned synchronously in `request()`, so it captures dispatch
|
|
399
|
+
* order even when host responses arrive out of order. The latest
|
|
400
|
+
* dispatched start that confirms wins, which is the right baseline
|
|
401
|
+
* for the rollback path: if a newer start later fails, rollback
|
|
402
|
+
* restores the most recently confirmed session, not an older one.
|
|
403
|
+
*
|
|
404
|
+
* Also advance the active pointer when it is strictly older than the
|
|
405
|
+
* newly-confirmed session. This handles the case where an even newer
|
|
406
|
+
* optimistic write has already failed and rolled active back to the
|
|
407
|
+
* previous confirmed session; without this, observe/actions for the
|
|
408
|
+
* newly-confirmed session would target the older app. A newer
|
|
409
|
+
* in-flight optimistic write (higher `dispatchedAt`) is preserved.
|
|
368
410
|
*/
|
|
369
411
|
private promoteStartIfCurrent(
|
|
370
412
|
attempted: ActiveAppControlSession | undefined,
|
|
@@ -373,7 +415,16 @@ export class HostAppControlProxy extends HostProxyBase<
|
|
|
373
415
|
if (activeAppControlSession?.conversationId !== attempted.conversationId) {
|
|
374
416
|
return;
|
|
375
417
|
}
|
|
418
|
+
if (
|
|
419
|
+
confirmedAppControlSession != null &&
|
|
420
|
+
attempted.dispatchedAt <= confirmedAppControlSession.dispatchedAt
|
|
421
|
+
) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
376
424
|
confirmedAppControlSession = attempted;
|
|
425
|
+
if (activeAppControlSession.dispatchedAt < attempted.dispatchedAt) {
|
|
426
|
+
activeAppControlSession = attempted;
|
|
427
|
+
}
|
|
377
428
|
}
|
|
378
429
|
|
|
379
430
|
/**
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
import type { HostProxyCapability, InterfaceId } from "../channels/types.js";
|
|
30
30
|
import { supportsHostProxy } from "../channels/types.js";
|
|
31
31
|
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
32
|
+
import { getLogger } from "../util/logger.js";
|
|
33
|
+
|
|
34
|
+
const log = getLogger("host-proxy-preactivation");
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Subset of Conversation/ProcessConversationContext that
|
|
@@ -36,9 +39,29 @@ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
|
36
39
|
* `ProcessConversationContext` satisfy this structurally.
|
|
37
40
|
*/
|
|
38
41
|
export interface HostProxyPreactivationTarget {
|
|
42
|
+
readonly conversationId: string;
|
|
39
43
|
addPreactivatedSkillId(id: string): void;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Why an attachment decision went the way it did. Logged per turn so that
|
|
48
|
+
* silent-gate failures (e.g. ATL-609: computer-use never reaches the LLM
|
|
49
|
+
* surface for a macOS user) can be diagnosed from production logs without
|
|
50
|
+
* extra instrumentation.
|
|
51
|
+
*/
|
|
52
|
+
export type HostProxyAttachmentReason =
|
|
53
|
+
| "native_support"
|
|
54
|
+
| "cross_client"
|
|
55
|
+
| "denied_no_interface"
|
|
56
|
+
| "denied_chrome_extension"
|
|
57
|
+
| "denied_no_clients";
|
|
58
|
+
|
|
59
|
+
export interface HostProxyAttachmentDecision {
|
|
60
|
+
shouldAttach: boolean;
|
|
61
|
+
reason: HostProxyAttachmentReason;
|
|
62
|
+
clientCount?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
/**
|
|
43
66
|
* Registry mapping each host-proxy capability to the skill that must be
|
|
44
67
|
* preactivated when that capability is supported by the source interface.
|
|
@@ -62,45 +85,89 @@ export const HOST_PROXY_SKILL_PREACTIVATIONS: ReadonlyArray<{
|
|
|
62
85
|
];
|
|
63
86
|
|
|
64
87
|
/**
|
|
65
|
-
* Returns
|
|
66
|
-
*
|
|
88
|
+
* Returns the full attachment decision for a host-proxy capability — used both
|
|
89
|
+
* to gate proxy instantiation and to feed the structured preactivation log so
|
|
90
|
+
* silent gates can be diagnosed without re-instrumenting after the fact.
|
|
67
91
|
*
|
|
68
|
-
* 1.
|
|
69
|
-
* 2.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
92
|
+
* 1. No source interface → `denied_no_interface`.
|
|
93
|
+
* 2. Source interface natively supports the capability → `native_support`.
|
|
94
|
+
* 3. `chrome-extension` source can never broker cross-client routing to a
|
|
95
|
+
* macOS client (security boundary) → `denied_chrome_extension`.
|
|
96
|
+
* 4. At least one connected client advertises the capability →
|
|
97
|
+
* `cross_client` with `clientCount`.
|
|
98
|
+
* 5. Otherwise → `denied_no_clients` with `clientCount: 0`.
|
|
73
99
|
*
|
|
74
|
-
*
|
|
75
|
-
|
|
100
|
+
* Single source of truth for preactivation and proxy instantiation.
|
|
101
|
+
*/
|
|
102
|
+
export function evaluateHostProxyAttachment(
|
|
103
|
+
capability: HostProxyCapability,
|
|
104
|
+
sourceInterface: InterfaceId | undefined,
|
|
105
|
+
): HostProxyAttachmentDecision {
|
|
106
|
+
if (!sourceInterface) {
|
|
107
|
+
return { shouldAttach: false, reason: "denied_no_interface" };
|
|
108
|
+
}
|
|
109
|
+
if (supportsHostProxy(sourceInterface, capability)) {
|
|
110
|
+
return { shouldAttach: true, reason: "native_support" };
|
|
111
|
+
}
|
|
112
|
+
if (sourceInterface === "chrome-extension") {
|
|
113
|
+
return { shouldAttach: false, reason: "denied_chrome_extension" };
|
|
114
|
+
}
|
|
115
|
+
const clientCount =
|
|
116
|
+
assistantEventHub.listClientsByCapability(capability).length;
|
|
117
|
+
if (clientCount > 0) {
|
|
118
|
+
return { shouldAttach: true, reason: "cross_client", clientCount };
|
|
119
|
+
}
|
|
120
|
+
return { shouldAttach: false, reason: "denied_no_clients", clientCount: 0 };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Boolean wrapper retained for the proxy-instantiation call sites that only
|
|
125
|
+
* need the gate result. Prefer `evaluateHostProxyAttachment` when the reason
|
|
126
|
+
* is also useful (e.g. for logging or telemetry).
|
|
76
127
|
*/
|
|
77
128
|
export function shouldAttachHostProxyForCapability(
|
|
78
129
|
capability: HostProxyCapability,
|
|
79
130
|
sourceInterface: InterfaceId | undefined,
|
|
80
131
|
): boolean {
|
|
81
|
-
|
|
82
|
-
if (supportsHostProxy(sourceInterface, capability)) return true;
|
|
83
|
-
if (sourceInterface === "chrome-extension") return false;
|
|
84
|
-
return assistantEventHub.listClientsByCapability(capability).length > 0;
|
|
132
|
+
return evaluateHostProxyAttachment(capability, sourceInterface).shouldAttach;
|
|
85
133
|
}
|
|
86
134
|
|
|
87
135
|
/**
|
|
88
136
|
* Preactivate every host-proxy-backed skill that the given source interface
|
|
89
|
-
* supports
|
|
137
|
+
* supports, and emit one structured `log.info` line per turn capturing each
|
|
138
|
+
* capability's decision + the final preactivated skill IDs.
|
|
139
|
+
*
|
|
140
|
+
* The log line fires unconditionally — even when `sourceInterface` is
|
|
141
|
+
* undefined — because "preactivation never ran because no interface" is
|
|
142
|
+
* itself the diagnostic signal we want visible in production.
|
|
90
143
|
*
|
|
91
144
|
* Callers are responsible for any additional gating (e.g. only preactivating
|
|
92
145
|
* when the conversation is idle vs. when re-adding after dequeue), since
|
|
93
|
-
* those constraints differ across create vs. drain paths.
|
|
94
|
-
* iterates the registry and dispatches.
|
|
146
|
+
* those constraints differ across create vs. drain paths.
|
|
95
147
|
*/
|
|
96
148
|
export function preactivateHostProxySkills(
|
|
97
149
|
conversation: HostProxyPreactivationTarget,
|
|
98
150
|
sourceInterface: InterfaceId | undefined,
|
|
99
151
|
): void {
|
|
100
|
-
|
|
152
|
+
const decisions: Record<string, HostProxyAttachmentDecision> = {};
|
|
153
|
+
const preactivatedSkillIds: string[] = [];
|
|
154
|
+
|
|
101
155
|
for (const { capability, skillId } of HOST_PROXY_SKILL_PREACTIVATIONS) {
|
|
102
|
-
|
|
156
|
+
const decision = evaluateHostProxyAttachment(capability, sourceInterface);
|
|
157
|
+
decisions[capability] = decision;
|
|
158
|
+
if (decision.shouldAttach) {
|
|
103
159
|
conversation.addPreactivatedSkillId(skillId);
|
|
160
|
+
preactivatedSkillIds.push(skillId);
|
|
104
161
|
}
|
|
105
162
|
}
|
|
163
|
+
|
|
164
|
+
log.info(
|
|
165
|
+
{
|
|
166
|
+
conversationId: conversation.conversationId,
|
|
167
|
+
sourceInterface,
|
|
168
|
+
decisions,
|
|
169
|
+
preactivatedSkillIds,
|
|
170
|
+
},
|
|
171
|
+
"host-proxy preactivation decision",
|
|
172
|
+
);
|
|
106
173
|
}
|