Skip to content

feat: add persistent-state-agent example - #122

Merged
anusha-telnyx merged 3 commits into
mainfrom
feat/persistent-state-agent
Aug 24, 2026
Merged

feat: add persistent-state-agent example#122
anusha-telnyx merged 3 commits into
mainfrom
feat/persistent-state-agent

Conversation

@anusha-telnyx

Copy link
Copy Markdown
Contributor

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:

  • 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

Component Role
Telnyx AI Assistant (Responder) Live voice, collects caller info, fetches context at call start
LangGraph Customer Actor (Orchestrator) Durable state, reasoning, Salesforce, AgentMail, SMS
Salesforce Lead creation, SDR assignment, meeting time/status, CRM system of record
AgentMail SDR confirmation email loop
SMS Meeting confirmations and proactive reschedule notifications

Files

  • 37 files (source, tests, docs, config)
  • 96 tests passing, typecheck clean
  • All phone numbers/emails sanitized to placeholders in committed files

Verification

  • python3 scripts/verify.py --only persistent-state-agent passes
  • npm run typecheck clean
  • npm test — 96/96 passing

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
Comment thread persistent-state-agent/src/agent-mail.ts Fixed
res.end();
return;
}
res.end(Buffer.from(await response.arrayBuffer()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Loading

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": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

💬 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": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

References: GHSA, CVE

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.

💬 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": {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

References: GHSA, CVE

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.

💬 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.
@anusha-telnyx
anusha-telnyx merged commit 57b8c30 into main Aug 24, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants