Skip to content

Commit ccd6181

Browse files
committed
Enhance support for hybrid refresh and interaction management in canonical ask workflow
1 parent 27cf484 commit ccd6181

11 files changed

Lines changed: 291 additions & 39 deletions

File tree

skills/canonical/ask/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export DOCMASON_FRONT_DOOR_STATE="canonical-ask"
155155

156156
- `completion_status` is optional when the caller is only re-entering a `waiting-shared-job` turn to let the hidden wrapper reconcile deterministic repo-owned shared-job truth.
157157
- supply `completion_status` only when the host is actively settling a still-unsettled governed multimodal refresh.
158+
- when the current-turn `hybrid_refresh_work.json` lists render or focus-render assets and the host can inspect images, inspect the relevant assets lightly and include `render_inspection_used` plus `inspected_render_assets` in `hybrid_refresh_summary`.
158159
- when a turn is paused in `waiting-shared-job`, re-enter through hidden `open` reuse, hidden `progress`, or hidden `finalize`; do not grep `runtime/control_plane/` or shared-job files manually.
159160

160161
- `finalize` request envelope:
@@ -265,6 +266,7 @@ If the environment cannot satisfy those capabilities, stop and explain the block
265266
- let the routed inner workflow own retrieval, trace, render inspection, and answer or composition drafting
266267
- if published artifacts are still insufficient because of hard-artifact semantic gaps, let the canonical routed path enter one governed narrowed hybrid refresh instead of improvising raw source fallback
267268
- this ask-owned narrowed hybrid refresh is the Lane C path and is the only ordinary ask follow-up settled through hidden `progress`
269+
- after a `covered` settlement, rerun retrieve and trace exactly on the post-refresh evidence; if the result is commit-admissible but still only partially supported, finalize honestly as `partially-grounded` instead of starting a second refresh
268270
- if that governed path becomes a shared wait or blocked boundary, keep the same turn paused or committed through the existing ask control-plane states rather than opening a side path
269271
7. Complete the turn through the supported completion path.
270272
- write only the final answer under `runtime/answers/<conversation_id>/<turn_id>.md`

skills/canonical/grounded-answer/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ If the agent cannot inspect required rendered evidence, stop and explain that th
6565
- when published sufficiency fails because of hard-artifact semantic gaps, the grounded-answer path must enter the governed ask-time multimodal refresh before any raw source inspection
6666
- use `recommended_hybrid_targets` as the only legal query-aware narrowing entrypoint
6767
- if the turn becomes a waiter on that governed refresh, keep the same turn paused and reuse the shared result
68-
- once the governed refresh picks a source, complete that source's current hybrid candidates, reretrieve, and retrace before treating the ask as ready to answer
68+
- once the governed refresh picks a source, complete that source's current hybrid candidates, inspect listed render or focus-render assets when present and answer-relevant, reretrieve, and retrace before treating the ask as ready to answer
69+
- record lightweight settlement audit fields such as `render_inspection_used` and `inspected_render_assets` when the work packet exposes visual assets
70+
- after one `covered` refresh and post-refresh retrieve/trace, close honestly with the remaining support boundary instead of starting a second same-turn refresh
6971
- if the governed refresh settles `blocked`, close the turn as `abstained + governed-boundary`
7072
5. Run provenance tracing for the strongest support when you need corroboration, contradiction checks, or answer-state clarification:
7173
- `docmason trace --source-id <source_id> --json --compact`

skills/canonical/grounded-composition/SKILL.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ If the environment cannot inspect the required evidence, stop and explain the bl
6565
- use `recommended_hybrid_targets` as the only legal narrowing entrypoint
6666
- write the current-turn `hybrid_refresh_work.json`
6767
- reuse a matching shared refresh result when the turn is a waiter
68-
- complete the selected source's current hybrid candidates, then rerun retrieve and trace before drafting the final synthesis
68+
- complete the selected source's current hybrid candidates, inspect listed render or focus-render assets when present and relevant to the deliverable, then rerun retrieve and trace before drafting the final synthesis
69+
- record lightweight settlement audit fields such as `render_inspection_used` and `inspected_render_assets` when the work packet exposes visual assets
70+
- after one `covered` refresh and post-refresh retrieve/trace, close honestly with the remaining support boundary instead of starting a second same-turn refresh
6971
- if the governed refresh settles `blocked`, stop with `abstained + governed-boundary` instead of improvising around the gap
7072
- inspect direct source files or rerender only when the published-artifact plan says the knowledge base is insufficient for style, visual structure, or low-level detail
7173
- bring in external verification or stable model knowledge only when the composition task genuinely needs it, and keep the support basis explicit

