1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//! Pending-continuation admission, owned by the canonical
//! [`session_document::SessionDocumentMachine`].
//!
//! The pending-boundary disposition (`RunPending` / `NoPendingBoundary`) is a
//! session-document-tail-derived SEMANTIC fact. It is decided by a
//! SessionDocumentMachine transition (`ResolvePendingContinuation`), not here:
//! this module only carries the PURE mechanical [`observe_session_tail`]
//! encoder (the `messages.last()` → [`ObservedSessionTailKind`] map) and a thin
//! driver that runs the machine op and MIRRORS the emitted disposition. No
//! decision lives in this shell.
use crate::session_document;
use crate::types::Message;
// Re-export the canonical pending-continuation vocabulary (owned by the
// SessionDocumentMachine) under this module so consumers have one ergonomic
// path for the encoder, the typed tail/disposition, and the driver.
pub use session_document::SessionDocumentError as PendingContinuationAdmissionError;
pub use session_document::{
ObservedSessionTailKind, PendingContinuationDisposition, PendingContinuationPublicTerminal,
};
/// Mirror of the [`session_document::SessionDocumentMachine`]
/// pending-continuation decision for a single resolution.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PendingContinuationResolution {
pub disposition: PendingContinuationDisposition,
pub public_terminal: Option<PendingContinuationPublicTerminal>,
}
/// Pure mechanical encoder: classify the last message of a transcript into the
/// typed [`ObservedSessionTailKind`]. This is a structural encoding only — it
/// makes no admission decision. The machine decides the disposition from this
/// observation.
#[must_use]
pub fn observe_session_tail(messages: &[Message]) -> ObservedSessionTailKind {
match messages.last() {
None => ObservedSessionTailKind::Empty,
Some(Message::System(_)) => ObservedSessionTailKind::System,
Some(Message::SystemNotice(_)) => ObservedSessionTailKind::SystemNotice,
Some(Message::User(_)) => ObservedSessionTailKind::User,
Some(Message::BlockAssistant(_)) => ObservedSessionTailKind::BlockAssistant,
Some(Message::ToolResults { .. }) => ObservedSessionTailKind::ToolResults,
}
}
/// Drive the canonical SessionDocumentMachine `ResolvePendingContinuation`
/// transition and MIRROR the emitted disposition (and public terminal witness).
/// The pending-continuation region of SessionDocumentMachine is stateless
/// (self-loop in `Ready`), so a fresh authority per resolution is canonical.
pub fn resolve_pending_continuation(
session_tail: ObservedSessionTailKind,
staged_tool_result_count: u64,
) -> Result<PendingContinuationResolution, PendingContinuationAdmissionError> {
let mut authority = session_document::SessionDocumentMachineAuthority::new();
let effects = authority.resolve_pending_continuation(session_tail, staged_tool_result_count)?;
let mut disposition = None;
let mut public_terminal = None;
for effect in effects {
match effect {
session_document::SessionDocumentEffect::PendingContinuationResolved {
disposition: value,
} => {
disposition = Some(value);
}
session_document::SessionDocumentEffect::PendingContinuationPublicTerminalResolved {
terminal,
} => {
public_terminal = Some(terminal);
}
_ => {}
}
}
let Some(disposition) = disposition else {
// Both pending-continuation transitions always emit
// `PendingContinuationResolved`, so this is unreachable; surface it as a
// typed authority error rather than panicking if the contract ever drifts.
return Err(PendingContinuationAdmissionError::new(
"pending_continuation_resolution_effect",
));
};
Ok(PendingContinuationResolution {
disposition,
public_terminal,
})
}