feat: expose agent session refs in api

This commit is contained in:
Ogulcan Celik 2026-05-28 05:06:04 +03:00
parent 986e4a1776
commit 2d8b4e2db4
6 changed files with 85 additions and 3 deletions

View File

@ -129,9 +129,13 @@ herdr pane report-agent <pane_id> \
--state idle|working|blocked|unknown \
[--message TEXT] \
[--custom-status TEXT] \
[--seq N]
[--seq N] \
[--agent-session-id ID] \
[--agent-session-path PATH]
```
`pane get`, `pane list`, `agent get`, and `agent list` include a read-only `agent_session` object when an official integration has reported a native session reference. If no native session reference is stored, the field is omitted.
Report display-only pane metadata without taking over semantic state:
```bash

View File

@ -163,6 +163,21 @@ Integrations report agent state with `pane.report_agent`.
`custom_status` is visual. It can show a short label like `indexing` without changing semantic behavior.
Official integrations can also report a native session reference. `pane.get`, `pane.list`, `agent.get`, and `agent.list` expose a read-only `agent_session` object when Herdr has a stored native session reference:
```json
{
"agent_session": {
"source": "herdr:codex",
"agent": "codex",
"kind": "id",
"value": "..."
}
}
```
If no native session reference is stored, the field is omitted.
Use `pane.report_metadata` when a user hook wants to customize presentation without taking over lifecycle state from a Herdr integration.
```json

View File

@ -803,6 +803,8 @@ pub struct AgentInfo {
pub custom_status: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub state_labels: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_session: Option<AgentSessionInfo>,
pub workspace_id: String,
pub tab_id: String,
pub pane_id: String,
@ -834,9 +836,19 @@ pub struct PaneInfo {
pub custom_status: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub state_labels: HashMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_session: Option<AgentSessionInfo>,
pub revision: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentSessionInfo {
pub source: String,
pub agent: String,
pub kind: crate::agent_resume::AgentSessionRefKind,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PaneReadResult {
pub pane_id: String,
@ -1402,6 +1414,7 @@ mod tests {
agent_status: AgentStatus::Unknown,
custom_status: None,
state_labels: HashMap::new(),
agent_session: None,
revision: 0,
},
worktree: WorktreeInfo {
@ -1451,6 +1464,7 @@ mod tests {
agent_status: AgentStatus::Unknown,
custom_status: None,
state_labels: HashMap::new(),
agent_session: None,
revision: 0,
},
},

View File

@ -417,6 +417,7 @@ impl App {
agent_status: pane.agent_status,
custom_status: pane.custom_status,
state_labels: pane.state_labels,
agent_session: pane.agent_session,
workspace_id: pane.workspace_id,
tab_id: pane.tab_id,
pane_id: pane.pane_id,

View File

@ -295,6 +295,7 @@ impl App {
agent_status: pane_agent_status(terminal.state, pane.seen),
custom_status: presentation.custom_status,
state_labels: presentation.state_labels,
agent_session: terminal_agent_session_info(terminal),
revision: terminal.revision,
})
}
@ -345,3 +346,28 @@ impl App {
}
}
}
fn terminal_agent_session_info(
terminal: &crate::terminal::TerminalState,
) -> Option<crate::api::schema::AgentSessionInfo> {
if let Some(authority) = terminal.hook_authority.as_ref() {
if let Some(session_ref) = authority.session_ref.as_ref() {
return Some(crate::api::schema::AgentSessionInfo {
source: authority.source.clone(),
agent: authority.agent_label.clone(),
kind: session_ref.kind,
value: session_ref.value.clone(),
});
}
}
terminal
.persisted_agent_session
.as_ref()
.map(|session| crate::api::schema::AgentSessionInfo {
source: session.source.clone(),
agent: session.agent.clone(),
kind: session.session_ref.kind,
value: session.session_ref.value.clone(),
})
}

View File

@ -1209,11 +1209,13 @@ fn pane_report_agent_updates_effective_state() {
thread::sleep(Duration::from_millis(100));
}
let session_path = base.join("pi-session.jsonl");
let hook = send_request(
&socket_path,
&format!(
r#"{{"id":"req_hook_5","method":"pane.report_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi","state":"working","message":"thinking"}}}}"#,
pane_id
r#"{{"id":"req_hook_5","method":"pane.report_agent","params":{{"pane_id":"{}","source":"herdr:pi","agent":"pi","state":"working","message":"thinking","agent_session_path":"{}"}}}}"#,
pane_id,
session_path.display()
),
);
assert_eq!(hook["result"]["type"], "ok");
@ -1227,6 +1229,16 @@ fn pane_report_agent_updates_effective_state() {
);
assert_eq!(pane["result"]["pane"]["agent"], "pi");
assert_eq!(pane["result"]["pane"]["agent_status"], "working");
assert_eq!(
pane["result"]["pane"]["agent_session"]["source"],
"herdr:pi"
);
assert_eq!(pane["result"]["pane"]["agent_session"]["agent"], "pi");
assert_eq!(pane["result"]["pane"]["agent_session"]["kind"], "path");
assert_eq!(
pane["result"]["pane"]["agent_session"]["value"],
session_path.display().to_string()
);
let metadata = send_request(
&socket_path,
@ -1259,6 +1271,16 @@ fn pane_report_agent_updates_effective_state() {
r#"{"id":"req_hook_metadata_agent","method":"agent.get","params":{"target":"pi"}}"#,
);
assert_eq!(agent["result"]["agent"]["agent"], "pi");
assert_eq!(
agent["result"]["agent"]["agent_session"]["source"],
"herdr:pi"
);
assert_eq!(agent["result"]["agent"]["agent_session"]["agent"], "pi");
assert_eq!(agent["result"]["agent"]["agent_session"]["kind"], "path");
assert_eq!(
agent["result"]["agent"]["agent_session"]["value"],
session_path.display().to_string()
);
assert_eq!(agent["result"]["agent"]["title"], "Refactor auth");
assert_eq!(agent["result"]["agent"]["display_agent"], "Pi auth");
assert_eq!(agent["result"]["agent"]["custom_status"], "middleware");