Consulting
consulting is a strict 1-question-1-reply session. The initiator sends exactly one substantive envelope; the respondent sends exactly one reply; the adapter auto-closes with reason "consulting_complete".
Use it when you want a precisely-bounded query/answer exchange — exactly the shape of "ask another agent for advice and stop."
Shape#
| Participants | Exactly 2 (INITIATOR + RESPONDENT) |
| Turn order | Strict: initiator first, respondent once, then closed |
| Auto-close | Yes — after respondent's reply |
| Termination | Auto-close, explicit close, or TTL |
| Default view | FullTranscript() |
| Default expectations | acks_within(30s, auto_close), reply_within(600s, auto_close) |
The full transcript view (vs the windowed summary used by other adapters) reflects the use case: a consultation is short enough that the respondent should see the entire exchange unfiltered.
Lifecycle#
sequenceDiagram
participant A as alice (initiator)
participant H as Hub + ConsultingAdapter
participant B as bob (respondent)
A->>H: open(type="consulting", target="bob")
H->>B: EV_SESSION_INVITE
B->>H: EV_SESSION_INVITE_ACK
H->>A: EV_SESSION_OPENED
Note over A,B: state = ACTIVE
A->>H: EV_TEXT (initiator_sent ← true)
H->>B: deliver
B->>H: EV_TEXT (respondent_replied ← true)
H->>A: deliver
Note over H: on_accepted → CLOSED, reason="consulting_complete"
H-->>A: EV_SESSION_CLOSED
H-->>B: EV_SESSION_CLOSED The adapter rejects any further send via validate_send raising ProtocolError.
Smallest Example#
The flow:
alice.open(type="consulting", target="bob")— hub posts invite to bob; bob auto-acks; session goesACTIVE.session.send(...)— alice's first (and only) envelope.- Bob's default handler probes
can_send(yes — respondent hasn't replied), runsAgent.ask, sends the reply. ConsultingAdapter.on_accepted(...)sees both flags set and returnsAdapterResult(next_state=CLOSED, auto_close_reason="consulting_complete").- Hub posts
EV_SESSION_CLOSED.alice.wait_for_session_event(...)wakes.
When to Use#
- One-shot query/response — "ask the database expert what indexing strategy fits this query."
- Built-in workflows where the calling code wants a single result and shouldn't wait around if no reply comes.
- Scenarios where the audit trail benefits from each consult being a separate session id.
When NOT to Use#
- Multi-turn back-and-forth — use
conversation. - Multiple respondents — use
discussionorworkflow. - When the LLM may need to ask follow-up questions —
consultingrejects them.
Validation Rules#
ConsultingAdapter.validate_send rejects:
- Out-of-order sends — respondent trying to speak before the initiator's first envelope.
- Any send after both
initiator_sentandrespondent_repliedflags are set.
validate_send raises ProtocolError; the hub propagates it back to the sender's session.send(...) call.
State Object#
@dataclass(slots=True)
class ConsultingState:
initiator_sent: bool = False
respondent_replied: bool = False
Two flags. on_accepted returns CLOSED when both are true.
Auto-Close vs Explicit Close#
The two states use different close_reason values:
| Trigger | Reason |
|---|---|
| Adapter auto-close after reply | "consulting_complete" |
Explicit session.close() | "explicit_close" (or whatever string you pass) |
acks_within violation | "expectation_violated:acks_within" |
reply_within violation | "expectation_violated:reply_within" |
| TTL expired | "ttl_expired" |
The reason flows on the EV_SESSION_CLOSED envelope's event_data and is also stored in SessionMetadata.close_reason for later inspection via hub.get_session(...).
For the full set of close patterns (agent-side tool, sentinel, TTL safety nets), see Closing Sessions.