vellum 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +299 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +62 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +163 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call-answer bridge: auto-consumes user replies in-thread as answers
|
|
3
|
+
* to pending call questions, routing them to the live call orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* When a call has a pending question and the user sends a normal message
|
|
6
|
+
* in the conversation thread, this bridge intercepts the message before
|
|
7
|
+
* the agent loop, forwards the answer to the orchestrator, and returns
|
|
8
|
+
* `{ handled: true }` so the caller can skip agent processing.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getLogger } from '../util/logger.js';
|
|
12
|
+
import {
|
|
13
|
+
getActiveCallSessionForConversation,
|
|
14
|
+
getPendingQuestion,
|
|
15
|
+
answerPendingQuestion,
|
|
16
|
+
recordCallEvent,
|
|
17
|
+
getCallSession,
|
|
18
|
+
} from './call-store.js';
|
|
19
|
+
import { getCallOrchestrator } from './call-state.js';
|
|
20
|
+
import * as conversationStore from '../memory/conversation-store.js';
|
|
21
|
+
|
|
22
|
+
const log = getLogger('call-bridge');
|
|
23
|
+
|
|
24
|
+
export interface CallBridgeResult {
|
|
25
|
+
handled: boolean;
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attempt to route a user message as an answer to a pending call question.
|
|
31
|
+
*
|
|
32
|
+
* @param conversationId - The conversation the message belongs to.
|
|
33
|
+
* @param userText - The user's message text.
|
|
34
|
+
* @param _userMessageId - The persisted message ID (reserved for future use).
|
|
35
|
+
* @returns `{ handled: true }` if the answer was consumed by the call system,
|
|
36
|
+
* `{ handled: false, reason }` otherwise.
|
|
37
|
+
*/
|
|
38
|
+
export async function tryHandlePendingCallAnswer(
|
|
39
|
+
conversationId: string,
|
|
40
|
+
userText: string,
|
|
41
|
+
_userMessageId?: string,
|
|
42
|
+
): Promise<CallBridgeResult> {
|
|
43
|
+
// 1. Find an active call for this conversation
|
|
44
|
+
const callSession = getActiveCallSessionForConversation(conversationId);
|
|
45
|
+
if (!callSession) {
|
|
46
|
+
return { handled: false, reason: 'no_active_call' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Check for a pending question
|
|
50
|
+
const pendingQuestion = getPendingQuestion(callSession.id);
|
|
51
|
+
if (!pendingQuestion) {
|
|
52
|
+
return { handled: false, reason: 'no_pending_question' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. Check that the orchestrator is alive and waiting
|
|
56
|
+
const orchestrator = getCallOrchestrator(callSession.id);
|
|
57
|
+
if (!orchestrator) {
|
|
58
|
+
// The call may have ended between the question being asked and the
|
|
59
|
+
// user replying. Persist a follow-up message so the user knows.
|
|
60
|
+
const freshSession = getCallSession(callSession.id);
|
|
61
|
+
const ended = freshSession && (freshSession.status === 'completed' || freshSession.status === 'failed');
|
|
62
|
+
if (ended) {
|
|
63
|
+
conversationStore.addMessage(
|
|
64
|
+
conversationId,
|
|
65
|
+
'assistant',
|
|
66
|
+
JSON.stringify([{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: 'The call ended before your answer could be relayed to the caller.',
|
|
69
|
+
}]),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return { handled: false, reason: 'orchestrator_not_found' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (orchestrator.getState() !== 'waiting_on_user') {
|
|
76
|
+
return { handled: false, reason: 'orchestrator_not_waiting' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 4. Route the answer through the orchestrator
|
|
80
|
+
const accepted = await orchestrator.handleUserAnswer(userText);
|
|
81
|
+
if (!accepted) {
|
|
82
|
+
return { handled: false, reason: 'orchestrator_rejected' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 5. Persist the answered state
|
|
86
|
+
answerPendingQuestion(pendingQuestion.id, userText);
|
|
87
|
+
recordCallEvent(callSession.id, 'user_answered', { answer: userText });
|
|
88
|
+
|
|
89
|
+
log.info(
|
|
90
|
+
{ conversationId, callSessionId: callSession.id, questionId: pendingQuestion.id },
|
|
91
|
+
'User reply routed as call answer via bridge',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return { handled: true };
|
|
95
|
+
}
|
|
@@ -1,10 +1,48 @@
|
|
|
1
|
+
import { getConfig } from '../config/loader.js';
|
|
2
|
+
|
|
1
3
|
// Emergency/high-risk numbers that should never be called
|
|
2
4
|
export const DENIED_NUMBERS = new Set([
|
|
3
|
-
'911', '112', '999', '000', '110', '119',
|
|
4
|
-
'+1911', '+1112',
|
|
5
|
+
'911', '112', '999', '000', '110', '119',
|
|
5
6
|
]);
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Check whether a phone number is a denied emergency number.
|
|
10
|
+
*
|
|
11
|
+
* Normalizes E.164 variants by stripping the leading '+' and then checking
|
|
12
|
+
* whether the resulting digit string exactly matches a denied number or could
|
|
13
|
+
* be a country-code-prefixed emergency number (e.g. +1911, +44999, +61000).
|
|
14
|
+
* Country codes are 1–3 digits, so we try every valid split.
|
|
15
|
+
*/
|
|
16
|
+
export function isDeniedNumber(phoneNumber: string): boolean {
|
|
17
|
+
// Strip leading '+' to get a digits-only string
|
|
18
|
+
const digits = phoneNumber.startsWith('+') ? phoneNumber.slice(1) : phoneNumber;
|
|
19
|
+
|
|
20
|
+
// Exact match (covers bare short codes like "911", "112")
|
|
21
|
+
if (DENIED_NUMBERS.has(digits)) return true;
|
|
22
|
+
|
|
23
|
+
// Try splitting off 1-, 2-, or 3-digit country codes and check if the
|
|
24
|
+
// remainder is a denied number. This catches patterns like +1911, +44999.
|
|
25
|
+
for (let ccLen = 1; ccLen <= 3; ccLen++) {
|
|
26
|
+
if (digits.length > ccLen) {
|
|
27
|
+
const remainder = digits.slice(ccLen);
|
|
28
|
+
if (DENIED_NUMBERS.has(remainder)) return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Call limits — backed by config with hardcoded fallbacks
|
|
36
|
+
export function getMaxCallDurationMs(): number {
|
|
37
|
+
return getConfig().calls.maxDurationSeconds * 1000;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getUserConsultationTimeoutMs(): number {
|
|
41
|
+
return getConfig().calls.userConsultTimeoutSeconds * 1000;
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
export const SILENCE_TIMEOUT_MS = 30 * 1000; // 30 seconds
|
|
45
|
+
|
|
46
|
+
// Legacy named exports for backward compatibility (use functions above for config-backed values)
|
|
47
|
+
export const MAX_CALL_DURATION_MS = 3600 * 1000; // fallback default; prefer getMaxCallDurationMs()
|
|
48
|
+
export const USER_CONSULTATION_TIMEOUT_MS = 120 * 1000; // fallback default; prefer getUserConsultationTimeoutMs()
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared domain functions for call operations.
|
|
3
|
+
*
|
|
4
|
+
* Both the tool implementations and the HTTP route handlers delegate
|
|
5
|
+
* to these functions so business logic lives in one place.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLogger } from '../util/logger.js';
|
|
9
|
+
import { isDeniedNumber } from './call-constants.js';
|
|
10
|
+
import {
|
|
11
|
+
createCallSession,
|
|
12
|
+
getCallSession,
|
|
13
|
+
getActiveCallSessionForConversation,
|
|
14
|
+
updateCallSession,
|
|
15
|
+
getPendingQuestion,
|
|
16
|
+
answerPendingQuestion,
|
|
17
|
+
expirePendingQuestions,
|
|
18
|
+
} from './call-store.js';
|
|
19
|
+
import { getCallOrchestrator, unregisterCallOrchestrator } from './call-state.js';
|
|
20
|
+
import { activeRelayConnections } from './relay-server.js';
|
|
21
|
+
import { TwilioConversationRelayProvider } from './twilio-provider.js';
|
|
22
|
+
import { getTwilioConfig } from './twilio-config.js';
|
|
23
|
+
import type { CallSession } from './types.js';
|
|
24
|
+
|
|
25
|
+
const log = getLogger('call-domain');
|
|
26
|
+
|
|
27
|
+
const E164_REGEX = /^\+\d+$/;
|
|
28
|
+
|
|
29
|
+
// ── Result types ─────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface StartCallResult {
|
|
32
|
+
ok: true;
|
|
33
|
+
session: CallSession;
|
|
34
|
+
callSid: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CallError {
|
|
38
|
+
ok: false;
|
|
39
|
+
error: string;
|
|
40
|
+
status?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type StartCallInput = {
|
|
44
|
+
phoneNumber: string;
|
|
45
|
+
task: string;
|
|
46
|
+
context?: string;
|
|
47
|
+
conversationId: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type CancelCallInput = {
|
|
51
|
+
callSessionId: string;
|
|
52
|
+
reason?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type AnswerCallInput = {
|
|
56
|
+
callSessionId: string;
|
|
57
|
+
answer: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── Domain operations ────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initiate a new outbound call.
|
|
64
|
+
*/
|
|
65
|
+
export async function startCall(input: StartCallInput): Promise<StartCallResult | CallError> {
|
|
66
|
+
const { phoneNumber, task, context: callContext, conversationId } = input;
|
|
67
|
+
|
|
68
|
+
if (!phoneNumber || typeof phoneNumber !== 'string') {
|
|
69
|
+
return { ok: false, error: 'phone_number is required and must be a string', status: 400 };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!E164_REGEX.test(phoneNumber)) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
error: 'phone_number must be in E.164 format (starts with + followed by digits, e.g. +14155551234)',
|
|
76
|
+
status: 400,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!task || typeof task !== 'string' || task.trim().length === 0) {
|
|
81
|
+
return { ok: false, error: 'task is required and must be a non-empty string', status: 400 };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (isDeniedNumber(phoneNumber)) {
|
|
85
|
+
return { ok: false, error: 'This phone number is not allowed to be called', status: 403 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let sessionId: string | null = null;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const config = getTwilioConfig();
|
|
92
|
+
const provider = new TwilioConversationRelayProvider();
|
|
93
|
+
|
|
94
|
+
const session = createCallSession({
|
|
95
|
+
conversationId,
|
|
96
|
+
provider: 'twilio',
|
|
97
|
+
fromNumber: config.phoneNumber,
|
|
98
|
+
toNumber: phoneNumber,
|
|
99
|
+
task: callContext ? `${task}\n\nContext: ${callContext}` : task,
|
|
100
|
+
});
|
|
101
|
+
sessionId = session.id;
|
|
102
|
+
|
|
103
|
+
log.info({ callSessionId: session.id, to: phoneNumber, task }, 'Initiating outbound call');
|
|
104
|
+
|
|
105
|
+
const baseUrl = config.webhookBaseUrl.replace(/\/$/, '');
|
|
106
|
+
const { callSid } = await provider.initiateCall({
|
|
107
|
+
from: config.phoneNumber,
|
|
108
|
+
to: phoneNumber,
|
|
109
|
+
webhookUrl: `${baseUrl}/webhooks/twilio/voice?callSessionId=${session.id}`,
|
|
110
|
+
statusCallbackUrl: `${baseUrl}/webhooks/twilio/status`,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
updateCallSession(session.id, { providerCallSid: callSid });
|
|
114
|
+
|
|
115
|
+
log.info({ callSessionId: session.id, callSid }, 'Call initiated successfully');
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
session: { ...session, providerCallSid: callSid },
|
|
120
|
+
callSid,
|
|
121
|
+
};
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
124
|
+
log.error({ err, phoneNumber }, 'Failed to initiate call');
|
|
125
|
+
|
|
126
|
+
// FK constraint failure on conversation_id means the conversationId is invalid
|
|
127
|
+
if (err instanceof Error && msg.includes('FOREIGN KEY constraint failed') && !sessionId) {
|
|
128
|
+
return { ok: false, error: `Invalid conversationId: no conversation found with ID ${conversationId}`, status: 400 };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (sessionId) {
|
|
132
|
+
updateCallSession(sessionId, {
|
|
133
|
+
status: 'failed',
|
|
134
|
+
endedAt: Date.now(),
|
|
135
|
+
lastError: msg,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { ok: false, error: `Error initiating call: ${msg}`, status: 500 };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get the status of a call session. If no callSessionId is provided,
|
|
145
|
+
* looks up the active call for the given conversationId.
|
|
146
|
+
*/
|
|
147
|
+
export function getCallStatus(
|
|
148
|
+
callSessionId?: string,
|
|
149
|
+
conversationId?: string,
|
|
150
|
+
): { ok: true; session: CallSession; pendingQuestion?: { id: string; questionText: string } } | CallError {
|
|
151
|
+
let session: CallSession | null = null;
|
|
152
|
+
|
|
153
|
+
if (callSessionId) {
|
|
154
|
+
session = getCallSession(callSessionId);
|
|
155
|
+
if (!session) {
|
|
156
|
+
return { ok: false, error: `No call session found with ID ${callSessionId}`, status: 404 };
|
|
157
|
+
}
|
|
158
|
+
} else if (conversationId) {
|
|
159
|
+
session = getActiveCallSessionForConversation(conversationId);
|
|
160
|
+
if (!session) {
|
|
161
|
+
return { ok: false, error: 'No active call found in the current conversation', status: 404 };
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
return { ok: false, error: 'Either callSessionId or conversationId is required', status: 400 };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
log.info({ callSessionId: session.id, status: session.status }, 'Checking call status');
|
|
168
|
+
|
|
169
|
+
const pendingQuestion = getPendingQuestion(session.id);
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
session,
|
|
173
|
+
pendingQuestion: pendingQuestion
|
|
174
|
+
? { id: pendingQuestion.id, questionText: pendingQuestion.questionText }
|
|
175
|
+
: undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Cancel an active call. Cleans up relay connections and orchestrators.
|
|
181
|
+
*/
|
|
182
|
+
export async function cancelCall(input: CancelCallInput): Promise<{ ok: true; session: CallSession } | CallError> {
|
|
183
|
+
const { callSessionId, reason } = input;
|
|
184
|
+
|
|
185
|
+
const session = getCallSession(callSessionId);
|
|
186
|
+
if (!session) {
|
|
187
|
+
return { ok: false, error: `No call session found with ID ${callSessionId}`, status: 404 };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (session.status === 'completed' || session.status === 'failed' || session.status === 'cancelled') {
|
|
191
|
+
return { ok: false, error: `Call session ${callSessionId} has already ended with status: ${session.status}`, status: 409 };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
log.info({ callSessionId, reason }, 'Cancelling call');
|
|
195
|
+
|
|
196
|
+
// Terminate the call via the provider API
|
|
197
|
+
if (session.providerCallSid) {
|
|
198
|
+
try {
|
|
199
|
+
const provider = new TwilioConversationRelayProvider();
|
|
200
|
+
await provider.endCall(session.providerCallSid);
|
|
201
|
+
} catch (endErr) {
|
|
202
|
+
log.warn({ err: endErr, callSessionId, callSid: session.providerCallSid }, 'Failed to terminate call via provider API — proceeding with cleanup');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// End the relay connection if active
|
|
207
|
+
const relayConnection = activeRelayConnections.get(callSessionId);
|
|
208
|
+
if (relayConnection) {
|
|
209
|
+
relayConnection.endSession(reason);
|
|
210
|
+
relayConnection.destroy();
|
|
211
|
+
activeRelayConnections.delete(callSessionId);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Clean up orchestrator
|
|
215
|
+
const orchestrator = getCallOrchestrator(callSessionId);
|
|
216
|
+
if (orchestrator) {
|
|
217
|
+
orchestrator.destroy();
|
|
218
|
+
unregisterCallOrchestrator(callSessionId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update session status
|
|
222
|
+
updateCallSession(callSessionId, {
|
|
223
|
+
status: 'cancelled',
|
|
224
|
+
endedAt: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Expire any pending questions so they don't linger
|
|
228
|
+
expirePendingQuestions(callSessionId);
|
|
229
|
+
|
|
230
|
+
// Re-check final status: a concurrent transition (e.g. Twilio callback) may have
|
|
231
|
+
// moved the session to a terminal state before our update, causing it to be skipped.
|
|
232
|
+
const updated = getCallSession(callSessionId);
|
|
233
|
+
if (updated && updated.status !== 'cancelled') {
|
|
234
|
+
log.warn({ callSessionId, finalStatus: updated.status }, 'Cancel lost race — session already transitioned to terminal state');
|
|
235
|
+
return { ok: false, error: `Call session ${callSessionId} transitioned to ${updated.status} before cancellation could be applied`, status: 409 };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
log.info({ callSessionId }, 'Call cancelled successfully');
|
|
239
|
+
|
|
240
|
+
return { ok: true, session: updated ?? { ...session, status: 'cancelled', endedAt: Date.now() } };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Answer a pending question for an active call.
|
|
245
|
+
*/
|
|
246
|
+
export async function answerCall(input: AnswerCallInput): Promise<{ ok: true; questionId: string } | CallError> {
|
|
247
|
+
const { callSessionId, answer } = input;
|
|
248
|
+
|
|
249
|
+
if (!answer || typeof answer !== 'string') {
|
|
250
|
+
return { ok: false, error: 'Missing answer', status: 400 };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const question = getPendingQuestion(callSessionId);
|
|
254
|
+
if (!question) {
|
|
255
|
+
return { ok: false, error: 'No pending question found', status: 404 };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const orchestrator = getCallOrchestrator(callSessionId);
|
|
259
|
+
if (!orchestrator) {
|
|
260
|
+
log.warn({ callSessionId }, 'answerCall: no active orchestrator for call session');
|
|
261
|
+
return { ok: false, error: 'No active orchestrator for this call', status: 409 };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const accepted = await orchestrator.handleUserAnswer(answer);
|
|
265
|
+
if (!accepted) {
|
|
266
|
+
log.warn(
|
|
267
|
+
{ callSessionId },
|
|
268
|
+
'answerCall: orchestrator rejected the answer (not in waiting_on_user state)',
|
|
269
|
+
);
|
|
270
|
+
return { ok: false, error: 'Orchestrator is not waiting for an answer', status: 409 };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
answerPendingQuestion(question.id, answer);
|
|
274
|
+
|
|
275
|
+
return { ok: true, questionId: question.id };
|
|
276
|
+
}
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
createPendingQuestion,
|
|
17
17
|
expirePendingQuestions,
|
|
18
18
|
} from './call-store.js';
|
|
19
|
-
import {
|
|
19
|
+
import { getMaxCallDurationMs, getUserConsultationTimeoutMs, SILENCE_TIMEOUT_MS } from './call-constants.js';
|
|
20
20
|
import type { RelayConnection } from './relay-server.js';
|
|
21
21
|
import { registerCallOrchestrator, unregisterCallOrchestrator, fireCallQuestionNotifier, fireCallCompletionNotifier } from './call-state.js';
|
|
22
|
+
import type { PromptSpeakerContext } from './speaker-identification.js';
|
|
22
23
|
|
|
23
24
|
const log = getLogger('call-orchestrator');
|
|
24
25
|
|
|
@@ -60,7 +61,7 @@ export class CallOrchestrator {
|
|
|
60
61
|
/**
|
|
61
62
|
* Handle a final caller utterance from the ConversationRelay.
|
|
62
63
|
*/
|
|
63
|
-
async handleCallerUtterance(transcript: string): Promise<void> {
|
|
64
|
+
async handleCallerUtterance(transcript: string, speaker?: PromptSpeakerContext): Promise<void> {
|
|
64
65
|
// If we're already processing or speaking, abort the in-flight generation
|
|
65
66
|
if (this.state === 'processing' || this.state === 'speaking') {
|
|
66
67
|
this.abortController.abort();
|
|
@@ -71,7 +72,10 @@ export class CallOrchestrator {
|
|
|
71
72
|
this.resetSilenceTimer();
|
|
72
73
|
|
|
73
74
|
// Append caller utterance
|
|
74
|
-
this.conversationHistory.push({
|
|
75
|
+
this.conversationHistory.push({
|
|
76
|
+
role: 'user',
|
|
77
|
+
content: this.formatCallerUtterance(transcript, speaker),
|
|
78
|
+
});
|
|
75
79
|
|
|
76
80
|
await this.runLlm();
|
|
77
81
|
}
|
|
@@ -100,7 +104,11 @@ export class CallOrchestrator {
|
|
|
100
104
|
// Append the user's answer as a special message the model recognizes
|
|
101
105
|
this.conversationHistory.push({ role: 'user', content: `[USER_ANSWERED: ${answerText}]` });
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
// Fire-and-forget: unblock the caller so the HTTP response and answer
|
|
108
|
+
// persistence happen immediately, before LLM streaming begins.
|
|
109
|
+
this.runLlm().catch((err) =>
|
|
110
|
+
log.error({ err, callSessionId: this.callSessionId }, 'runLlm failed after user answer'),
|
|
111
|
+
);
|
|
104
112
|
return true;
|
|
105
113
|
}
|
|
106
114
|
|
|
@@ -130,6 +138,11 @@ export class CallOrchestrator {
|
|
|
130
138
|
// ── Private ──────────────────────────────────────────────────────
|
|
131
139
|
|
|
132
140
|
private buildSystemPrompt(): string {
|
|
141
|
+
const config = getConfig();
|
|
142
|
+
const disclosureRule = config.calls.disclosure.enabled
|
|
143
|
+
? `1. ${config.calls.disclosure.text}`
|
|
144
|
+
: '1. Begin the conversation naturally.';
|
|
145
|
+
|
|
133
146
|
return [
|
|
134
147
|
'You are on a live phone call on behalf of your user.',
|
|
135
148
|
this.task ? `Task: ${this.task}` : '',
|
|
@@ -138,18 +151,27 @@ export class CallOrchestrator {
|
|
|
138
151
|
'Respond naturally and conversationally — speak as you would in a real phone conversation.',
|
|
139
152
|
'',
|
|
140
153
|
'IMPORTANT RULES:',
|
|
141
|
-
|
|
154
|
+
disclosureRule,
|
|
142
155
|
'2. Be concise — phone conversations should be brief and natural.',
|
|
143
156
|
'3. If the callee asks something you don\'t know, include [ASK_USER: your question here] in your response along with a hold message like "Let me check on that for you."',
|
|
144
157
|
'4. If the callee provides information preceded by [USER_ANSWERED: ...], use that answer naturally in the conversation.',
|
|
145
158
|
'5. When the call\'s purpose is fulfilled, include [END_CALL] in your response along with a polite goodbye.',
|
|
146
159
|
'6. Do not make up information — ask the user if unsure.',
|
|
147
160
|
'7. Keep responses short — 1-3 sentences is ideal for phone conversation.',
|
|
161
|
+
'8. When caller text includes [SPEAKER id="..." label="..."], treat each speaker as a distinct person and personalize responses using that speaker\'s prior context in this call.',
|
|
148
162
|
]
|
|
149
163
|
.filter(Boolean)
|
|
150
164
|
.join('\n');
|
|
151
165
|
}
|
|
152
166
|
|
|
167
|
+
private formatCallerUtterance(transcript: string, speaker?: PromptSpeakerContext): string {
|
|
168
|
+
if (!speaker) return transcript;
|
|
169
|
+
const safeId = speaker.speakerId.replaceAll('"', '\'');
|
|
170
|
+
const safeLabel = speaker.speakerLabel.replaceAll('"', '\'');
|
|
171
|
+
const confidencePart = speaker.speakerConfidence !== null ? ` confidence="${speaker.speakerConfidence.toFixed(2)}"` : '';
|
|
172
|
+
return `[SPEAKER id="${safeId}" label="${safeLabel}" source="${speaker.source}"${confidencePart}] ${transcript}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
153
175
|
/**
|
|
154
176
|
* Run the LLM with the current conversation history and stream
|
|
155
177
|
* the response back through the relay.
|
|
@@ -215,7 +237,8 @@ export class CallOrchestrator {
|
|
|
215
237
|
'[ASK_USER:'.startsWith(afterBracket) ||
|
|
216
238
|
'[END_CALL]'.startsWith(afterBracket) ||
|
|
217
239
|
afterBracket.startsWith('[ASK_USER:') ||
|
|
218
|
-
afterBracket
|
|
240
|
+
afterBracket === '[END_CALL' ||
|
|
241
|
+
afterBracket.startsWith('[END_CALL]');
|
|
219
242
|
|
|
220
243
|
if (!couldBeControl) {
|
|
221
244
|
// Not a control marker prefix — flush up to the next '[' (if any)
|
|
@@ -295,7 +318,7 @@ export class CallOrchestrator {
|
|
|
295
318
|
updateCallSession(this.callSessionId, { status: 'in_progress' });
|
|
296
319
|
expirePendingQuestions(this.callSessionId);
|
|
297
320
|
}
|
|
298
|
-
},
|
|
321
|
+
}, getUserConsultationTimeoutMs());
|
|
299
322
|
return;
|
|
300
323
|
}
|
|
301
324
|
|
|
@@ -329,15 +352,18 @@ export class CallOrchestrator {
|
|
|
329
352
|
}
|
|
330
353
|
|
|
331
354
|
private startDurationTimer(): void {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
this.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
355
|
+
const maxDurationMs = getMaxCallDurationMs();
|
|
356
|
+
const warningMs = maxDurationMs - 2 * 60 * 1000; // 2 minutes before max
|
|
357
|
+
|
|
358
|
+
if (warningMs > 0) {
|
|
359
|
+
this.durationWarningTimer = setTimeout(() => {
|
|
360
|
+
log.info({ callSessionId: this.callSessionId }, 'Call duration warning');
|
|
361
|
+
this.relay.sendTextToken(
|
|
362
|
+
'Just to let you know, we\'re running low on time for this call.',
|
|
363
|
+
true,
|
|
364
|
+
);
|
|
365
|
+
}, warningMs);
|
|
366
|
+
}
|
|
341
367
|
|
|
342
368
|
this.durationTimer = setTimeout(() => {
|
|
343
369
|
log.info({ callSessionId: this.callSessionId }, 'Call duration limit reached');
|
|
@@ -351,7 +377,7 @@ export class CallOrchestrator {
|
|
|
351
377
|
updateCallSession(this.callSessionId, { status: 'completed', endedAt: Date.now() });
|
|
352
378
|
recordCallEvent(this.callSessionId, 'call_ended', { reason: 'max_duration' });
|
|
353
379
|
}, 3000);
|
|
354
|
-
},
|
|
380
|
+
}, maxDurationMs);
|
|
355
381
|
}
|
|
356
382
|
|
|
357
383
|
private resetSilenceTimer(): void {
|