signalwire-agents 0.1.0
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/CLAUDE.md +112 -0
- package/README.md +417 -0
- package/dist/AgentBase.d.ts +481 -0
- package/dist/AgentBase.js +1155 -0
- package/dist/AgentBase.js.map +1 -0
- package/dist/AgentServer.d.ts +60 -0
- package/dist/AgentServer.js +128 -0
- package/dist/AgentServer.js.map +1 -0
- package/dist/AuthHandler.d.ts +56 -0
- package/dist/AuthHandler.js +119 -0
- package/dist/AuthHandler.js.map +1 -0
- package/dist/ConfigLoader.d.ts +68 -0
- package/dist/ConfigLoader.js +150 -0
- package/dist/ConfigLoader.js.map +1 -0
- package/dist/ContextBuilder.d.ts +431 -0
- package/dist/ContextBuilder.js +751 -0
- package/dist/ContextBuilder.js.map +1 -0
- package/dist/DataMap.d.ts +179 -0
- package/dist/DataMap.js +321 -0
- package/dist/DataMap.js.map +1 -0
- package/dist/Logger.d.ts +91 -0
- package/dist/Logger.js +191 -0
- package/dist/Logger.js.map +1 -0
- package/dist/PomBuilder.d.ts +144 -0
- package/dist/PomBuilder.js +249 -0
- package/dist/PomBuilder.js.map +1 -0
- package/dist/PromptManager.d.ts +85 -0
- package/dist/PromptManager.js +108 -0
- package/dist/PromptManager.js.map +1 -0
- package/dist/SchemaUtils.d.ts +42 -0
- package/dist/SchemaUtils.js +148 -0
- package/dist/SchemaUtils.js.map +1 -0
- package/dist/ServerlessAdapter.d.ts +100 -0
- package/dist/ServerlessAdapter.js +166 -0
- package/dist/ServerlessAdapter.js.map +1 -0
- package/dist/SessionManager.d.ts +86 -0
- package/dist/SessionManager.js +155 -0
- package/dist/SessionManager.js.map +1 -0
- package/dist/SslConfig.d.ts +77 -0
- package/dist/SslConfig.js +100 -0
- package/dist/SslConfig.js.map +1 -0
- package/dist/SwaigFunction.d.ts +88 -0
- package/dist/SwaigFunction.js +125 -0
- package/dist/SwaigFunction.js.map +1 -0
- package/dist/SwaigFunctionResult.d.ts +442 -0
- package/dist/SwaigFunctionResult.js +735 -0
- package/dist/SwaigFunctionResult.js.map +1 -0
- package/dist/SwmlBuilder.d.ts +37 -0
- package/dist/SwmlBuilder.js +55 -0
- package/dist/SwmlBuilder.js.map +1 -0
- package/dist/cli/agent-loader.d.ts +22 -0
- package/dist/cli/agent-loader.js +106 -0
- package/dist/cli/agent-loader.js.map +1 -0
- package/dist/cli/mock-data.d.ts +37 -0
- package/dist/cli/mock-data.js +79 -0
- package/dist/cli/mock-data.js.map +1 -0
- package/dist/cli/swaig-test.d.ts +32 -0
- package/dist/cli/swaig-test.js +405 -0
- package/dist/cli/swaig-test.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/prefabs/ConciergeAgent.d.ts +64 -0
- package/dist/prefabs/ConciergeAgent.js +188 -0
- package/dist/prefabs/ConciergeAgent.js.map +1 -0
- package/dist/prefabs/FAQBotAgent.d.ts +68 -0
- package/dist/prefabs/FAQBotAgent.js +206 -0
- package/dist/prefabs/FAQBotAgent.js.map +1 -0
- package/dist/prefabs/InfoGathererAgent.d.ts +73 -0
- package/dist/prefabs/InfoGathererAgent.js +202 -0
- package/dist/prefabs/InfoGathererAgent.js.map +1 -0
- package/dist/prefabs/ReceptionistAgent.d.ts +65 -0
- package/dist/prefabs/ReceptionistAgent.js +205 -0
- package/dist/prefabs/ReceptionistAgent.js.map +1 -0
- package/dist/prefabs/SurveyAgent.d.ts +84 -0
- package/dist/prefabs/SurveyAgent.js +324 -0
- package/dist/prefabs/SurveyAgent.js.map +1 -0
- package/dist/prefabs/index.d.ts +13 -0
- package/dist/prefabs/index.js +14 -0
- package/dist/prefabs/index.js.map +1 -0
- package/dist/skills/SkillBase.d.ts +134 -0
- package/dist/skills/SkillBase.js +105 -0
- package/dist/skills/SkillBase.js.map +1 -0
- package/dist/skills/SkillManager.d.ts +83 -0
- package/dist/skills/SkillManager.js +159 -0
- package/dist/skills/SkillManager.js.map +1 -0
- package/dist/skills/SkillRegistry.d.ts +105 -0
- package/dist/skills/SkillRegistry.js +195 -0
- package/dist/skills/SkillRegistry.js.map +1 -0
- package/dist/skills/builtin/api_ninjas_trivia.d.ts +35 -0
- package/dist/skills/builtin/api_ninjas_trivia.js +161 -0
- package/dist/skills/builtin/api_ninjas_trivia.js.map +1 -0
- package/dist/skills/builtin/claude_skills.d.ts +34 -0
- package/dist/skills/builtin/claude_skills.js +156 -0
- package/dist/skills/builtin/claude_skills.js.map +1 -0
- package/dist/skills/builtin/custom_skills.d.ts +59 -0
- package/dist/skills/builtin/custom_skills.js +231 -0
- package/dist/skills/builtin/custom_skills.js.map +1 -0
- package/dist/skills/builtin/datasphere.d.ts +34 -0
- package/dist/skills/builtin/datasphere.js +163 -0
- package/dist/skills/builtin/datasphere.js.map +1 -0
- package/dist/skills/builtin/datasphere_serverless.d.ts +54 -0
- package/dist/skills/builtin/datasphere_serverless.js +155 -0
- package/dist/skills/builtin/datasphere_serverless.js.map +1 -0
- package/dist/skills/builtin/datetime.d.ts +31 -0
- package/dist/skills/builtin/datetime.js +95 -0
- package/dist/skills/builtin/datetime.js.map +1 -0
- package/dist/skills/builtin/google_maps.d.ts +33 -0
- package/dist/skills/builtin/google_maps.js +233 -0
- package/dist/skills/builtin/google_maps.js.map +1 -0
- package/dist/skills/builtin/index.d.ts +28 -0
- package/dist/skills/builtin/index.js +75 -0
- package/dist/skills/builtin/index.js.map +1 -0
- package/dist/skills/builtin/info_gatherer.d.ts +52 -0
- package/dist/skills/builtin/info_gatherer.js +237 -0
- package/dist/skills/builtin/info_gatherer.js.map +1 -0
- package/dist/skills/builtin/joke.d.ts +32 -0
- package/dist/skills/builtin/joke.js +142 -0
- package/dist/skills/builtin/joke.js.map +1 -0
- package/dist/skills/builtin/math.d.ts +33 -0
- package/dist/skills/builtin/math.js +120 -0
- package/dist/skills/builtin/math.js.map +1 -0
- package/dist/skills/builtin/mcp_gateway.d.ts +35 -0
- package/dist/skills/builtin/mcp_gateway.js +85 -0
- package/dist/skills/builtin/mcp_gateway.js.map +1 -0
- package/dist/skills/builtin/native_vector_search.d.ts +44 -0
- package/dist/skills/builtin/native_vector_search.js +248 -0
- package/dist/skills/builtin/native_vector_search.js.map +1 -0
- package/dist/skills/builtin/play_background_file.d.ts +34 -0
- package/dist/skills/builtin/play_background_file.js +135 -0
- package/dist/skills/builtin/play_background_file.js.map +1 -0
- package/dist/skills/builtin/spider.d.ts +34 -0
- package/dist/skills/builtin/spider.js +164 -0
- package/dist/skills/builtin/spider.js.map +1 -0
- package/dist/skills/builtin/swml_transfer.d.ts +44 -0
- package/dist/skills/builtin/swml_transfer.js +204 -0
- package/dist/skills/builtin/swml_transfer.js.map +1 -0
- package/dist/skills/builtin/weather_api.d.ts +34 -0
- package/dist/skills/builtin/weather_api.js +136 -0
- package/dist/skills/builtin/weather_api.js.map +1 -0
- package/dist/skills/builtin/web_search.d.ts +34 -0
- package/dist/skills/builtin/web_search.js +141 -0
- package/dist/skills/builtin/web_search.js.map +1 -0
- package/dist/skills/builtin/wikipedia_search.d.ts +34 -0
- package/dist/skills/builtin/wikipedia_search.js +166 -0
- package/dist/skills/builtin/wikipedia_search.js.map +1 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +10 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/docs/agent-guide.md +869 -0
- package/docs/api-reference.md +2419 -0
- package/docs/architecture.md +843 -0
- package/docs/cli-guide.md +579 -0
- package/docs/configuration.md +422 -0
- package/docs/contexts-guide.md +787 -0
- package/docs/datamap-guide.md +1141 -0
- package/docs/prefabs-guide.md +830 -0
- package/docs/security.md +436 -0
- package/docs/serverless-guide.md +513 -0
- package/docs/skills-guide.md +845 -0
- package/docs/swaig-reference.md +1627 -0
- package/examples/call-flow.ts +60 -0
- package/examples/contexts-steps.ts +62 -0
- package/examples/datamap-tools.ts +52 -0
- package/examples/dynamic-config.ts +51 -0
- package/examples/multi-agent.ts +67 -0
- package/examples/pom-prompt.ts +68 -0
- package/examples/simple-agent.ts +33 -0
- package/package.json +35 -0
- package/src/AgentBase.ts +1279 -0
- package/src/AgentServer.ts +149 -0
- package/src/AuthHandler.ts +141 -0
- package/src/ConfigLoader.ts +165 -0
- package/src/ContextBuilder.ts +796 -0
- package/src/DataMap.ts +365 -0
- package/src/Logger.ts +197 -0
- package/src/PomBuilder.ts +298 -0
- package/src/PromptManager.ts +133 -0
- package/src/SchemaUtils.ts +170 -0
- package/src/ServerlessAdapter.ts +218 -0
- package/src/SessionManager.ts +161 -0
- package/src/SslConfig.ts +118 -0
- package/src/SwaigFunction.ts +168 -0
- package/src/SwaigFunctionResult.ts +864 -0
- package/src/SwmlBuilder.ts +62 -0
- package/src/cli/agent-loader.ts +122 -0
- package/src/cli/mock-data.ts +110 -0
- package/src/cli/swaig-test.ts +440 -0
- package/src/index.ts +99 -0
- package/src/prefabs/ConciergeAgent.ts +251 -0
- package/src/prefabs/FAQBotAgent.ts +263 -0
- package/src/prefabs/InfoGathererAgent.ts +276 -0
- package/src/prefabs/ReceptionistAgent.ts +277 -0
- package/src/prefabs/SurveyAgent.ts +413 -0
- package/src/prefabs/index.ts +23 -0
- package/src/skills/SkillBase.ts +184 -0
- package/src/skills/SkillManager.ts +177 -0
- package/src/skills/SkillRegistry.ts +231 -0
- package/src/skills/builtin/api_ninjas_trivia.ts +230 -0
- package/src/skills/builtin/claude_skills.ts +239 -0
- package/src/skills/builtin/custom_skills.ts +326 -0
- package/src/skills/builtin/datasphere.ts +225 -0
- package/src/skills/builtin/datasphere_serverless.ts +194 -0
- package/src/skills/builtin/datetime.ts +115 -0
- package/src/skills/builtin/google_maps.ts +347 -0
- package/src/skills/builtin/index.ts +79 -0
- package/src/skills/builtin/info_gatherer.ts +331 -0
- package/src/skills/builtin/joke.ts +180 -0
- package/src/skills/builtin/math.ts +150 -0
- package/src/skills/builtin/mcp_gateway.ts +99 -0
- package/src/skills/builtin/native_vector_search.ts +327 -0
- package/src/skills/builtin/play_background_file.ts +189 -0
- package/src/skills/builtin/spider.ts +232 -0
- package/src/skills/builtin/swml_transfer.ts +285 -0
- package/src/skills/builtin/weather_api.ts +193 -0
- package/src/skills/builtin/web_search.ts +212 -0
- package/src/skills/builtin/wikipedia_search.ts +248 -0
- package/src/skills/index.ts +12 -0
- package/src/types.ts +99 -0
- package/tests/AgentBase.test.ts +693 -0
- package/tests/AgentServer.test.ts +102 -0
- package/tests/AuthHandler.test.ts +121 -0
- package/tests/ConfigLoader.test.ts +121 -0
- package/tests/DataMap.test.ts +286 -0
- package/tests/Logger.test.ts +218 -0
- package/tests/PomBuilder.test.ts +164 -0
- package/tests/ProxyDetection.test.ts +185 -0
- package/tests/SchemaUtils.test.ts +106 -0
- package/tests/ServerlessAdapter.test.ts +200 -0
- package/tests/SessionManager.test.ts +139 -0
- package/tests/SslConfig.test.ts +97 -0
- package/tests/SwaigFunctionResult.test.ts +298 -0
- package/tests/cli/mock-data.test.ts +94 -0
- package/tests/cli/swaig-test.test.ts +96 -0
- package/tests/prefabs/prefabs.test.ts +854 -0
- package/tests/setup.ts +6 -0
- package/tests/skills/SkillBase.test.ts +118 -0
- package/tests/skills/SkillManager.test.ts +164 -0
- package/tests/skills/SkillRegistry.test.ts +124 -0
- package/tests/skills/builtin.test.ts +924 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
# SignalWire AI Agents TypeScript SDK -- Architecture
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Core Components](#core-components)
|
|
7
|
+
- [AgentBase](#agentbase)
|
|
8
|
+
- [PromptManager and PomBuilder](#promptmanager-and-pombuilder)
|
|
9
|
+
- [SwmlBuilder](#swmlbuilder)
|
|
10
|
+
- [SwaigFunction](#swaigfunction)
|
|
11
|
+
- [SwaigFunctionResult](#swaigfunctionresult)
|
|
12
|
+
- [SessionManager](#sessionmanager)
|
|
13
|
+
- [ContextBuilder](#contextbuilder)
|
|
14
|
+
- [DataMap](#datamap)
|
|
15
|
+
- [Composition Architecture](#composition-architecture)
|
|
16
|
+
- [SWML Rendering Pipeline](#swml-rendering-pipeline)
|
|
17
|
+
- [Request Flow](#request-flow)
|
|
18
|
+
- [SWML Request (GET/POST /)](#swml-request-getpost-)
|
|
19
|
+
- [SWAIG Function Dispatch (POST /swaig)](#swaig-function-dispatch-post-swaig)
|
|
20
|
+
- [Post-Prompt Handler (POST /post_prompt)](#post-prompt-handler-post-post_prompt)
|
|
21
|
+
- [Tool System](#tool-system)
|
|
22
|
+
- [SwaigFunction Wrapper](#swaigfunction-wrapper)
|
|
23
|
+
- [Handler Signature](#handler-signature)
|
|
24
|
+
- [Result Serialization](#result-serialization)
|
|
25
|
+
- [DataMap Tools](#datamap-tools)
|
|
26
|
+
- [Session Management](#session-management)
|
|
27
|
+
- [Token Format](#token-format)
|
|
28
|
+
- [Token Generation](#token-generation)
|
|
29
|
+
- [Token Validation](#token-validation)
|
|
30
|
+
- [Proxy Detection](#proxy-detection)
|
|
31
|
+
- [Security Layers](#security-layers)
|
|
32
|
+
- [Basic Authentication](#basic-authentication)
|
|
33
|
+
- [CORS](#cors)
|
|
34
|
+
- [Security Headers](#security-headers)
|
|
35
|
+
- [Rate Limiting](#rate-limiting)
|
|
36
|
+
- [Allowed Hosts](#allowed-hosts)
|
|
37
|
+
- [Request Size Limits](#request-size-limits)
|
|
38
|
+
- [Extension Points](#extension-points)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
The SignalWire AI Agents TypeScript SDK implements an agent-as-microservice architecture. Each agent is an HTTP server that serves SWML documents and handles SWAIG tool callbacks. The SDK is built on the [Hono](https://hono.dev/) web framework and uses Node.js crypto for session management.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
+--------------------------------------------------+
|
|
48
|
+
| AgentBase |
|
|
49
|
+
| |
|
|
50
|
+
| +----------------+ +---------------------+ |
|
|
51
|
+
| | PromptManager | | SessionManager | |
|
|
52
|
+
| | +----------+ | | (HMAC tokens, | |
|
|
53
|
+
| | |PomBuilder| | | session metadata) | |
|
|
54
|
+
| | +----------+ | +---------------------+ |
|
|
55
|
+
| +----------------+ |
|
|
56
|
+
| |
|
|
57
|
+
| +----------------+ +---------------------+ |
|
|
58
|
+
| | SwmlBuilder | | Tool Registry | |
|
|
59
|
+
| | (5-phase SWML | | Map<string, | |
|
|
60
|
+
| | document) | | SwaigFunction | | |
|
|
61
|
+
| +----------------+ | Record<>> | | |
|
|
62
|
+
| +---------------------+ |
|
|
63
|
+
| |
|
|
64
|
+
| +----------------+ +---------------------+ |
|
|
65
|
+
| | ContextBuilder | | SkillManager | |
|
|
66
|
+
| | (optional) | | (optional) | |
|
|
67
|
+
| +----------------+ +---------------------+ |
|
|
68
|
+
| |
|
|
69
|
+
| +--------------------------------------------+ |
|
|
70
|
+
| | Hono HTTP App | |
|
|
71
|
+
| | GET/POST / -> renderSwml() | |
|
|
72
|
+
| | POST /swaig -> tool dispatch | |
|
|
73
|
+
| | POST /post_prompt -> onSummary() | |
|
|
74
|
+
| | GET /health -> { status: ok } | |
|
|
75
|
+
| | GET /ready -> { status: ready } | |
|
|
76
|
+
| +--------------------------------------------+ |
|
|
77
|
+
+--------------------------------------------------+
|
|
78
|
+
|
|
79
|
+
Inbound Call
|
|
80
|
+
|
|
|
81
|
+
v
|
|
82
|
+
SignalWire Platform --GET/POST--> AgentBase / (returns SWML)
|
|
83
|
+
|
|
|
84
|
+
v
|
|
85
|
+
AI engine processes call using SWML config
|
|
86
|
+
|
|
|
87
|
+
v
|
|
88
|
+
AI invokes tool --POST--> AgentBase /swaig (executes handler)
|
|
89
|
+
|
|
|
90
|
+
v
|
|
91
|
+
Call ends --POST--> AgentBase /post_prompt (delivers summary)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### AgentServer (Multi-Agent)
|
|
95
|
+
|
|
96
|
+
For hosting multiple agents on a single port, `AgentServer` mounts each agent's Hono app at its route prefix:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
AgentServer (Hono)
|
|
100
|
+
|-- /sales -> SalesAgent.getApp()
|
|
101
|
+
|-- /support -> SupportAgent.getApp()
|
|
102
|
+
|-- /health -> global health check
|
|
103
|
+
|-- /ready -> global readiness check
|
|
104
|
+
|-- / -> agent listing (if no agent mounted at /)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Core Components
|
|
110
|
+
|
|
111
|
+
### AgentBase
|
|
112
|
+
|
|
113
|
+
**File**: `src/AgentBase.ts` (~1280 lines)
|
|
114
|
+
|
|
115
|
+
The central class that composes all other components. It is responsible for:
|
|
116
|
+
|
|
117
|
+
- **HTTP server lifecycle**: Creating and configuring the Hono app, binding middleware, registering routes, starting the server.
|
|
118
|
+
- **SWML rendering**: Assembling the 5-phase SWML document from all configured verbs, prompts, tools, and AI parameters.
|
|
119
|
+
- **Tool dispatch**: Routing incoming SWAIG function calls to the correct handler, validating secure tokens.
|
|
120
|
+
- **Proxy detection**: Auto-detecting reverse proxy configurations from request headers.
|
|
121
|
+
- **Dynamic configuration**: Creating ephemeral copies and invoking the dynamic config callback per-request.
|
|
122
|
+
- **Lifecycle hooks**: Providing `onSummary`, `onFunctionCall`, `onSwmlRequest`, `onDebugEvent`, and `validateBasicAuth` for subclass customization.
|
|
123
|
+
|
|
124
|
+
Key internal state:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Managers (composition)
|
|
128
|
+
private promptManager: PromptManager;
|
|
129
|
+
private sessionManager: SessionManager;
|
|
130
|
+
private swmlBuilder: SwmlBuilder;
|
|
131
|
+
private toolRegistry: Map<string, SwaigFunction | Record<string, unknown>>;
|
|
132
|
+
|
|
133
|
+
// AI configuration
|
|
134
|
+
private hints: string[];
|
|
135
|
+
private languages: LanguageConfig[];
|
|
136
|
+
private pronounce: PronunciationRule[];
|
|
137
|
+
private params: Record<string, unknown>;
|
|
138
|
+
private globalData: Record<string, unknown>;
|
|
139
|
+
private promptLlmParams: Record<string, unknown>;
|
|
140
|
+
private postPromptLlmParams: Record<string, unknown>;
|
|
141
|
+
|
|
142
|
+
// Call flow verbs (5 phases)
|
|
143
|
+
private preAnswerVerbs: [string, Record<string, unknown>][];
|
|
144
|
+
private answerConfig: Record<string, unknown>;
|
|
145
|
+
private postAnswerVerbs: [string, Record<string, unknown>][];
|
|
146
|
+
private postAiVerbs: [string, Record<string, unknown>][];
|
|
147
|
+
|
|
148
|
+
// Dynamic config
|
|
149
|
+
private dynamicConfigCallback: DynamicConfigCallback | null;
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### PromptManager and PomBuilder
|
|
153
|
+
|
|
154
|
+
**Files**: `src/PromptManager.ts`, `src/PomBuilder.ts`
|
|
155
|
+
|
|
156
|
+
`PromptManager` is a facade that manages the agent's prompt text. It supports two modes:
|
|
157
|
+
|
|
158
|
+
1. **Raw text mode** -- A plain string set via `setPromptText()`. When set, it takes precedence over POM.
|
|
159
|
+
2. **POM mode** -- A `PomBuilder` instance that assembles structured sections into Markdown.
|
|
160
|
+
|
|
161
|
+
`PomBuilder` maintains an ordered list of `PomSection` objects. Each section has:
|
|
162
|
+
|
|
163
|
+
- `title` -- Rendered as a Markdown heading (e.g., `## Title`).
|
|
164
|
+
- `body` -- Paragraph text below the heading.
|
|
165
|
+
- `bullets` -- Rendered as `- item` or `1. item` (numbered).
|
|
166
|
+
- `subsections` -- Nested `PomSection` instances rendered at a deeper heading level.
|
|
167
|
+
|
|
168
|
+
The rendering pipeline:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
PomBuilder.renderMarkdown()
|
|
172
|
+
-> for each PomSection:
|
|
173
|
+
PomSection.renderMarkdown(level=2, sectionNumber=[])
|
|
174
|
+
-> heading: "## [N.] Title"
|
|
175
|
+
-> body paragraph
|
|
176
|
+
-> bullet list (- or 1.)
|
|
177
|
+
-> recurse into subsections at level+1
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`PromptManager.getPrompt()` returns:
|
|
181
|
+
1. `rawText` if set (bypasses POM).
|
|
182
|
+
2. `pom.renderMarkdown()` if POM sections exist.
|
|
183
|
+
3. Empty string otherwise.
|
|
184
|
+
|
|
185
|
+
### SwmlBuilder
|
|
186
|
+
|
|
187
|
+
**File**: `src/SwmlBuilder.ts` (~62 lines)
|
|
188
|
+
|
|
189
|
+
A minimal builder that assembles SWML documents. A SWML document has the structure:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"version": "1.0.0",
|
|
194
|
+
"sections": {
|
|
195
|
+
"main": [
|
|
196
|
+
{ "answer": {} },
|
|
197
|
+
{ "ai": { ... } },
|
|
198
|
+
{ "hangup": {} }
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Key methods:
|
|
205
|
+
|
|
206
|
+
| Method | Description |
|
|
207
|
+
|--------|-------------|
|
|
208
|
+
| `reset()` | Clears the document back to an empty `{ version, sections: { main: [] } }`. |
|
|
209
|
+
| `addVerb(verbName, config)` | Appends `{ [verbName]: config }` to the `main` section. |
|
|
210
|
+
| `addVerbToSection(section, verb, config)` | Appends to a named section (creates if absent). |
|
|
211
|
+
| `renderDocument()` | Returns `JSON.stringify(document)`. |
|
|
212
|
+
|
|
213
|
+
`SwmlBuilder` is stateless between render cycles. `AgentBase.renderSwml()` calls `reset()` at the start, builds the 5 phases by calling `addVerb()`, then calls `renderDocument()`.
|
|
214
|
+
|
|
215
|
+
### SwaigFunction
|
|
216
|
+
|
|
217
|
+
**File**: `src/SwaigFunction.ts` (~168 lines)
|
|
218
|
+
|
|
219
|
+
Wraps a tool handler with metadata for SWAIG registration:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
class SwaigFunction {
|
|
223
|
+
name: string; // Tool name
|
|
224
|
+
handler: SwaigHandler; // The callback function
|
|
225
|
+
description: string; // Shown to the AI
|
|
226
|
+
parameters: Record; // JSON Schema for arguments
|
|
227
|
+
secure: boolean; // Requires HMAC token
|
|
228
|
+
fillers?: Record; // Per-language filler phrases
|
|
229
|
+
waitFile?: string; // Audio to play while executing
|
|
230
|
+
waitFileLoops?: number; // Loop count for wait file
|
|
231
|
+
webhookUrl?: string; // External webhook (bypasses local handler)
|
|
232
|
+
required: string[]; // Required parameter names
|
|
233
|
+
extraFields: Record; // Additional SWAIG fields
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Two primary operations:
|
|
238
|
+
|
|
239
|
+
1. **`execute(args, rawData)`** -- Invokes the handler and normalizes the result:
|
|
240
|
+
- `SwaigFunctionResult` -> `result.toDict()`
|
|
241
|
+
- Object with `response` key -> returned as-is
|
|
242
|
+
- Plain object -> wrapped with "Function completed successfully" message
|
|
243
|
+
- String -> wrapped in `SwaigFunctionResult`
|
|
244
|
+
- Exception -> returns a generic error message
|
|
245
|
+
|
|
246
|
+
2. **`toSwaig(baseUrl, token?, callId?)`** -- Serializes to the SWAIG wire format for SWML.
|
|
247
|
+
|
|
248
|
+
### SwaigFunctionResult
|
|
249
|
+
|
|
250
|
+
**File**: `src/SwaigFunctionResult.ts` (~864 lines)
|
|
251
|
+
|
|
252
|
+
A fluent builder for SWAIG function responses. Carries:
|
|
253
|
+
|
|
254
|
+
- `response: string` -- Text returned to the AI.
|
|
255
|
+
- `action: Record<string, unknown>[]` -- Ordered list of actions.
|
|
256
|
+
- `postProcess: boolean` -- Whether actions run after the AI responds.
|
|
257
|
+
|
|
258
|
+
The `toDict()` serialization:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
toDict(): Record<string, unknown> {
|
|
262
|
+
const result: Record<string, unknown> = {};
|
|
263
|
+
if (this.response) result['response'] = this.response;
|
|
264
|
+
if (this.action.length > 0) result['action'] = this.action;
|
|
265
|
+
if (this.postProcess && this.action.length > 0) result['post_process'] = true;
|
|
266
|
+
if (Object.keys(result).length === 0) result['response'] = 'Action completed.';
|
|
267
|
+
return result;
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
The fallback `"Action completed."` ensures the response is never empty.
|
|
272
|
+
|
|
273
|
+
Action categories:
|
|
274
|
+
|
|
275
|
+
| Category | Methods |
|
|
276
|
+
|----------|---------|
|
|
277
|
+
| Call control | `connect()`, `hangup()`, `hold()`, `stop()`, `waitForUser()` |
|
|
278
|
+
| Audio | `say()`, `playBackgroundFile()`, `stopBackgroundFile()` |
|
|
279
|
+
| Speech | `addDynamicHints()`, `clearDynamicHints()`, `setEndOfSpeechTimeout()` |
|
|
280
|
+
| Data | `updateGlobalData()`, `removeGlobalData()`, `setMetadata()`, `removeMetadata()` |
|
|
281
|
+
| SWML | `executeSwml()`, `swmlChangeStep()`, `swmlChangeContext()`, `swmlUserEvent()` |
|
|
282
|
+
| Context | `switchContext()` |
|
|
283
|
+
| Functions | `toggleFunctions()`, `enableFunctionsOnTimeout()`, `updateSettings()` |
|
|
284
|
+
| Comms | `sendSms()`, `recordCall()`, `stopRecordCall()`, `tap()`, `stopTap()`, `sipRefer()` |
|
|
285
|
+
| Rooms | `joinRoom()`, `joinConference()` |
|
|
286
|
+
| RPC | `executeRpc()`, `rpcDial()`, `rpcAiMessage()`, `rpcAiUnhold()` |
|
|
287
|
+
| Payments | `pay()` |
|
|
288
|
+
|
|
289
|
+
### SessionManager
|
|
290
|
+
|
|
291
|
+
**File**: `src/SessionManager.ts` (~161 lines)
|
|
292
|
+
|
|
293
|
+
Stateless HMAC-SHA256 token manager. See [Session Management](#session-management) for details.
|
|
294
|
+
|
|
295
|
+
### ContextBuilder
|
|
296
|
+
|
|
297
|
+
**File**: `src/ContextBuilder.ts` (~797 lines)
|
|
298
|
+
|
|
299
|
+
Builds multi-context, multi-step conversation workflows. A `ContextBuilder` contains named `Context` objects, each containing ordered `Step` objects. Steps have prompt content, completion criteria, function restrictions, and navigation rules.
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
ContextBuilder
|
|
303
|
+
|-- Context "greeting"
|
|
304
|
+
| |-- Step "welcome" (text, criteria, functions)
|
|
305
|
+
| |-- Step "identify" (gather_info questions)
|
|
306
|
+
|
|
|
307
|
+
|-- Context "support"
|
|
308
|
+
|-- Step "diagnose" (POM sections, valid_steps)
|
|
309
|
+
|-- Step "resolve" (end: true)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Contexts are serialized into the `contexts` key of the `ai` verb in SWML.
|
|
313
|
+
|
|
314
|
+
### DataMap
|
|
315
|
+
|
|
316
|
+
**File**: `src/DataMap.ts` (~365 lines)
|
|
317
|
+
|
|
318
|
+
Fluent builder for server-side SWAIG tools that execute on SignalWire without webhook callbacks. A DataMap can include:
|
|
319
|
+
|
|
320
|
+
- **Webhooks**: HTTP calls to external APIs with response template interpolation.
|
|
321
|
+
- **Expressions**: Pattern-matching rules evaluated against template variables.
|
|
322
|
+
- **Output templates**: `SwaigFunctionResult` instances serialized for response formatting.
|
|
323
|
+
|
|
324
|
+
DataMap tools are registered as raw dictionary objects in the tool registry (not `SwaigFunction` instances).
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Composition Architecture
|
|
329
|
+
|
|
330
|
+
The Python SDK uses 8 mixins to compose agent behavior. The TypeScript SDK replaces this with **internal composition** -- `AgentBase` holds manager instances as private fields:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
Python SDK (Mixins) TypeScript SDK (Composition)
|
|
334
|
+
-------------------------- ---------------------------------
|
|
335
|
+
AgentBase( class AgentBase {
|
|
336
|
+
PromptMixin, private promptManager: PromptManager;
|
|
337
|
+
SwaigMixin, private toolRegistry: Map<...>;
|
|
338
|
+
SessionMixin, private sessionManager: SessionManager;
|
|
339
|
+
SwmlMixin, private swmlBuilder: SwmlBuilder;
|
|
340
|
+
HttpMixin, private _app: Hono;
|
|
341
|
+
ConfigMixin, private dynamicConfigCallback: ...;
|
|
342
|
+
HintsMixin, private hints: string[];
|
|
343
|
+
LanguageMixin private languages: LanguageConfig[];
|
|
344
|
+
) }
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Design decisions:
|
|
348
|
+
|
|
349
|
+
1. **No multiple inheritance**: TypeScript does not natively support mixins in the same way Python does. Composition provides cleaner encapsulation.
|
|
350
|
+
2. **Private managers**: `PromptManager`, `SessionManager`, and `SwmlBuilder` are private. Public methods on `AgentBase` delegate to them (e.g., `setPromptText()` delegates to `promptManager.setPromptText()`).
|
|
351
|
+
3. **Tool registry is a Map**: Supports both `SwaigFunction` instances (handler-based tools) and plain `Record<string, unknown>` objects (DataMap/raw definitions).
|
|
352
|
+
4. **Single public surface**: All configuration happens through `AgentBase` methods. Users do not need to interact with internal managers directly.
|
|
353
|
+
5. **Ephemeral copies**: The `createEphemeralCopy()` method shallow-copies the agent, then deep-copies all mutable collections (hints, languages, params, verbs, tool registry, etc.) and creates fresh `PromptManager` and `SwmlBuilder` instances.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## SWML Rendering Pipeline
|
|
358
|
+
|
|
359
|
+
The `renderSwml(callId?)` method on `AgentBase` assembles the complete SWML document in five sequential phases:
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
renderSwml(callId?)
|
|
363
|
+
|
|
|
364
|
+
|-- 1. swmlBuilder.reset()
|
|
365
|
+
|
|
|
366
|
+
|-- 2. Build webhook URLs (swaig, post_prompt)
|
|
367
|
+
| - Combine proxy base URL + route + endpoint
|
|
368
|
+
| - Embed basic auth credentials in URL
|
|
369
|
+
| - Append query params (swaigQueryParams, __token)
|
|
370
|
+
|
|
|
371
|
+
|-- 3. Build SWAIG object
|
|
372
|
+
| - native_functions[]
|
|
373
|
+
| - includes[]
|
|
374
|
+
| - internal_fillers{}
|
|
375
|
+
| - functions[] (iterate toolRegistry)
|
|
376
|
+
| For each SwaigFunction:
|
|
377
|
+
| - name, description, parameters (ensure schema structure)
|
|
378
|
+
| - fillers, wait_file, wait_file_loops
|
|
379
|
+
| - web_hook_url (with token for secure tools)
|
|
380
|
+
| For each raw dict (DataMap):
|
|
381
|
+
| - spread as-is with function name
|
|
382
|
+
| - defaults.web_hook_url
|
|
383
|
+
|
|
|
384
|
+
|-- PHASE 1: Pre-Answer Verbs
|
|
385
|
+
| for [verb, config] of preAnswerVerbs:
|
|
386
|
+
| swmlBuilder.addVerb(verb, config)
|
|
387
|
+
|
|
|
388
|
+
|-- PHASE 2: Answer Verb
|
|
389
|
+
| if autoAnswer:
|
|
390
|
+
| swmlBuilder.addVerb('answer', answerConfig)
|
|
391
|
+
|
|
|
392
|
+
|-- PHASE 3: Post-Answer Verbs
|
|
393
|
+
| if recordCall:
|
|
394
|
+
| swmlBuilder.addVerb('record_call', { format, stereo })
|
|
395
|
+
| for [verb, config] of postAnswerVerbs:
|
|
396
|
+
| swmlBuilder.addVerb(verb, config)
|
|
397
|
+
|
|
|
398
|
+
|-- PHASE 4: AI Verb
|
|
399
|
+
| Build aiConfig object:
|
|
400
|
+
| prompt: { text: ..., ...promptLlmParams }
|
|
401
|
+
| post_prompt: { text: ..., ...postPromptLlmParams }
|
|
402
|
+
| post_prompt_url: ...
|
|
403
|
+
| SWAIG: swaigObj
|
|
404
|
+
| contexts: contextsBuilder.toDict() (if defined)
|
|
405
|
+
| hints: [...]
|
|
406
|
+
| languages: [...]
|
|
407
|
+
| pronounce: [...]
|
|
408
|
+
| params: {...}
|
|
409
|
+
| global_data: {...}
|
|
410
|
+
| debug_webhook_url / debug_webhook_level (if enabled)
|
|
411
|
+
| swmlBuilder.addVerb('ai', aiConfig)
|
|
412
|
+
|
|
|
413
|
+
|-- PHASE 5: Post-AI Verbs
|
|
414
|
+
| for [verb, config] of postAiVerbs:
|
|
415
|
+
| swmlBuilder.addVerb(verb, config)
|
|
416
|
+
|
|
|
417
|
+
|-- Return swmlBuilder.renderDocument() (JSON string)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
The resulting SWML document structure:
|
|
421
|
+
|
|
422
|
+
```json
|
|
423
|
+
{
|
|
424
|
+
"version": "1.0.0",
|
|
425
|
+
"sections": {
|
|
426
|
+
"main": [
|
|
427
|
+
{ "play": { "url": "..." } },
|
|
428
|
+
{ "answer": {} },
|
|
429
|
+
{ "record_call": { "format": "mp4", "stereo": true } },
|
|
430
|
+
{
|
|
431
|
+
"ai": {
|
|
432
|
+
"prompt": { "text": "..." },
|
|
433
|
+
"post_prompt": { "text": "..." },
|
|
434
|
+
"post_prompt_url": "https://...",
|
|
435
|
+
"SWAIG": {
|
|
436
|
+
"functions": [ ... ],
|
|
437
|
+
"defaults": { "web_hook_url": "https://..." }
|
|
438
|
+
},
|
|
439
|
+
"hints": [ "SignalWire", "HIPAA" ],
|
|
440
|
+
"params": { "temperature": 0.7 },
|
|
441
|
+
"global_data": { "company": "Acme" }
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{ "hangup": {} }
|
|
445
|
+
]
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Request Flow
|
|
453
|
+
|
|
454
|
+
### SWML Request (GET/POST /)
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
Client Request
|
|
458
|
+
|
|
|
459
|
+
v
|
|
460
|
+
Hono Middleware Stack:
|
|
461
|
+
1. Security headers (X-Content-Type-Options, X-Frame-Options, etc.)
|
|
462
|
+
2. Request size limit check (SWML_MAX_REQUEST_SIZE)
|
|
463
|
+
3. Allowed hosts check (SWML_ALLOWED_HOSTS)
|
|
464
|
+
4. Rate limiter (SWML_RATE_LIMIT)
|
|
465
|
+
5. CORS (SWML_CORS_ORIGINS)
|
|
466
|
+
6. Basic Auth (credential validation)
|
|
467
|
+
|
|
|
468
|
+
v
|
|
469
|
+
handleSwml():
|
|
470
|
+
1. Parse request body (JSON, or empty for GET)
|
|
471
|
+
2. detectProxyFromRequest(c) -- inspect headers for proxy info
|
|
472
|
+
3. await onSwmlRequest(body) -- lifecycle hook
|
|
473
|
+
4. If dynamicConfigCallback is set:
|
|
474
|
+
a. createEphemeralCopy() -- deep-copy mutable state
|
|
475
|
+
b. Extract queryParams, headers from request
|
|
476
|
+
c. await dynamicConfigCallback(queryParams, body, headers, copy)
|
|
477
|
+
d. Use copy for rendering
|
|
478
|
+
5. Extract call_id from body (if present)
|
|
479
|
+
6. agentToUse.renderSwml(callId) -- 5-phase rendering
|
|
480
|
+
7. Return JSON response
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### SWAIG Function Dispatch (POST /swaig)
|
|
484
|
+
|
|
485
|
+
```
|
|
486
|
+
Client Request (from SignalWire platform)
|
|
487
|
+
|
|
|
488
|
+
v
|
|
489
|
+
Middleware Stack (same as above)
|
|
490
|
+
|
|
|
491
|
+
v
|
|
492
|
+
handleSwaig():
|
|
493
|
+
1. Parse request body
|
|
494
|
+
2. Extract function name from body["function"]
|
|
495
|
+
3. Look up in toolRegistry
|
|
496
|
+
- Not found -> 404 { error: "Unknown function: ..." }
|
|
497
|
+
- Not a SwaigFunction -> 404
|
|
498
|
+
4. If fn.secure:
|
|
499
|
+
a. Extract __token from URL query params
|
|
500
|
+
b. Extract call_id from body
|
|
501
|
+
c. sessionManager.validateToken(callId, fnName, token)
|
|
502
|
+
d. Invalid -> 403 { error: "Invalid or expired token" }
|
|
503
|
+
5. Extract args from body["argument"]
|
|
504
|
+
6. await onFunctionCall(fnName, args, body) -- lifecycle hook
|
|
505
|
+
7. result = await fn.execute(args, body)
|
|
506
|
+
8. Return JSON response (result dict)
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Post-Prompt Handler (POST /post_prompt)
|
|
510
|
+
|
|
511
|
+
```
|
|
512
|
+
Client Request (from SignalWire platform, end of call)
|
|
513
|
+
|
|
|
514
|
+
v
|
|
515
|
+
Middleware Stack
|
|
516
|
+
|
|
|
517
|
+
v
|
|
518
|
+
handlePostPrompt():
|
|
519
|
+
1. Parse request body
|
|
520
|
+
2. findSummary(body):
|
|
521
|
+
a. Check body["summary"]
|
|
522
|
+
b. Check body["post_prompt_data"]["parsed"][0]
|
|
523
|
+
c. Try JSON.parse(body["post_prompt_data"]["raw"])
|
|
524
|
+
d. Return raw value as fallback
|
|
525
|
+
e. Return null if nothing found
|
|
526
|
+
3. await onSummary(summary, body) -- lifecycle hook
|
|
527
|
+
4. Return { ok: true }
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Tool System
|
|
533
|
+
|
|
534
|
+
### SwaigFunction Wrapper
|
|
535
|
+
|
|
536
|
+
Each tool registered via `defineTool()` creates a `SwaigFunction` instance stored in the `toolRegistry` Map. The registry also supports raw dictionary entries (DataMap tools registered via `registerSwaigFunction()`).
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// Handler-based tool
|
|
540
|
+
toolRegistry.set('get_weather', new SwaigFunction({ name, handler, description, ... }));
|
|
541
|
+
|
|
542
|
+
// DataMap (raw dict)
|
|
543
|
+
toolRegistry.set('lookup_zip', { function: 'lookup_zip', description: '...', data_map: {...} });
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
During SWML rendering, the registry is iterated. `SwaigFunction` instances are serialized with their schema, webhook URLs, and optional tokens. Raw dicts are spread as-is.
|
|
547
|
+
|
|
548
|
+
### Handler Signature
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
type SwaigHandler = (
|
|
552
|
+
args: Record<string, unknown>, // AI-extracted arguments
|
|
553
|
+
rawData: Record<string, unknown>, // Full request payload
|
|
554
|
+
) => SwaigFunctionResult | Record<string, unknown> | string
|
|
555
|
+
| Promise<SwaigFunctionResult | Record<string, unknown> | string>;
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The handler is intentionally flexible in its return type. The `execute()` method normalizes all return types into a consistent dict format.
|
|
559
|
+
|
|
560
|
+
### Result Serialization
|
|
561
|
+
|
|
562
|
+
The `execute()` method in `SwaigFunction` handles all return type normalization:
|
|
563
|
+
|
|
564
|
+
```
|
|
565
|
+
Handler returns:
|
|
566
|
+
|
|
|
567
|
+
|-- SwaigFunctionResult --> result.toDict()
|
|
568
|
+
|-- { response: "..." } --> returned as-is
|
|
569
|
+
|-- { ... } (no response) --> SwaigFunctionResult("Function completed successfully").toDict()
|
|
570
|
+
|-- "string" --> SwaigFunctionResult(string).toDict()
|
|
571
|
+
|-- throws Error --> SwaigFunctionResult("Sorry, I couldn't...").toDict()
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
`SwaigFunctionResult.toDict()` produces:
|
|
575
|
+
|
|
576
|
+
```json
|
|
577
|
+
{
|
|
578
|
+
"response": "The weather is 72F and sunny.",
|
|
579
|
+
"action": [
|
|
580
|
+
{ "set_global_data": { "last_city": "Austin" } }
|
|
581
|
+
],
|
|
582
|
+
"post_process": false
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
If both `response` and `action` are empty, the fallback `"Action completed."` is used.
|
|
587
|
+
|
|
588
|
+
### DataMap Tools
|
|
589
|
+
|
|
590
|
+
DataMap tools differ from handler-based tools in that they execute entirely on the SignalWire platform. They are serialized as raw dictionaries with a `data_map` key instead of a `web_hook_url`:
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{
|
|
594
|
+
"function": "check_status",
|
|
595
|
+
"description": "Check order status",
|
|
596
|
+
"parameters": { "type": "object", "properties": { ... } },
|
|
597
|
+
"data_map": {
|
|
598
|
+
"webhooks": [
|
|
599
|
+
{
|
|
600
|
+
"url": "https://api.example.com/orders/${args.order_id}",
|
|
601
|
+
"method": "GET",
|
|
602
|
+
"output": { "response": "Order status: ${status}" }
|
|
603
|
+
}
|
|
604
|
+
]
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## Session Management
|
|
612
|
+
|
|
613
|
+
The `SessionManager` provides stateless HMAC-SHA256 token generation and validation. No server-side session state is stored for tokens -- all information is encoded within the token itself.
|
|
614
|
+
|
|
615
|
+
### Token Format
|
|
616
|
+
|
|
617
|
+
Tokens are base64url-encoded strings containing five dot-separated fields:
|
|
618
|
+
|
|
619
|
+
```
|
|
620
|
+
base64url( callId . functionName . expiry . nonce . hmacSignature )
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
| Field | Description |
|
|
624
|
+
|-------|-------------|
|
|
625
|
+
| `callId` | The call ID this token is bound to. |
|
|
626
|
+
| `functionName` | The SWAIG function name this token authorizes. |
|
|
627
|
+
| `expiry` | Unix timestamp (seconds) when the token expires. |
|
|
628
|
+
| `nonce` | Random 4-byte hex string for uniqueness. |
|
|
629
|
+
| `hmacSignature` | First 16 hex chars of HMAC-SHA256 over `callId:functionName:expiry:nonce`. |
|
|
630
|
+
|
|
631
|
+
### Token Generation
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
generateToken(functionName: string, callId: string): string {
|
|
635
|
+
const expiry = Math.floor(Date.now() / 1000) + this.tokenExpirySecs;
|
|
636
|
+
const nonce = randomBytes(4).toString('hex');
|
|
637
|
+
const message = `${callId}:${functionName}:${expiry}:${nonce}`;
|
|
638
|
+
const signature = createHmac('sha256', this.secretKey)
|
|
639
|
+
.update(message)
|
|
640
|
+
.digest('hex')
|
|
641
|
+
.slice(0, 16);
|
|
642
|
+
const token = `${callId}.${functionName}.${expiry}.${nonce}.${signature}`;
|
|
643
|
+
return Buffer.from(token).toString('base64url');
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
The secret key is a random 32-byte hex string generated at startup (per `SessionManager` instance). Tokens are generated during SWML rendering and embedded in the `__token` query parameter of each secure tool's webhook URL.
|
|
648
|
+
|
|
649
|
+
### Token Validation
|
|
650
|
+
|
|
651
|
+
On each `/swaig` POST for a secure tool:
|
|
652
|
+
|
|
653
|
+
1. Decode the base64url token.
|
|
654
|
+
2. Split into 5 parts; reject if not exactly 5.
|
|
655
|
+
3. Verify `functionName` matches the requested function.
|
|
656
|
+
4. Verify `expiry` has not passed.
|
|
657
|
+
5. Recompute HMAC-SHA256 over the same message format.
|
|
658
|
+
6. Compare the first 16 hex chars of the computed signature to the token's signature.
|
|
659
|
+
7. Verify `callId` matches the request's call ID.
|
|
660
|
+
|
|
661
|
+
All checks must pass. Any failure returns a `403 Forbidden` response.
|
|
662
|
+
|
|
663
|
+
### Session Metadata
|
|
664
|
+
|
|
665
|
+
`SessionManager` also provides an in-memory metadata store keyed by session ID:
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
setSessionMetadata(sessionId: string, metadata: Record<string, unknown>): void;
|
|
669
|
+
getSessionMetadata(sessionId: string): Record<string, unknown> | undefined;
|
|
670
|
+
deleteSessionMetadata(sessionId: string): boolean;
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
This is used for storing per-session state that persists across multiple tool invocations within the same call.
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Proxy Detection
|
|
678
|
+
|
|
679
|
+
When running behind a reverse proxy (nginx, Cloudflare, AWS ALB, etc.), the agent needs to know its external URL to generate correct webhook URLs in SWML. The proxy detection system checks multiple sources in priority order:
|
|
680
|
+
|
|
681
|
+
### Detection Priority
|
|
682
|
+
|
|
683
|
+
1. **Environment variable** (`SWML_PROXY_URL_BASE`) -- Highest priority. Once set, header-based detection is disabled.
|
|
684
|
+
2. **Manual override** (`manualSetProxyUrl()`) -- Programmatic override.
|
|
685
|
+
3. **X-Forwarded-Host + X-Forwarded-Proto** headers.
|
|
686
|
+
4. **Forwarded** header (RFC 7239) -- Parses `host=` and `proto=` directives.
|
|
687
|
+
5. **X-Original-Host + X-Forwarded-Proto** headers.
|
|
688
|
+
6. **X-Forwarded-For** -- Detected but cannot determine host; logs a warning suggesting manual configuration.
|
|
689
|
+
7. **Fallback** -- Uses `http://localhost:{port}{route}`.
|
|
690
|
+
|
|
691
|
+
The detection runs on every SWML request (`detectProxyFromRequest()`). Once the environment variable is set, it is never overridden by header detection.
|
|
692
|
+
|
|
693
|
+
### Webhook URL Building
|
|
694
|
+
|
|
695
|
+
```typescript
|
|
696
|
+
buildWebhookUrl(endpoint: string, extraParams?: Record<string, string>): string
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
This method:
|
|
700
|
+
|
|
701
|
+
1. Calls `getFullUrl(includeAuth=true)` to get the base URL with embedded basic auth credentials.
|
|
702
|
+
2. Appends the endpoint path (e.g., `/swaig`, `/post_prompt`).
|
|
703
|
+
3. Appends query parameters (SWAIG query params, `__token` for secure tools).
|
|
704
|
+
|
|
705
|
+
Example generated URL:
|
|
706
|
+
```
|
|
707
|
+
https://admin:s3cret@agents.example.com/support/swaig?__token=abc123&tenant=acme
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## Security Layers
|
|
713
|
+
|
|
714
|
+
The SDK implements multiple security layers, all configured through the Hono middleware stack.
|
|
715
|
+
|
|
716
|
+
### Basic Authentication
|
|
717
|
+
|
|
718
|
+
All endpoints except `/health` and `/ready` require HTTP Basic Authentication. Credentials are resolved in priority order:
|
|
719
|
+
|
|
720
|
+
1. `basicAuth` constructor option.
|
|
721
|
+
2. `SWML_BASIC_AUTH_USER` / `SWML_BASIC_AUTH_PASSWORD` environment variables.
|
|
722
|
+
3. Auto-generated (agent name as username, random 8-byte hex as password).
|
|
723
|
+
|
|
724
|
+
The `validateBasicAuth(username, password)` hook can be overridden in subclasses to add custom validation logic (e.g., checking against a database).
|
|
725
|
+
|
|
726
|
+
Credentials are embedded in webhook URLs so that SignalWire can authenticate when calling back to the agent.
|
|
727
|
+
|
|
728
|
+
### CORS
|
|
729
|
+
|
|
730
|
+
Controlled by `SWML_CORS_ORIGINS`:
|
|
731
|
+
|
|
732
|
+
```typescript
|
|
733
|
+
// Default: allow all origins
|
|
734
|
+
cors({ origin: '*', credentials: true })
|
|
735
|
+
|
|
736
|
+
// Restricted: SWML_CORS_ORIGINS=https://app.example.com,https://admin.example.com
|
|
737
|
+
cors({ origin: ['https://app.example.com', 'https://admin.example.com'], credentials: true })
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
### Security Headers
|
|
741
|
+
|
|
742
|
+
Applied to all responses via middleware:
|
|
743
|
+
|
|
744
|
+
| Header | Value |
|
|
745
|
+
|--------|-------|
|
|
746
|
+
| `X-Content-Type-Options` | `nosniff` |
|
|
747
|
+
| `X-Frame-Options` | `DENY` |
|
|
748
|
+
| `X-XSS-Protection` | `1; mode=block` |
|
|
749
|
+
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
|
750
|
+
|
|
751
|
+
### Rate Limiting
|
|
752
|
+
|
|
753
|
+
When `SWML_RATE_LIMIT` is set (requests per minute per IP):
|
|
754
|
+
|
|
755
|
+
- IP is extracted from `X-Forwarded-For` (first entry), `X-Real-IP`, or falls back to `"unknown"`.
|
|
756
|
+
- An in-memory Map tracks `{ count, resetAt }` per IP.
|
|
757
|
+
- The window resets every 60 seconds.
|
|
758
|
+
- Exceeding the limit returns `429 Too Many Requests`.
|
|
759
|
+
|
|
760
|
+
### Allowed Hosts
|
|
761
|
+
|
|
762
|
+
When `SWML_ALLOWED_HOSTS` is set (comma-separated hostnames):
|
|
763
|
+
|
|
764
|
+
- The `Host` header is extracted (port stripped, lowercased).
|
|
765
|
+
- If the host is not in the allowed set, returns `403 Forbidden`.
|
|
766
|
+
|
|
767
|
+
### Request Size Limits
|
|
768
|
+
|
|
769
|
+
When `SWML_MAX_REQUEST_SIZE` is set (bytes, default 1 MB):
|
|
770
|
+
|
|
771
|
+
- The `Content-Length` header is checked.
|
|
772
|
+
- Requests exceeding the limit return `413 Request Too Large`.
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## Extension Points
|
|
777
|
+
|
|
778
|
+
The SDK provides multiple mechanisms for customizing agent behavior without modifying the core:
|
|
779
|
+
|
|
780
|
+
### Lifecycle Hooks (Override in Subclass)
|
|
781
|
+
|
|
782
|
+
| Hook | Signature | When Called |
|
|
783
|
+
|------|-----------|------------|
|
|
784
|
+
| `defineTools()` | `protected defineTools(): void` | Called explicitly by the subclass constructor to register tools. |
|
|
785
|
+
| `onSummary(summary, rawData)` | `void \| Promise<void>` | When a post-prompt summary is received at `/post_prompt`. |
|
|
786
|
+
| `onFunctionCall(name, args, rawData)` | `void \| Promise<void>` | Before each SWAIG function handler executes at `/swaig`. |
|
|
787
|
+
| `onSwmlRequest(rawData)` | `void \| Promise<void>` | On every SWML request at `/` before rendering. |
|
|
788
|
+
| `onDebugEvent(event)` | `void \| Promise<void>` | When a debug event webhook is received at `/debug_events`. |
|
|
789
|
+
| `validateBasicAuth(username, password)` | `boolean \| Promise<boolean>` | Called during auth middleware to add custom validation. |
|
|
790
|
+
|
|
791
|
+
### Dynamic Configuration Callback
|
|
792
|
+
|
|
793
|
+
```typescript
|
|
794
|
+
agent.setDynamicConfigCallback(async (queryParams, bodyParams, headers, ephemeralAgent) => {
|
|
795
|
+
// Mutate ephemeralAgent -- original agent is unaffected
|
|
796
|
+
});
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
The callback receives an ephemeral copy created by `createEphemeralCopy()`. This copy has:
|
|
800
|
+
|
|
801
|
+
- Fresh `PromptManager` and `SwmlBuilder` instances.
|
|
802
|
+
- Shallow copies of all mutable arrays and objects (hints, languages, params, globalData, verbs, toolRegistry).
|
|
803
|
+
- The same prototype chain as the original (so subclass methods are available).
|
|
804
|
+
|
|
805
|
+
### Declarative PROMPT_SECTIONS
|
|
806
|
+
|
|
807
|
+
```typescript
|
|
808
|
+
static PROMPT_SECTIONS?: {
|
|
809
|
+
title: string;
|
|
810
|
+
body?: string;
|
|
811
|
+
bullets?: string[];
|
|
812
|
+
numbered?: boolean;
|
|
813
|
+
}[];
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Applied automatically in the `AgentBase` constructor via `promptAddSection()` for each entry.
|
|
817
|
+
|
|
818
|
+
### Skills System
|
|
819
|
+
|
|
820
|
+
Skills are modular bundles of tools, prompt sections, hints, and global data:
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
await agent.addSkill(new WeatherSkill({ apiKey: '...' }));
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
`addSkill()` iterates the skill's contributed tools, prompt sections, hints, and global data, registering each with the agent.
|
|
827
|
+
|
|
828
|
+
### Webhook URL Overrides
|
|
829
|
+
|
|
830
|
+
```typescript
|
|
831
|
+
agent.setWebHookUrl('https://custom-swaig-endpoint.example.com/swaig');
|
|
832
|
+
agent.setPostPromptUrl('https://custom-endpoint.example.com/post_prompt');
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
These override the auto-generated webhook URLs in the rendered SWML document.
|
|
836
|
+
|
|
837
|
+
### Debug Events
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
agent.enableDebugEvents(2); // level 1-3
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
When enabled, the SWML document includes `debug_webhook_url` and `debug_webhook_level` in the AI configuration. SignalWire posts debug events to `/debug_events`, which calls `onDebugEvent()`.
|