fix(agent): isolate response language from connector context (toby) - #1990
fix(agent): isolate response language from connector context (toby)#1990OliverBryant wants to merge 5 commits into
Conversation
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
ci is failing |
e4d2e19 to
476d0fa
Compare
rogercloud
left a comment
There was a problem hiding this comment.
This PR separates user-authored display text from connector-enriched execution context when deriving response language. It introduces a request-only language harness and threads it through root system context, Auto/ReAct guidance, DAG planning, and DAG completion while preserving caller-pinned request_context.output_language as the hard authority. It also adds and updates prompt-level regression assertions for the new seam.
Blocking: yes — recommended event: REQUEST_CHANGES
Approach
Verdict: acceptable-with-reservations. The hard-versus-soft authority split and the shared output_language_directives seam fit the stated problem and cover the relevant response paths. However, the request-only helper currently couples language-rule text to a complete request value; structured plan and completion payloads already carry that value, so this design introduces avoidable unbounded prompt duplication and context-capacity risk. That reservation is a blocking major finding below.
Update summary
The current PR head contains three commits. 01b4efce (fix(agent): isolate response language from connector context) changed production code to add the request-only harness and pass user-authored request text through the root, planner, and completion language paths, with initial coverage. 7af3fad5 (chore(tests): apply isort formatting) only formatted the new harness test, while 476d0fad (test(agent): update language prompt assertions) updated assertions across the Auto, context, DAG, and ReAct tests. The complete history contains no prior technical findings; the only conversation roots are the Gemini quota warning (5488913970) and rogercloud's ci is failing note (5489203088), both informational.
Findings (major)
1. src/xagent/core/agent/context/execution.py:527 — blank display text falls back to connector-enriched execution text
Severity: major
Blocking: yes
- Reachable trigger: A supported
AgentRunner/AgentExecutionAdapter/ExecutionRegistrycontinuation supplies a non-emptyexecution_messagecontaining connector or file enrichment and explicitly suppliesdisplay_messageas""or whitespace, with no caller-pinnedrequest_context.output_language. The core injection contract rejects a missing value (None) in the relevant split case, but accepts an explicit blank value and persists it inMessage.metadata; file-only or attachment-backed turns are supported by this contract. - Observable impact:
_current_user_request_text(prefer_display=True)only returnsdisplay_messagewhendisplay.strip()is non-empty, then falls back to the fullMessage.content. This new root call therefore givesrequest_only_language_harnessconnector/file text while labeling it as the user-authored request. Foreign-language connector metadata, addresses, quoted source text, or file instructions can steer root/Auto/ReAct prose, DAG plan prose, and DAG completion/final-answer prose into the connector language instead of the user's conversational language, violating the user-visible response-language contract. The same fallback is reached by the new planner calls atsrc/xagent/core/agent/pattern/dag/plan_generator.py:564,577-583and completion calls atsrc/xagent/core/agent/pattern/dag/dag.py:1506,1518-1527. - Why the caller/type/API contract permits it: The core APIs expose separate
execution_messageanddisplay_messagevalues and do not require a non-empty display string; endpoint-specific non-empty text validators do not constrain these core live-control callers. Persistence and checkpoint restoration retain the display metadata, so a blank display value remains a real supported turn rather than being normalized away. A caller-provided output-language pin is a valid mitigation, but the unpinned mode is explicitly supported and is supposed to use user-authored display text as its soft language signal. - PR causation: Before this change, the root system-context, planner, and completion language boundaries did not pass these request values into the new request-only harness. The PR's new
request=arguments at the root and structured DAG consumers directly expose the existing non-empty-display fallback as the sole quoted language source. - Required fix: Make the language-specific extractors distinguish a missing
display_messagekey from a present blank/whitespace value. Preserve any compatibility fallback only for a missing key; for a present blank value, return an empty language request and do not substituteMessage.content. Apply that strict semantics to both_current_user_request_textandlatest_user_text, and add root, plan, and completion regressions using blank and whitespace display text with foreign connector-enriched execution text. Do not reject blank display text outright, because that would break the supported file-only/attachment turn contract.
2. src/xagent/core/agent/language.py:498 — structured prompts duplicate an unbounded complete request
Severity: major
Blocking: yes
- Reachable trigger: An unpinned DAG request is large enough to be near the selected provider's finite context window. The supported request contracts permit arbitrary non-empty content (the public schema has no maximum, legacy message inputs are unconstrained strings, and persisted content is
Text), while the bespoke planner and completion payloads have no token budgeting or compaction for these added fields. - Observable impact:
request_only_language_harnessnow serializes the complete request intooutput_language_policy. The plan payload already carrieslatest_user_requestand the execution transcript inmessages, so this adds approximately one request-sized copy; the completion payload adds the request tooutput_language_policyand again touser_authored_language_request, on top of its existing transcript fields, adding approximately two copies. The verifier measured a 3,000-character request at roughly 3,422 estimated tokens for the baseline plan payload versus 4,450 now, and roughly 3,098 versus 4,886 for completion using the repository's four-characters-per-token estimator. Thus a provider call that fit a 4,096-token configuration before this PR can be rejected for context length, or lose budget needed forstep_resultsandcandidate_output; even below the limit, every request incurs proportional extra input-token and cost work. - Why the caller/type/API contract permits it: Complete request text is reachable and cannot simply be truncated: explicit target-language instructions may occur anywhere in the request, and the language contract requires preserving them. No provider-side deduplication contract removes the copies, and context compaction only handles stored
ExecutionContextmessages, not these structured plan/completion JSON payloads. The structured consumers already have a clean field to reference (latest_user_requestinplan_generator.py:576anduser_authored_language_requestindag.py:1524-1526), so carrying the value once is compatible with the existing payload contract. - PR causation: In the base code, the plan and completion consumers passed no request to
output_language_directives, so their policy text was fixed-size. This PR changedlanguage.py:583-601to render the full request-only harness for unpinned sections and added the structured request fields/callsites, creating the new request-sized copies. - Required fix: Separate rule text from request-value carriage for structured consumers. Keep the complete JSON quote only at the root system-context boundary (or another boundary that genuinely lacks a clean request field); make the plan policy a fixed-size instruction that references
latest_user_request, and make the completion policy referenceuser_authored_language_request. Preserve the clean request once per structured payload and update payload tests to assert field references and the absence of a second full quote. Do not solve this by truncating the request.
Prior-findings checklist
- No prior technical findings or technical threads require resolution. Review bodies and inline review comments were empty, with no technical replies, duplicate occurrences, or tracking references.
- [i] Informational history only: comment
5488913970was a Gemini quota warning, and comment5489203088was rogercloud'sci is failingstatement. Neither contains a technical finding or reply chain.
Review limitations
The Simplification Lens was unavailable due to the usage limit. No simplification finding is generated, and no empty Simplification opportunities section is included.
Blocking status & recommended decision
Blocking: yes.
Blocking issues:
src/xagent/core/agent/context/execution.py:527— major — blank display text can make connector/file-enriched execution content control the user-visible response language. [new]src/xagent/core/agent/language.py:498— major — unbounded request copies can turn a baseline-fitting DAG call into a provider context-length failure and consume completion payload budget. [new]
Recommended decision: REQUEST_CHANGES
rogercloud
left a comment
There was a problem hiding this comment.
Summary
The runtime keeps two forms of each user turn: the display_message the human actually typed, and an execution message enriched with connector context (sender names, email addresses, localized metadata). The previous soft language policy pointed vaguely at "the current user request," so a question asked in English over, say, a Spanish-language email connector could come back in Spanish. This PR makes request_context.output_language the only hard authority and, when it is absent, feeds the model only the JSON-quoted display_message as language evidence — applied consistently across the root system context, Auto, ReAct, and DAG planning/steps/completion, plus the final-answer schemas.
Round 0 design verdict: acceptable-with-reservations. The fix is the right shape given the constraint: separating display text from connector-enriched execution text is the minimum viable seam, and the deeper root cause — connector context being concatenated into user content at all instead of carried as separate metadata — is a much larger refactor that is reasonably out of scope here. The reservations are about accumulation: the codebase now carries roughly seven overlapping language-prose snippets (output_language_policy, response_language_rules, final_answer_language_rule, dag_step_language_rules, plan_language_rules, request_only_language_harness, _structured_request_language_policy), and that duplication has already produced a real defect (F4 below). The empty-request case also appears never to have been designed for explicitly; a single if not request: return response_language_rules() guard in the two harness builders would have removed several of the findings below at the source.
Update since last review
Since the last round the branch picked up 8c3efb2b ("harden request language boundaries"), 476d0fad ("update language prompt assertions"), 7af3fad5 (isort), and 01b4efce. The substantive change is 8c3efb2b: it introduces the tri-state display_message_override() extractor (distinguishing key-absent from present-but-blank from non-string) and switches the DAG plan and completion payloads to reference-only language policies that name a field instead of re-embedding its value. That addresses both prior findings. The same mechanism, however, introduced several new issues listed under Findings.
Prior findings
Prior Finding 1 — blank display_message fell back to connector-enriched execution content: FIXED. src/xagent/core/agent/context/enrichment.py:113-126 now implements a tri-state contract — missing key / non-dict metadata / non-string value yield None (execution-content fallback preserved only there), while a present string, blank included, is .strip()'d and treated as authoritative-empty with no fallback. Both ExecutionContext._current_user_request_text(prefer_display=True) and latest_user_text(prefer_display=True) route through it. tests/core/agent/test_request_language_harness.py:72-141 (15 tests, run and passing) parametrizes blank and whitespace display_message across the root, DAG-plan, and DAG-completion paths and asserts polluted connector text never reaches the language-governing text. Thread already resolved.
Prior Finding 2 — DAG plan/completion payloads embedded the whole request a second time inside output_language_policy: FIXED. _structured_request_language_policy() at src/xagent/core/agent/language.py:511-526 now references the field name only (e.g. "the latest_user_request field") instead of re-quoting the value. tests/core/agent/test_request_language_harness.py::test_structured_language_payloads_include_a_large_request_exactly_once (run and passing) confirms each structured payload contains the request exactly once. Thread already resolved.
Findings
F1 — MAJOR · Blocking: yes · src/xagent/core/agent/pattern/dag/dag.py:1710-1714
_step_instruction was not updated by this PR: it calls output_language_directives(effective_output_language(root_context), section="dag_step_instruction") with no request=. The dispatcher in language.py has no explicit branch for that section, so it falls through to request_only_language_harness(request) with the default request="". When no output language is pinned — the default case — this renders a harness that JSON-quotes an empty string as the "user-authored request" and tells the model to pick a language from that empty quote. The same DAG step's system prompt already carries a correct harness quoting the real request (dag_step_request_anchor, src/xagent/core/agent/context/execution.py:598-611), and both land in the same LLM call, so the model gets two contradictory language harnesses at once. This is a regression against base, where the same fallthrough emitted output_language_policy(language) — self-contained and non-contradictory even when empty.
Fix: thread request=latest_user_text(root_context, prefer_display=True) through this call site (root_context is already in scope, and the same helper is used at src/xagent/core/agent/pattern/dag/dag.py:1506), or restore output_language_policy(language) as the dag_step_instruction fallback in language.py.
Blocking rationale: every DAG step without a pinned output language emits a self-contradictory language instruction inside a single model call, directly undermining the reliability this PR exists to deliver.
T4 (test, MINOR, tied to F1): tests/core/agent/test_output_language_seam.py:161-163 and tests/core/agent/test_dag.py:5298 currently assert the empty-request harness as the correct DAG-step-instruction output, in setups that explicitly construct a real non-empty user request first. Both codify the defect and must be updated to assert the real request is preserved when F1 is fixed.
F2 — MAJOR · Blocking: yes · src/xagent/core/agent/context/execution.py:521-541
On the root (non-DAG) path with no display_message metadata — the common non-connector case, where display falls back to content — current_task (line 524) and request (line 530, via prefer_display=True) resolve to the same string, and the root system context now appends it twice: once raw under "Current user request:", then again JSON-quoted inside request_only_language_harness(request). Verified empirically: context._system_context().count(request) == 2 for this case. Base used response_language_rules() for the no-pinned-language branch, which embeds no request text at all, so this is a new per-turn token cost proportional to request length on every root LLM call without a pinned output language. The new regression test only covers the deliberately-constructed case where display_message differs from content, so it never exercises this fallback.
Fix: when the harness is appended immediately after the same text was printed verbatim as "Current user request," either drop the quoted copy inside the harness or drop the raw line and rely on the harness alone.
Blocking rationale: an unbounded per-call token regression on the default chat path, untested — for large supported requests it recreates exactly the context-budget risk Prior Finding 2 was fixed for, just in a different code path.
F3 — MAJOR · Blocking: no · src/xagent/core/agent/language.py:529-541 (+ call sites pattern/auto/auto.py:1370,1391, pattern/react/react.py:1086,2075,2091)
The default subject of final_answer_language_rule changed from the self-contained "current user request" to a pure pointer, "authoritative output language guidance in the system context". But the root language block at src/xagent/core/agent/context/execution.py:526 is emitted only when current_task is truthy (if current_task and not dag_step_id:), and current_task can be empty — src/xagent/core/agent/runner.py:227 only calls add_user_message(task, ...) if task:, so an attachment- or context-ref-only turn (and some resumed/scheduled invocations) produces no language guidance block at all, while all five final-answer/decision schemas still tell the model to follow "the authoritative guidance in the system context" — a dangling reference. The base default degraded gracefully in the same scenario. No test exercises the empty-current_task path; test_output_language_seam.py's helpers always add a non-empty user message.
Fix: keep the old self-contained default subject as a base, or fall back to it when the directive block will not render.
Blocking rationale: a real regression with zero coverage, but it requires an edge-case invocation shape (no free-text task) off the mainstream chat path — worth fixing, not worth gating merge.
F4 — MINOR · src/xagent/core/agent/language.py:511-526
subject = f"the \{request_field}` field"is passed intoresponse_language_rules(subject=...), whose template already prepends "the " before {subject}in three places. Rendering the actual output confirms it reads "Use the same natural language as **the the**latest_user_requestfield…", three times, in every DAG plan-generation and completion-assessment prompt._structured_request_language_policy is new in this PR; the helper it misuses is unchanged. Existing tests assert only that the substring "latest_user_request` field" appears, so nothing catches it.
Fix: subject = f"\{request_field}` field"`.
F5 — MINOR · src/xagent/core/agent/pattern/dag/dag.py:1524-1526
When a language is pinned, the payload still emits "user_authored_language_request": "" — a dead key the accompanying policy text never references. Cosmetic clutter, no functional or budget impact; suggest omitting the key entirely when output_language is set.
F6 — LOW, informational · src/xagent/core/agent/runner.py:902-908 (pre-existing) × src/xagent/core/agent/context/enrichment.py:113-126
runner.py unconditionally coerces any present-but-non-string display_message (JSON null included) to "" before display_message_override() ever sees it, so the function's documented "legacy non-string values retain the execution-content fallback" branch is unreachable through the production runner path — it is reachable only from hand-constructed test contexts, which is exactly how test_unsupported_display_metadata_preserves_execution_content_fallback is written. This is practically safe and not a re-opening of Prior Finding 1: the resulting authoritative-empty harness still tells the model to derive language from conversation context, not from polluted execution content. It is a docstring-accuracy and coverage gap. Suggested follow-up: either correct the docstring to match real reachability, or have runner.py preserve None so the documented branch becomes live, plus an end-to-end runner test with display_message: null.
Test quality notes
- T1 —
tests/core/agent/test_request_language_harness.py:40-59:test_request_language_harness_preserves_the_whole_request_without_detectionis parametrized over five inputs of differing language and length, but four of its five assertions check invariant boilerplate constants of the function under test (request_only_language_harnessdoes not branch on input language or length). Only thejson.dumps(...) in harnessassertion depends on the parametrized value, so all five cases hit identical code with identical branch coverage while implying cross-language discrimination testing that isn't happening. - T2 —
tests/core/agent/test_request_language_harness.py:243-251:test_final_answer_schemas_follow_the_shared_language_guidanceasserts on literal substrings copied fromfinal_answer_language_rule(). It does verify the guidance is wired into both the ReAct and Auto schemas (it would catch an entirely missing call), but as a string-echo check it would miss the right-looking text landing on the wrong field, or a swappedsubject=that still happens to contain those substrings. - T3 —
tests/core/agent/test_context.py:743-747:test_dag_step_language_quote_uses_the_typed_messagewas loosened fromquote.startswith(typed)totyped in quote. The loosening is legitimately forced by the new JSON-string wrapping, but a tighter JSON-aware form —quote.startswith(json.dumps(typed, ensure_ascii=False))— was available and would keep a positional guarantee; today the only remaining safety net is the separate"Attached file(s)" not in quotecheck. - T4 — see under F1.
Simplification opportunities
src/xagent/core/agent/language.pyL484: shrink —request_only_language_harness(L484) and_structured_request_language_policy(L511) are two ~20-line near-duplicate prose blocks (same "not language evidence" list, same Chinese Simplified/Traditional clause, same "controls language only" clause), differing only in quote-the-value versus name-the-field framing; a single parameterized helper (subject, optionalquoted_value, an empty-request wording toggle) covers both call sites — and would have prevented F4, which arose precisely because these two blocks are maintained by hand.
net: -8 to -12 lines possible
Blocking status & recommended decision
Blocking: yes — recommended event: REQUEST_CHANGES
- [new] F1 —
src/xagent/core/agent/pattern/dag/dag.py:1710-1714: missed call-site update emits an empty-request language harness that contradicts the correct harness in the same DAG-step model call, on the default no-pinned-language path. - [new] F2 —
src/xagent/core/agent/context/execution.py:521-541: the root system context now duplicates the full user request text on every unpinned root call, an untested token-cost regression proportional to request length.
F3-F6 and the test notes are not merge blockers, but F3 and F4 are cheap and worth folding into the same round.
|
Addressed the overall-review items in c785684. DAG step instructions now use fixed-size guidance that points to the authoritative real-request anchor already present in child system context, with no empty JSON quote or duplicated request. Runner behavior remains intentionally unchanged: present null/non-string display values are normalized to authoritative empty text before extraction, and the extractor documentation plus runner-level regressions now make the direct/restored-context fallback boundary explicit.\n\nThe test refactor preserves the explicit Spanish target, Simplified Chinese, Traditional Chinese, short request, mixed-language, Unicode, and special-character cases with diagnostic IDs. Serialization assertions now anchor at the exact JSON field boundary, schema assertions verify exact field placement and complete guidance, and both defective DAG instruction assertions now require the real request in system context but not in step text. Shared setup and soft-policy prose were consolidated without merging the distinct root, structured, empty, step, or pinned behaviors. The 32-test targeted suite, 29 language tests, 13 context-language tests, 8 relevant Auto/ReAct/DAG/runner siblings, isort, Ruff check/format, py_compile, and diff checks pass. Separate F1, F2, and F3 mutations each made its regression fail before restoration. Local pytest-cov support is unavailable in the validated environment, so no local coverage percentage is claimed. |
c785684 to
92f9611
Compare
rogercloud
left a comment
There was a problem hiding this comment.
This PR separates response-language guidance from connector/file-enriched execution text across root, Auto, ReAct, DAG planning/steps/completion, and final-answer schemas, while retaining request_context.output_language as the only caller-controlled hard authority. It introduces shared request-only guidance and carries the display/request boundary through fresh and resumed contexts. The approach is directionally sound, but this re-review confirms three major PR-caused blockers: a blank display loses a DAG child language anchor, a resumed clarification answer becomes completion-language authority, and the root harness adds unbounded request-sized duplication.
Blocking: yes — recommended event: REQUEST_CHANGES
Update summary
Since the previous review, the rebased five-commit sequence is b6402e07 (isolate response language from connector context), c67b55e9 (apply test isort formatting), 37cde4fa (update language-prompt assertions), 47e1257d (harden request-language boundaries), and 92f9611d (close remaining language-prompt review gaps). This review is against the exact range BASE=31a2bb27b1d5174d142b5d8089ef9286eee6e81e through HEAD=92f9611db01499b7327dc9415ae003581233d96f (13 files, +590/-80); the inline anchors below were recomputed for this range.
Round 0 design verdict
Acceptable with reservations. The normalized hard authority, shared renderer, structured-field references, and absence of a persisted heuristic language label fit the stated problem. The design still needed an explicit presentation-versus-authorship boundary, correct classification of resumed DAG clarification answers, and finite-budget ownership for full request text. The synthetic file-only presentation-placeholder concern was independently dropped: those placeholders predate this PR and no deterministic wrong-language regression was established. The resumed-DAG selector and unbudgeted duplication remain confirmed, and Round 1 additionally found the concrete blank-display DAG-child anchor regression below.
Prior-history checklist
All duplicate occurrences and replies are consolidated under one canonical root; no historical root is reopened.
- FIXED — Blank/whitespace display fallback.
- FIXED — Structured plan/completion policy re-embedded request.
- FIXED — DAG-step empty harness / missing step anchor.
- FIXED — Missing-display root request duplication.
- FIXED — Dangling default final-answer guidance.
- FIXED — Doubled
the thestructured policy. - FIXED — Pinned completion dead field.
- DROPPED — Runner normalization/documentation reachability concern; the relevant code is pre-existing and unchanged in this range.
- FIXED — Parameterized harness test-invariant redundancy.
- FIXED — Weak schema test assertions.
- FIXED — Weakened typed-quote assertion.
Current blocking findings
1. Present blank display suppresses the DAG child language anchor — src/xagent/core/agent/context/execution.py:539 — [major][blocking]
Trigger: An unpinned, supported context has a nonempty execution request containing connector/file enrichment and a present display_message equal to "" or whitespace; a DAG child is then created or restored. Evidence: The tri-state display helper returns a present blank string, and the new branch at execution.py:539 accepts it as authoritative. The child system-context builder passes that value to the DAG request-anchor dispatcher, which still omits the request-only harness when the request is empty. Provider-facing message serialization excludes message metadata, so the child is left with generic current-request guidance while the enriched connector/file text remains visible. Impact: A supported DAG step can use auxiliary connector/attachment text as language evidence and emit user-facing prose or persisted tool arguments in the wrong language, diverging from the root/planner/completion contract. Fix: Preserve the present-empty state with a presence-aware section or equivalent and emit explicit empty-request language guidance for DAG children, without falling back to enriched content; add fresh and restored blank/whitespace child regressions.
2. DAG completion treats a waiting clarification answer as language authority — src/xagent/core/agent/pattern/dag/dag.py:1506 — [major][blocking]
Trigger: An unpinned DAG starts with a substantive request in one language, a child pauses through ask_user/send_message(expect_response=True), and the user supplies substantive clarification in another language without requesting a language change; the run resumes and completes. Evidence: The added selector uses unfiltered latest_user_text(context, prefer_display=True), which reverse-scans user messages without excluding response_to_waiting_for_user or DAG scaffolding. The actual forwarding path marks only the child copy; the root answer seen by completion remains unmarked. When no hard language is pinned, the new user_authored_language_request field therefore contains the clarification answer, and its field-only policy makes that value the sole soft language authority; completion strips metadata and has no downstream language validator to mitigate it. Impact: Final DAG synthesis can switch to the clarification answer's language even though the original request established the language and the answer did not request translation. Fix: Preserve pending-question classification on forwarded root answers (including checkpoint-restored resumes) and use the marker-aware top-level request selector for completion and replanning; add live and restored cross-language wait/resume regressions.
3. Root request-only harness duplicates the full request without a finite budget — src/xagent/core/agent/language.py:496 — [major][blocking]
Trigger: Send a nonempty request through a supported WebSocket or Slack file/connector turn with no pinned output language, so the display request is R while the execution request is R+F. Evidence: The added json.dumps(request, ensure_ascii=False) always serializes the complete request with no bound or deduplication. The root system context already renders the enriched execution request, and the user message carries that same enriched request; the new harness adds another full R. Normal compaction truncates visible messages only, does not budget generated system context, and ordinary Auto/ReAct calls do not supply a max-token limit. Impact: A finite request can fit the BASE prompt but exceed the provider context in HEAD, causing a context-length failure before the answer or forcing loss of required context. Fix: Keep language isolation while carrying one canonical full request/reference and account for it in the actual prompt budget, separating enrichment where necessary; do not silently truncate the request because language-bearing instructions may occur mid-request.
Limitations
No local tests, linters, formatters, dependency installs, or runtime validation were run. All 15 reported CI checks completed successfully at the final preflight. The history extractor and simplification scans were unavailable (usage_limit_reached); prior history was manually reconstructed from the complete review, conversation, and inline-comment exports. No linked issues were supplied.
Blocking status & recommended decision
Blocking: yes — recommended event: REQUEST_CHANGES
Blocking issues:
src/xagent/core/agent/context/execution.py:539, major — present blank display suppresses the DAG child request-only language anchor and permits enriched auxiliary text to bias user-visible language. [new]src/xagent/core/agent/pattern/dag/dag.py:1506, major — an unfiltered resumed clarification answer can become the sole completion-language request and switch final synthesis language. [new]src/xagent/core/agent/language.py:496, major — an unbounded duplicate request can push a previously fitting enriched turn over the finite provider context limit. [new]
Each listed root has a supported trigger, concrete impact, and current-code evidence of PR causation against the exact BASE/HEAD range. The fixed and dropped historical roots do not contribute to the blocking decision.
| display = str(message.metadata.get("display_message") or "").strip() | ||
| if display: | ||
| display = display_message_override(metadata) | ||
| if display is not None: |
There was a problem hiding this comment.
[major][blocking] A present blank display_message is now accepted here, but the DAG child still passes that empty value to its request-anchor dispatcher, whose not request path emits no request-only language guidance. With an unpinned request whose execution content contains connector/file enrichment, fresh and restored children then retain only generic current-request guidance while provider messages hide display metadata, so auxiliary text can determine the language. Preserve the present-empty state and emit an explicit empty request-only anchor without falling back to enriched content; add fresh/restored DAG regressions.
| return assessment | ||
|
|
||
| def _completion_assessment_messages(self, context: Any) -> list[dict[str, Any]]: | ||
| language_request = latest_user_text(context, prefer_display=True) or "" |
There was a problem hiding this comment.
[major][blocking] This new selector scans the latest display-preferred user message without excluding response_to_waiting_for_user. In a supported unpinned DAG wait/resume, a cross-language clarification answer becomes user_authored_language_request, and the field-only completion policy can make final prose follow that answer rather than the original request. Mark forwarded root answers on live and restored paths and use the marker-aware root selector so completion retains the original language request.
| return ( | ||
| "Request-only response language harness:\n" | ||
| "User-authored request (JSON string):\n" | ||
| f"{json.dumps(request, ensure_ascii=False)}\n\n" |
There was a problem hiding this comment.
[major][blocking] json.dumps(request, ensure_ascii=False) unconditionally serializes the full request into the harness. For supported unpinned file/connector turns, execution content already contains that request plus enrichment and is sent in both system context and user messages; normal compaction does not budget generated system context. A request that fit BASE can therefore exceed the finite provider context in HEAD and fail before answering. Carry one canonical request/reference and account for it in the prompt budget without truncating language-bearing input.
Summary
request_context.output_languageas the only hard language authoritydisplay_messageas a request-only language harness when no output language is pinnedRoot cause
The runtime can receive an execution prompt containing connector context such as sender names, email addresses, and localized metadata. Although the user's original request is English, the answering model can incorrectly treat that auxiliary text as evidence for another response language. The existing soft policy referred to the "current user request" without consistently separating the user-authored request from the enriched execution prompt.
This change does not restore router- or planner-derived language labels as hard policy.
request_context.output_languageremains the only caller-controlled hard authority. When it is absent, the answering model still owns the language decision, but it makes that decision from the complete user-authoreddisplay_messagealone. Explicit and implicit cross-language requests remain supported, while short, mixed-language, or context-dependent requests safely fall back to model reasoning without heuristic language detection.Validation
upstream/mainpy_compilechecks passed