src/docmason/ask.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
interaction_ingest_snapshot,
6161
interaction_overlay_relevance,
6262
maybe_reconcile_active_thread,
63+
suppress_interaction_promotion,
6364
)
6465
from .project import (
6566
WorkspacePaths,
@@ -1396,11 +1397,26 @@ def _shared_evidence_write_state(
13961397
status: str,
13971398
*,
13981399
notice: str,
1399-
) -> dict[str, str]:
1400+
) -> dict[str, Any]:
1401+
next_actions = {
1402+
"active": "complete-shared-evidence-refresh",
1403+
"covered": "reretrieve-and-retrace-before-finalize",
1404+
"blocked": "return-governed-boundary",
1405+
}
1406+
close_policies = {
1407+
"active": "wait-for-single-governed-refresh",
1408+
"covered": "post-refresh-retrieve-trace-then-close-honestly-with-remaining-boundary",
1409+
"blocked": "close-governed-boundary",
1410+
}
14001411
return {
1412+
"schema_version": 1,
14011413
"kind": "shared-evidence-truth-write",
14021414
"status": status,
14031415
"notice": notice,
1416+
"next_action": next_actions.get(status, "inspect-shared-evidence-state"),
1417+
"refresh_attempt_limit": 1,
1418+
"refresh_attempt_exhausted": status in {"covered", "blocked"},
1419+
"post_refresh_close_policy": close_policies.get(status, "inspect-shared-evidence-state"),
14041420
}
14051421

14061422

