vellum 0.2.13 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +113 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +137 -18
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +62 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +27 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -1,9 +1,60 @@
|
|
|
1
1
|
import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { searchContacts } from '../../../../contacts/contact-store.js';
|
|
3
|
+
import type { ContactWithChannels } from '../../../../contacts/types.js';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
function formatContactSummary(c: ContactWithChannels): string {
|
|
6
|
+
const parts = [`- **${c.displayName}** (ID: ${c.id})`];
|
|
7
|
+
if (c.relationship) parts.push(` Relationship: ${c.relationship}`);
|
|
8
|
+
parts.push(` Importance: ${c.importance.toFixed(2)} | Interactions: ${c.interactionCount}`);
|
|
9
|
+
if (c.channels.length > 0) {
|
|
10
|
+
const channelList = c.channels
|
|
11
|
+
.map((ch) => `${ch.type}:${ch.address}${ch.isPrimary ? '*' : ''}`)
|
|
12
|
+
.join(', ');
|
|
13
|
+
parts.push(` Channels: ${channelList}`);
|
|
14
|
+
}
|
|
15
|
+
return parts.join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function executeContactSearch(
|
|
5
19
|
input: Record<string, unknown>,
|
|
6
|
-
|
|
20
|
+
_context: ToolContext,
|
|
7
21
|
): Promise<ToolExecutionResult> {
|
|
8
|
-
|
|
22
|
+
const query = input.query as string | undefined;
|
|
23
|
+
const channelAddress = input.channel_address as string | undefined;
|
|
24
|
+
const channelType = input.channel_type as string | undefined;
|
|
25
|
+
const relationship = input.relationship as string | undefined;
|
|
26
|
+
const limit = input.limit as number | undefined;
|
|
27
|
+
|
|
28
|
+
if (!query && !channelAddress && !relationship) {
|
|
29
|
+
return {
|
|
30
|
+
content: 'Error: At least one search criterion is required (query, channel_address, or relationship)',
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const results = searchContacts({
|
|
37
|
+
query,
|
|
38
|
+
channelAddress,
|
|
39
|
+
channelType,
|
|
40
|
+
relationship,
|
|
41
|
+
limit,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (results.length === 0) {
|
|
45
|
+
return { content: 'No contacts found matching the search criteria.', isError: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lines = [`Found ${results.length} contact(s):\n`];
|
|
49
|
+
for (const contact of results) {
|
|
50
|
+
lines.push(formatContactSummary(contact));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { content: lines.join('\n'), isError: false };
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
+
return { content: `Error: ${msg}`, isError: true };
|
|
57
|
+
}
|
|
9
58
|
}
|
|
59
|
+
|
|
60
|
+
export { executeContactSearch as run };
|
|
@@ -1,9 +1,66 @@
|
|
|
1
1
|
import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
|
|
2
|
-
import {
|
|
2
|
+
import { upsertContact } from '../../../../contacts/contact-store.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
function formatContact(c: ReturnType<typeof upsertContact>): string {
|
|
5
|
+
const lines = [
|
|
6
|
+
`Contact ${c.id}`,
|
|
7
|
+
` Name: ${c.displayName}`,
|
|
8
|
+
];
|
|
9
|
+
if (c.relationship) lines.push(` Relationship: ${c.relationship}`);
|
|
10
|
+
lines.push(` Importance: ${c.importance.toFixed(2)}`);
|
|
11
|
+
if (c.responseExpectation) lines.push(` Response expectation: ${c.responseExpectation}`);
|
|
12
|
+
if (c.preferredTone) lines.push(` Preferred tone: ${c.preferredTone}`);
|
|
13
|
+
if (c.interactionCount > 0) lines.push(` Interactions: ${c.interactionCount}`);
|
|
14
|
+
if (c.channels.length > 0) {
|
|
15
|
+
lines.push(' Channels:');
|
|
16
|
+
for (const ch of c.channels) {
|
|
17
|
+
const primary = ch.isPrimary ? ' (primary)' : '';
|
|
18
|
+
lines.push(` - ${ch.type}: ${ch.address}${primary}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function executeContactUpsert(
|
|
5
25
|
input: Record<string, unknown>,
|
|
6
|
-
|
|
26
|
+
_context: ToolContext,
|
|
7
27
|
): Promise<ToolExecutionResult> {
|
|
8
|
-
|
|
28
|
+
const displayName = input.display_name as string | undefined;
|
|
29
|
+
if (!displayName || typeof displayName !== 'string' || displayName.trim().length === 0) {
|
|
30
|
+
return { content: 'Error: display_name is required and must be a non-empty string', isError: true };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const importance = input.importance as number | undefined;
|
|
34
|
+
if (importance !== undefined && (typeof importance !== 'number' || importance < 0 || importance > 1)) {
|
|
35
|
+
return { content: 'Error: importance must be a number between 0 and 1', isError: true };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const rawChannels = input.channels as Array<{ type: string; address: string; is_primary?: boolean }> | undefined;
|
|
39
|
+
const channels = rawChannels?.map((ch) => ({
|
|
40
|
+
type: ch.type,
|
|
41
|
+
address: ch.address,
|
|
42
|
+
isPrimary: ch.is_primary,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const contact = upsertContact({
|
|
47
|
+
id: input.id as string | undefined,
|
|
48
|
+
displayName: displayName.trim(),
|
|
49
|
+
relationship: input.relationship as string | undefined,
|
|
50
|
+
importance,
|
|
51
|
+
responseExpectation: input.response_expectation as string | undefined,
|
|
52
|
+
preferredTone: input.preferred_tone as string | undefined,
|
|
53
|
+
channels,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
content: `${contact.created ? 'Created' : 'Updated'} contact:\n${formatContact(contact)}`,
|
|
58
|
+
isError: false,
|
|
59
|
+
};
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
+
return { content: `Error: ${msg}`, isError: true };
|
|
63
|
+
}
|
|
9
64
|
}
|
|
65
|
+
|
|
66
|
+
export { executeContactUpsert as run };
|
|
@@ -37,7 +37,7 @@ Telegram uses a bot token (not OAuth). Install and load the **telegram-setup** s
|
|
|
37
37
|
- Then call `skill_load` with `skill: "telegram-setup"`.
|
|
38
38
|
- Tell the user: *"I've loaded a setup guide for Telegram. It will walk you through connecting a Telegram bot to your assistant."*
|
|
39
39
|
|
|
40
|
-
The telegram-setup skill handles: verifying the bot token from @BotFather, generating a webhook secret, registering the
|
|
40
|
+
The telegram-setup skill handles: verifying the bot token from @BotFather, generating a webhook secret, registering bot commands, and storing credentials securely via the secure credential prompt flow. **Never accept a Telegram bot token pasted in plaintext chat — always use the secure prompt.** Webhook registration with Telegram is handled automatically by the gateway on startup and whenever credentials change.
|
|
41
41
|
|
|
42
42
|
## Platform Selection
|
|
43
43
|
|
|
@@ -47,7 +47,7 @@ The telegram-setup skill handles: verifying the bot token from @BotFather, gener
|
|
|
47
47
|
|
|
48
48
|
## Capabilities
|
|
49
49
|
|
|
50
|
-
### Universal (
|
|
50
|
+
### Universal (Slack, Gmail)
|
|
51
51
|
- **Auth Test**: Verify connection and show account info
|
|
52
52
|
- **List Conversations**: Show channels, inboxes, DMs with unread counts
|
|
53
53
|
- **Read Messages**: Read message history from a conversation
|
|
@@ -56,6 +56,21 @@ The telegram-setup skill handles: verifying the bot token from @BotFather, gener
|
|
|
56
56
|
- **Reply**: Reply in a thread (medium risk)
|
|
57
57
|
- **Mark Read**: Mark conversation as read
|
|
58
58
|
|
|
59
|
+
### Telegram
|
|
60
|
+
Telegram is supported as a messaging provider with limited capabilities compared to Slack and Gmail due to Bot API constraints:
|
|
61
|
+
|
|
62
|
+
- **Send**: Send a message to a known chat ID (high risk — requires user approval)
|
|
63
|
+
- **Auth Test**: Verify bot token and show bot info
|
|
64
|
+
|
|
65
|
+
**Not available** (Bot API limitations):
|
|
66
|
+
- List conversations — the Bot API does not expose a method to enumerate chats a bot belongs to
|
|
67
|
+
- Read message history — bots cannot retrieve past messages from a chat
|
|
68
|
+
- Search messages — no search API is available for bots
|
|
69
|
+
|
|
70
|
+
**Bot-account limits:**
|
|
71
|
+
- The bot can only message users or groups that have previously interacted with it (sent `/start` or been added to a group). Bots cannot initiate conversations with arbitrary phone numbers.
|
|
72
|
+
- Future support for MTProto user-account sessions may lift some of these restrictions.
|
|
73
|
+
|
|
59
74
|
### Slack-specific
|
|
60
75
|
- **Add Reaction**: Add an emoji reaction to a message
|
|
61
76
|
- **Leave Channel**: Leave a Slack channel
|
|
@@ -20,7 +20,10 @@ export async function run(input: Record<string, unknown>, _context: ToolContext)
|
|
|
20
20
|
try {
|
|
21
21
|
const provider = resolveProvider(platform);
|
|
22
22
|
return withProviderToken(provider, async (token) => {
|
|
23
|
-
const result = await provider.sendMessage(token, conversationId, text, {
|
|
23
|
+
const result = await provider.sendMessage(token, conversationId, text, {
|
|
24
|
+
threadId,
|
|
25
|
+
});
|
|
26
|
+
|
|
24
27
|
return ok(`Reply sent (ID: ${result.id}).`);
|
|
25
28
|
});
|
|
26
29
|
} catch (e) {
|
|
@@ -18,7 +18,11 @@ export async function run(input: Record<string, unknown>, _context: ToolContext)
|
|
|
18
18
|
try {
|
|
19
19
|
const provider = resolveProvider(platform);
|
|
20
20
|
return withProviderToken(provider, async (token) => {
|
|
21
|
-
const result = await provider.sendMessage(token, conversationId, text, {
|
|
21
|
+
const result = await provider.sendMessage(token, conversationId, text, {
|
|
22
|
+
subject,
|
|
23
|
+
inReplyTo,
|
|
24
|
+
});
|
|
25
|
+
|
|
22
26
|
return ok(`Message sent (ID: ${result.id}).`);
|
|
23
27
|
});
|
|
24
28
|
} catch (e) {
|
|
@@ -37,11 +37,16 @@ export function resolveProvider(platformInput?: string): MessagingProvider {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Execute a callback with a valid OAuth token for the given provider.
|
|
40
|
+
* Providers that manage their own auth (e.g. Telegram with a bot token)
|
|
41
|
+
* expose isConnected() and don't need an OAuth access_token lookup.
|
|
40
42
|
*/
|
|
41
43
|
export async function withProviderToken<T>(
|
|
42
44
|
provider: MessagingProvider,
|
|
43
45
|
fn: (token: string) => Promise<T>,
|
|
44
46
|
): Promise<T> {
|
|
47
|
+
if (provider.isConnected?.()) {
|
|
48
|
+
return fn('');
|
|
49
|
+
}
|
|
45
50
|
return withValidToken(provider.credentialService, fn);
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -21,13 +21,13 @@ When a call is placed:
|
|
|
21
21
|
5. The transcript is relayed live to the user's conversation thread
|
|
22
22
|
|
|
23
23
|
Three voice quality modes are available:
|
|
24
|
-
- **`twilio_standard`** (default) — Standard Twilio TTS with Google voices. No extra setup required.
|
|
25
|
-
- **`twilio_elevenlabs_tts`** — Uses ElevenLabs voices through Twilio ConversationRelay for more natural speech.
|
|
26
|
-
- **`elevenlabs_agent`** — Full ElevenLabs conversational agent mode
|
|
24
|
+
- **`twilio_standard`** (default) — Fully supported. Standard Twilio TTS with Google voices. No extra setup required.
|
|
25
|
+
- **`twilio_elevenlabs_tts`** — Fully supported. Uses ElevenLabs voices through Twilio ConversationRelay for more natural speech.
|
|
26
|
+
- **`elevenlabs_agent`** — **Experimental/restricted.** Full ElevenLabs conversational agent mode. Consultation bridging (`waiting_on_user`) is not yet supported in this mode; the runtime guard blocks it before any ElevenLabs API calls are made. See the "Runtime behavior" section below for fallback and strict-fail details.
|
|
27
27
|
|
|
28
28
|
You can keep using Twilio only — no changes needed. Enabling ElevenLabs can improve naturalness and quality.
|
|
29
29
|
|
|
30
|
-
The user's assistant gets its own personal phone number through Twilio.
|
|
30
|
+
The user's assistant gets its own personal phone number through Twilio. All implicit calls (without an explicit mode) always use this assistant number. Optionally, users can call from their own phone number if it's authorized with the Twilio account — this must be explicitly requested per call via `caller_identity_mode="user_number"`.
|
|
31
31
|
|
|
32
32
|
## Step 1: Check Current Configuration
|
|
33
33
|
|
|
@@ -40,11 +40,11 @@ vellum config get calls.enabled
|
|
|
40
40
|
Also check for existing credentials:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
credential_store action=
|
|
44
|
-
credential_store action=get service=credential:twilio:auth_token
|
|
45
|
-
credential_store action=get service=credential:twilio:phone_number
|
|
43
|
+
credential_store action=list
|
|
46
44
|
```
|
|
47
45
|
|
|
46
|
+
Look for entries with service `twilio` and fields `account_sid`, `auth_token`, and `phone_number`.
|
|
47
|
+
|
|
48
48
|
If all three credentials exist and `calls.enabled` is `true`, skip to the **Making Calls** section. If credentials are partially configured, skip to whichever step is still needed.
|
|
49
49
|
|
|
50
50
|
## Step 2: Create a Twilio Account
|
|
@@ -74,26 +74,26 @@ Once the user provides their credentials, store them securely using the `credent
|
|
|
74
74
|
|
|
75
75
|
**Account SID:**
|
|
76
76
|
```
|
|
77
|
-
credential_store action=
|
|
77
|
+
credential_store action=store service=twilio field=account_sid value=<their_account_sid>
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
**Auth Token:**
|
|
81
81
|
```
|
|
82
|
-
credential_store action=
|
|
82
|
+
credential_store action=store service=twilio field=auth_token value=<their_auth_token>
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
**Phone Number** (must be in E.164 format, e.g. `+14155551234`):
|
|
86
86
|
```
|
|
87
|
-
credential_store action=
|
|
87
|
+
credential_store action=store service=twilio field=phone_number value=<their_phone_number>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
After storing, verify each credential was saved:
|
|
91
91
|
```
|
|
92
|
-
credential_store action=
|
|
93
|
-
credential_store action=get service=credential:twilio:auth_token
|
|
94
|
-
credential_store action=get service=credential:twilio:phone_number
|
|
92
|
+
credential_store action=list
|
|
95
93
|
```
|
|
96
94
|
|
|
95
|
+
Confirm that entries for service `twilio` with fields `account_sid`, `auth_token`, and `phone_number` appear in the output.
|
|
96
|
+
|
|
97
97
|
**Important:** Credentials are stored in the OS keychain (macOS Keychain / Linux secret-service) or encrypted at rest. They are never logged or exposed in plaintext.
|
|
98
98
|
|
|
99
99
|
## Step 4: Set Up Public Ingress
|
|
@@ -139,7 +139,7 @@ vellum config get calls.enabled
|
|
|
139
139
|
|
|
140
140
|
Before making real calls, offer a quick verification:
|
|
141
141
|
|
|
142
|
-
1. Confirm credentials are stored: all three `
|
|
142
|
+
1. Confirm credentials are stored: all three Twilio credentials (`account_sid`, `auth_token`, `phone_number`) must be present
|
|
143
143
|
2. Confirm ingress is running: `ingress.publicBaseUrl` must be set and the tunnel active
|
|
144
144
|
3. Confirm calls are enabled: `calls.enabled` must be `true`
|
|
145
145
|
|
|
@@ -147,34 +147,75 @@ Suggest a test call to the user's own phone: **"Want to do a quick test call to
|
|
|
147
147
|
|
|
148
148
|
If they agree, ask for their personal phone number and place a test call with a simple task like "Introduce yourself and confirm the call system is working."
|
|
149
149
|
|
|
150
|
+
## Caller Identity
|
|
151
|
+
|
|
152
|
+
All implicit calls (calls without an explicit `caller_identity_mode`) always use the assistant's Twilio phone number. This is the number that appears on the recipient's caller ID.
|
|
153
|
+
|
|
154
|
+
### User-number mode (per-call only)
|
|
155
|
+
|
|
156
|
+
If the user wants a specific call to appear as coming from their own phone number, they must explicitly pass `caller_identity_mode: 'user_number'` on that call. The user's phone number must be either owned by or verified with the same Twilio account.
|
|
157
|
+
|
|
158
|
+
**To configure a user phone number:**
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
credential_store action=store service=twilio field=user_phone_number value=+14155559999
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**To use it for a specific call**, pass `caller_identity_mode: 'user_number'` when calling `call_start` — see the Making Calls section for examples. User-number mode cannot be set as a global default; it must be requested explicitly per call.
|
|
165
|
+
|
|
166
|
+
### Configuration reference
|
|
167
|
+
|
|
168
|
+
| Setting | Description | Default |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `calls.callerIdentity.allowPerCallOverride` | Whether per-call mode selection is allowed | `true` |
|
|
171
|
+
| `calls.callerIdentity.userNumber` | Optional E.164 phone number for user-number mode (alternative to storing via `credential_store`) | *(empty)* |
|
|
172
|
+
|
|
150
173
|
## Optional: Higher Quality Voice with ElevenLabs
|
|
151
174
|
|
|
152
175
|
ElevenLabs integration is entirely optional. The standard Twilio-only setup works unchanged — this section is only relevant if you want to improve voice quality.
|
|
153
176
|
|
|
154
177
|
### Mode: `twilio_elevenlabs_tts`
|
|
155
178
|
|
|
156
|
-
Uses ElevenLabs voices through Twilio's ConversationRelay. Speech is more natural-sounding than the default Google TTS voices.
|
|
179
|
+
Uses ElevenLabs voices through Twilio's ConversationRelay. Speech is more natural-sounding than the default Google TTS voices.
|
|
157
180
|
|
|
158
|
-
**
|
|
181
|
+
**Recommended user-friendly workflow (no technical IDs required):**
|
|
159
182
|
|
|
160
|
-
1.
|
|
161
|
-
2.
|
|
183
|
+
1. Ask what kind of voice the user wants (examples: "warm", "professional", "playful", "calm", "deeper", "brighter")
|
|
184
|
+
2. If the user doesn't care, keep `twilio_standard` (simplest path)
|
|
185
|
+
3. If they want higher-quality voice, switch to `twilio_elevenlabs_tts` and choose a matching ElevenLabs voice on their behalf
|
|
186
|
+
|
|
187
|
+
The user should not need to know what a `voiceId` is unless they explicitly want advanced/manual control.
|
|
188
|
+
|
|
189
|
+
**Manual/advanced setup (optional):**
|
|
162
190
|
|
|
163
191
|
```bash
|
|
164
192
|
vellum config set calls.voice.mode twilio_elevenlabs_tts
|
|
165
193
|
vellum config set calls.voice.elevenlabs.voiceId "<your-voice-id>"
|
|
166
194
|
```
|
|
167
195
|
|
|
168
|
-
|
|
196
|
+
By default, the system sends a **bare** `voiceId` to Twilio ConversationRelay (no model/tuning suffix). This is the safest default across voice IDs.
|
|
197
|
+
|
|
198
|
+
If you want to force Twilio's extended voice spec, you can optionally set a model ID:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
vellum config set calls.voice.elevenlabs.voiceModelId "flash_v2_5"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
When `voiceModelId` is set, the emitted voice string becomes:
|
|
205
|
+
`voiceId-model-speed_stability_similarity`.
|
|
206
|
+
|
|
207
|
+
### Mode: `elevenlabs_agent` (experimental/restricted)
|
|
169
208
|
|
|
170
209
|
Full ElevenLabs conversational agent mode. This requires an ElevenLabs account with an agent configured on their platform.
|
|
171
210
|
|
|
211
|
+
**Restriction:** This mode is currently restricted because consultation bridging (`waiting_on_user`) is not yet supported. A runtime guard in `handleVoiceWebhook` blocks `elevenlabs_agent` before any ElevenLabs API calls are made.
|
|
212
|
+
|
|
172
213
|
**Setup:**
|
|
173
214
|
|
|
174
215
|
1. Store your ElevenLabs API key securely:
|
|
175
216
|
|
|
176
217
|
```
|
|
177
|
-
credential_store action=
|
|
218
|
+
credential_store action=store service=elevenlabs field=api_key value=<your_api_key>
|
|
178
219
|
```
|
|
179
220
|
|
|
180
221
|
2. Set the voice mode and agent ID:
|
|
@@ -184,9 +225,21 @@ vellum config set calls.voice.mode elevenlabs_agent
|
|
|
184
225
|
vellum config set calls.voice.elevenlabs.agentId "<your-agent-id>"
|
|
185
226
|
```
|
|
186
227
|
|
|
187
|
-
### Fallback behavior
|
|
228
|
+
### Fallback behavior and `fallbackToStandardOnError`
|
|
188
229
|
|
|
189
|
-
By default, `calls.voice.fallbackToStandardOnError` is `true`. This
|
|
230
|
+
By default, `calls.voice.fallbackToStandardOnError` is `true`. This setting controls what happens when an ElevenLabs mode encounters errors or is restricted.
|
|
231
|
+
|
|
232
|
+
#### Invalid configuration (e.g., missing voiceId or agentId)
|
|
233
|
+
|
|
234
|
+
- **`true` (default):** The profile resolver silently falls back to `twilio_standard` mode and logs a warning. The call proceeds with standard Twilio TTS.
|
|
235
|
+
- **`false`:** The voice webhook returns **HTTP 500** with the specific configuration error details (e.g., `"Voice quality configuration error: calls.voice.elevenlabs.voiceId is required..."`).
|
|
236
|
+
|
|
237
|
+
#### `elevenlabs_agent` mode guard (consultation bridging unsupported)
|
|
238
|
+
|
|
239
|
+
- **`true` (default):** The `elevenlabs_agent` mode is silently downgraded to standard ConversationRelay TwiML with a warning log. The call proceeds normally with standard Twilio TTS. No ElevenLabs API calls are made.
|
|
240
|
+
- **`false`:** The voice webhook returns **HTTP 501** with the message: `"elevenlabs_agent mode is restricted: consultation bridging (waiting_on_user) is not yet supported."`. No ElevenLabs API calls are made.
|
|
241
|
+
|
|
242
|
+
You can disable fallback if you want strict ElevenLabs-only behavior:
|
|
190
243
|
|
|
191
244
|
```bash
|
|
192
245
|
vellum config set calls.voice.fallbackToStandardOnError false
|
|
@@ -224,6 +277,22 @@ call_start phone_number="+18005551234" task="Check if they have a specific produ
|
|
|
224
277
|
call_start phone_number="+12125551234" task="Confirm the dentist appointment scheduled for next Tuesday at 2pm" context="The appointment is under the name Jane Doe, DOB 03/15/1990."
|
|
225
278
|
```
|
|
226
279
|
|
|
280
|
+
### Caller identity in calls
|
|
281
|
+
|
|
282
|
+
Implicit calls always use the assistant's Twilio number (`assistant_number`). Only specify `caller_identity_mode` when the user explicitly requests a different identity for a specific call.
|
|
283
|
+
|
|
284
|
+
**Default call (assistant number):**
|
|
285
|
+
```
|
|
286
|
+
call_start phone_number="+14155551234" task="Check store hours for today"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Call from the user's own number:**
|
|
290
|
+
```
|
|
291
|
+
call_start phone_number="+14155551234" task="Check store hours for today" caller_identity_mode="user_number"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Decision rule:** Implicit calls (no explicit mode) always use the assistant's Twilio number. Only use `caller_identity_mode="user_number"` when the user explicitly requests it for a specific call.
|
|
295
|
+
|
|
227
296
|
### Phone number format
|
|
228
297
|
|
|
229
298
|
Phone numbers MUST be in E.164 format: `+` followed by country code and number with no spaces, dashes, or parentheses.
|
|
@@ -261,24 +330,40 @@ By default, always show the live transcript of the call as it happens. When a ca
|
|
|
261
330
|
|
|
262
331
|
4. Continue monitoring until the call completes or fails
|
|
263
332
|
|
|
264
|
-
###
|
|
333
|
+
### Interacting with a live call
|
|
334
|
+
|
|
335
|
+
During an active call, the user can type messages in the chat thread to interact with the AI voice agent in real time. Messages are automatically routed to the call via the call bridge, which decides how to handle them based on the call's current state:
|
|
336
|
+
|
|
337
|
+
#### Mode 1: Answering questions
|
|
265
338
|
|
|
266
|
-
|
|
339
|
+
When the AI voice agent encounters something it needs user input for, a **pending question** appears in the chat. The call status changes to `waiting_on_user`.
|
|
267
340
|
|
|
268
|
-
1.
|
|
269
|
-
2.
|
|
270
|
-
3. Present the question prominently to the user:
|
|
341
|
+
1. A **pending question** appears in `call_status` output
|
|
342
|
+
2. Present the question prominently to the user:
|
|
271
343
|
|
|
272
344
|
```
|
|
273
345
|
❓ The person on the call asked something the assistant needs your help with:
|
|
274
346
|
"They're asking if you'd prefer the smoking or non-smoking section?"
|
|
275
347
|
```
|
|
276
348
|
|
|
277
|
-
|
|
278
|
-
|
|
349
|
+
3. The user replies directly in the chat — since there is a pending question, the reply is automatically routed as an **answer** to the AI voice agent
|
|
350
|
+
4. The AI voice agent receives the answer and continues the conversation naturally
|
|
279
351
|
|
|
280
352
|
**Important:** Respond to pending questions quickly. There is a consultation timeout (default: 2 minutes). If no answer is provided in time, the AI voice agent will move on.
|
|
281
353
|
|
|
354
|
+
#### Mode 2: Steering with instructions
|
|
355
|
+
|
|
356
|
+
When there is **no pending question** but the call is still active, any message the user types in the chat is treated as a **steering instruction**. This lets the user proactively guide the call in real time — for example:
|
|
357
|
+
|
|
358
|
+
- "Ask them about their cancellation policy too"
|
|
359
|
+
- "Wrap up the call, we have what we need"
|
|
360
|
+
- "Switch to asking about weekend availability instead"
|
|
361
|
+
- "Be more assertive about getting a discount"
|
|
362
|
+
|
|
363
|
+
The instruction is injected into the AI voice agent's conversation context as high-priority input, and the agent adjusts its behavior accordingly. A confirmation message ("Instruction relayed to active call.") appears in the chat thread.
|
|
364
|
+
|
|
365
|
+
**The user does not need to do anything special** — just type a message. The system automatically determines whether it should be an answer or an instruction based on whether a question is pending.
|
|
366
|
+
|
|
282
367
|
### Call status values
|
|
283
368
|
|
|
284
369
|
- **initiated** — Call is being placed
|
|
@@ -345,12 +430,18 @@ All call-related settings can be managed via `vellum config`:
|
|
|
345
430
|
| `calls.disclosure.enabled` | Whether the AI announces itself at call start | `true` |
|
|
346
431
|
| `calls.disclosure.text` | The disclosure message spoken at call start | `"I should let you know that I'm an AI assistant calling on behalf of my user."` |
|
|
347
432
|
| `calls.model` | Override LLM model for call orchestration | *(uses default model)* |
|
|
433
|
+
| `calls.callerIdentity.allowPerCallOverride` | Allow per-call caller identity selection | `true` |
|
|
434
|
+
| `calls.callerIdentity.userNumber` | E.164 phone number for user-number mode | *(empty)* |
|
|
348
435
|
| `calls.voice.mode` | Voice quality mode (`twilio_standard`, `twilio_elevenlabs_tts`, `elevenlabs_agent`) | `twilio_standard` |
|
|
349
436
|
| `calls.voice.language` | Language code for TTS and transcription | `en-US` |
|
|
350
437
|
| `calls.voice.transcriptionProvider` | Speech-to-text provider (`Deepgram`, `Google`) | `Deepgram` |
|
|
351
438
|
| `calls.voice.fallbackToStandardOnError` | Auto-fallback to standard Twilio TTS on ElevenLabs errors | `true` |
|
|
352
|
-
| `calls.voice.elevenlabs.voiceId` | ElevenLabs voice
|
|
439
|
+
| `calls.voice.elevenlabs.voiceId` | Advanced/internal ElevenLabs voice identifier. Usually set by the assistant based on requested voice style | *(empty)* |
|
|
440
|
+
| `calls.voice.elevenlabs.voiceModelId` | Optional Twilio ConversationRelay model suffix. Leave empty to send bare `voiceId` | *(empty)* |
|
|
353
441
|
| `calls.voice.elevenlabs.agentId` | ElevenLabs agent ID (for `elevenlabs_agent` mode) | *(empty)* |
|
|
442
|
+
| `calls.voice.elevenlabs.speed` | Playback speed (`0.7` – `1.2`) | `1.0` |
|
|
443
|
+
| `calls.voice.elevenlabs.stability` | Voice stability (`0.0` – `1.0`) | `0.5` |
|
|
444
|
+
| `calls.voice.elevenlabs.similarityBoost` | Voice similarity boost (`0.0` – `1.0`) | `0.75` |
|
|
354
445
|
|
|
355
446
|
### Adjusting settings
|
|
356
447
|
|
|
@@ -389,6 +480,15 @@ Run the **public-ingress** skill to set up ngrok and configure `ingress.publicBa
|
|
|
389
480
|
- The ConversationRelay WebSocket may not be connecting. Check that `ingress.publicBaseUrl` is correct and the tunnel is active
|
|
390
481
|
- Verify the gateway is running on `http://127.0.0.1:${GATEWAY_PORT:-7830}`
|
|
391
482
|
|
|
483
|
+
### "Number not eligible for caller identity"
|
|
484
|
+
The user's phone number is not owned by or verified with the Twilio account. The number must be either purchased through Twilio or added as a verified caller ID at https://console.twilio.com/us1/develop/phone-numbers/manage/verified.
|
|
485
|
+
|
|
486
|
+
### "Per-call caller identity override is disabled"
|
|
487
|
+
The setting `calls.callerIdentity.allowPerCallOverride` is set to `false`, so per-call `caller_identity_mode` selection is not allowed. Re-enable overrides with `vellum config set calls.callerIdentity.allowPerCallOverride true`.
|
|
488
|
+
|
|
489
|
+
### Caller identity call fails on trial account
|
|
490
|
+
Twilio trial accounts can only place calls to verified numbers, regardless of caller identity mode. The user's phone number must also be verified with Twilio. Upgrade to a paid account or verify both the source and destination numbers.
|
|
491
|
+
|
|
392
492
|
### "This phone number is not allowed to be called"
|
|
393
493
|
Emergency numbers (911, 112, 999, 000, 110, 119) are permanently blocked for safety.
|
|
394
494
|
|
|
@@ -404,11 +504,19 @@ The system has a 30-second silence timeout. If nobody speaks for 30 seconds, the
|
|
|
404
504
|
|
|
405
505
|
### Call quality didn't improve after enabling ElevenLabs
|
|
406
506
|
- Verify `calls.voice.mode` is set to `twilio_elevenlabs_tts` or `elevenlabs_agent` (not still `twilio_standard`)
|
|
407
|
-
-
|
|
507
|
+
- Ask for the desired voice style again and try a different voice selection
|
|
508
|
+
- If configuring manually: check that `calls.voice.elevenlabs.voiceId` contains a valid ElevenLabs voice ID
|
|
408
509
|
- If mode is `elevenlabs_agent`, ensure `calls.voice.elevenlabs.agentId` is also set
|
|
409
510
|
|
|
511
|
+
### Twilio says "application error" right after answer
|
|
512
|
+
- This often means ConversationRelay rejected voice configuration after TwiML fetch
|
|
513
|
+
- Keep `calls.voice.elevenlabs.voiceModelId` empty first (bare `voiceId` mode)
|
|
514
|
+
- If you set `voiceModelId`, try clearing it and retesting:
|
|
515
|
+
`vellum config set calls.voice.elevenlabs.voiceModelId ""`
|
|
516
|
+
|
|
410
517
|
### ElevenLabs mode falls back to standard
|
|
411
|
-
When `calls.voice.fallbackToStandardOnError` is `true` (the default), the system silently falls back to standard Twilio TTS if ElevenLabs encounters an error. Check:
|
|
412
|
-
- For `elevenlabs_agent` mode:
|
|
518
|
+
When `calls.voice.fallbackToStandardOnError` is `true` (the default), the system silently falls back to standard Twilio TTS if ElevenLabs encounters an error or restriction. Check:
|
|
519
|
+
- For `elevenlabs_agent` mode: this mode is currently restricted (consultation bridging not yet supported) and will always fall back to standard when fallback is enabled. If fallback is disabled, the voice webhook returns HTTP 501.
|
|
413
520
|
- For `twilio_elevenlabs_tts` mode: verify `calls.voice.elevenlabs.voiceId` is set to a valid voice ID
|
|
414
|
-
-
|
|
521
|
+
- For invalid configs (missing voiceId/agentId): if fallback is disabled, the voice webhook returns HTTP 500 with the config error
|
|
522
|
+
- Review daemon logs for warning messages about fallback or guard activation
|
|
@@ -1,9 +1,98 @@
|
|
|
1
|
+
import { and, eq } from 'drizzle-orm';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
1
3
|
import type { ToolContext, ToolExecutionResult } from '../../../../tools/types.js';
|
|
2
|
-
import {
|
|
4
|
+
import { getDb } from '../../../../memory/db.js';
|
|
5
|
+
import { computeMemoryFingerprint } from '../../../../memory/fingerprint.js';
|
|
6
|
+
import { memoryItems } from '../../../../memory/schema.js';
|
|
7
|
+
import { enqueueMemoryJob } from '../../../../memory/jobs-store.js';
|
|
8
|
+
import type { Playbook, PlaybookAutonomyLevel } from '../../../../playbooks/types.js';
|
|
9
|
+
import { truncate } from '../../../../util/truncate.js';
|
|
3
10
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
const VALID_AUTONOMY_LEVELS = new Set<string>(['auto', 'draft', 'notify']);
|
|
12
|
+
|
|
13
|
+
export async function executePlaybookCreate(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
14
|
+
const trigger = input.trigger as string;
|
|
15
|
+
const action = input.action as string;
|
|
16
|
+
|
|
17
|
+
if (!trigger || typeof trigger !== 'string') {
|
|
18
|
+
return { content: 'Error: trigger is required and must be a string', isError: true };
|
|
19
|
+
}
|
|
20
|
+
if (!action || typeof action !== 'string') {
|
|
21
|
+
return { content: 'Error: action is required and must be a string', isError: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const channel = typeof input.channel === 'string' ? input.channel : '*';
|
|
25
|
+
const category = typeof input.category === 'string' ? input.category : 'general';
|
|
26
|
+
const autonomyLevel: PlaybookAutonomyLevel =
|
|
27
|
+
typeof input.autonomy_level === 'string' && VALID_AUTONOMY_LEVELS.has(input.autonomy_level)
|
|
28
|
+
? (input.autonomy_level as PlaybookAutonomyLevel)
|
|
29
|
+
: 'draft';
|
|
30
|
+
const priority = typeof input.priority === 'number' ? input.priority : 0;
|
|
31
|
+
|
|
32
|
+
const playbook: Playbook = { trigger, channel, category, action, autonomyLevel, priority };
|
|
33
|
+
const statement = JSON.stringify(playbook);
|
|
34
|
+
const subject = truncate(`Playbook: ${trigger}`, 80, '');
|
|
35
|
+
const scopeId = context.memoryScopeId ?? 'default';
|
|
36
|
+
|
|
37
|
+
const fingerprint = computeMemoryFingerprint(scopeId, 'playbook', subject, statement);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
|
|
42
|
+
const existing = db
|
|
43
|
+
.select()
|
|
44
|
+
.from(memoryItems)
|
|
45
|
+
.where(and(eq(memoryItems.fingerprint, fingerprint), eq(memoryItems.scopeId, scopeId)))
|
|
46
|
+
.get();
|
|
47
|
+
|
|
48
|
+
if (existing) {
|
|
49
|
+
return {
|
|
50
|
+
content: `A playbook with this exact configuration already exists (ID: ${existing.id}).`,
|
|
51
|
+
isError: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = uuid();
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
|
|
58
|
+
db.insert(memoryItems).values({
|
|
59
|
+
id,
|
|
60
|
+
kind: 'playbook',
|
|
61
|
+
subject,
|
|
62
|
+
statement,
|
|
63
|
+
status: 'active',
|
|
64
|
+
confidence: 0.95,
|
|
65
|
+
importance: 0.8,
|
|
66
|
+
fingerprint,
|
|
67
|
+
verificationState: 'user_confirmed',
|
|
68
|
+
scopeId,
|
|
69
|
+
firstSeenAt: now,
|
|
70
|
+
lastSeenAt: now,
|
|
71
|
+
lastUsedAt: null,
|
|
72
|
+
}).run();
|
|
73
|
+
|
|
74
|
+
enqueueMemoryJob('embed_item', { itemId: id });
|
|
75
|
+
|
|
76
|
+
const autonomyLabel = autonomyLevel === 'auto' ? 'execute automatically'
|
|
77
|
+
: autonomyLevel === 'draft' ? 'draft for review' : 'notify only';
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
content: [
|
|
81
|
+
'Playbook created successfully.',
|
|
82
|
+
` ID: ${id}`,
|
|
83
|
+
` Trigger: ${trigger}`,
|
|
84
|
+
` Channel: ${channel}`,
|
|
85
|
+
` Category: ${category}`,
|
|
86
|
+
` Action: ${action}`,
|
|
87
|
+
` Autonomy: ${autonomyLabel}`,
|
|
88
|
+
` Priority: ${priority}`,
|
|
89
|
+
].join('\n'),
|
|
90
|
+
isError: false,
|
|
91
|
+
};
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return { content: `Error creating playbook: ${msg}`, isError: true };
|
|
95
|
+
}
|
|
9
96
|
}
|
|
97
|
+
|
|
98
|
+
export { executePlaybookCreate as run };
|