补齐Agent Runtime状态观测

新增每个 Agent 的 runtime state 和 event JSONL 持久化

单 Agent 聊天和角色 brief 生成写入 task、session、run 状态

开发聊天页和项目 Agent 对话展示 runtime 状态与读取失败

排除 runtime 目录对 checkpoint、index 和 restore 的干扰

补充 Runtime V1 文档和回归测试
This commit is contained in:
AIGameCreator App
2026-07-09 22:36:18 +08:00
parent 3200e3b48c
commit 2a64aa8fe3
10 changed files with 1327 additions and 78 deletions
File diff suppressed because it is too large Load Diff
@@ -245,7 +245,11 @@ pub(crate) async fn chat_with_game_creator_role_agent(
) -> Result<GameCreatorChatAgentReply, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
chat_with_game_creator_role_agent_at(root, agent_id.trim(), prompt.trim()).await
enforce_project_permission_policy(root, "conversation.write")?;
let _lock = acquire_project_write_lock(root, "conversation.write")?;
chat_with_game_creator_role_agent_runtime_at(root, agent_id.trim(), prompt.trim(), "")
.await
.map(|(reply, _runtime)| reply)
}
#[tauri::command]
@@ -261,10 +265,23 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
let run_id = run_id.trim().to_string();
let root = Path::new(project_path.as_str());
enforce_project_permission_policy(root, "conversation.read")?;
enforce_project_permission_policy(root, "conversation.write")?;
let _lock = acquire_project_write_lock(root, "conversation.write")?;
let emit_app = app.clone();
let event_project_path = project_path.clone();
let event_agent_id = agent_id.clone();
let event_run_id = run_id.clone();
let mut runtime_state =
start_game_creator_agent_runtime_turn_at(root, agent_id.as_str(), prompt.trim(), &run_id)?;
runtime_state = advance_game_creator_agent_runtime_turn_at(
root,
runtime_state,
"llm",
"请求 Agent LLM",
"已读取项目上下文,正在让 Agent 独立推理。",
)?;
let mut streaming_runtime_state = runtime_state.clone();
streaming_runtime_state.current_action = "正在接收 Agent 回复".to_string();
let _ = app.emit(
"game-creator-role-agent-chat-stream",
GameCreatorRoleAgentChatStreamEvent {
@@ -275,6 +292,11 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
delta_text: String::new(),
accumulated_text: String::new(),
finish_reason: None,
session_id: Some(runtime_state.session_id.clone()),
runtime_status: Some(runtime_state.status.clone()),
runtime_phase: Some(runtime_state.phase.clone()),
runtime_summary: Some(runtime_state.current_action.clone()),
runtime_state: Some(runtime_state.clone()),
},
);
let result = chat_with_game_creator_role_agent_stream_at(
@@ -292,6 +314,11 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
delta_text: delta.delta_text.clone(),
accumulated_text: delta.accumulated_text.clone(),
finish_reason: delta.finish_reason.clone(),
session_id: None,
runtime_status: Some("running".to_string()),
runtime_phase: Some("llm".to_string()),
runtime_summary: Some("正在接收 Agent 回复".to_string()),
runtime_state: Some(streaming_runtime_state.clone()),
},
);
},
@@ -299,6 +326,8 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
.await;
match result {
Ok(reply) => {
let completed_runtime =
finish_game_creator_agent_runtime_turn_at(root, runtime_state, &reply.reply_text)?;
let _ = app.emit(
"game-creator-role-agent-chat-stream",
GameCreatorRoleAgentChatStreamEvent {
@@ -309,11 +338,18 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
delta_text: String::new(),
accumulated_text: reply.reply_text.clone(),
finish_reason: None,
session_id: Some(completed_runtime.session_id.clone()),
runtime_status: Some(completed_runtime.status.clone()),
runtime_phase: Some(completed_runtime.phase.clone()),
runtime_summary: Some(completed_runtime.current_action.clone()),
runtime_state: Some(completed_runtime),
},
);
Ok(reply)
}
Err(error) => {
let failed_runtime =
fail_game_creator_agent_runtime_turn_at(root, runtime_state, &error).ok();
let _ = app.emit(
"game-creator-role-agent-chat-stream",
GameCreatorRoleAgentChatStreamEvent {
@@ -324,6 +360,17 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
delta_text: String::new(),
accumulated_text: String::new(),
finish_reason: None,
session_id: failed_runtime
.as_ref()
.map(|runtime| runtime.session_id.clone()),
runtime_status: failed_runtime
.as_ref()
.map(|runtime| runtime.status.clone()),
runtime_phase: failed_runtime.as_ref().map(|runtime| runtime.phase.clone()),
runtime_summary: failed_runtime
.as_ref()
.map(|runtime| runtime.current_action.clone()),
runtime_state: failed_runtime,
},
);
Err(error)
@@ -331,6 +378,16 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream(
}
}
#[tauri::command]
pub(crate) fn read_game_creator_agent_runtime(
project_path: String,
agent_id: String,
) -> Result<AgentRuntimeResult, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "conversation.read")?;
read_game_creator_agent_runtime_at(root, agent_id.trim())
}
#[tauri::command]
pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus {
check_game_creator_llm_config_from_config()
@@ -125,6 +125,86 @@ struct GameCreatorRoleAgentChatStreamEvent {
delta_text: String,
accumulated_text: String,
finish_reason: Option<String>,
session_id: Option<String>,
runtime_status: Option<String>,
runtime_phase: Option<String>,
runtime_summary: Option<String>,
runtime_state: Option<AgentRuntimeState>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeState {
#[serde(default)]
schema_version: String,
#[serde(default)]
agent_id: String,
#[serde(default)]
task_id: String,
#[serde(default)]
session_id: String,
#[serde(default)]
run_id: String,
#[serde(default)]
source: String,
#[serde(default)]
status: String,
#[serde(default)]
phase: String,
#[serde(default)]
current_task: String,
#[serde(default)]
current_action: String,
#[serde(default)]
plan: Vec<String>,
#[serde(default)]
observations: Vec<String>,
#[serde(default)]
allowed_tools: Vec<String>,
#[serde(default)]
last_response: Option<String>,
#[serde(default)]
error: Option<String>,
#[serde(default)]
updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeEvent {
#[serde(default)]
schema_version: String,
#[serde(default)]
agent_id: String,
#[serde(default)]
task_id: String,
#[serde(default)]
session_id: String,
#[serde(default)]
run_id: String,
#[serde(default)]
source: String,
#[serde(default)]
event_type: String,
#[serde(default)]
status: String,
#[serde(default)]
phase: String,
#[serde(default)]
summary: String,
#[serde(default)]
detail: Option<String>,
#[serde(default)]
updated_at: u64,
}
#[derive(Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
struct AgentRuntimeResult {
state: AgentRuntimeState,
session_path: String,
event_path: String,
recent_events: Vec<AgentRuntimeEvent>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
@@ -531,6 +611,8 @@ const PROJECT_PERMISSION_POLICY_PATH: &str = ".agent/policy.json";
const PROJECT_INDEX_PATH: &str = ".agent/project.index.json";
const PROJECT_WRITE_LOCK_PATH: &str = ".agent/project.lock";
const LOCAL_CONVERSATION_SCHEMA_VERSION: &str = "game-creator-conversation.v1";
const AGENT_RUNTIME_SCHEMA_VERSION: &str = "game-creator-agent-runtime.v1";
const AGENT_RUNTIME_RECENT_EVENT_LIMIT: usize = 20;
const GAME_CREATOR_CONVERSATION_CONTEXT_MAX_MESSAGES: usize = 12;
const MAX_CANVAS_EXPORT_FILES: usize = 500;
const MAX_CANVAS_EXPORT_BYTES: u64 = 512 * 1024 * 1024;
@@ -941,6 +1023,7 @@ fn main() {
chat_with_game_creator_agent,
chat_with_game_creator_role_agent,
chat_with_game_creator_role_agent_stream,
read_game_creator_agent_runtime,
check_game_creator_llm_config,
read_game_creator_app_config,
write_game_creator_app_config,
@@ -1315,6 +1315,7 @@ pub(crate) fn should_skip_project_index_path(relative_path: &str) -> bool {
relative_path == PROJECT_WRITE_LOCK_PATH
|| relative_path == PROJECT_INDEX_PATH
|| relative_path.starts_with(".agent/checkpoints/")
|| relative_path.starts_with(".agent/runtime/")
}
pub(crate) fn should_skip_project_restore_path(relative_path: &str) -> bool {
@@ -1324,6 +1325,7 @@ pub(crate) fn should_skip_project_restore_path(relative_path: &str) -> bool {
|| relative_path == PROJECT_INDEX_PATH
|| relative_path.starts_with(".agent/logs/")
|| relative_path.starts_with(".agent/conversations/")
|| relative_path.starts_with(".agent/runtime/")
}
pub(crate) fn normalize_checkpoint_id(checkpoint_id: &str) -> Result<String, String> {
@@ -1150,6 +1150,112 @@ async fn chat_with_game_creator_role_agent_stream_emits_deltas() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn role_agent_runtime_turn_persists_session_events_and_index() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
let base_url = spawn_mock_llm_server("先确认角色规范,再推进首张主角图。".to_string());
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"art-director": {{
"apiKey": "art-key",
"baseUrl": {base_url:?},
"model": "art-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
let (reply, runtime) = chat_with_game_creator_role_agent_runtime_at(
&root,
"art-director",
"我要生成主角图",
"runtime-test-run",
)
.await
.expect("runtime role chat");
assert_eq!(reply.reply_text, "先确认角色规范,再推进首张主角图。");
assert_eq!(runtime.status, "idle");
assert_eq!(runtime.phase, "completed");
assert_eq!(runtime.run_id, "runtime-test-run");
assert_eq!(runtime.session_id, "agent-session-art-director");
assert_eq!(runtime.task_id, "art-director");
assert_eq!(runtime.source, "agent-chat");
assert!(runtime
.allowed_tools
.contains(&"conversation.read".to_string()));
let result =
read_game_creator_agent_runtime_at(&root, "art-director").expect("read runtime state");
assert!(result
.session_path
.ends_with(".agent/runtime/agents/art-director.json"));
assert!(result
.event_path
.ends_with(".agent/runtime/events/art-director.jsonl"));
assert_eq!(
result.state.last_response.as_deref(),
Some("先确认角色规范,再推进首张主角图。")
);
let event_types = result
.recent_events
.iter()
.map(|event| event.event_type.as_str())
.collect::<Vec<_>>();
assert!(event_types.contains(&"turn.started"));
assert!(event_types.contains(&"turn.progress"));
assert!(event_types.contains(&"turn.completed"));
fs::OpenOptions::new()
.append(true)
.open(root.join(".agent/runtime/events/art-director.jsonl"))
.expect("open runtime events")
.write_all(b"{broken runtime event\n")
.expect("append broken runtime event");
let result_with_broken_event =
read_game_creator_agent_runtime_at(&root, "art-director").expect("read runtime state");
assert!(result_with_broken_event
.recent_events
.iter()
.any(|event| event.event_type == "turn.completed"));
let sensitive_runtime = start_game_creator_agent_runtime_task_at(
&root,
"art-director",
"token sk-runtime-secret\nAuthorization: Bearer secret-token",
"runtime-sensitive-run",
"agent-chat",
"读取敏感输入",
vec!["测试 runtime 脱敏".to_string()],
)
.expect("sensitive runtime start");
finish_game_creator_agent_runtime_turn_at(
&root,
sensitive_runtime,
"回复里包含 sk-runtime-reply 和 Authorization: Bearer reply-token",
)
.expect("sensitive runtime finish");
let sensitive_state =
read_game_creator_agent_runtime_at(&root, "art-director").expect("read sensitive runtime");
let serialized_state =
serde_json::to_string(&sensitive_state.state).expect("runtime state json");
assert!(!serialized_state.contains("sk-runtime-secret"));
assert!(!serialized_state.contains("secret-token"));
assert!(!serialized_state.contains("sk-runtime-reply"));
assert!(!serialized_state.contains("reply-token"));
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(agent_db.contains("\"recordType\":\"agent.runtime.turn\""));
assert!(agent_db.contains("\"recordType\":\"agent.runtime.completed\""));
assert!(agent_db.contains("\"taskId\":\"art-director\""));
assert!(!agent_db.contains("art-key"));
assert!(!agent_db.contains("sk-runtime-secret"));
assert!(!agent_db.contains("secret-token"));
assert!(!agent_db.contains("sk-runtime-reply"));
assert!(!agent_db.contains("reply-token"));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn agent_loop_uses_per_agent_llm_overrides() {
let root = unique_project_path();
@@ -1312,6 +1418,7 @@ async fn agent_role_briefs_run_same_wave_llm_agents_in_parallel() {
"Planner 规格",
"Evaluator 反馈",
&agenda,
"role-brief-test-run",
1,
)
.await
@@ -1328,6 +1435,19 @@ async fn agent_role_briefs_run_same_wave_llm_agents_in_parallel() {
assert!(captured_requests
.iter()
.any(|request| request.contains("design-foundation-model")));
let director_runtime =
read_game_creator_agent_runtime_at(&root, "design-director").expect("director runtime");
assert_eq!(director_runtime.state.status, "idle");
assert_eq!(director_runtime.state.phase, "completed");
assert_eq!(director_runtime.state.source, "generate-draft");
assert_eq!(director_runtime.state.run_id, "role-brief-test-run");
assert!(director_runtime
.recent_events
.iter()
.any(|event| event.event_type == "turn.completed"));
let foundation_runtime =
read_game_creator_agent_runtime_at(&root, "design-foundation").expect("foundation runtime");
assert_eq!(foundation_runtime.state.source, "generate-draft");
let design_group = briefs
.iter()
.find(|brief| brief.definition.id == "design")
@@ -2137,8 +2257,12 @@ async fn generate_local_game_draft_fails_after_max_passes_without_final_artifact
.lines()
.map(|line| serde_json::from_str::<Value>(line).expect("agent db record"))
.collect::<Vec<_>>();
assert_eq!(records.len(), 1);
assert_eq!(records[0]["recordType"], "project.init");
assert!(records
.iter()
.any(|record| record["recordType"] == "project.init"));
assert!(!records
.iter()
.any(|record| record["recordType"] == "game.generate_draft"));
fs::remove_dir_all(root).ok();
}
@@ -3842,6 +3966,12 @@ fn local_project_checkpoint_diff_restore_and_index_are_recorded() {
write_local_project_file_at(&root, "game/notes.txt", "v2").expect("change notes");
write_local_project_file_at(&root, "game/extra.txt", "new").expect("add file");
fs::create_dir_all(root.join(".agent/runtime/agents")).expect("runtime agent dir");
fs::write(
root.join(".agent/runtime/agents/design-director.json"),
r#"{"agentId":"design-director","status":"running"}"#,
)
.expect("runtime state");
delete_local_project_file_at(&root, "exports/README.md").expect("delete tracked file");
let diff = diff_local_project_checkpoint_at(&root, &checkpoint.checkpoint_id).expect("diff");
@@ -3857,9 +3987,17 @@ fn local_project_checkpoint_diff_restore_and_index_are_recorded() {
.deleted
.iter()
.any(|entry| entry.path == "exports/README.md"));
assert!(!diff
.added
.iter()
.any(|entry| entry.path.starts_with(".agent/runtime/")));
let index = build_local_project_index_at(&root).expect("project index");
assert!(index.file_count > 0);
assert!(!index
.files
.iter()
.any(|file| file.path.starts_with(".agent/runtime/")));
assert!(root.join(PROJECT_INDEX_PATH).exists());
let restored =
@@ -3871,6 +4009,9 @@ fn local_project_checkpoint_diff_restore_and_index_are_recorded() {
);
assert!(root.join("exports/README.md").exists());
assert!(!root.join("game/extra.txt").exists());
assert!(root
.join(".agent/runtime/agents/design-director.json")
.exists());
assert_eq!(restored.deleted_count, 1);
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
+235 -17
View File
@@ -220,6 +220,52 @@ interface GameCreatorRoleAgentChatStreamEvent {
deltaText: string;
accumulatedText: string;
finishReason: string | null;
sessionId?: string | null;
runtimeStatus?: string | null;
runtimePhase?: string | null;
runtimeSummary?: string | null;
runtimeState?: AgentRuntimeState | null;
}
interface AgentRuntimeState {
schemaVersion: string;
agentId: string;
taskId: string;
sessionId: string;
runId: string;
source: string;
status: string;
phase: string;
currentTask: string;
currentAction: string;
plan: string[];
observations: string[];
allowedTools: string[];
lastResponse: string | null;
error: string | null;
updatedAt: number;
}
interface AgentRuntimeEventRecord {
schemaVersion: string;
agentId: string;
taskId: string;
sessionId: string;
runId: string;
source: string;
eventType: string;
status: string;
phase: string;
summary: string;
detail: string | null;
updatedAt: number;
}
interface AgentRuntimeResult {
state: AgentRuntimeState;
sessionPath: string;
eventPath: string;
recentEvents: AgentRuntimeEventRecord[];
}
interface GameCreatorLlmConfigStatus {
@@ -428,6 +474,58 @@ function createAgentChatRunId(prefix: string) {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
function AgentRuntimeStatusPanel({
runtime,
error,
}: {
runtime: AgentRuntimeState | null;
error?: string | null;
}) {
if (!runtime && error) {
return (
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
<header>
<strong>Runtime </strong>
</header>
<p>{error}</p>
</section>
);
}
if (!runtime) {
return null;
}
const planItems = runtime.plan.slice(0, 3);
const observations = runtime.observations.slice(-2);
return (
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
<header>
<strong>{`${runtime.status} / ${runtime.phase}`}</strong>
<small>{runtime.sessionId}</small>
</header>
<small>{`task: ${runtime.taskId} · ${runtime.source}`}</small>
{runtime.runId ? <small>{`run: ${runtime.runId}`}</small> : null}
{runtime.currentTask ? <p>{runtime.currentTask}</p> : null}
<small>{runtime.currentAction}</small>
{planItems.length > 0 ? (
<ol>
{planItems.map((item, index) => (
<li key={`${runtime.sessionId}-plan-${index}`}>{item}</li>
))}
</ol>
) : null}
{observations.length > 0 ? (
<div>
{observations.map((item, index) => (
<small key={`${runtime.sessionId}-observation-${index}`}>{item}</small>
))}
</div>
) : null}
{runtime.error ? <p>{runtime.error}</p> : null}
{error ? <p>{error}</p> : null}
</section>
);
}
interface ProjectPermissionPolicy {
deniedCommands: string[];
confirmCommands: string[];
@@ -2220,6 +2318,9 @@ export function WorkspaceLauncher({
useState<GameCreatorLlmConfigStatus | null>(null);
const [agentChatLlmStatus, setAgentChatLlmStatus] =
useState('尚未检查 LLM 配置');
const [agentChatRuntime, setAgentChatRuntime] =
useState<AgentRuntimeState | null>(null);
const [agentChatRuntimeError, setAgentChatRuntimeError] = useState('');
const agentChatLoadVersionRef = useRef(0);
useEffect(() => {
@@ -2921,6 +3022,8 @@ export function WorkspaceLauncher({
const loadVersion = agentChatLoadVersionRef.current + 1;
agentChatLoadVersionRef.current = loadVersion;
setAgentChatBusy(true);
setAgentChatRuntime(null);
setAgentChatRuntimeError('');
setAgentChatStatus('正在读取');
try {
const result = await invoke<LocalConversationResult>(
@@ -2934,6 +3037,26 @@ export function WorkspaceLauncher({
return;
}
setAgentChatMessages(result.messages);
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: projectPathForChat,
agentId: agent.id,
},
);
if (agentChatLoadVersionRef.current === loadVersion) {
setAgentChatRuntime(runtime.state);
setAgentChatRuntimeError('');
}
} catch (error) {
if (agentChatLoadVersionRef.current === loadVersion) {
setAgentChatRuntime(null);
setAgentChatRuntimeError(
error instanceof Error ? error.message : String(error),
);
}
}
setAgentChatStatus(`已读取 ${result.messages.length} 条:${result.path}`);
} catch (error) {
if (agentChatLoadVersionRef.current !== loadVersion) {
@@ -3009,8 +3132,14 @@ export function WorkspaceLauncher({
) {
return;
}
if (payload.runtimeState) {
setAgentChatRuntime(payload.runtimeState);
setAgentChatRuntimeError('');
}
if (payload.status === 'started') {
setAgentChatStatus('Agent 已连接,正在等待回复');
setAgentChatStatus(
payload.runtimeSummary ?? 'Agent 已连接,正在等待回复',
);
return;
}
if (payload.status === 'delta') {
@@ -3029,11 +3158,13 @@ export function WorkspaceLauncher({
return;
}
if (payload.status === 'completed') {
setAgentChatStatus('Agent 回复完成,正在保存');
setAgentChatStatus(payload.runtimeSummary ?? 'Agent 回复完成,正在保存');
return;
}
if (payload.status === 'failed') {
setAgentChatStatus('Agent 流式回复失败,正在记录错误');
setAgentChatStatus(
payload.runtimeSummary ?? 'Agent 流式回复失败,正在记录错误',
);
}
},
);
@@ -3063,6 +3194,25 @@ export function WorkspaceLauncher({
if (agentChatLoadVersionRef.current !== saveVersion) {
return;
}
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: projectPathForChat,
agentId: agent.id,
},
);
if (agentChatLoadVersionRef.current === saveVersion) {
setAgentChatRuntime(runtime.state);
setAgentChatRuntimeError('');
}
} catch (error) {
if (agentChatLoadVersionRef.current === saveVersion) {
setAgentChatRuntimeError(
error instanceof Error ? error.message : String(error),
);
}
}
setAgentChatStatus('正在保存 Agent 回复');
setAgentChatMessages([
...savedUserMessages,
@@ -3790,17 +3940,23 @@ export function WorkspaceLauncher({
: '请选择 Agent'}
</small>
</header>
{currentAgentChatLlmWarning ? (
<div className="agent-llm-warning" role="status">
<strong>{currentAgentChatLlmWarning}</strong>
<button
type="button"
onClick={() => setRuntimeConfigOpen(true)}
>
</button>
</div>
) : null}
<section className="launcher-agent-runtime-stack">
{currentAgentChatLlmWarning ? (
<div className="agent-llm-warning" role="status">
<strong>{currentAgentChatLlmWarning}</strong>
<button
type="button"
onClick={() => setRuntimeConfigOpen(true)}
>
</button>
</div>
) : null}
<AgentRuntimeStatusPanel
runtime={agentChatRuntime}
error={agentChatRuntimeError}
/>
</section>
<div className="launcher-agent-chat-messages" aria-label="Agent 聊天记录">
{agentChatMessages.length > 0 ? (
agentChatMessages.map((message, index) => (
@@ -11157,6 +11313,10 @@ export function App() {
const [agentConversationMessages, setAgentConversationMessages] = useState<
LocalConversationMessageRecord[]
>([]);
const [agentConversationRuntime, setAgentConversationRuntime] =
useState<AgentRuntimeState | null>(null);
const [agentConversationRuntimeError, setAgentConversationRuntimeError] =
useState('');
const [agentConversationVisibleCount, setAgentConversationVisibleCount] =
useState(CONVERSATION_INITIAL_VISIBLE_COUNT);
const [agentConversationSaving, setAgentConversationSaving] = useState(false);
@@ -12042,6 +12202,8 @@ export function App() {
setSelectedAgent(null);
setAgentConversationInput('');
setAgentConversationMessages([]);
setAgentConversationRuntime(null);
setAgentConversationRuntimeError('');
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setAgentConversationStatus('未选择 agent');
setAgentConversationSaving(false);
@@ -12131,6 +12293,8 @@ export function App() {
setSelectedAgent(agent);
setAgentConversationInput('');
setAgentConversationMessages([]);
setAgentConversationRuntime(null);
setAgentConversationRuntimeError('');
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setAgentMemoryContent('');
const invoke = resolveTauriInvoke();
@@ -12187,6 +12351,26 @@ export function App() {
return;
}
setAgentConversationMessages(result.messages);
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current === loadVersion) {
setAgentConversationRuntime(runtime.state);
setAgentConversationRuntimeError('');
}
} catch (error) {
if (agentConversationLoadVersionRef.current === loadVersion) {
setAgentConversationRuntime(null);
setAgentConversationRuntimeError(
error instanceof Error ? error.message : String(error),
);
}
}
setAgentConversationStatus(
`已读取 ${result.messages.length} 条:${result.path}`,
);
@@ -12257,6 +12441,7 @@ export function App() {
setSelectedAgent(null);
setAgentConversationInput('');
setAgentConversationMessages([]);
setAgentConversationRuntime(null);
setAgentConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT);
setAgentConversationStatus('未选择 agent');
setAgentConversationSaving(false);
@@ -12417,8 +12602,14 @@ export function App() {
) {
return;
}
if (payload.runtimeState) {
setAgentConversationRuntime(payload.runtimeState);
setAgentConversationRuntimeError('');
}
if (payload.status === 'started') {
setAgentConversationStatus('Agent 已连接,正在等待回复');
setAgentConversationStatus(
payload.runtimeSummary ?? 'Agent 已连接,正在等待回复',
);
return;
}
if (payload.status === 'delta') {
@@ -12437,11 +12628,15 @@ export function App() {
return;
}
if (payload.status === 'completed') {
setAgentConversationStatus('Agent 回复完成,正在保存');
setAgentConversationStatus(
payload.runtimeSummary ?? 'Agent 回复完成,正在保存',
);
return;
}
if (payload.status === 'failed') {
setAgentConversationStatus('Agent 流式回复失败,正在记录错误');
setAgentConversationStatus(
payload.runtimeSummary ?? 'Agent 流式回复失败,正在记录错误',
);
}
},
);
@@ -12471,6 +12666,25 @@ export function App() {
if (agentConversationLoadVersionRef.current !== saveVersion) {
return;
}
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: nextProjectPath,
agentId: agent.id,
},
);
if (agentConversationLoadVersionRef.current === saveVersion) {
setAgentConversationRuntime(runtime.state);
setAgentConversationRuntimeError('');
}
} catch (error) {
if (agentConversationLoadVersionRef.current === saveVersion) {
setAgentConversationRuntimeError(
error instanceof Error ? error.message : String(error),
);
}
}
setAgentConversationStatus('正在保存 Agent 回复');
setAgentConversationMessages([
...savedUserMessages,
@@ -19643,6 +19857,10 @@ export function App() {
</button>
</div>
) : null}
<AgentRuntimeStatusPanel
runtime={agentConversationRuntime}
error={agentConversationRuntimeError}
/>
<div
className="agent-conversation-list"
onScroll={handleAgentConversationScroll}
+63
View File
@@ -1201,6 +1201,19 @@ textarea {
color: #fff;
}
.launcher-agent-runtime-stack {
display: grid;
gap: 8px;
}
.launcher-agent-runtime-stack:empty {
display: none;
}
.launcher-agent-runtime-stack .agent-runtime-status {
margin-top: 0;
}
.agent-llm-warning {
display: flex;
align-items: center;
@@ -1231,6 +1244,56 @@ textarea {
color: #8a3b12;
}
.agent-runtime-status {
display: grid;
gap: 6px;
margin: 12px 14px 0;
padding: 10px 12px;
border: 1px solid #d8dde5;
border-radius: 8px;
background: #f8fafd;
color: #111827;
}
.agent-conversation-panel > .agent-runtime-status {
margin: 0;
}
.agent-runtime-status header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.agent-runtime-status strong {
font-size: 13px;
}
.agent-runtime-status p,
.agent-runtime-status ol {
margin: 0;
}
.agent-runtime-status p,
.agent-runtime-status li,
.agent-runtime-status small {
color: #647084;
font-size: 12px;
overflow-wrap: anywhere;
}
.agent-runtime-status ol {
display: grid;
gap: 3px;
padding-left: 18px;
}
.agent-runtime-status div {
display: grid;
gap: 3px;
}
.launcher-project-development {
padding-top: 92px;
}
@@ -1069,6 +1069,36 @@ describe('AI 游戏创作 App 界面边界', () => {
let releaseStream: (() => void) | null = null;
let streamHandler: ((event: { payload: Record<string, unknown> }) => void) | null =
null;
const startedRuntimeState = {
schemaVersion: 'game-creator-agent-runtime.v1',
agentId: 'design-director',
taskId: 'design-director',
sessionId: 'agent-session-design-director',
runId: 'launcher-agent-chat-test',
source: 'agent-chat',
status: 'running',
phase: 'llm',
currentTask: '请流式回答',
currentAction: '请求 Agent LLM',
plan: ['读取项目上下文', '按角色职责推理', '流式回复'],
observations: ['已创建本轮 Agent Runtime run。'],
allowedTools: ['conversation.read', 'conversation.write'],
lastResponse: null,
error: null,
updatedAt: 3000,
};
const completedRuntimeState = {
...startedRuntimeState,
status: 'idle',
phase: 'completed',
currentAction: '等待下一轮输入',
lastResponse: '专业 Agent 已流式完成。',
observations: [
'已创建本轮 Agent Runtime run。',
'Agent 已完成回复,assistant 消息等待或已经由前端落盘。',
],
updatedAt: 3001,
};
const listen = vi.fn(
async (
eventName: string,
@@ -1120,6 +1150,16 @@ describe('AI 游戏创作 App 界面边界', () => {
})),
};
}
if (command === 'read_game_creator_agent_runtime') {
return {
state: completedRuntimeState,
sessionPath:
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
eventPath:
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
recentEvents: [],
};
}
if (command === 'chat_with_game_creator_role_agent_stream') {
const payloadBase = {
projectPath: args?.projectPath,
@@ -1133,6 +1173,8 @@ describe('AI 游戏创作 App 界面边界', () => {
status: 'started',
deltaText: '',
accumulatedText: '',
runtimeSummary: '请求 Agent LLM',
runtimeState: startedRuntimeState,
},
});
streamHandler?.({
@@ -1160,6 +1202,8 @@ describe('AI 游戏创作 App 界面边界', () => {
status: 'completed',
deltaText: '',
accumulatedText: '专业 Agent 已流式完成。',
runtimeSummary: '等待下一轮输入',
runtimeState: completedRuntimeState,
},
});
return {
@@ -1201,6 +1245,10 @@ describe('AI 游戏创作 App 界面边界', () => {
fireEvent.click(screen.getByRole('button', { name: '发送' }));
expect(await screen.findByText('专业 Agent')).not.toBeNull();
expect(await screen.findByLabelText('Agent Runtime 状态')).not.toBeNull();
expect(screen.getByText('running / llm')).not.toBeNull();
expect(screen.getByText('agent-session-design-director')).not.toBeNull();
expect(screen.getAllByText('请求 Agent LLM').length).toBeGreaterThan(0);
expect(screen.getByText('正在接收 Agent 回复')).not.toBeNull();
expect(invoke).toHaveBeenCalledWith(
'chat_with_game_creator_role_agent_stream',
@@ -1219,6 +1267,8 @@ describe('AI 游戏创作 App 界面边界', () => {
});
expect(await screen.findByText('专业 Agent 已流式完成。')).not.toBeNull();
expect(await screen.findByText('idle / completed')).not.toBeNull();
expect(screen.getAllByText('等待下一轮输入').length).toBeGreaterThan(0);
expect(await screen.findByText(/已保存 2 条/)).not.toBeNull();
expect(persistedMessages).toEqual([
{
@@ -1234,6 +1284,59 @@ describe('AI 游戏创作 App 界面边界', () => {
]);
});
it('shows developer agent runtime read failures', async () => {
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'check_game_creator_llm_config') {
return {
configured: true,
apiKeyPresent: true,
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-5.5',
apiKind: 'openai_chat',
stream: true,
error: null,
agents: [
{
agentId: 'design-director',
label: '拆解创作方向',
configured: true,
apiKeyPresent: true,
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-5.5',
apiKind: 'openai_chat',
stream: true,
error: null,
},
],
};
}
if (command === 'read_local_conversation') {
return {
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
agentId: args?.agentId,
messages: [],
};
}
if (command === 'read_game_creator_agent_runtime') {
throw new Error('runtime json broken');
}
throw new Error(`unexpected invoke ${command}`);
},
);
window.__TAURI__ = { core: { invoke } };
renderLauncherAgentChatAt('/?agent-chat');
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
target: { value: '/tmp/authorized-game' },
});
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
expect(await screen.findByText(/已读取 0 条/)).not.toBeNull();
expect(await screen.findByText('Runtime 状态读取失败')).not.toBeNull();
expect(screen.getByText('runtime json broken')).not.toBeNull();
});
it('persists developer agent chat reply failures after saving the user message', async () => {
const persistedMessages: Array<{
role: 'user' | 'assistant';
@@ -4041,6 +4041,7 @@
- 2026-06-24 调整:同一本地项目多次 `game.generate_draft` 必须追加 `memory/session.md``memory/project.md`,不得覆盖历史对话和创作目标记录。
- 2026-07-01 调整:AI 游戏创作 App 在 `memory/session.md``memory/project.md` 之外新增项目级黑板 `memory/blackboard.md`,只记录重要跨 agent 决策、依赖和风险摘要;每个角色 agent 拥有私有记忆 `memory/agents/<group>/<role>.md`。角色 brief 必须读取自己的私有记忆和项目黑板;`game.generate_draft` 通过 Evaluator 与 `game.static_smoke` 后,追加项目黑板摘要和各角色成功产出摘要,不得覆盖既有记忆。失败 run 仍只保留 trace 和 pass 快照,不写最终记忆摘要。
- 2026-07-06 调整:AI 游戏创作 App 主聊天普通文本改为进入主聊天 Agent,而不是直接排队 `game.generate_draft`;主聊天 Agent 读取短期记忆、长期记忆、项目黑板、最近项目对话和本地资产摘要作为背景,支持 `agentLlm.chat` 单独 provider 配置,但只做自然语言交互、澄清和 slash 命令建议,不写项目、不运行工具、不伪装生成结果。显式 `/generate <创作想法>``/draft <创作想法>` 才进入 `game.generate_draft` 待确认流。
- 2026-07-09 调整:AI 游戏创作 App 新增 Agent Runtime V1 最小可观测状态。单 Agent 对话和生成 loop 中的角色 brief 必须写 `.agent/runtime/agents/<agentId>.json``.agent/runtime/events/<agentId>.jsonl`,记录 `agentId``taskId``sessionId``runId``source``status``phase`、当前任务 / 动作、计划、观测、允许工具、最近回复和错误;单 Agent 流式聊天事件要回传最新 `runtimeState`,开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示该状态,读取失败必须可见提示,不得静默伪装为空状态。`source=agent-chat` 表示开发者单 Agent 对话,`source=generate-draft` 表示生成 loop 角色 briefcarry-over brief 只记录继承和完成,不伪装成重新调用 LLM。Runtime state 写入使用临时文件替换,event JSONL 读取跳过坏行,用户 prompt / 回复摘要进入 runtime 与 `agent.db` 前复用敏感上下文过滤;`.agent/runtime/` 是运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。该层仍是本地 JSONL 状态与事件,不引入 SQLite、常驻独立进程、远程 runner 或可中断上游 LLM 的承诺。
- 2026-07-01 调整:AI 游戏创作 App 借鉴 Godcoder 的本地工程护栏,但只收敛到五项本地机制:`ArtifactWriter` 写入前 checkpoint、写入后 diff、用户确认 restore;进入 LLM 前过滤密钥和本机配置痕迹;`.agent/agent.db` 继续作为轻量 JSONL 项目索引,`/index` 额外刷新 `.agent/project.index.json`;同一项目写入通过 `.agent/project.lock` 串行化;`.agent/policy.json` 记录项目级命令拒绝 / 确认策略。v1 不引入通用 IDE 插件、云工作区、SQLite 或任意 shell 代理。
- 2026-07-03 调整:主窗口最近 checkpoint 列表必须直接展示 checkpoint id、文件数、大小和创建时间,并提供直接对比、填入 `/diff`、确认回滚和填入 `/restore` 的轻量操作;回滚仍走 `project.restore` 确认卡,不在列表按钮中直接写项目文件。
- 2026-06-24 调整:普通用户通过聊天输入 `/help` 发现可用内置命令;命令发现必须留在聊天消息里,不得因此暴露开发面板。
@@ -31,7 +31,7 @@ Agent Runtime 负责:
- 用户能力:聊天入口、上传文件;正式用户窗口不展示任务、文件、预览、日志、能力清单或开发专用单 Agent 聊天入口。
- 开发窗口能力:debug 构建额外打开 `developer` 窗口,走 `index.html?agent-chat`;开发者可选择 Agent、授权本地项目路径,并通过 `read_local_conversation` / `append_local_conversation_message` 读写 `.agent/conversations/agents/<agentId>.jsonl`,通过 `agentLlm.<agentId>` 调用该 Agent 的独立 LLM 路由做真实对话,用于单独调试某个 Agent 的长期对话上下文。
- 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。
- 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作。
- 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作Runtime V1 会为单 Agent 对话和生成 loop 中的角色 brief 写入独立 runtime state / event,先解决“每个 Agent 正在做什么、跑到哪一步、最近一次 task/run 是什么”的可观测性
- 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。
- 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、项目级黑板 `memory/blackboard.md` 和角色私有记忆 `memory/agents/<group>/<role>.md`;黑板用于共享重要跨 agent 记忆,角色私有记忆只给对应角色 brief 读取和追加。最近 project / agent conversation 会作为短期 prompt 上下文读取,不替代正式 memory 文件。
- 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;普通聊天写 `.agent/conversations/project.jsonl`,进入单个 agent 后只写对应 `.agent/conversations/agents/<agentId>.jsonl`,不把原始对话混进项目黑板或角色私有记忆。
@@ -66,6 +66,11 @@ game-project/
project.jsonl
agents/
<agentId>.jsonl
runtime/
agents/
<agentId>.json
events/
<agentId>.jsonl
activity.jsonl
output.jsonl
context.bundle.json
@@ -264,10 +269,11 @@ game-project/
- loop 每次运行会写 `.agent/run.latest.json``.agent/runs/<runId>.json`,记录 `Planner` / `Orchestrator` agenda / 16 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` 质量评审 / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/conversations/project.jsonl``.agent/conversations/agents/``.agent/manifest.json` 和 agenda 等上下文来源,其中角色 agent 必须包含自己的 `memory/agents/<group>/<role>.md``memory/blackboard.md`conversation 输入只取最近少量 project / agent 对话摘要,不读取全量历史;每个 step 必须带 phase、taskId、group 和 role`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 按文件修改时间先载入最近 20 个历史 run,滚动时再按批次读取剩余历史,普通用户窗口不展示。
- `.agent/run.latest.json``artifacts` 使用结构化记录,包含相对路径、字节数和 `fnv1a64:` checksum;除最终本地产物外,也会收集 `.agent/passes/pass-N/` 快照,便于确认返工前后的产物差异。
- 通过 Evaluator 和 `game.static_smoke` 后,Agent loop 会把本次 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md``memory/project.md`,把重要跨 agent 决策 / 依赖 / 风险摘要追加到 `memory/blackboard.md`,并把各角色本轮成功产出的角色摘要追加到 `memory/agents/<group>/<role>.md`;下一次 Planner、组内角色和 Generator 会通过记忆输入自然读取上一轮稳定原型状态,而不只依赖开发窗口 trace。
- 单 agent 对话入口读取对应 agent conversation;用户提交后先追加用户消息,再调用 `chat_with_game_creator_role_agent` 让对应 `agentLlm.<agentId>` 结合项目上下文、Agent 私有记忆和本 Agent 历史对话生成回复,随后把回复写入对应 `.agent/conversations/agents/<agentId>.jsonl`。单 agent 面板可把当前输入手动追加到对应 `memory/agents/<group>/<role>.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md``memory/agents/<group>/<role>.md`
- 单 agent 对话入口读取对应 agent conversation;用户提交后先追加用户消息,再调用 `chat_with_game_creator_role_agent` / `chat_with_game_creator_role_agent_stream` 让对应 `agentLlm.<agentId>` 结合项目上下文、Agent 私有记忆和本 Agent 历史对话生成回复,随后把回复写入对应 `.agent/conversations/agents/<agentId>.jsonl`每轮对话会同步写 `.agent/runtime/agents/<agentId>.json``.agent/runtime/events/<agentId>.jsonl`,字段包含 `agentId``taskId``sessionId``runId``source``status``phase``currentTask``currentAction``plan``observations``allowedTools``lastResponse``error`;流式事件会把最新 `runtimeState` 回传给界面。Runtime state 写入使用临时文件替换,event JSONL 读取会跳过坏行;`currentTask`、event detail、`lastResponse``agent.db` 摘要复用敏感上下文过滤,不保存明显 API Key / Bearer / Cookie 片段。单 agent 面板可把当前输入手动追加到对应 `memory/agents/<group>/<role>.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md``memory/agents/<group>/<role>.md`
- 生成 loop 中的角色 brief 也写同一套 Agent Runtime state / eventactive 角色用 `source=generate-draft` 和当前 `runId` 标记正在读取上下文、调用角色专属 LLM 或本地编排、生成 brief、完成或失败;carry-over 角色同样写入开始 / 完成事件,但不会伪装成重新调用 LLM。开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示当前 Agent 的 runtime 状态、最近 task/run、阶段、动作、计划和观测;这只是 V1 可观测性,不代表已经有独立后台常驻进程或可中断任意上游 LLM 请求。
- `.agent/agent.db` 当前作为最小本地项目索引文件使用 JSONL:初始化写入 `project.init`,每次 `game.generate_draft` 追加目标、标题、本地产物路径、checkpoint 和 diff 摘要,上传 / 登记 / 画板导入资产时追加 `asset.register``asset.update`v1 不引入 SQLite 依赖。
- `game.generate_draft`、资产登记 / 导入、记忆写入、预览状态写入、checkpoint / restore、agent 生命周期控制、画板资源回流 / 生成和 policy 写入会先按 `.agent/policy.json` 判断本次命令是否被项目策略拒绝,再拿项目级 `.agent/project.lock` 串行化;锁只保护同一本地项目,v1 不做后台锁管理。`confirmCommands` 可把索引、状态读取、资产登记、checkpoint、预览、agent 生命周期、画板资源回流 / 生成、memory 读写删除和 conversation 读写等命令转成项目策略确认,命中时用户确认后才执行;用户可用 `/policy-confirm 命令` 加入确认列表,用 `/policy-auto 命令` 移除确认项。
- `ArtifactWriter` 写入最终产物前把当前项目文件保存到 `.agent/checkpoints/<checkpointId>/`,写入后把新增、修改、删除计数记录到 `.agent/agent.db`;聊天命令 `/checkpoint``/checkpoints``/diff checkpoint-id``/restore checkpoint-id` 允许用户手动保存、列出最近 checkpoint、对比和确认回滚到 checkpoint,回滚时会删除 checkpoint 后新增的受跟踪项目文件。
- `ArtifactWriter` 写入最终产物前把当前项目文件保存到 `.agent/checkpoints/<checkpointId>/`,写入后把新增、修改、删除计数记录到 `.agent/agent.db`;聊天命令 `/checkpoint``/checkpoints``/diff checkpoint-id``/restore checkpoint-id` 允许用户手动保存、列出最近 checkpoint、对比和确认回滚到 checkpoint,回滚时会删除 checkpoint 后新增的受跟踪项目文件。`.agent/runtime/` 属于运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。
- Planner、组内角色和 Generator 读取上下文前会先做安全过滤:拒绝 `.env*``game-creator.config*`、Authorization / Cookie / API Key / Token / Bearer 等密钥样式内容,并清理 `sk-*` / `tnr_sk_*` tokenmemory、资产摘要和 conversation JSONL 中被过滤的内容不进入 LLM prompt。
- `.agent/run.latest.json` 的 schema 固定为共享契约 `GAME_CREATION_AGENT_RUN_SCHEMA_VERSION = game-creator-agent-run.v1`;TS 与 Rust 都从共享契约读取 run trace 类型,避免开发窗口和 Tauri 写入结构漂移。
- `.agent/run.latest.json` 增加可选 `lifecycleStatus`,把一次生成 run 映射到本地最小生命周期:`scheduled / running / waiting / pending / done / failed / killed`。聊天命令 `/agent-status` 读取最近 run`/agent-kill` 标记为 `killed``/agent-retry``/agent-resume [说明]` 标记为 `pending`,并写入 `.agent/activity.jsonl``.agent/output.jsonl``.agent/context.bundle.json`;状态 / 控制结果消息可一键填入 `/read .agent/output.jsonl` 草稿继续查看 run 输出,Agent 状态栏也可填入 output / activity / context bundle 的 `/read` 草稿。v1 只做本地状态控制,不承诺真正中断已在上游执行中的 LLM 请求;后续引入独立 runner 后再把 `pending` 接入 claim。