@@ -2230,6 +2246,11 @@ def prepare_ask_turn(
22302246
updates={"captured_interaction_ids": latest_captured_interaction_ids},
22312247
refresh_workspace_snapshot=False,
22322248
)
2249+
suppress_interaction_promotion(
2250+
paths,
2251+
interaction_ids=latest_captured_interaction_ids,
2252+
reason="canonical-ask-self-capture",
2253+
)
22332254
current_turn = {
22342255
"conversation_id": opened["conversation_id"],
22352256
"turn_id": opened["turn_id"],
@@ -3689,7 +3710,15 @@ def complete_ask_turn(
36893710
{"conversation_id": conversation_id, "turn_id": turn_id, **updated},
36903711
status=result_status,
36913712
detail=response_excerpt if result_status == "boundary" else None,
3692-
support_notice=support_fulfillment_notice(support_fulfillment),
3713+
support_notice=support_fulfillment_notice(
3714+
support_fulfillment,
3715+
terminal=result_status in {"completed", "boundary"},
3716+
hybrid_refresh_completion_status=(
3717+
resolved_hybrid_refresh_completion_status
3718+
if isinstance(resolved_hybrid_refresh_completion_status, str)
3719+
else None
3720+
),
3721+
),
36933722
)
36943723
updated = update_conversation_turn(
36953724
paths,

src/docmason/ask_contracts.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@ def build_support_fulfillment(
341341

342342
def support_fulfillment_notice(
343343
support_fulfillment: dict[str, Any] | None,
344+
*,
345+
terminal: bool = False,
346+
hybrid_refresh_completion_status: str | None = None,
344347
) -> str | None:
345348
"""Return a short host-facing explanation derived from support fulfillment."""
346349
fulfillment = dict(support_fulfillment) if isinstance(support_fulfillment, dict) else {}
@@ -358,6 +361,12 @@ def support_fulfillment_notice(
358361
"preserve the required channels."
359362
)
360363
if primary_gap_type == "source-escalation-required":
364+
if terminal and hybrid_refresh_completion_status == "covered":
365+
return (
366+
"DocMason completed one governed evidence refresh and reran retrieval "
367+
"and trace; some content still remains only partially supported by "
368+
"the published evidence."
369+
)
361370
return (
362371
"Published artifacts still need governed source escalation before "
363372
"the ask contract can close."

src/docmason/front_controller.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def write_hybrid_refresh_work(
8787
write_json(
8888
work_path,
8989
{
90+
"schema_version": 1,
9091
"generated_at": datetime.now(tz=UTC).isoformat().replace("+00:00", "Z"),
9192
"target": target,
9293
"query": query,
@@ -103,6 +104,40 @@ def write_hybrid_refresh_work(
103104
target=target,
104105
source_ids=source_ids,
105106
),
107+
"operation_checklist": [
108+
"inspect_target_render_or_focus_render_assets_when_present",
109+
"write_additive_semantic_overlay_only",
110+
"record_render_inspection_used_and_assets_in_progress_summary",
111+
"settle_hidden_ask_progress_with_covered_or_blocked",
112+
],
113+
"render_inspection_guidance": {
114+
"required_when_render_assets_present": True,
115+
"preferred_asset_fields": [
116+
"target_focus_render_assets",
117+
"focus_render_assets",
118+
"target_render_assets",
119+
"render_assets",
120+
"render_refs",
121+
],
122+
"audit_summary_fields": [
123+
"render_inspection_used",
124+
"inspected_render_assets",
125+
],
126+
"gate": "guidance-only",
127+
},
128+
"settlement_summary_contract": {
129+
"expected_fields": [
130+
"mode",
131+
"work_path",
132+
"selected_source_id",
133+
"covered_unit_ids",
134+
"overlay_paths",
135+
"render_inspection_used",
136+
"inspected_render_assets",
137+
"summary",
138+
],
139+
"status_values": ["covered", "blocked"],
140+
},
106141
},
107142
)
108143
return str(work_path.relative_to(paths.root))

src/docmason/host_integration.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -419,21 +419,36 @@ def _turn_log_context(turn: dict[str, Any]) -> dict[str, str] | None:
419419
)
420420

421421

422-
def _turn_detail(turn: dict[str, Any], *, fallback: str | None = None) -> str | None:
422+
def _turn_detail(
423+
turn: dict[str, Any],
424+
*,
425+
fallback: str | None = None,
426+
status: str | None = None,
427+
) -> str | None:
428+
terminal_completed = status == "completed"
423429
shared_evidence_write_state = _host_shared_evidence_write_state(turn)
424430
shared_evidence_notice = (
425431
_nonempty_string(shared_evidence_write_state.get("notice"))
426-
if shared_evidence_write_state is not None
432+
if shared_evidence_write_state is not None and not terminal_completed
427433
else None
428434
)
435+
support_notice = support_fulfillment_notice(
436+
_mapping(turn.get("support_fulfillment")) or None,
437+
terminal=status in {"completed", "boundary"},
438+
hybrid_refresh_completion_status=_nonempty_string(
439+
turn.get("hybrid_refresh_completion_status")
440+
),
441+
)
429442
for candidate in (
430-
fallback,
443+
fallback if (not terminal_completed or support_notice is None) else None,
431444
shared_evidence_notice,
432-
_nonempty_string(turn.get("freshness_notice")),
445+
_nonempty_string(turn.get("freshness_notice")) if not terminal_completed else None,
433446
_nonempty_string(turn.get("confirmation_prompt")),
434-
support_fulfillment_notice(_mapping(turn.get("support_fulfillment")) or None),
435-
_nonempty_string(turn.get("response_excerpt")),
436-
_nonempty_string(turn.get("route_reason")),
447+
support_notice,
448+
_nonempty_string(turn.get("response_excerpt"))
449+
if status in {"boundary", "blocked"} or (terminal_completed and support_notice is None)
450+
else None,
451+
_nonempty_string(turn.get("route_reason")) if not terminal_completed else None,
437452
_nonempty_string(turn.get("primary_issue_code")),
438453
):
439454
if candidate is not None:
@@ -514,7 +529,13 @@ def _host_turn_payload(
514529
support_fulfillment = _mapping(turn.get("support_fulfillment")) or None
515530
workflow_outcome = _mapping(turn.get("workflow_outcome")) or None
516531
shared_evidence_write_state = _host_shared_evidence_write_state(turn)
517-
support_notice = support_fulfillment_notice(support_fulfillment)
532+
support_notice = support_fulfillment_notice(
533+
support_fulfillment,
534+
terminal=status in {"completed", "boundary"},
535+
hybrid_refresh_completion_status=_nonempty_string(
536+
turn.get("hybrid_refresh_completion_status")
537+
),
538+
)
518539
next_step = _next_step(status)
519540
result_explanation = build_result_explanation(
520541
turn,
@@ -551,7 +572,7 @@ def _host_turn_payload(
551572
"support_basis": _nonempty_string(turn.get("support_basis")),
552573
"session_ids": _string_list(turn.get("session_ids")),
553574
"trace_ids": _string_list(turn.get("trace_ids")),
554-
"detail": _turn_detail(turn, fallback=detail),
575+
"detail": _turn_detail(turn, fallback=detail, status=status),
555576
"primary_issue_code": _nonempty_string(turn.get("primary_issue_code")),
556577
"issue_codes": _string_list(turn.get("issue_codes")),
557578
"answer_file_path": _nonempty_string(turn.get("answer_file_path")),

src/docmason/interaction.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,37 @@ def pending_interaction_entries(paths: WorkspacePaths) -> list[dict[str, Any]]:
462462
return entries
463463

464464

465+
def suppress_interaction_promotion(
466+
paths: WorkspacePaths,
467+
*,
468+
interaction_ids: list[str],
469+
reason: str,
470+
) -> dict[str, Any]:
471+
"""Mark captured interaction entries as audit-only without touching promoted memories."""
472+
suppressed_ids: list[str] = []
473+
for interaction_id in interaction_ids:
474+
if not isinstance(interaction_id, str) or not interaction_id:
475+
continue
476+
entry_path = _interaction_entry_path(paths, interaction_id)
477+
entry = read_json(entry_path)
478+
if not isinstance(entry, dict) or not entry:
479+
continue
480+
if entry.get("status") == "promoted":
481+
continue
482+
entry["pending_promotion"] = False
483+
entry["status"] = "operator-evidence-only"
484+
entry["promotion_suppressed_reason"] = reason
485+
entry["updated_at"] = utc_now()
486+
entry["entry_fingerprint"] = _sha256_text(json.dumps(entry, sort_keys=True))
487+
write_json(entry_path, entry)
488+
suppressed_ids.append(interaction_id)
489+
overlay_manifest = refresh_interaction_overlay(paths)
490+
return {
491+
"suppressed_interaction_ids": suppressed_ids,
492+
"pending_overlay": overlay_manifest,
493+
}
494+
495+
465496
def _entry_channel_descriptors(
466497
entry: dict[str, Any],
467498
*,

0 commit comments

Comments
 (0)