补齐Agent任务历史可视化
Agent Runtime 增加每个 Agent 的任务历史 JSONL 和最近任务读取。 任务记录状态改为 running、completed、failed 的任务视角。 开发窗口和项目 Agent 弹窗的 Runtime 面板展示最近任务。 补充 Rust 与前端测试,并更新实施计划和共享决策记录。
This commit is contained in:
@@ -195,6 +195,7 @@ pub(crate) fn read_game_creator_agent_runtime_at(
|
||||
validate_project_root(root)?;
|
||||
let session_path = game_creator_agent_runtime_session_path(root, agent_id);
|
||||
let event_path = game_creator_agent_runtime_event_path(root, agent_id);
|
||||
let task_path = game_creator_agent_runtime_task_path(root, agent_id);
|
||||
let mut state = match fs::read_to_string(&session_path) {
|
||||
Ok(content) => serde_json::from_str::<AgentRuntimeState>(&content).map_err(|error| {
|
||||
format!(
|
||||
@@ -214,11 +215,14 @@ pub(crate) fn read_game_creator_agent_runtime_at(
|
||||
};
|
||||
normalize_game_creator_agent_runtime_state(&mut state, agent_id);
|
||||
let recent_events = read_recent_game_creator_agent_runtime_events(&event_path)?;
|
||||
let recent_tasks = read_recent_game_creator_agent_runtime_tasks(&task_path)?;
|
||||
Ok(AgentRuntimeResult {
|
||||
state,
|
||||
session_path: session_path.to_string_lossy().into_owned(),
|
||||
event_path: event_path.to_string_lossy().into_owned(),
|
||||
task_path: task_path.to_string_lossy().into_owned(),
|
||||
recent_events,
|
||||
recent_tasks,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -942,6 +946,7 @@ pub(crate) fn start_game_creator_agent_runtime_task_at(
|
||||
state.observations = vec!["已创建本轮 Agent Runtime run。".to_string()];
|
||||
state.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_state(root, &state)?;
|
||||
append_game_creator_agent_runtime_task(root, &state)?;
|
||||
append_game_creator_agent_runtime_event(
|
||||
root,
|
||||
&state,
|
||||
@@ -982,6 +987,7 @@ pub(crate) fn advance_game_creator_agent_runtime_turn_at(
|
||||
}
|
||||
state.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_state(root, &state)?;
|
||||
append_game_creator_agent_runtime_task(root, &state)?;
|
||||
append_game_creator_agent_runtime_event(
|
||||
root,
|
||||
&state,
|
||||
@@ -1009,6 +1015,7 @@ pub(crate) fn finish_game_creator_agent_runtime_turn_at(
|
||||
.push("Agent 已完成回复,assistant 消息等待或已经由前端落盘。".to_string());
|
||||
state.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_state(root, &state)?;
|
||||
append_game_creator_agent_runtime_task(root, &state)?;
|
||||
append_game_creator_agent_runtime_event(
|
||||
root,
|
||||
&state,
|
||||
@@ -1044,6 +1051,7 @@ pub(crate) fn fail_game_creator_agent_runtime_turn_at(
|
||||
state.error = Some(sanitize_agent_runtime_text(error, 500));
|
||||
state.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_state(root, &state)?;
|
||||
append_game_creator_agent_runtime_task(root, &state)?;
|
||||
append_game_creator_agent_runtime_event(
|
||||
root,
|
||||
&state,
|
||||
@@ -1184,6 +1192,45 @@ fn normalize_game_creator_agent_runtime_event(event: &mut AgentRuntimeEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_game_creator_agent_runtime_task(record: &mut AgentRuntimeTaskRecord) {
|
||||
if record.schema_version.trim().is_empty() {
|
||||
record.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string();
|
||||
}
|
||||
if record.agent_id.trim().is_empty() {
|
||||
record.agent_id = record.task_id.clone();
|
||||
}
|
||||
if record.task_id.trim().is_empty() {
|
||||
record.task_id = record.agent_id.clone();
|
||||
}
|
||||
if record.session_id.trim().is_empty() {
|
||||
record.session_id = format!("agent-session-{}", record.agent_id);
|
||||
}
|
||||
if record.run_id.trim().is_empty() {
|
||||
record.run_id = format!("agent-runtime-{}-{}", record.agent_id, record.updated_at);
|
||||
}
|
||||
if record.source.trim().is_empty() {
|
||||
record.source = "agent-chat".to_string();
|
||||
}
|
||||
if record.status.trim().is_empty() {
|
||||
record.status = "idle".to_string();
|
||||
}
|
||||
if record.phase == "completed" && record.status == "idle" {
|
||||
record.status = "completed".to_string();
|
||||
}
|
||||
if record.phase == "failed" && record.status != "failed" {
|
||||
record.status = "failed".to_string();
|
||||
}
|
||||
if record.phase.trim().is_empty() {
|
||||
record.phase = "idle".to_string();
|
||||
}
|
||||
if record.current_action.trim().is_empty() {
|
||||
record.current_action = "等待输入".to_string();
|
||||
}
|
||||
if record.updated_at == 0 {
|
||||
record.updated_at = unix_timestamp();
|
||||
}
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_session_path(root: &Path, agent_id: &str) -> PathBuf {
|
||||
root.join(".agent")
|
||||
.join("runtime")
|
||||
@@ -1198,6 +1245,13 @@ fn game_creator_agent_runtime_event_path(root: &Path, agent_id: &str) -> PathBuf
|
||||
.join(format!("{agent_id}.jsonl"))
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_task_path(root: &Path, agent_id: &str) -> PathBuf {
|
||||
root.join(".agent")
|
||||
.join("runtime")
|
||||
.join("tasks")
|
||||
.join(format!("{agent_id}.jsonl"))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AgentRuntimeTaskLock {
|
||||
path: PathBuf,
|
||||
@@ -1353,6 +1407,56 @@ fn append_game_creator_agent_runtime_event(
|
||||
.map_err(|error| format!("写入 Agent Runtime 事件失败:{}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn append_game_creator_agent_runtime_task(
|
||||
root: &Path,
|
||||
state: &AgentRuntimeState,
|
||||
) -> Result<(), String> {
|
||||
let path = game_creator_agent_runtime_task_path(root, &state.agent_id);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| {
|
||||
format!(
|
||||
"创建 Agent Runtime 任务目录失败:{}: {error}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let record = AgentRuntimeTaskRecord {
|
||||
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
||||
agent_id: state.agent_id.clone(),
|
||||
task_id: state.task_id.clone(),
|
||||
session_id: state.session_id.clone(),
|
||||
run_id: state.run_id.clone(),
|
||||
source: state.source.clone(),
|
||||
task: state.current_task.clone(),
|
||||
status: game_creator_agent_runtime_task_status(state),
|
||||
phase: state.phase.clone(),
|
||||
current_action: state.current_action.clone(),
|
||||
error: state.error.clone(),
|
||||
updated_at: state.updated_at,
|
||||
};
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|error| format!("打开 Agent Runtime 任务失败:{}: {error}", path.display()))?;
|
||||
serde_json::to_writer(&mut file, &record)
|
||||
.map_err(|error| format!("序列化 Agent Runtime 任务失败:{error}"))?;
|
||||
file.write_all(b"\n")
|
||||
.map_err(|error| format!("写入 Agent Runtime 任务失败:{}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_task_status(state: &AgentRuntimeState) -> String {
|
||||
if state.error.is_some() || state.status == "failed" || state.phase == "failed" {
|
||||
"failed".to_string()
|
||||
} else if state.phase == "completed" {
|
||||
"completed".to_string()
|
||||
} else if state.status == "running" {
|
||||
"running".to_string()
|
||||
} else {
|
||||
state.status.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_recent_game_creator_agent_runtime_events(
|
||||
path: &Path,
|
||||
) -> Result<Vec<AgentRuntimeEvent>, String> {
|
||||
@@ -1391,6 +1495,53 @@ fn read_recent_game_creator_agent_runtime_events(
|
||||
}
|
||||
}
|
||||
|
||||
fn read_recent_game_creator_agent_runtime_tasks(
|
||||
path: &Path,
|
||||
) -> Result<Vec<AgentRuntimeTaskRecord>, String> {
|
||||
let mut records = Vec::new();
|
||||
match File::open(path) {
|
||||
Ok(file) => {
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line = line.map_err(|error| {
|
||||
format!("读取 Agent Runtime 任务失败:{}: {error}", path.display())
|
||||
})?;
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match serde_json::from_str::<AgentRuntimeTaskRecord>(line) {
|
||||
Ok(mut record) => {
|
||||
normalize_game_creator_agent_runtime_task(&mut record);
|
||||
records.push(record);
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"读取 Agent Runtime 任务失败:{}: {error}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut recent = Vec::new();
|
||||
let mut seen_run_ids: Vec<String> = Vec::new();
|
||||
for record in records.into_iter().rev() {
|
||||
if seen_run_ids.iter().any(|run_id| run_id == &record.run_id) {
|
||||
continue;
|
||||
}
|
||||
seen_run_ids.push(record.run_id.clone());
|
||||
recent.push(record);
|
||||
if recent.len() >= AGENT_RUNTIME_RECENT_TASK_LIMIT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
recent.reverse();
|
||||
Ok(recent)
|
||||
}
|
||||
|
||||
fn truncate_agent_runtime_text(value: &str, max_chars: usize) -> String {
|
||||
let value = value.trim();
|
||||
let mut output = String::new();
|
||||
|
||||
@@ -198,13 +198,44 @@ struct AgentRuntimeEvent {
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimeTaskRecord {
|
||||
#[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)]
|
||||
task: String,
|
||||
#[serde(default)]
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
phase: String,
|
||||
#[serde(default)]
|
||||
current_action: String,
|
||||
#[serde(default)]
|
||||
error: 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,
|
||||
task_path: String,
|
||||
recent_events: Vec<AgentRuntimeEvent>,
|
||||
recent_tasks: Vec<AgentRuntimeTaskRecord>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
@@ -613,6 +644,7 @@ 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 AGENT_RUNTIME_RECENT_TASK_LIMIT: usize = 12;
|
||||
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;
|
||||
|
||||
@@ -1374,6 +1374,16 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
|
||||
|
||||
let runtime_result =
|
||||
read_game_creator_agent_runtime_at(&root, "design-director").expect("runtime result");
|
||||
assert!(runtime_result
|
||||
.task_path
|
||||
.ends_with(".agent/runtime/tasks/design-director.jsonl"));
|
||||
assert!(runtime_result
|
||||
.recent_tasks
|
||||
.iter()
|
||||
.any(|task| task.run_id == "design-loop-run"
|
||||
&& task.status == "completed"
|
||||
&& task.phase == "completed"
|
||||
&& task.task == "后台分析当前玩法循环"));
|
||||
let event_types = runtime_result
|
||||
.recent_events
|
||||
.iter()
|
||||
@@ -1472,6 +1482,15 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() {
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("file.read:ok")));
|
||||
let runtime_result =
|
||||
read_game_creator_agent_runtime_at(&root, "design-director").expect("runtime result");
|
||||
assert!(runtime_result
|
||||
.recent_tasks
|
||||
.iter()
|
||||
.any(|task| task.run_id == "design-confirm-run"
|
||||
&& task.status == "completed"
|
||||
&& task.phase == "completed"
|
||||
&& task.task == "后台分析当前玩法循环"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
@@ -1575,6 +1594,18 @@ async fn background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies(
|
||||
design_runtime.last_response.as_deref(),
|
||||
Some("策划后台任务完成:先收敛核心循环。")
|
||||
);
|
||||
let art_runtime_result =
|
||||
read_game_creator_agent_runtime_at(&root, "art-director").expect("art runtime result");
|
||||
let design_runtime_result = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("design runtime result");
|
||||
assert!(art_runtime_result
|
||||
.recent_tasks
|
||||
.iter()
|
||||
.any(|task| task.run_id == "art-background-run" && task.status == "completed"));
|
||||
assert!(design_runtime_result
|
||||
.recent_tasks
|
||||
.iter()
|
||||
.any(|task| task.run_id == "design-background-run" && task.status == "completed"));
|
||||
|
||||
let art_conversation =
|
||||
read_local_conversation_at(&root, Some("art-director")).expect("art conversation");
|
||||
|
||||
@@ -244,6 +244,7 @@ interface AgentRuntimeState {
|
||||
lastResponse: string | null;
|
||||
error: string | null;
|
||||
updatedAt: number;
|
||||
recentTasks?: AgentRuntimeTaskRecord[];
|
||||
}
|
||||
|
||||
interface AgentRuntimeEventRecord {
|
||||
@@ -261,11 +262,28 @@ interface AgentRuntimeEventRecord {
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface AgentRuntimeTaskRecord {
|
||||
schemaVersion: string;
|
||||
agentId: string;
|
||||
taskId: string;
|
||||
sessionId: string;
|
||||
runId: string;
|
||||
source: string;
|
||||
task: string;
|
||||
status: string;
|
||||
phase: string;
|
||||
currentAction: string;
|
||||
error: string | null;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface AgentRuntimeResult {
|
||||
state: AgentRuntimeState;
|
||||
sessionPath: string;
|
||||
eventPath: string;
|
||||
taskPath?: string;
|
||||
recentEvents: AgentRuntimeEventRecord[];
|
||||
recentTasks?: AgentRuntimeTaskRecord[];
|
||||
}
|
||||
|
||||
interface GameCreatorLlmConfigStatus {
|
||||
@@ -474,6 +492,15 @@ function createAgentChatRunId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function agentRuntimeStateFromResult(
|
||||
result: AgentRuntimeResult,
|
||||
): AgentRuntimeState {
|
||||
return {
|
||||
...result.state,
|
||||
recentTasks: result.recentTasks ?? result.state.recentTasks ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function AgentRuntimeStatusPanel({
|
||||
runtime,
|
||||
error,
|
||||
@@ -496,6 +523,7 @@ function AgentRuntimeStatusPanel({
|
||||
}
|
||||
const planItems = runtime.plan.slice(0, 3);
|
||||
const observations = runtime.observations.slice(-2);
|
||||
const recentTasks = (runtime.recentTasks ?? []).slice(-3).reverse();
|
||||
return (
|
||||
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
|
||||
<header>
|
||||
@@ -520,6 +548,16 @@ function AgentRuntimeStatusPanel({
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{recentTasks.length > 0 ? (
|
||||
<div aria-label="Agent 最近任务">
|
||||
<strong>最近任务</strong>
|
||||
{recentTasks.map((task) => (
|
||||
<small key={`${task.runId}-${task.updatedAt}`}>
|
||||
{`${task.status} / ${task.phase} · ${task.task || task.currentAction}`}
|
||||
</small>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{runtime.error ? <p>{runtime.error}</p> : null}
|
||||
{error ? <p>{error}</p> : null}
|
||||
</section>
|
||||
@@ -3047,7 +3085,7 @@ export function WorkspaceLauncher({
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current === loadVersion) {
|
||||
setAgentChatRuntime(runtime.state);
|
||||
setAgentChatRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentChatRuntimeError('');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -3204,7 +3242,7 @@ export function WorkspaceLauncher({
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current === saveVersion) {
|
||||
setAgentChatRuntime(runtime.state);
|
||||
setAgentChatRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentChatRuntimeError('');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -3324,7 +3362,7 @@ export function WorkspaceLauncher({
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatRuntime(runtime.state);
|
||||
setAgentChatRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentChatRuntimeError('');
|
||||
const conversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
@@ -12436,7 +12474,7 @@ export function App() {
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current === loadVersion) {
|
||||
setAgentConversationRuntime(runtime.state);
|
||||
setAgentConversationRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentConversationRuntimeError('');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -12753,7 +12791,7 @@ export function App() {
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current === saveVersion) {
|
||||
setAgentConversationRuntime(runtime.state);
|
||||
setAgentConversationRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentConversationRuntimeError('');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -12902,7 +12940,7 @@ export function App() {
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentConversationRuntime(runtime.state);
|
||||
setAgentConversationRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentConversationRuntimeError('');
|
||||
const conversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
@@ -19917,7 +19955,7 @@ export function App() {
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div className="agent-status-list">
|
||||
<div className="agent-status-list" aria-label="Agent 状态列表">
|
||||
{agentStatusCards.map((agent) => {
|
||||
const agentLlmStatus = formatAgentCardLlmStatus(
|
||||
llmConfigStatus,
|
||||
|
||||
@@ -1308,6 +1308,20 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
error: null,
|
||||
updatedAt: 4000,
|
||||
};
|
||||
const runningRuntimeTask = {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'design-director',
|
||||
taskId: 'design-director',
|
||||
sessionId: 'agent-session-design-director',
|
||||
runId: 'launcher-agent-task-test',
|
||||
source: 'agent-background-task',
|
||||
task: '后台整理角色规范',
|
||||
status: 'running',
|
||||
phase: 'action',
|
||||
currentAction: '调用工具 file.read',
|
||||
error: null,
|
||||
updatedAt: 4000,
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'check_game_creator_llm_config') {
|
||||
@@ -1352,10 +1366,14 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||||
eventPath:
|
||||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||||
taskPath:
|
||||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||||
recentEvents: [],
|
||||
recentTasks: [runningRuntimeTask],
|
||||
};
|
||||
}
|
||||
if (command === 'start_game_creator_agent_runtime_task') {
|
||||
const runId = String(args?.runId ?? runningRuntimeState.runId);
|
||||
persistedMessages.push({
|
||||
role: 'user',
|
||||
content: String(args?.task ?? ''),
|
||||
@@ -1364,13 +1382,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
return {
|
||||
state: {
|
||||
...runningRuntimeState,
|
||||
runId: String(args?.runId ?? runningRuntimeState.runId),
|
||||
runId,
|
||||
},
|
||||
sessionPath:
|
||||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||||
eventPath:
|
||||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||||
taskPath:
|
||||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||||
recentEvents: [],
|
||||
recentTasks: [{ ...runningRuntimeTask, runId }],
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
@@ -1399,6 +1420,8 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(screen.getByText('回复开发者')).not.toBeNull();
|
||||
expect(screen.getByText('思考摘要:需要先看项目笔记')).not.toBeNull();
|
||||
expect(screen.getByText('file.read:ok · 已读取 game/notes.txt')).not.toBeNull();
|
||||
expect(screen.getByText('最近任务')).not.toBeNull();
|
||||
expect(screen.getByText(/running \/ action · 后台整理角色规范/)).not.toBeNull();
|
||||
expect(screen.getByText(/已启动后台任务:launcher-agent-task-/)).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'start_game_creator_agent_runtime_task',
|
||||
@@ -13058,11 +13081,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('想做什么游戏?');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const agentStatusList = screen.getByLabelText('Agent 状态列表');
|
||||
fireEvent.click(
|
||||
within(agentStatusList).getByRole('button', { name: /拆解创作方向/ }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(releaseOldConversation).not.toBeNull();
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ }));
|
||||
fireEvent.click(
|
||||
within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('新 Agent 历史消息')).not.toBeNull();
|
||||
expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain(
|
||||
@@ -13178,16 +13206,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('想做什么游戏?');
|
||||
const agentStatusPane = screen.getByLabelText('Agent 状态');
|
||||
const agentStatusList = screen.getByLabelText('Agent 状态列表');
|
||||
fireEvent.click(
|
||||
within(agentStatusPane).getByRole('button', { name: /拆解创作方向/ }),
|
||||
within(agentStatusList).getByRole('button', { name: /拆解创作方向/ }),
|
||||
);
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
fireEvent.change(input, { target: { value: '旧 Agent 保存回包' } });
|
||||
fireEvent.submit(input.closest('form') as HTMLFormElement);
|
||||
await screen.findByText('正在保存用户消息');
|
||||
fireEvent.click(
|
||||
within(agentStatusPane).getByRole('button', { name: /确定视觉方向/ }),
|
||||
within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('新 Agent 留存消息')).not.toBeNull();
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
## 2026-07-09 AI 游戏创作 App Runtime V1 增加单 Agent 后台任务
|
||||
|
||||
- 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。
|
||||
- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event、追加用户任务到 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 先输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行只读工具并记录 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复;完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。不同 Agent 使用独立 `.agent/runtime/locks/<agentId>.lock`,允许并行运行;同一 Agent 同时只允许一个后台任务。该能力仍不是独立 OS 进程、持久队列或离线常驻 worker。
|
||||
- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 先输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行只读工具并记录 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复;完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。每个 Agent 的任务历史落在 `.agent/runtime/tasks/<agentId>.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `running / completed / failed`,UI 在 Runtime 面板展示最近任务。不同 Agent 使用独立 `.agent/runtime/locks/<agentId>.lock`,允许并行运行;同一 Agent 同时只允许一个后台任务。该能力仍不是独立 OS 进程、可排队 pending 队列或离线常驻 worker。
|
||||
- 影响范围:`apps/ai-game-creator-shell` 的 Tauri command、Agent Runtime state/event、开发窗口单 Agent 聊天、项目内 Agent 对话弹窗、`appSurface.test.ts` 和 AI 游戏创作 App 实施计划。
|
||||
- 验证方式:运行 Tauri Rust 后台 Agent 并行测试、壳前端 appSurface 测试、壳 typecheck、编码检查和 `git diff --check`。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
@@ -32,7 +32,7 @@ Agent Runtime 负责:
|
||||
- 开发窗口能力: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 是什么”的可观测性。
|
||||
- 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/<agentId>.json`、`.agent/runtime/events/<agentId>.jsonl` 和 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:先让该 Agent 输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复并追加回对话。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 同时只允许一个后台任务。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程、持久队列或离线常驻 worker。
|
||||
- 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/<agentId>.json`、`.agent/runtime/events/<agentId>.jsonl`、`.agent/runtime/tasks/<agentId>.jsonl` 和 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:先让该 Agent 输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复并追加回对话。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务,开发窗口和项目内 Agent 对话弹窗在 Runtime 面板展示最近任务。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 同时只允许一个后台任务。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程、可排队 pending 队列或离线常驻 worker。
|
||||
- 任务图能力:每轮 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`,不把原始对话混进项目黑板或角色私有记忆。
|
||||
@@ -252,7 +252,7 @@ game-project/
|
||||
- Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。
|
||||
- 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。
|
||||
- v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。
|
||||
- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/<agentId>.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task` / `agent.runtime.tool_observation` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱只开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index` 和 `file.read`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。
|
||||
- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/<agentId>.jsonl` 追加任务视角记录,任务状态使用 `running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/<agentId>.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task` / `agent.runtime.tool_observation` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱只开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index` 和 `file.read`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。
|
||||
- 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。
|
||||
- 单窗口首页和项目组页可选择、打开、新建或显示当前输入的项目绝对路径;最近项目行也可显示目录,非法或相对路径不会调用系统文件管理器。
|
||||
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
|
||||
|
||||
Reference in New Issue
Block a user