Skip to content

Commit bc70a8b

Browse files
committed
feat(kimi-agent): session include_subagents read surface (G-1.4 close)
Closes the resume/replay data gap recorded in CODEX §1.4 #2: - session/list gains include_subagents (default false) and now filters subagent records (swarm-*/task-* written via Agent::durable_state) from the default user-facing list — a behavior fix - session/get_context gains include_subagents; when set, the response carries a subagents summary list (agent_id, title, message_count, updated_at) — agent_id is the session id since subagents persist under agent_id keys in the same sessions table - wire.gen.ts regenerated via gen-wire-contract (141 types, idempotent); SessionSummaryRpc/SessionListResult/SessionContextResult untouched, so kimi-server is unaffected (cargo check -p kimi-server clean) - integration test: default list hides subagents, opt-in lists them, get_context returns the summary only when requested - vscode host wiring (replay-adapter consumption shape) left for a separate confirmation; documented in CODEX
1 parent afa1d79 commit bc70a8b

6 files changed

Lines changed: 354 additions & 11 deletions

File tree

CODEX_MIGRATION_PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
## 1.4 已知缺口(不阻塞主线的记录)
4848

4949
1. ~~**compaction summarizer 双通道**~~**已补(2026-08-10)**`LlmCompactionDelegate`(任意 `LLM` 实现)+ `ensure_compaction_delegate` 宿主分支——无 native LLM 的 host-proxy 会话经 `HostLlmProxy``HostCallbacks::llm_chat`)走宿主通道 compact(原报 `compaction.unable`);单测 + stdio 集成适配(`session_compact_host_proxy_gets_a_summarizer_delegate`
50-
2. **子代理 replay 数据源**:🔶 部分补(2026-08-10:Task 工具子代理在任务被跟踪时(task_service 存在)经 `run_child_agent_persistent_with_model` 持久化对话到 session store(agent_id = task_id,swarm 同一机制)——resume 面的**数据源已建立**;剩余:resume/session RPC`include_subagents` 读取面(引擎无此参数)待补,vscode replay 测试仍跳过
50+
2. ~~**子代理 replay 数据源**~~**已补(2026-08-10 数据源 + 2026-08-11 读取面)**:Task 工具子代理在任务被跟踪时(task_service 存在)经 `run_child_agent_persistent_with_model` 持久化对话到 session store(agent_id = task_id,swarm 同一机制);读取面:`session/list` 支持 `include_subagents`(默认过滤子代理记录,opt-in 全量),`session/get_context` 支持 `include_subagents` → 响应 `subagents` 摘要列表(agent_id/消息数/updated_at,`SubagentSummaryRpc`);单测 + stdio 集成(`session_list_and_context_include_subagents`)。**宿主接线待定**:vscode harness`includeSubagents` 尚未透传到 RPC、replay-adapter 消费的是完整 replay 而非摘要(`parentAgentId` 关联引擎未持久化)——vscode 为白名单包,改需单独确认
5151
3. **用户真实 config.toml 损坏**`duplicate defaultModel`(defaultModel 与 default_model 并存)导致 Rust TOML 严格解析拒绝整个配置(用户禁止修改真实文件,隔离配置验证绕开;建议用户侧删 camelCase 行)
5252

5353
> **2026-08-10 收口复核**:原第 4-6 项已消失——vscode typecheck 全过(`replay-adapter.ts` 错误已随 sdk-local 完成消除);kosong 已退役(flaky 测试随包);发布打包已接入 CI(`_rust-bin-build.yml` + release 注入);**vscode legacy backfill 已闭环**(kimi-runtime.ts 迁移会话读 `kimi_cli_source_path` → 写 `vscode_legacy_approval` metadata → session-runtime 消费,create/resume 全链路完整)。另:apps/kimi-code 的 `@moonshot-ai/kimi-agent` devDep 已删(全仓无真实 import,仅注释引用 wire.gen)。

crates/kimi-protocol/src/wire_types.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,16 @@ pub struct SessionIdParams {
9595
pub session_id: String,
9696
}
9797

98+
/// Input for `session/get_context` — session id plus the resume replay
99+
/// switch: when `include_subagents` is true, the result carries subagent
100+
/// session summaries (Task/swarm children persisted under their agent_id).
101+
#[derive(Debug, Deserialize)]
102+
pub struct SessionGetContextParams {
103+
pub session_id: String,
104+
#[serde(default)]
105+
pub include_subagents: Option<bool>,
106+
}
107+
98108
/// Input for session/fork.
99109
#[derive(Debug, Deserialize)]
100110
pub struct SessionForkParams {
@@ -472,6 +482,12 @@ pub struct SessionListParams {
472482
pub limit: Option<usize>,
473483
#[serde(default)]
474484
pub offset: Option<usize>,
485+
/// When true, subagent sessions (Task/swarm children persisted under
486+
/// their agent_id — see `run_child_agent_persistent_with_model`) are
487+
/// included in `sessions`. Default false: the list is main sessions
488+
/// only — subagent records are replay data, not user sessions.
489+
#[serde(default)]
490+
pub include_subagents: Option<bool>,
475491
}
476492

477493
/// A message in the conversation history.
@@ -801,6 +817,39 @@ pub struct SessionListResult {
801817
pub sessions: Vec<SessionSummaryRpc>,
802818
}
803819

820+
/// A subagent session summary returned by `session/get_context` when
821+
/// `include_subagents` is true. Task/swarm children persist their
822+
/// conversation in the same `sessions` table keyed by agent id, so
823+
/// `agent_id` IS the record's session id — the two are one value.
824+
#[derive(Debug, Serialize)]
825+
pub struct SubagentSummaryRpc {
826+
/// The child agent id — the session id the conversation was persisted
827+
/// under (`task_id` for tracked Task children, `swarm-<ts>-<rand>` for
828+
/// swarm children).
829+
pub agent_id: String,
830+
/// Display title. Subagent records carry no title of their own; hosts
831+
/// render the task description or agent id instead. Kept for symmetry
832+
/// with `SessionSummaryRpc`.
833+
#[serde(default)]
834+
pub title: String,
835+
/// Message count (context history length).
836+
pub message_count: usize,
837+
/// ISO-8601 last-write timestamp of the persisted record.
838+
pub updated_at: String,
839+
}
840+
841+
/// Result of `session/get_context` when `include_subagents` is requested:
842+
/// the context snapshot plus subagent session summaries. `subagents` is
843+
/// omitted when empty, keeping the wire identical to `AgentContextData`
844+
/// for callers that do not opt in.
845+
#[derive(Debug, Serialize)]
846+
pub struct SessionGetContextResult {
847+
pub history: Vec<crate::context::ContextMessage>,
848+
pub token_count: u64,
849+
#[serde(default, skip_serializing_if = "Vec::is_empty")]
850+
pub subagents: Vec<SubagentSummaryRpc>,
851+
}
852+
804853
/// A plugin summary returned by `plugin/list` (and embedded in `plugin/get`).
805854
#[derive(Debug, Serialize)]
806855
pub struct PluginSummaryRpc {

packages/kimi-agent/src/main.rs

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,17 +1243,53 @@ async fn main() -> anyhow::Result<()> {
12431243
RpcServer::register_arc(&server, types::methods::SESSION_GET_CONTEXT, move |params| {
12441244
let mgr = mgr.clone();
12451245
Box::pin(async move {
1246-
let input: types::SessionGoalParams = serde_json::from_value(params)
1246+
let input: types::SessionGetContextParams = serde_json::from_value(params)
12471247
.map_err(|e| types::JsonRpcError::internal_error(format!("Invalid params: {e}")))?;
12481248
let mut manager = mgr.lock().await;
1249-
let agent = manager.get_agent(&input.session_id).ok_or_else(|| {
1250-
types::JsonRpcError::internal_error(format!(
1251-
"no agent for session: {}",
1252-
input.session_id
1253-
))
1254-
})?;
1255-
serde_json::to_value(agent.context.data())
1256-
.map_err(|e| types::JsonRpcError::internal_error(format!("serialize context: {e}")))
1249+
let data = {
1250+
let agent = manager.get_agent(&input.session_id).ok_or_else(|| {
1251+
types::JsonRpcError::internal_error(format!(
1252+
"no agent for session: {}",
1253+
input.session_id
1254+
))
1255+
})?;
1256+
agent.context.data()
1257+
};
1258+
// Resume replay surface (include_subagents): Task/swarm children
1259+
// persist their conversations under their agent_id in the same
1260+
// store; summarize them when the host opts in. The page is
1261+
// effectively unbounded (100k) — subagent summaries are small
1262+
// and never user-facing.
1263+
let subagents = if input.include_subagents.unwrap_or(false) {
1264+
manager
1265+
.list_persisted(100_000, 0)
1266+
.map_err(|e| types::JsonRpcError::internal_error(e.to_string()))?
1267+
.into_iter()
1268+
.filter(|record| record.is_subagent())
1269+
.map(|record| {
1270+
let message_count = record
1271+
.state_json
1272+
.get("context")
1273+
.and_then(|v| v.as_array())
1274+
.map(|context| context.len())
1275+
.unwrap_or(0);
1276+
types::SubagentSummaryRpc {
1277+
agent_id: record.id,
1278+
title: String::new(),
1279+
message_count,
1280+
updated_at: record.updated_at,
1281+
}
1282+
})
1283+
.collect()
1284+
} else {
1285+
Vec::new()
1286+
};
1287+
serde_json::to_value(types::SessionGetContextResult {
1288+
history: data.history,
1289+
token_count: data.token_count,
1290+
subagents,
1291+
})
1292+
.map_err(|e| types::JsonRpcError::internal_error(format!("serialize context: {e}")))
12571293
})
12581294
});
12591295

@@ -1821,11 +1857,16 @@ async fn main() -> anyhow::Result<()> {
18211857
Box::pin(async move {
18221858
let input: types::SessionListParams =
18231859
serde_json::from_value(params).unwrap_or_default();
1860+
let include_subagents = input.include_subagents.unwrap_or(false);
18241861
let manager = mgr.lock().await;
18251862
let sessions = manager
18261863
.list_persisted(input.limit.unwrap_or(50), input.offset.unwrap_or(0))
18271864
.map_err(|e| types::JsonRpcError::internal_error(e.to_string()))?
18281865
.into_iter()
1866+
// Default: main sessions only. Subagent records (Task/swarm
1867+
// children persisted under their agent_id) are replay data,
1868+
// not user sessions — including them pollutes the list.
1869+
.filter(|record| include_subagents || !record.is_subagent())
18291870
.map(|record| {
18301871
// The rich session record (work_dir/title) lives inside
18311872
// state_json; degrade gracefully to the id-only shape.

packages/kimi-agent/src/persistence/session_store.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,25 @@ impl SessionStore {
115115
}
116116
}
117117

118+
impl SessionRecord {
119+
/// True when this record is a subagent session — a Task/swarm child
120+
/// persisted under its agent_id (`TaskInfoBase.agent_id`):
121+
///
122+
/// - main sessions serialize `state_json` from
123+
/// `crate::session::types::SessionRecord` (a valid shape), while
124+
/// - subagent children write `Agent::durable_state()` (context / goal /
125+
/// metadata …), which lacks the record's key fields and fails (or
126+
/// fails the shape check) on deserialization.
127+
///
128+
/// Unreadable or shape-invalid legacy records classify as subagent —
129+
/// they are not trustworthy main sessions either way.
130+
pub fn is_subagent(&self) -> bool {
131+
serde_json::from_value::<crate::session::types::SessionRecord>(self.state_json.clone())
132+
.map(|record| !record.is_valid_shape())
133+
.unwrap_or(true)
134+
}
135+
}
136+
118137
#[cfg(test)]
119138
mod tests {
120139
use super::*;
@@ -185,4 +204,63 @@ mod tests {
185204
store.delete_session("sess-1").unwrap();
186205
assert!(store.load_session("sess-1").unwrap().is_none());
187206
}
188-
}
207+
208+
#[test]
209+
fn test_is_subagent_classifies_records() {
210+
use crate::session::types::{ModelConfig, SessionRecord as MainSessionRecord};
211+
212+
let store = SessionStore::new(SqliteStore::in_memory().unwrap());
213+
214+
// Main-session shape: state_json is a serialized SessionRecord.
215+
let mut main = MainSessionRecord::new("sess-main", ModelConfig::default());
216+
main.title = "main".to_string();
217+
main.work_dir = "/work".to_string();
218+
main.agent_state = serde_json::json!({
219+
"goal": null,
220+
"context": [{"role": "user", "content": "hi"}],
221+
"metadata": {},
222+
});
223+
store
224+
.save_session(&SessionRecord {
225+
id: main.id.clone(),
226+
created_at: main.created_at.clone(),
227+
updated_at: main.updated_at.clone(),
228+
config_json: Value::Null,
229+
state_json: serde_json::to_value(&main).unwrap(),
230+
})
231+
.unwrap();
232+
233+
// Subagent shape: Agent::durable_state() — no SessionRecord key
234+
// fields (id/created_at/updated_at), so deserialization fails.
235+
store
236+
.save_session(&SessionRecord {
237+
id: "task-abc12345".into(),
238+
created_at: "2025-01-01T00:00:00Z".into(),
239+
updated_at: "2025-01-01T01:00:00Z".into(),
240+
config_json: Value::Null,
241+
state_json: serde_json::json!({
242+
"goal": null,
243+
"context": [
244+
{"role": "user", "content": "a"},
245+
{"role": "assistant", "content": "b"},
246+
],
247+
"turn_counter": 1,
248+
"token_count": 12,
249+
"metadata": {},
250+
}),
251+
})
252+
.unwrap();
253+
254+
let all = store.list_sessions(100, 0).unwrap();
255+
let main = all.iter().find(|r| r.id == "sess-main").unwrap();
256+
let sub = all.iter().find(|r| r.id == "task-abc12345").unwrap();
257+
assert!(
258+
!main.is_subagent(),
259+
"main-session record must not classify as subagent"
260+
);
261+
assert!(
262+
sub.is_subagent(),
263+
"durable-state record must classify as subagent"
264+
);
265+
}
266+
}

packages/kimi-agent/src/rpc/wire.gen.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ export interface SessionIdParams {
304304
session_id: string;
305305
}
306306

307+
export interface SessionGetContextParams {
308+
session_id: string;
309+
include_subagents?: boolean | undefined;
310+
}
311+
307312
export interface SessionForkParams {
308313
/** The source session to fork from. */
309314
session_id: string;
@@ -539,6 +544,7 @@ export interface SessionRenameParams {
539544
export interface SessionListParams {
540545
limit?: number | undefined;
541546
offset?: number | undefined;
547+
include_subagents?: boolean | undefined;
542548
}
543549

544550
export type ContentBlock =
@@ -724,6 +730,22 @@ export interface SessionListResult {
724730
sessions: Array<SessionSummaryRpc>;
725731
}
726732

733+
export interface SubagentSummaryRpc {
734+
/** The child agent id — the session id the conversation was persisted under (`task_id` for tracked Task children, `swarm-<ts>-<rand>` for swarm children). */
735+
agent_id: string;
736+
title?: string;
737+
/** Message count (context history length). */
738+
message_count: number;
739+
/** ISO-8601 last-write timestamp of the persisted record. */
740+
updated_at: string;
741+
}
742+
743+
export interface SessionGetContextResult {
744+
history: Array<ContextMessage>;
745+
token_count: number;
746+
subagents?: Array<SubagentSummaryRpc>;
747+
}
748+
727749
export interface PluginSummaryRpc {
728750
id: string;
729751
display_name: string;

0 commit comments

Comments
 (0)