补齐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:
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");
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 角色 brief;carry-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 / event:active 角色用 `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_*` token;memory、资产摘要和 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。
|
||||
|
||||
Reference in New Issue
Block a user