Skip to content

Commit 2a9232f

Browse files
authored
fix(session-goal): judge goal completion on host evidence, not the model's claim (#4852)
1 parent 68f7e7b commit 2a9232f

36 files changed

Lines changed: 1834 additions & 231 deletions

core/agent_harness/AGENTS.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ are separate layers.
2626

2727
Outer `SessionGoal` (`session/session_goal.py` + `turns/session_goal_loop.py`) is
2828
**not** the inner ReAct `Goal` / `goal_review`. While a session goal is active,
29-
`run_turn` suppresses investigation Want-me-to closers.
29+
`run_turn` suppresses investigation Want-me-to closers. Completion is judged by
30+
`session/session_goal_evaluate.py` (independent of model self-report): checklist
31+
complete via `done=` indices, or `session_goal:achieved` **only** with tool
32+
evidence when there is no checklist — bare `achieved` is ignored. Optional LLM
33+
confirm for the tool-evidence path: `build_session_goal_llm_evaluator` in
34+
`session/session_goal_review.py` (pass as `evaluate=` to the outer loop).
3035

3136
**Evidence kinds (open/closed):** vocabulary + per-kind policy live in
3237
`turns/evidence_kind.py` (`EvidenceKind` + `EvidenceKindPolicy`). Add a kind by

core/agent_harness/prompts/action/multi_step_policy.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,11 @@
6060
shell_run): when the user wants a multi-step chat checklist / walkthrough
6161
with no local create/run/file work — "walk through these N steps in chat",
6262
"checklist without asking whether to continue", "keep going until every
63-
item is done" — emit assistant_handoff with session_goal (and preferably
64-
session_goal_items). The host outer loop continues turns; do not invent
65-
shell side effects to "prove" progress.
63+
item is done" — emit ONE assistant_handoff and set the schema field
64+
session_goal=true (REQUIRED — omitting it leaves the host unable to continue
65+
across turns). Prefer session_goal_items with one string per checklist item
66+
in order. Do not invent shell side effects to "prove" progress; do not put
67+
session_goal only in content prose when the boolean field is available.
6668
Still assistant_handoff (no execution requested):
6769
* capability questions — "do you support consecutive steps?", "can you loop?"
6870
* explicit plan-only requests — "do not write any code yet; first create a

core/agent_harness/prompts/action/text.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,12 +582,21 @@
582582
decides L0 vs L1 from that field plus live connectivity. Omit only for pure
583583
explain/docs chat about analytics with no number request.
584584
- evidence_kind=incident — bare incident / symptom handoffs.
585-
- session_goal=true — multi-step or "keep going until done" work that should
586-
continue across turns without asking whether to continue.
585+
- session_goal=true — REQUIRED on every handoff for multi-step or
586+
"keep going until done" chat checklists / walkthroughs (no local shell
587+
work). The host outer loop keys off this boolean; omitting it drops
588+
continuation. Prefer session_goal_items=["…", …] for checklist criteria.
587589
- session_goal_max_turns=<n> — optional outer-turn cap for that goal.
588590
- session_goal_items=["…", …] — checklist success criteria (one string per
589591
item, in order). The host tracks completion via session_goal:done=<index>
590592
in later replies; do not invent checklist items from synonyms.
593+
When a host session goal is active (or you just emitted session_goal:achieved
594+
for one), finish the reply without a Want me to: closer — the outer loop owns
595+
continuation; do not ask the user whether to continue or clear the goal.
596+
Never emit session_goal:achieved in the same turn as investigation_start —
597+
starting RCA is not finishing the goal. After investigation results (or other
598+
real tool answers) are in the reply and the condition's deliverables are met
599+
(issue id, count, next action, …), then emit session_goal:achieved.
591600
Legacy content-string tags still work if you must put them in content
592601
(``evidence_kind:metric_read`` or ``evidence_kind=metric_read``, same for
593602
``session_goal`` / ``session_goal_item``). Prefer the schema fields above so the

core/agent_harness/prompts/assistant/observation.py

Lines changed: 71 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,62 @@
1212
"incident_description:",
1313
)
1414

