feat: add persistent-state-agent example - #122
Conversation
Voice-first durable customer actor on Telnyx Edge Compute with LangGraph orchestration, Salesforce CRM integration, AgentMail SDR confirmation, and proactive SMS. The actor is the persistent agent state for one customer, keyed by the customer's phone number. It survives across: - first voice call (sales inquiry → Salesforce Lead → AgentMail → SMS) - Salesforce activity (SDR confirmation, manual reschedule) - AgentMail loop (SDR confirms via email reply) - outbound SMS (meeting confirmations, proactive reschedule notifications) - time passing (durable state persists across deploys and restarts) - Salesforce reschedule (manual field change detected via Flow webhook) - second voice call (context-aware greeting based on durable state) Components: - Telnyx AI Assistant (Responder): handles live voice, collects caller info, fetches existing context at call start via dynamic variables webhook - LangGraph Customer Actor (Orchestrator): durable state, reasoning, Salesforce coordination, AgentMail, outbound SMS, reschedule detection - Salesforce: Lead creation, SDR assignment, meeting time/status, CRM system of record - AgentMail: SDR confirmation email loop - SMS: meeting confirmations and proactive reschedule notifications
| res.end(); | ||
| return; | ||
| } | ||
| res.end(Buffer.from(await response.arrayBuffer())); |
There was a problem hiding this comment.
Semgrep identified an issue in your code:
Untrusted input could be used to tamper with a web page rendering, which can lead to a Cross-site scripting (XSS) vulnerability. XSS vulnerabilities occur when untrusted input executes malicious JavaScript code, leading to issues such as account compromise and sensitive information leakage. To prevent this vulnerability, validate the user input, perform contextual output encoding or sanitize the input.
Dataflow graph
flowchart LR
classDef invis fill:white, stroke: none
classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none
subgraph File0["<b>persistent-state-agent/scripts/local-dev.ts</b>"]
direction LR
%% Source
subgraph Source
direction LR
v0["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L39 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 39] req</a>"]
end
%% Intermediate
subgraph Traces0[Traces]
direction TB
v2["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L39 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 39] req</a>"]
v3["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L50 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 50] request</a>"]
v4["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L57 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 57] await</a>"]
v5["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L57 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 57] response</a>"]
v6["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L64 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 64] await</a>"]
end
v2 --> v3
v3 --> v4
v4 --> v5
v5 --> v6
%% Sink
subgraph Sink
direction LR
v1["<a href=https://github.com/team-telnyx/telnyx-code-examples/blob/9df5ad51c92cc614fe9e7a7567d559ec6666a53c/persistent-state-agent/scripts/local-dev.ts#L64 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 64] Buffer.from(await response.arrayBuffer())</a>"]
end
end
%% Class Assignment
Source:::invis
Sink:::invis
Traces0:::invis
File0:::invis
%% Connections
Source --> Traces0
Traces0 --> Sink
To resolve this comment:
🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by tainted-direct-response-http.
You can view more details about this finding in the Semgrep AppSec Platform.
| "dev": true, | ||
| "license": "MIT" | ||
| }, | ||
| "node_modules/esbuild": { |
There was a problem hiding this comment.
Medium severity vulnerability may affect your project—review required:
Line 1724 lists a dependency (esbuild) with a known Medium severity vulnerability.
ℹ️ Why this matters
Affected versions of esbuild are vulnerable to Origin Validation Error. esbuild's development server responds to every request, including Server-Sent Events connections, with Access-Control-Allow-Origin: *. Any website a developer visits can therefore make cross-origin requests to the local dev server and read the responses, leaking bundled source code, source maps, and served file paths. Starting the dev server via serve() reaches the vulnerable code path.
References: GHSA
To resolve this comment:
Check if you run esbuild with the --serve flag to start the development server.
- If you're affected, upgrade this dependency to at least version 0.25.0 at persistent-state-agent/package-lock.json.
- If you're not affected, comment
/fp we don't use this [condition]
💬 Ignore this finding
To ignore this, reply with:
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
You can view more details on this finding in the Semgrep AppSec Platform here.
| "uuid": "dist/bin/uuid" | ||
| } | ||
| }, | ||
| "node_modules/vite": { |
There was a problem hiding this comment.
High severity vulnerability may affect your project—review required:
Line 2280 lists a dependency (vite) with a known High severity vulnerability.
ℹ️ Why this matters
Affected versions of vite and vite-plus are vulnerable to Exposure of Sensitive Information to an Unauthorized Actor / Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'). Vite's server.fs.deny blocklist—which protects sensitive files such as .env and certificate files from being served—can be bypassed on Windows using alternate path representations (NTFS Alternate Data Stream syntax like /.env::$DATA?raw, or 8.3 short filenames), allowing an attacker to read otherwise-denied files when the dev server is exposed to the network.
To resolve this comment:
Check if you expose the Vite dev server or vite-plus to the network by configuring a non-loopback address using the --host CLI flag on Windows.
- If you're affected, upgrade this dependency to at least version 6.4.3 at persistent-state-agent/package-lock.json.
- If you're not affected, comment
/fp we don't use this [condition]
💬 Ignore this finding
To ignore this, reply with:
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
You can view more details on this finding in the Semgrep AppSec Platform here.
| "url": "https://opencollective.com/vitest" | ||
| } | ||
| }, | ||
| "node_modules/vitest": { |
There was a problem hiding this comment.
Critical severity vulnerability may affect your project—review required:
Line 2363 lists a dependency (vitest) with a known Critical severity vulnerability.
ℹ️ Why this matters
Affected versions of vitest are vulnerable to Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') / Missing Authorization. When the Vitest UI server is listening, the deprecated isFileServingAllowed check is applied without normalizing the URL before filesystem operations, allowing path traversal that lets an attacker read, write, and execute arbitrary files outside the project directory.
To resolve this comment:
Check if you run the Vitest UI on Windows, or you expose the Vitest UI server to the network with the --api.host flag or api.host config option.
- If you're affected, upgrade this dependency to at least version 3.2.6 at persistent-state-agent/package-lock.json.
- If you're not affected, comment
/fp we don't use this [condition]
💬 Ignore this finding
To ignore this, reply with:
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
You can view more details on this finding in the Semgrep AppSec Platform here.
CodeQL flagged logging apiKey.length as clear-text sensitive information. Removed the apiKeyLength field from the console.log output.
The Edge function now computes a greeting_text field based on caller
context:
- New callers: generic Telnyx AI infrastructure intro
- Returning callers with pending reschedule: acknowledges the meeting
change and asks if that's why they're calling
- Returning callers without reschedule: recognizes them and references
their assigned SDR
The AI assistant greeting template renders {{greeting_text}} so the
first thing the caller hears is context-aware.
persistent-state-agent
Voice-first durable customer actor on Telnyx Edge Compute with LangGraph orchestration, Salesforce CRM integration, AgentMail SDR confirmation, and proactive SMS.
The actor is the persistent agent state for one customer, keyed by the customer's phone number.
It survives across:
Components
Files
Verification
python3 scripts/verify.py --only persistent-state-agentpassesnpm run typecheckcleannpm test— 96/96 passing