Repository navigation
refactor(voice): step 5a-1 — behavior anchors + tuned-prompt factory extraction (LiveAgentRuntime slice 1) - #1964
Merged
Merged
Conversation
…(interaction-planes step 1) A task producer is a (source, interaction_type) pair, not a source alone. This stamps the pair at every write site — purely additive, old readers ignore the extra line, no consumer behavior changes: - message: slack/telegram/discord bridges, twilio SMS + voicemail, chat template - realtime_audio: voice work-tool (task-bridge.ts), phone work-tool (delegateTask) - tool_initiated: writeChatTask (Gemini inline tool), agent-api /task (agent-to-agent) - system_event: health-check --emit-task, context-drop (app + task-bridge), github-webhook, twilio incoming-call, phone call-end summary - remote-gateway-bridge: passes through the gateway's interaction_type when present, defaults to message (all current gateway traffic is Matrix room messages) tests/interaction-type-header.test.py sweeps every producer write site and fails if a source: serialization lacks the paired interaction_type within the write window — so new producers can't silently ship without it. First step of the interaction-planes refactor (snapshot: v0.5.0-pre-interaction-planes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…delegateTask Round-2 improvements on the same PR: - remote-gateway-bridge: whitelist the pass-through against the six-value interaction-plane vocabulary — the gateway is outside the trust boundary, so an unknown remote value degrades to "message" instead of landing verbatim in the local task file. - conversation-server delegateTask: add the missing source: phone so the producer carries the full (source, interaction_type) pair. This activates two consumers that always listed "phone" but never matched because the writer predates the source header: task_priority phone→urgent, and discord-bridge DM_FALLBACK_SOURCES (a result landing after the call ended now DMs the owner instead of rotting unclaimed). - call-end summary writer deliberately does NOT get source: phone — its task instructions already have the core DM the owner, so the bridge fallback would deliver the same summary twice. Comment at the writer + a serialized- form-only test assertion guard the double-DM regression. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ichi/sutando into feat/interaction-type-header
The #1933 coverage gate (first PR through it) flagged the new remote-gateway-bridge lines as uncovered — the sweep test greps write sites but never executes them. This exercises _write_task directly: whitelisted pass-through, unknown-value degradation to message (trust boundary), absent-field default, full vocabulary round-trip, and the access_tier-last ordering guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…to staging-interaction-planes
… tests, reader switch-over (step 3) (#1954) * fix(task-body-guard): defang interaction_type in untrusted bodies The new header must be in confine_user_content's _HEADER_KEYS like every other header — otherwise a message body containing a literal 'interaction_type: ...' line survives into the task file undefanged and could forge the field under a full-scan reader. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(protocol): local_task_protocol read-side module + golden corpus tests (step 3a) The Local Task Protocol's task-file half as pure stdlib functions (the result-marker half already lives in result_markers.py, #873): - schema constants: INTERACTION_TYPES (single source, asserted equal to the gateway whitelist), PRIORITIES, ACCESS_TIERS, KNOWN_HEADER_KEYS (from a 3.4k-file archive corpus survey) - valid_task_id — the filename/traversal gate for ids arriving in content - parse_task_headers — the safe stop-at-task: reader (PR #982 delimiter rule) - parse_task_headers_trusted — full-scan last-wins, for writers that neutralize bodies (_one_line strip or confine_user_content defang); last-wins is load-bearing for the gateway's access_tier-written-last defense - archive rules: month partitioning, find_archived_task candidate walk, iter_archived_tasks corpus sweep Writers untouched (read-side-first). The module docstring records the real shape taxonomy the corpus survey established: TS writers + agent-api /task are task-last; every Python bridge is task-mid — which means stop-at-task: readers under-read bridge files (e.g. parse_priority_from_text has never seen a bridge-written priority: field). Reader consolidation and writer convergence are follow-up steps with per-bridge byte-identical acceptance. Tests: 33 checks — per-shape fixtures (discord/gateway/voice/chat/phone- legacy/health/forged-body), id validation, archive rules, vocabulary parity with the gateway, plus a live-corpus sweep (3,397 archived files parse clean) that skips gracefully where no workspace exists (CI). R1 honored: stdlib-only, no I/O beyond the explicit archive helpers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run CI on staging-interaction-planes PRs and pushes The branch filter predates this staging branch — without it, refactor-step PRs (like this one) get zero CI. Same pattern staging-workspace-revamp used. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: run the coverage gate on staging-interaction-planes PRs too Same branch-filter gap as ci.yml — without it, refactor-step PRs with Python changes skip the 95% diff-coverage bar until the staging→main merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): task_priority reads via local_task_protocol (step 3b, reader 1/3) parse_priority_from_text delegates header extraction to the protocol module's safe parser; the legacy blank/'---' stop is preserved locally as a documented pre-#982 quirk. Deliberate tightening: header keys are canonical lowercase at column 0 — asserted as the ONLY behavioral delta in the new invariance test, which dual-runs a verbatim copy of the legacy implementation against the new one over adversarial fixtures and the live corpus (3,399 archived files, zero verdict changes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(protocol): discord _task_source reads via parse_task_headers_lenient (step 3b, reader 2/3) New protocol parser: parse_task_headers_lenient — full scan, FIRST occurrence per key wins. This is the shape-union reader: the archive proves producer field order changed across eras (May-2026 voice tasks are task-MID; today's are task-last), and _task_source classifies files of any age. The stricter stop-at-task: parser flips the DM-fallback verdict on 23 real archived voice tasks — caught by this commit's corpus dual-run, which now asserts extraction-identical over all 3,401 files. Known pre-existing first-wins spoofability (a body line can supply a key the file legitimately lacks) is documented at the parser and pinned by a fixture; hardening it changes historical verdicts and is an owner decision, not a read-side refactor. Gateway _valid_tid deliberately NOT switched: it is a remote-compat gate (any dot/dash id from older gateways), a different contract from the local task-id canon in valid_task_id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): self-sufficient local_task_protocol import in task_priority tests/task-priority-stop-at-task-delimiter.test.py (and any other consumer) loads task_priority standalone via importlib without src/ on sys.path — the bare import broke it in CI, which also failed the coverage gate (suite dies under instrumentation before coverage is measurable). Same self-pathing pattern the bridges use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: retire staging-workspace-revamp from workflow branch filters That refactor's staging branch merged 2026-06-01 (M0); the filter entry is dead config. Owner-requested cleanup alongside the staging-interaction-planes addition. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(protocol): cover the archive-walk helpers with fixtures; pragma the unmeasurable discord glue The coverage gate runs without a live workspace (corpus sweeps skip) and without discord.py (the bridge can't import under instrumentation): - find_archived_task / iter_archived_tasks now have fixture-based tests (candidate walk across live/processed/flat/month dirs, stray-dir and malformed-id gating) instead of relying on the live-corpus path. - the two _task_source glue lines get pragma: no cover with the rationale at the site — their semantics are pinned by the invariance test's corpus dual-run, but the module itself is unimportable in the gate env. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): define per-parser body contract + lossless task_body() reader (Codex blocker) The trusted/lenient parsers' body looked like the full work item but silently dropped continuation lines (health-check failure bullets, phone hint/transcript sections). A header scan cannot losslessly split task-mid continuation content from trailing headers, so stop pretending: - TaskHeaders.body is now DOCUMENTED per parser: full work item for the task-last safe parser; scalar first-task:-line value for trusted/lenient. - New task_body(text): the shape-independent lossless work-item reader — everything from the first task: line onward, verbatim. For task-mid files that includes trailing header lines; the honest trade is stated at the function. - Tests pin the contract: trusted body excludes the health bullet (scalar by contract, not by accident), task_body keeps health bullets and phone hint/transcript, equals safe-parser body on task-last shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(protocol): vocabulary-locked parsers + lossless task-mid bodies (Codex P2s) Both P2s from the re-review, fixed as one coherent mechanism: 1. Parsers promote ONLY KNOWN_HEADER_KEYS to headers, and task_body_guard._HEADER_KEYS now IS that list (imported — single source of truth). Lockstep is the invariant: no key a parser would trust can survive undefanged in user-supplied content. Side benefit: junk key-shaped lines (transcript 'Caller: hi') can no longer masquerade as metadata. 2. With the vocabulary boundary in place, task-mid bodies become losslessly splittable: trusted/lenient body = the task: line's content plus every subsequent NON-vocabulary line — health-check bullets and phone transcript dialogue stay in body, trailing real headers are excluded. Every post-task: line lands in exactly one of headers/body. Tests: lockstep assertion (guard set == parser vocabulary), forged non-classic keys (instructions/hint/from/transcript/attempts) defanged, health bullets + transcript preserved in trusted body, and a corpus-wide body-fidelity sweep (3,415 archived files: no non-header line lost). Full suite: injection-guard, dm-fallback-guard, both invariance tests, interaction-type, gateway, priority — all green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…gion tripwires Pins the voice agent's tuned behavior surface before the LiveAgentRuntime extraction moves anything: 126 importable tool definitions (name, description, execution mode, parameter keys + per-key descriptions) snapshotted to a committed fixture, plus SHA-256 tripwires on the non-importable tuned regions of voice-agent.ts (instructions factory, greeting/reconnect factory, tool-table composition — the module runs main() at load, so tests cannot import it). Every step-5 slice must keep these green or deliberately regenerate with the diff called out. Verified environment-independent: identical anchors with an empty CLAUDE_CONFIG_DIR (manifest tools resolve from the repo tree). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ig.ts (step 5a-1) PURE MOVE of the greeting factory and system-instruction factory out of voice-agent.ts (which runs main() at module load and is therefore untestable) into an importable module. voice-agent.ts keeps its state (mode resolver, meeting flag, session gates) and threads it in through VoiceConfigContext; env-dependent reads (stand identity, voice context, repo URL, agent context) run verbatim by default with test-only overrides. Equivalence evidence: - all 87 tuned string-array entries identical IN ORDER between the old region and the new module (programmatic comparison) - template-literal set identical except the intentional override branch - behavior anchors upgraded from source-hash tripwires to REAL factory output: fixed-env instruction hash, exact meeting-greeting string, conditional-variant assertions (meeting/googleSearch/mode-marker placement), reconnect replay-guard shape — 10/10 green across normal, repeat, and empty-CLAUDE_CONFIG_DIR runs tsc clean; task-bridge suites green. Phone (conversation-server) untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Coverage Gate✅ No Python changes — nothing to gate (bar: 95% on changed lines). |
john-the-dev
reviewed
Jul 7, 2026
john-the-dev
left a comment
Collaborator
There was a problem hiding this comment.
Cold review (Rui Bot):
LGTM — clean verbatim move.
Checked:
- Reset order:
ctx.resetNoteViewingDebounce()(line 131) precedesctx.resetSessionGates()(line 138) inbuildGreeting— matches originalvoice-agent.tsorder ✓ googleSearchcapture:VOICE_GOOGLE_SEARCHwas already a module-level constant (voice-agent.ts:206) — capturing it asgoogleSearch: VOICE_GOOGLE_SEARCHin_configCtxis correct; no per-call evaluation was ever happening ✓- Anchor upgrade: fixture-based assertions on real factory output are a meaningful improvement over the pre-5a-1 source-hash tripwires; deterministic overrides for env-dependent segments are the right approach ✓
- CLAUDE.md compliance: no prompt string changes, only importability refactor ✓
Nit: the _configCtx object is module-level (defined once). The isMeetingActive: () => meetingActive closure is correct (reads live module state). No issues found.
First CI run of the anchors showed the tool table is machine-specific after all: manifest-loaded skill tools (gws-gmail read_email, report_feedback, screenshot_explain, …) load on installed hosts but not on CI — the empty-CLAUDE_CONFIG_DIR simulation missed them because the manifest scan also covers other paths. inline-tools now exports envDependentToolNames (manifest tools + presenter-sentinel conditionals) and the anchor pins the 80-tool STATIC surface, which is identical everywhere. Excluding per-env extras is sound: a tool absent from an env's exclusion set is also absent from that env's tool list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chi/sutando into feat/step5-behavior-anchors
The prompt deliberately embeds the installed tool list, so a raw hash of buildInstructions output can never be env-stable (CI lacks the manifest tools). The anchor now hashes the instructions with the two dynamic segments masked (the joined tool-names line and per-tool '. Instant.' lines) — pinning all tuned prose exactly — with a separate well-formedness assertion that the dynamic lines exist. 11/11 in both local and empty-config runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
@cla-assistant check |
john-the-dev
approved these changes
Jul 7, 2026
john-the-dev
left a comment
Collaborator
There was a problem hiding this comment.
CI green across all checks. Upgrading from comment to approval — LGTM.
This was referenced Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First slice of step 5 (LiveAgentRuntime extraction), kept deliberately small and verifiable:
Commit 1 — behavior anchors: 126-tool snapshot fixture (names/descriptions/execution/param surface — the tuned strings Gemini sees) + tripwires, established BEFORE moving anything.
Commit 2 — the move: greeting + system-instruction factories extracted verbatim from voice-agent.ts (unimportable: runs
main()at load) into importablesrc/voice-agent-config.ts. State threads in viaVoiceConfigContext; env reads run verbatim with test-only overrides.Equivalence evidence (the whole point of anchors-first):
Phone (conversation-server) untouched — that's 5b. Next slice (5a-2): LiveAgentRuntime skeleton + WebVoiceLiveAdapter, now verifiable against these anchors. Live smoke call with the owner before 5a lands as behavior-effective.
— @sutando-qingyun-001