15+
_ON_SCREEN_FRAMING = (
16+
"A read-only discovery command was just run to answer the user's question; "
17+
"its output is below. Summarize it to answer the user's question directly, "
18+
"citing the relevant status. The output is already on screen, so keep "
19+
"**Here's what that looks like:** brief or omit it when it would repeat "
20+
"what the user just saw. Still end with **Want me to:** and a specific "
21+
"next step tied to the finding (for integration questions: connect another "
22+
"integration, verify a failed service, or set up a missing one)."
23+
)
24+
25+
_ON_SCREEN_FRAMING_NO_WANT_ME_TO = (
26+
"A read-only discovery command was just run to answer the user's question; "
27+
"its output is below. Summarize it to answer the user's question directly, "
28+
"citing the relevant status. The output is already on screen, so keep "
29+
"**Here's what that looks like:** brief or omit it when it would repeat "
30+
"what the user just saw."
31+
)
32+
33+
_OFF_SCREEN_FRAMING = (
34+
"Live data was just gathered from the connected integrations to answer the "
35+
"user's question; the tool results are below and are NOT otherwise shown to "
36+
"the user. Answer using the three-part response shape from the system "
37+
"prompt: **I found:**, **Here's what that looks like:**, and **Want me to:** "
38+
"with a specific next step. Cite concrete findings (issues, log lines, or "
39+
"metrics). If the data does not contain the answer, say so plainly. You have "
40+
"ALREADY queried the connected sources, so do NOT tell the user to paste an "
41+
"alert or to run `opensre investigate`; instead report what each source "
42+
"returned and, if you need more signal, ask for the specific detail (error "
43+
"string, service, version, or time window) that would let you narrow it down "
44+
"here."
45+
)
46+
47+
_OFF_SCREEN_FRAMING_NO_WANT_ME_TO = (
48+
"Live data was just gathered from the connected integrations to answer the "
49+
"user's question; the tool results are below and are NOT otherwise shown to "
50+
"the user. Answer using **I found:** and **Here's what that looks like:** — "
51+
"omit **Want me to:** entirely (an outer session goal owns continuation). "
52+
"Cite concrete findings (issues, log lines, or metrics). If the data does "
53+
"not contain the answer, say so plainly. You have ALREADY queried the "
54+
"connected sources, so do NOT tell the user to paste an alert or to run "
55+
"`opensre investigate`; instead report what each source returned and, if "
56+
"you need more signal, ask for the specific detail (error string, service, "
57+
"version, or time window) that would let you narrow it down here."
58+
)
59+
60+
_CLOSER_WANT_ME_TO = (
61+
"Do NOT request, plan, or emit any further tool calls or actions in this "
62+
"turn — phrase next steps only as prose in **Want me to:**."
63+
)
64+
65+
_CLOSER_NO_WANT_ME_TO = (
66+
"Do NOT request, plan, or emit any further tool calls or actions in this "
67+
"turn. Do NOT close with **Want me to:** — phrase any next diagnostic step "
68+
"as plain prose in the body if the goal asks for it."
69+
)
70+
1571

1672
def _guidance_for_handoff_tag(tag: str) -> str | None:
1773
"""Resolve exact handoff tags, then known prefixes (e.g. ``database_query:``)."""
@@ -33,40 +89,25 @@ def build_handoff_guidance_block(handoff_contents: tuple[str, ...]) -> str:
3389
return "".join(blocks)
3490

3591

