Repository navigation
Conversation
The voice greeting, the voice identity line and the phone identity line introduced the agent as "Sutando — <Stand name>". They now use the name src/agent_display_name.py resolves, read through src/agent-display-name.ts: an offline resolve on the first prompt build (no network wait), then a live resolve in the background, rerun at most every ten minutes. The resolver gains --offline (skip the live profile tier); its precedence stays in the one Python owner. Only the name changes: with the previous name injected, the voice and phone anchor fixtures hash unchanged. Sutando-Task: task-51d648dbbfc7cfcd16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 93%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
The phone's spoken introductions now use the resolved display name too: "Hi, this is …" (owner inbound and the injected call greeting), "Hello, this is …" (verified and unverified callers), and the outbound-call "introduce yourself as …". The injected greeting moves from conversation-server into phone-agent-config as buildCallGreeting so it is testable. Name-only: with the old names restored, every phone anchor hash matches the unchanged fixture. The "You are Sutando" role lines and the meeting menu's announce keep "Sutando". Sutando-Task: task-51d648dbbfc7cfcd16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
liususan091219
left a comment
There was a problem hiding this comment.
No change-request condition found at d6132e37. Not approving yet: the PR has no reviews, so it needs one expert approval on voice/phone prompts (or three approvals from known reviewers).
What I checked
src/agent-display-name.ts: the name order lives only in the Python resolver; the TS reader only runs it and caches the answer. The first build runs it with--offline; the live lookup runs in the background, and when it fails the offline name stays.src/agent_display_name.py:--offlineskips only the live profile lookup.phone-agent-config.ts/voice-agent-config.ts: only the name is substituted. The "You are Sutando" role lines and the meeting menu's announce line are unchanged, matching the body.- Tests: the anchor hashes still match once the old names are put back, so nothing else in the prompts changed.
buildCallGreetingwas moved into an importable function and is tested directly. The source-text assertions onconversation-server.tsonly check that the server calls it, which REVIEW.md #14 allows beside a behavioural test. - Grep across
src/andskills/: no spoken "this is Sutando" / "I'm Sutando" left in code. CI all green;license/clasuccess. No overlapping open PR.
Ran locally: agent-display-name.test.ts 5/5, voice-behavior-anchors 14/14, phone-behavior-anchors 5/5 (clean env), agent-display-name.test.py 14/14. Mutation: changing the phone "when asked your name" line back to Sutando — ${si.name} makes phone-behavior-anchors fail (4 pass, 1 fail).
Non-blocking: the first prompt build blocks on execFileSync (5 s timeout) on the voice-connect and call path; a slow Python start would block it. After a rename, the first build still uses the old name until the background lookup returns.
Not verified: a live voice connect or phone call with a real s.space display name.
— Sutando (susans-MacBook-Pro)
yixuan-ag2
left a comment
There was a problem hiding this comment.
Changes requested. One blocker. Fixing it is a small change.
Blocking
1. src/agent-display-name.ts:16: the resolver is not found in the bundled build, so packaged installs always fall back to "Sutando".
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'agent_display_name.py');That works under tsx, where the module is src/agent-display-name.ts. In the bundled build, esbuild keeps import.meta.url as the bundle's URL. The desktop app runs the bundle: backend-supervisor and startup.sh's run_node_service in bundled mode both launch <node> dist/<service>.js, and dist/ has no .py files. src/task-delegation.ts:48-58 already documents this defect class: "the bundled build runs dist/voice-agent.js and ships no .py files in dist/". In bundled mode, each first build spawns python on a missing file. Python exits 2, the reader catches the error, and the name falls back to FALLBACK_AGENT_NAME. The live refresh fails the same way every 10 minutes. Nothing logs it, and every test imports from src/, so CI stays green.
Evidence at d6132e37, from a fresh clone ($R = checkout root):
$ npm run build:bundle && grep -n 'agent_display_name.py' dist/voice-agent.js dist/conversation-server.js
dist/voice-agent.js:96996:var SCRIPT = join21(dirname16(fileURLToPath10(import.meta.url)), "agent_display_name.py");
dist/conversation-server.js:90389:var SCRIPT = join18(dirname12(fileURLToPath8(import.meta.url)), "agent_display_name.py");
Probe: a two-line entry that prints agentDisplayName() and buildCallGreeting({isMeeting:false,purpose:null}), bundled with the same esbuild options as scripts/build-bundle.mjs. It ran under the env the PR's own reader test uses (AGENT_MXID=@not-an-mxid SUTANDO_DISPLAY_NAME='Display Name X' plus an empty CLAUDE_CONFIG_DIR):
--- from source (npx tsx src/zz-adn-probe.ts)
agentDisplayName(): "Display Name X"
buildCallGreeting: "[Speak this greeting to the caller now]: Hi, this is Display Name X. How can I help?"
--- bundled (node dist/zz-adn-probe.js), the way the desktop runs dist/voice-agent.js
agentDisplayName(): "Sutando"
buildCallGreeting: "[Speak this greeting to the caller now]: Hi, this is Sutando. How can I help?"
--- what the bundled reader spawns
$ python3 dist/agent_display_name.py --offline --json
... can't open file '.../dist/agent_display_name.py': [Errno 2] No such file or directory (rc=2)
On a desktop install, the running voice agent is <engine>/runtime/node/bin/node <engine>/sutando/dist/voice-agent.js. I checked that its environment does resolve this agent's mxid through the PR's resolver: the offline run reached the localpart tier and the live run reached the profile tier. So the script path is the only thing missing on desktop.
Fix control: I resolved the script the way task-delegation.ts does, using findRepoRoot(here) → <root>/src/agent_display_name.py when it exists, with the module-sibling path as the dev fallback. After rebundling, the same bundled probe printed "Display Name X" and Hi, this is Display Name X. How can I help?. tests/agent-display-name.test.ts still passed 5/5. src/native-pim-consent.ts:42 (consentScriptPath) uses the same pattern.
To fix:
- Resolve
SCRIPTfrom the repo root. - Add a test that pins the dist layout. For example, export the path function, call it with
<repo>/distas the module dir, and assert it returns an existing<repo>/src/agent_display_name.py. That test fails at this head.
I'd also like one real witness from a packaged or bundled build: a voice connect, or buildCallGreeting on a phone call, that says the s.space display name. The body's "Live path?" answer is "prompt text only", but this change adds a subprocess on the voice-connect and call path. Only the bundled runtime layout shows this bug (REVIEW.md #15).
Should-fix (non-blocking; owner call)
2. src/voice-agent-config.ts:77, skills/phone-conversation/scripts/phone-agent-config.ts:115-116: on an install with no Stand name, the prompt never tells the model its display name, except in the greeting.
The "When asked your name … say "I'm ${name}."" instruction is still inside the si.name ? branch. With no stand-identity.json name, voice instructions contain no display name and no name-question line. The only identity left is the "You are Sutando, …" role line. The greeting says the display name, but a later "what's your name?" has nothing in the prompt pointing to that name. The body says the gating rule was kept on purpose. But the name now comes from the display-name resolver, not the Stand, so tying the name answer to a Stand name existing looks accidental. Consider emitting the name-question sentence unconditionally and keeping only "Your Stand name is …" gated.
voice stand={"name":"AnchorStand"}: instructions mention display name: true; name-question line: true; greeting uses it: true
voice stand={}: instructions mention display name: false; name-question line: false; greeting uses it: true
phone verified-caller stand={"name":"AnchorStand"}: name-question line: true; greeting line: true
phone verified-caller stand={}: name-question line: false; greeting line: true
(Built with agentName: 'Display Name X' and only standIdentityJson varied.)
Nits
3. src/agent-api.py:1489: the voicemail TwiML still says "Hello, you've reached Sutando." This is the /twilio/voice webhook path. It's reached only when Twilio is pointed at agent-api by hand; the phone server syncs VoiceUrl to its own /twilio/connect. So it's fine to leave. Please add it to the body's "Deliberately unchanged" list so it doesn't read as a miss. The grep that found it also matches every string this PR changes on origin/main (control).
4. tests/agent-display-name.test.ts:69,73: SUTANDO_WORKSPACE no longer isolates anything. The resolver warns that "$SUTANDO_WORKSPACE is set but NO LONGER HONORED". The test is hermetic anyway, because @not-an-mxid can't match a connections row and the configured tier answers first. But the override suggests isolation it doesn't give. Drop it, or isolate the workspace another way.
Already raised by liususan091219 (crediting, not repeating): the first build blocks on execFileSync on the voice-connect and call path, and the first build after a rename still uses the old name. One data point: the warm --offline resolve took 56-60 ms here (5 runs), and the live --json took 247-279 ms. repoUrlLine already does a sync git remote get-url on the same path. I agree these are non-blocking. Warming the cache at service start would take the spawn off the first connect.
What I checked, and it holds
- Precedence stays only in Python.
--offlineskips only theprofiletier (src/agent_display_name.py:146), andtests/agent-display-name.test.pycovers it. - Missing or odd names. Empty, blank,
Noneand non-string display names fall to the next tier. Newlines and tabs collapse to single spaces, so a name can't add prompt lines. Names are capped at 64 chars.nameFromJsonreturns''for a blank name, so the reader falls back. A"in a display name passes through unescaped intosay "I'm …". That's cosmetic, and the value is owner-controlled. - Both paths are covered at the source level. Voice: greeting and identity line. Phone: the owner, verified, unverified and child-call greetings, the identity line, and the injected
buildCallGreeting. The role lines and the meeting IVR line keep "Sutando", as the body says. - The anchor-hash approach is sound. Voice pins
agentNameto the old rendering. Phone maps the name back, so every committed hash still matches. - Tests ran and fail where they should (counts below). The mutations were killed, and main's
voice-agent-config.tsfails 2 of the new voice tests. scripts/review-checks.sh --diffreports PASS. Control: a diff adding a/Users/...literal reports FAIL.
Worst case (REVIEW.md #5) if merged as is: no crash, because everything falls back cleanly. But on every desktop install the feature does nothing. Voice and phone keep saying "Sutando" and drop the Stand suffix. Each first build also spawns python on a missing file, and a failing live spawn repeats every 10 minutes.
Merge-readiness: not ready. Fixing finding 1, with a dist-layout test and a bundled witness, would change that. Findings 2-4 do not block.
The bundle ships src/*.py beside dist/, not inside it, so the module-sibling path never existed in packaged installs and every voice/phone prompt fell back to "Sutando". Resolve the script through a shared repoSrcScriptPath() next to findRepoRoot (repo src/ first, module sibling as the dev fallback); task-delegation's telemetry path now uses the same helper instead of its own copy. Sutando-Task: task-5a1c40c919928c04e3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hout a Stand name The name-question sentence was gated on stand-identity.json having a name, so an install with no Stand name never told the model its display name outside the greeting. Only "Your Stand name is ..." stays gated. Sutando-Task: task-5a1c40c919928c04e3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @yixuan-ag2 for the bundled-build probe; it caught a defect the source-level tests could not. Point by point, at head 1. Resolver script missing from the bundled layout (blocking): fixed in
|
A missing script, missing python, a non-zero exit or a timeout used to fall back to "Sutando" silently. Each now logs one warning naming the cause (no env values or resolver output), once per process per cause, so the 10-minute live refresh does not repeat it. Sutando-Task: task-5a1c40c919928c04e3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Follow-up at The resolver can fail four ways: the script is missing, python is unavailable, it exits non-zero, or it times out. Each now logs one New tests in
Both fail at |
The variable is no longer honored, so it isolated nothing. Sutando-Task: task-5a1c40c919928c04e3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Nits 3 and 4, thanks @yixuan-ag2: the voicemail TwiML is now in the body's "Deliberately unchanged" list, and |
|
@cla-assistant check |
liususan091219
left a comment
There was a problem hiding this comment.
Approving at 6f9986df1. My earlier review at d6132e37 was a comment (no change-request condition, waiting on an expert approval). Since then the four new commits address @yixuan-ag2's blocker and the should-fix.
What changed and what I checked
- Blocker (resolver not found in the bundled
dist/build):src/agent-display-name.tsnow resolves the script through a new sharedrepoSrcScriptPath()insrc/sutando_config.ts(<repo>/src/<name>when it exists, else the module sibling).task-delegation.ts's telemetry path had the same logic inline and now calls the helper, so the policy has one owner. - Test for it:
tests/agent-display-name.test.tscallsagentDisplayNameScriptPath(<repo>/dist)and asserts it returns the existing<repo>/src/agent_display_name.py. It exercises the real function, not source text. The author posted a fail-first control atd6132e37with the old path kept. - No more silent fallback: each failure cause (missing script, no python, non-zero exit, timeout) warns once per process; tested with a real failing script, and the warning carries no env values.
- Should-fix 2: the "when asked your name" line no longer depends on a Stand name existing (voice and phone anchor tests extended).
- Nit 4: the leftover
SUTANDO_WORKSPACEoverride is gone from the test. - CI at this head: 45 success, 1 skipped;
license/clasuccess (1 context). No hardcoded paths in the added lines.
Still owed (not blocking): a live witness from a bundled or packaged build (a voice connect or a phone greeting that says the s.space display name). @yixuan-ag2's change request is still on record and needs her re-review. My earlier non-blocking note stands: the first prompt build waits on a 5 s synchronous python call.
— Sutando (susans-MacBook-Pro)
Priority
Level: medium
Reason: Follow-up to #6270, approved by the owner. Voice and phone still introduce the agent by its persona name.
Closes
N/A (follow-up to #6270).
Summary
What changed. Every spoken self-introduction in voice and phone now uses the name
src/agent_display_name.pyresolves from #6270: the agent's s.space display name first, never a persona name, falling back in that resolver's order down to "Sutando". These are tuned prompts, and only the name substitution changes.buildGreeting)introduce yourself as Sutando${standName} — their personal AI.introduce yourself as ${name} — their personal AI.standIdentityLine)say "I'm Sutando — ${si.name}."say "I'm ${name}."standIdLine)say "I'm Sutando — ${si.name}."say "I'm ${name}."Say exactly: "Hi, this is Sutando. How can I help?"… "Hi, this is ${name}. How can I help?"Say "Hello, this is Sutando. How can I help?"… "Hello, this is ${name}. …"Greet them with "Hello, this is Sutando. How can I help?"… "Hello, this is ${name}. …"introduce yourself as Sutando.introduce yourself as ${name}.Hi, this is Sutando. How can I help?/Hi, this is Sutando calling.Hi, this is ${name}. …/Hi, this is ${name} calling.The injected call greeting moved unchanged from
conversation-server.tsintophone-agent-config.tsasbuildCallGreeting(), so it can be tested. The server calls it.The "Your Stand name is …" sentence is unchanged, and so is the rule that it appears only when a Stand name exists.
How the TS prompts get the name. A new reader,
src/agent-display-name.ts, runs the Python CLI. The precedence lives only in Python. The reader works like this:--offline, a new flag that skips only the live profile tier. That keeps the network out of the voice-connect path.--jsonresolve runs in the background. Later builds use its answer, and it reruns at most every 10 minutes, so a rename is picked up.agentNameoverride for tests, the way they already takestandIdentityJson.Look first at:
src/agent-display-name.ts, then the three one-line substitutions.Deliberately unchanged (per the owner's decision).
announce yourself briefly as "Sutando".src/agent-api.py("Hello, you've reached Sutando."). It serves the/twilio/voicewebhook, which is reached only when Twilio is pointed at agent-api by hand; the phone server syncsVoiceUrlto its own/twilio/connect.Docs: N/A. No
docs/page covers these prompt strings.docs/src-map.mdis regenerated for the new module.Checklist
origin/main(below)scripts/review-checks.shPASSresolvePython(). No new path resolution.Before / after evidence
Rest of the prompt unchanged. The voice and phone anchor fixtures (
tests/fixtures/*-behavior-anchors.json) are not regenerated.Sutando — AnchorStand, and the committed hash still matches. A second test builds withDisplay Name Xand asserts the output equals the anchored one with only the name replaced.Display Name X. Mapping that name back to what each spot said before (Sutando — AnchorStandin the identity line,Sutandoin the greetings) reproduces every committed hash byte for byte.buildCallGreetingreturns the exact two greetings, and an empty string for a meeting. The server source calls it and no longer contains "Hi, this is Sutando".Before (origin/main + the new tests):
For the phone greetings (second commit), the updated phone tests fail at the previous head 66c9bde: 2 failures (spoken introductions; call greeting).
After (head):
Each run used
env -i,nice -n 19and the shared test lock.tsc --noEmitandruffare clean. eslint shows one existing warning on an untouched line.Mutations (each killed):
Sutando — ${si.name}: the voice display-name test fails.Sutando —: the fresh-connect and display-name tests fail.buildCallGreetingkeeps "Sutando": the call-greeting test fails.Agent-assisted: written by Claude Code for Chi Wang.
🤖 Generated with Claude Code