36-
def build_observation_block(tool_observation: str | None, *, on_screen: bool = True) -> str:
37-
"""Wrap freshly-gathered tool output so the assistant summarizes it directly."""
92+
def build_observation_block(
93+
tool_observation: str | None,
94+
*,
95+
on_screen: bool = True,
96+
omit_want_me_to: bool = False,
97+
) -> str:
98+
"""Wrap freshly-gathered tool output so the assistant summarizes it directly.
99+
100+
When ``omit_want_me_to`` is true (outer session goal active), skip the
101+
Want-me-to closer — the host loop owns continuation.
102+
"""
38103
if not tool_observation or not tool_observation.strip():
39104
return ""
40105
if on_screen:
41-
framing = (
42-
"A read-only discovery command was just run to answer the user's question; "
43-
"its output is below. Summarize it to answer the user's question directly, "
44-
"citing the relevant status. The output is already on screen, so keep "
45-
"**Here's what that looks like:** brief or omit it when it would repeat "
46-
"what the user just saw. Still end with **Want me to:** and a specific "
47-
"next step tied to the finding (for integration questions: connect another "
48-
"integration, verify a failed service, or set up a missing one)."
49-
)
106+
framing = _ON_SCREEN_FRAMING_NO_WANT_ME_TO if omit_want_me_to else _ON_SCREEN_FRAMING
50107
else:
51-
framing = (
52-
"Live data was just gathered from the connected integrations to answer the "
53-
"user's question; the tool results are below and are NOT otherwise shown to "
54-
"the user. Answer using the three-part response shape from the system "
55-
"prompt: **I found:**, **Here's what that looks like:**, and **Want me to:** "
56-
"with a specific next step. Cite concrete findings (issues, log lines, or "
57-
"metrics). If the data does not contain the answer, say so plainly. You have "
58-
"ALREADY queried the connected sources, so do NOT tell the user to paste an "
59-
"alert or to run `opensre investigate`; instead report what each source "
60-
"returned and, if you need more signal, ask for the specific detail (error "
61-
"string, service, version, or time window) that would let you narrow it down "
62-
"here."
63-
)
64-
return (
65-
f"{framing} Do NOT request, plan, or emit any further tool calls or "
66-
"actions in this turn — phrase next steps only as prose in "
67-
"**Want me to:**.\n\n"
68-
f"--- tool_results ---\n{tool_observation}\n\n"
69-
)
108+
framing = _OFF_SCREEN_FRAMING_NO_WANT_ME_TO if omit_want_me_to else _OFF_SCREEN_FRAMING
109+
closer = _CLOSER_NO_WANT_ME_TO if omit_want_me_to else _CLOSER_WANT_ME_TO
110+
return f"{framing} {closer}\n\n--- tool_results ---\n{tool_observation}\n\n"
70111

71112

72113
# Legacy private name used by older tests.

core/agent_harness/prompts/assistant/text.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@
103103
"(comma-separate multiple). When every item is done, include "
104104
"`session_goal:achieved`. Put those tags on their own at the end of "
105105
"the reply; the harness strips them before the user sees the text. "
106-
"Do not ask whether to continue while the goal is active.\n\n"
106+
"Do not ask whether to continue while the goal is active. "
107+
"Do NOT close with **Want me to:** (no investigation offer, no "
108+
"follow-up menu) — the outer loop owns continuation.\n\n"
107109
),
108110
# Prefix key: ``build_handoff_guidance_block`` matches any
109111
# ``database_query:<topic>`` tag (mysql_active_connections, mariadb_dashboard, …).

core/agent_harness/prompts/assistant/turn.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ def log_diagnostics(self, reason: str) -> None:
6868
"""Emit grounding-cache diagnostics for ``reason``."""
6969

7070

71+
def _handoff_has_session_goal(handoff_contents: tuple[str, ...]) -> bool:
72+
return any(tag.startswith("session_goal:") for tag in handoff_contents)
73+
74+
7175
def _assistant_context_blocks(
7276
*,
7377
turn_snapshot: TurnSnapshot,
@@ -76,11 +80,16 @@ def _assistant_context_blocks(
7680
tool_observation_on_screen: bool,
7781
suggested_prompt: str = SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST,
7882
) -> str:
83+
omit_want_me_to = _handoff_has_session_goal(handoff_contents)
7984
return "".join(
8085
(
8186
_build_integration_guard(turn_snapshot),
8287
build_handoff_guidance_block(handoff_contents),
83-
build_observation_block(tool_observation, on_screen=tool_observation_on_screen),
88+
build_observation_block(
89+
tool_observation,
90+
on_screen=tool_observation_on_screen,
91+
omit_want_me_to=omit_want_me_to,
92+
),
8493
synthetic_failure.build_block(turn_snapshot, suggested_prompt=suggested_prompt),
8594
)
8695
)

0 commit comments

Comments
 (0)