Merge branch 'codex/ai-game-creator-app' into fix/llm-parsing-and-null-output
This commit is contained in:
@@ -88,6 +88,19 @@ struct GameCreatorLlmConfigStatus {
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRunControlResult {
|
||||
run_id: String,
|
||||
status: String,
|
||||
lifecycle_status: String,
|
||||
next_step: String,
|
||||
message: String,
|
||||
activity_path: String,
|
||||
output_path: String,
|
||||
context_bundle_path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct UploadLocalAssetResult {
|
||||
@@ -754,6 +767,19 @@ fn get_local_game_manifest(project_path: String) -> Result<GameCreationAppManife
|
||||
read_manifest_for_project(Path::new(project_path.trim()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn control_agent_run(
|
||||
project_path: String,
|
||||
action: String,
|
||||
detail: Option<String>,
|
||||
) -> Result<AgentRunControlResult, String> {
|
||||
update_agent_run_lifecycle(
|
||||
Path::new(project_path.trim()),
|
||||
action.trim(),
|
||||
detail.as_deref().map(str::trim).filter(|value| !value.is_empty()),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn generate_local_game_draft(
|
||||
app: tauri::AppHandle,
|
||||
@@ -3544,6 +3570,7 @@ fn write_agent_run_trace(
|
||||
run_id: run_id.to_string(),
|
||||
command_id: "game.generate_draft".to_string(),
|
||||
status: status.to_string(),
|
||||
lifecycle_status: Some(agent_run_lifecycle_status(status).to_string()),
|
||||
passes,
|
||||
max_passes: GAME_CREATOR_AGENT_LOOP_MAX_PASSES,
|
||||
tool_call_count,
|
||||
@@ -3584,6 +3611,216 @@ fn agent_run_stop_reason(status: &str, error: Option<&str>) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_run_lifecycle_status(status: &str) -> &'static str {
|
||||
match status {
|
||||
"running" | "needs-revision" | "artifacts-written" | "preview-running" => "running",
|
||||
"waiting" | "preview-stopped" => "waiting",
|
||||
"pending" => "pending",
|
||||
"killed" => "killed",
|
||||
"failed" => "failed",
|
||||
"passed" => "done",
|
||||
_ => "scheduled",
|
||||
}
|
||||
}
|
||||
|
||||
fn append_agent_run_activity(
|
||||
root: &Path,
|
||||
run_id: &str,
|
||||
event: &str,
|
||||
message: &str,
|
||||
) -> Result<(), String> {
|
||||
append_agent_run_jsonl(
|
||||
root,
|
||||
".agent/activity.jsonl",
|
||||
&serde_json::json!({
|
||||
"timestamp": unix_timestamp(),
|
||||
"runId": run_id,
|
||||
"event": event,
|
||||
"message": message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn append_agent_run_output(
|
||||
root: &Path,
|
||||
run_id: &str,
|
||||
event: &str,
|
||||
message: &str,
|
||||
) -> Result<(), String> {
|
||||
append_agent_run_jsonl(
|
||||
root,
|
||||
".agent/output.jsonl",
|
||||
&serde_json::json!({
|
||||
"timestamp": unix_timestamp(),
|
||||
"runId": run_id,
|
||||
"event": event,
|
||||
"content": message,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn append_agent_run_jsonl(
|
||||
root: &Path,
|
||||
relative_path: &str,
|
||||
value: &serde_json::Value,
|
||||
) -> Result<(), String> {
|
||||
let path = root.join(relative_path);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("创建 Agent 事件目录失败:{}: {error}", parent.display()))?;
|
||||
}
|
||||
let mut line = serde_json::to_string(value)
|
||||
.map_err(|error| format!("序列化 Agent 事件失败:{error}"))?;
|
||||
line.push('\n');
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.and_then(|mut file| file.write_all(line.as_bytes()))
|
||||
.map_err(|error| format!("写入 Agent 事件失败:{}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn write_agent_run_context_bundle(root: &Path, trace: &GameCreationAgentRunTrace) -> Result<(), String> {
|
||||
let bundle_path = root.join(".agent/context.bundle.json");
|
||||
if let Some(parent) = bundle_path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| {
|
||||
format!(
|
||||
"创建 Agent context bundle 目录失败:{}: {error}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let manifest = read_manifest_for_project(root).ok();
|
||||
let payload = serde_json::json!({
|
||||
"schemaVersion": "game-creator-context-bundle.v1",
|
||||
"runId": trace.run_id,
|
||||
"commandId": trace.command_id,
|
||||
"goal": trace.goal,
|
||||
"lifecycleStatus": trace.lifecycle_status.as_deref().unwrap_or_else(|| agent_run_lifecycle_status(&trace.status)),
|
||||
"status": trace.status,
|
||||
"nextStep": trace.next_step,
|
||||
"memory": {
|
||||
"short": "memory/session.md",
|
||||
"long": "memory/project.md"
|
||||
},
|
||||
"manifest": manifest,
|
||||
"trace": ".agent/run.latest.json",
|
||||
"activity": ".agent/activity.jsonl",
|
||||
"output": ".agent/output.jsonl",
|
||||
"updatedAt": unix_timestamp()
|
||||
});
|
||||
let content = serde_json::to_string_pretty(&payload)
|
||||
.map_err(|error| format!("生成 Agent context bundle 失败:{error}"))?;
|
||||
fs::write(&bundle_path, content).map_err(|error| {
|
||||
format!(
|
||||
"写入 Agent context bundle 失败:{}: {error}",
|
||||
bundle_path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn update_agent_run_lifecycle(
|
||||
root: &Path,
|
||||
action: &str,
|
||||
detail: Option<&str>,
|
||||
) -> Result<AgentRunControlResult, String> {
|
||||
let trace_path = root.join(".agent/run.latest.json");
|
||||
let content = fs::read_to_string(&trace_path).map_err(|error| {
|
||||
format!(
|
||||
"读取 Agent run trace 失败:{}: {error}",
|
||||
trace_path.display()
|
||||
)
|
||||
})?;
|
||||
let mut trace =
|
||||
serde_json::from_str::<GameCreationAgentRunTrace>(&content).map_err(|error| {
|
||||
format!(
|
||||
"解析 Agent run trace 失败:{}: {error}",
|
||||
trace_path.display()
|
||||
)
|
||||
})?;
|
||||
let (status, lifecycle_status, next_step, event, message) = match action {
|
||||
"status" => {
|
||||
let lifecycle = trace
|
||||
.lifecycle_status
|
||||
.clone()
|
||||
.unwrap_or_else(|| agent_run_lifecycle_status(&trace.status).to_string());
|
||||
(
|
||||
trace.status.clone(),
|
||||
lifecycle.clone(),
|
||||
trace.next_step.clone(),
|
||||
"agent.run_status",
|
||||
format!("run {} 当前状态:{} / {}", trace.run_id, trace.status, lifecycle),
|
||||
)
|
||||
}
|
||||
"kill" => (
|
||||
"killed".to_string(),
|
||||
"killed".to_string(),
|
||||
"resume-or-retry".to_string(),
|
||||
"agent.kill",
|
||||
format!("run {} 已标记为 killed", trace.run_id),
|
||||
),
|
||||
"retry" => (
|
||||
"pending".to_string(),
|
||||
"pending".to_string(),
|
||||
"runner-claim".to_string(),
|
||||
"agent.retry",
|
||||
format!("run {} 已重试,等待下一次 claim", trace.run_id),
|
||||
),
|
||||
"resume" => (
|
||||
"pending".to_string(),
|
||||
"pending".to_string(),
|
||||
"runner-claim".to_string(),
|
||||
"agent.resume",
|
||||
format!(
|
||||
"run {} 已恢复:{}",
|
||||
trace.run_id,
|
||||
detail.unwrap_or("等待下一次 claim")
|
||||
),
|
||||
),
|
||||
_ => return Err("未知 Agent run 控制动作".to_string()),
|
||||
};
|
||||
|
||||
if action != "status" {
|
||||
trace.status = status;
|
||||
trace.lifecycle_status = Some(lifecycle_status);
|
||||
trace.next_step = next_step;
|
||||
trace.stop_reason = match action {
|
||||
"kill" => "killed",
|
||||
"retry" => "retry-requested",
|
||||
"resume" => "human-resume",
|
||||
_ => trace.stop_reason.as_str(),
|
||||
}
|
||||
.to_string();
|
||||
trace.error = if action == "kill" {
|
||||
Some("用户请求停止当前 run".to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
trace.updated_at = unix_timestamp();
|
||||
write_agent_run_trace_payload(root, &trace)?;
|
||||
}
|
||||
|
||||
append_agent_run_activity(root, &trace.run_id, event, &message)?;
|
||||
append_agent_run_output(root, &trace.run_id, event, &message)?;
|
||||
write_agent_run_context_bundle(root, &trace)?;
|
||||
|
||||
Ok(AgentRunControlResult {
|
||||
run_id: trace.run_id,
|
||||
status: trace.status,
|
||||
lifecycle_status: trace
|
||||
.lifecycle_status
|
||||
.unwrap_or_else(|| agent_run_lifecycle_status("scheduled").to_string()),
|
||||
next_step: trace.next_step,
|
||||
message,
|
||||
activity_path: root.join(".agent/activity.jsonl").to_string_lossy().to_string(),
|
||||
output_path: root.join(".agent/output.jsonl").to_string_lossy().to_string(),
|
||||
context_bundle_path: root
|
||||
.join(".agent/context.bundle.json")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn count_agent_tool_calls(steps: &[GameCreationAgentRunStep]) -> Result<u16, String> {
|
||||
let count = steps.iter().try_fold(0u16, |current, step| {
|
||||
let step_count = u16::try_from(step.tool_calls.len())
|
||||
@@ -6005,6 +6242,7 @@ fn main() {
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
init_local_game_project,
|
||||
control_agent_run,
|
||||
generate_local_game_draft,
|
||||
check_game_creator_llm_config,
|
||||
upload_local_asset,
|
||||
@@ -8086,6 +8324,61 @@ GENARRATIVE_GAME_CREATOR_LLM_STREAM=true
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_run_control_updates_lifecycle_and_jsonl() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "像素动作原型").expect("project init");
|
||||
write_agent_run_trace(
|
||||
&root,
|
||||
"run-control-1",
|
||||
"做一个可控 run",
|
||||
"running",
|
||||
1,
|
||||
&[agent_trace_step(
|
||||
1,
|
||||
"Planner",
|
||||
"completed",
|
||||
&[".agent/manifest.json"],
|
||||
&[".agent/spec.md"],
|
||||
"已拆解目标",
|
||||
"llm.chat.planner",
|
||||
)],
|
||||
None,
|
||||
)
|
||||
.expect("run trace");
|
||||
|
||||
let killed =
|
||||
update_agent_run_lifecycle(&root, "kill", None).expect("kill should update trace");
|
||||
assert_eq!(killed.status, "killed");
|
||||
assert_eq!(killed.lifecycle_status, "killed");
|
||||
assert_eq!(killed.next_step, "resume-or-retry");
|
||||
let trace: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/run.latest.json")).unwrap())
|
||||
.expect("run trace json");
|
||||
assert_eq!(trace["status"], "killed");
|
||||
assert_eq!(trace["lifecycleStatus"], "killed");
|
||||
assert_eq!(trace["stopReason"], "killed");
|
||||
|
||||
let retried =
|
||||
update_agent_run_lifecycle(&root, "retry", None).expect("retry should mark pending");
|
||||
assert_eq!(retried.status, "pending");
|
||||
assert_eq!(retried.lifecycle_status, "pending");
|
||||
assert_eq!(retried.next_step, "runner-claim");
|
||||
let status =
|
||||
update_agent_run_lifecycle(&root, "status", None).expect("status should read trace");
|
||||
assert_eq!(status.status, "pending");
|
||||
assert_eq!(status.lifecycle_status, "pending");
|
||||
let activity = fs::read_to_string(root.join(".agent/activity.jsonl")).expect("activity");
|
||||
assert!(activity.contains("agent.kill"));
|
||||
assert!(activity.contains("agent.retry"));
|
||||
assert!(activity.contains("agent.run_status"));
|
||||
let output = fs::read_to_string(root.join(".agent/output.jsonl")).expect("output");
|
||||
assert!(output.contains("agent.kill"));
|
||||
assert!(root.join(".agent/context.bundle.json").exists());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limited_local_command_rejects_placeholder_game_smoke() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -139,6 +139,17 @@ interface AgentProgressEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface AgentRunControlResult {
|
||||
runId: string;
|
||||
status: string;
|
||||
lifecycleStatus: string;
|
||||
nextStep: string;
|
||||
message: string;
|
||||
activityPath: string;
|
||||
outputPath: string;
|
||||
contextBundlePath: string;
|
||||
}
|
||||
|
||||
export type PendingCommand =
|
||||
| {
|
||||
id: 'game.generate_draft';
|
||||
@@ -165,6 +176,10 @@ export type PendingCommand =
|
||||
| {
|
||||
id: 'preview.open';
|
||||
}
|
||||
| {
|
||||
id: 'agent.kill' | 'agent.retry' | 'agent.resume';
|
||||
detail?: string;
|
||||
}
|
||||
| {
|
||||
id: 'memory.write';
|
||||
scope: MemoryScope;
|
||||
@@ -257,6 +272,10 @@ const chatCommandHelp = [
|
||||
'/status:查看项目状态',
|
||||
'/tasks:查看任务拆分',
|
||||
'/trace 或 /loop:查看最近一次 Agent loop trace',
|
||||
'/agent-status:查看最近 run 生命周期',
|
||||
'/agent-kill:标记最近 run 为 killed',
|
||||
'/agent-retry:把最近 run 标记为 pending 等待重试',
|
||||
'/agent-resume [说明]:把最近 run 标记为 pending 等待继续',
|
||||
'/files:列出本地项目文件',
|
||||
'/assets:列出本地项目资产',
|
||||
'/read 路径:读取本地项目内文本文件',
|
||||
@@ -681,6 +700,9 @@ export function isAbsoluteProjectPath(value: string) {
|
||||
export function needsInitializedChatProject(commandId: PendingCommand['id']) {
|
||||
return [
|
||||
'asset.upload',
|
||||
'agent.kill',
|
||||
'agent.retry',
|
||||
'agent.resume',
|
||||
'command.run_limited',
|
||||
'game.generate_draft',
|
||||
'game.run_local',
|
||||
@@ -909,6 +931,13 @@ function pendingCommandTitle(command: PendingCommand) {
|
||||
if (command.id === 'project.create') {
|
||||
return command.id;
|
||||
}
|
||||
if (
|
||||
command.id === 'agent.kill' ||
|
||||
command.id === 'agent.retry' ||
|
||||
command.id === 'agent.resume'
|
||||
) {
|
||||
return command.id;
|
||||
}
|
||||
if (command.id === 'memory.write' || command.id === 'memory.delete') {
|
||||
return `${command.id} · ${memoryScopeLabel(command.scope)}`;
|
||||
}
|
||||
@@ -952,6 +981,15 @@ export function pendingCommandDetail(
|
||||
if (command.id === 'preview.open') {
|
||||
return '打开当前本地预览';
|
||||
}
|
||||
if (command.id === 'agent.kill') {
|
||||
return `标记 ${projectPath}/.agent/run.latest.json 为 killed,并写入 activity/output`;
|
||||
}
|
||||
if (command.id === 'agent.retry') {
|
||||
return `标记 ${projectPath}/.agent/run.latest.json 为 pending,等待 runner claim`;
|
||||
}
|
||||
if (command.id === 'agent.resume') {
|
||||
return `附加用户说明并标记 ${projectPath}/.agent/run.latest.json 为 pending`;
|
||||
}
|
||||
if (command.id === 'memory.write') {
|
||||
return `${
|
||||
command.mode === 'replace' ? '覆盖保存到' : '追加到'
|
||||
@@ -1241,6 +1279,50 @@ export function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt === '/agent-status') {
|
||||
void executeAgentRunControl('status', undefined, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt === '/agent-kill') {
|
||||
if (!requireChatProjectForUserAction()) {
|
||||
return;
|
||||
}
|
||||
queuePendingCommand({ id: 'agent.kill' });
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: '准备标记最近 run 为 killed。' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt === '/agent-retry') {
|
||||
if (!requireChatProjectForUserAction()) {
|
||||
return;
|
||||
}
|
||||
queuePendingCommand({ id: 'agent.retry' });
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: '准备把最近 run 标记为 pending。' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt === '/agent-resume' || prompt.startsWith('/agent-resume ')) {
|
||||
if (!requireChatProjectForUserAction()) {
|
||||
return;
|
||||
}
|
||||
queuePendingCommand({
|
||||
id: 'agent.resume',
|
||||
detail: prompt.slice('/agent-resume'.length).trim(),
|
||||
});
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: '准备恢复最近 run。' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prompt === '/smoke') {
|
||||
if (!requireChatProjectForUserAction()) {
|
||||
return;
|
||||
@@ -1707,6 +1789,16 @@ export function App() {
|
||||
void executePreviewStart(true);
|
||||
} else if (command.id === 'preview.open') {
|
||||
void executePreviewOpen(true);
|
||||
} else if (
|
||||
command.id === 'agent.kill' ||
|
||||
command.id === 'agent.retry' ||
|
||||
command.id === 'agent.resume'
|
||||
) {
|
||||
void executeAgentRunControl(
|
||||
command.id.slice('agent.'.length),
|
||||
command.detail,
|
||||
true,
|
||||
);
|
||||
} else if (command.id === 'memory.write') {
|
||||
void executeMemoryWrite(
|
||||
command.scope,
|
||||
@@ -1735,7 +1827,7 @@ export function App() {
|
||||
command.canvasProjectId,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
} else if (command.id === 'game.generate_draft') {
|
||||
void executeGameDraft(command.prompt);
|
||||
}
|
||||
}
|
||||
@@ -2088,6 +2180,72 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function executeAgentRunControl(
|
||||
action: string,
|
||||
detail: string | undefined,
|
||||
announceToChat: boolean,
|
||||
) {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setAgentRunStatus('需要在 Tauri App 内运行');
|
||||
if (announceToChat) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: '需要在 Tauri App 内运行。' },
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const nextProjectPath = requireChatProjectForUserAction();
|
||||
if (!nextProjectPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await invoke<AgentRunControlResult>('control_agent_run', {
|
||||
projectPath: nextProjectPath,
|
||||
action,
|
||||
detail,
|
||||
});
|
||||
setAgentRunStatus(
|
||||
`${result.status} · ${result.lifecycleStatus} · ${result.nextStep}`,
|
||||
);
|
||||
setCommandLog((current) => [
|
||||
...current,
|
||||
`agent.${action}`,
|
||||
'file.write .agent/activity.jsonl',
|
||||
'file.write .agent/output.jsonl',
|
||||
'file.write .agent/context.bundle.json',
|
||||
]);
|
||||
await refreshAgentRunTrace(nextProjectPath);
|
||||
if (announceToChat) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: [
|
||||
result.message,
|
||||
`状态:${result.status} / ${result.lifecycleStatus}`,
|
||||
`下一步:${result.nextStep}`,
|
||||
`事件:${result.activityPath}`,
|
||||
`输出:${result.outputPath}`,
|
||||
`上下文包:${result.contextBundlePath}`,
|
||||
].join('\n'),
|
||||
},
|
||||
]);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setAgentRunStatus(message);
|
||||
if (announceToChat) {
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: message },
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function executeAgentAuditChat() {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
|
||||
@@ -93,6 +93,8 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(
|
||||
screen.getByText(/\/audit:审计当前项目的 Agent 能力证据/),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText(/\/agent-status:查看最近 run 生命周期/)).not.toBeNull();
|
||||
expect(screen.getByText(/\/agent-kill:标记最近 run 为 killed/)).not.toBeNull();
|
||||
expect(screen.queryByLabelText('开发环境')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -2142,6 +2144,116 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('controls agent run lifecycle from chat through the authorized local project path', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
'未命名游戏原型',
|
||||
);
|
||||
const trace: GameCreationAgentRunTrace = {
|
||||
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
||||
runId: 'run-control-chat',
|
||||
commandId: 'game.generate_draft',
|
||||
status: 'pending',
|
||||
lifecycleStatus: 'pending',
|
||||
passes: 1,
|
||||
maxPasses: 3,
|
||||
toolCallCount: 1,
|
||||
maxToolCalls: 128,
|
||||
stopReason: 'retry-requested',
|
||||
goal: '做一个反弹弹幕厨房游戏',
|
||||
coordination: 'filesystem',
|
||||
steps: [],
|
||||
artifacts: [],
|
||||
taskGraph: {
|
||||
goal: '做一个反弹弹幕厨房游戏',
|
||||
readyTaskIds: [],
|
||||
activeTaskIds: [],
|
||||
carriedTaskIds: [],
|
||||
repairFocus: [],
|
||||
repairRoutes: [],
|
||||
tasks: createGameCreationAppSeedTasks(),
|
||||
},
|
||||
passPlans: [],
|
||||
nextStep: 'runner-claim',
|
||||
error: null,
|
||||
updatedAt: 1,
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'append_local_permission_log') {
|
||||
return {};
|
||||
}
|
||||
if (command === 'init_local_game_project') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
return {
|
||||
projectPath,
|
||||
manifestPath: `${projectPath}/.agent/manifest.json`,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
if (command === 'control_agent_run') {
|
||||
return {
|
||||
runId: 'run-control-chat',
|
||||
status: args?.action === 'kill' ? 'killed' : 'pending',
|
||||
lifecycleStatus: args?.action === 'kill' ? 'killed' : 'pending',
|
||||
nextStep:
|
||||
args?.action === 'kill' ? 'resume-or-retry' : 'runner-claim',
|
||||
message:
|
||||
args?.action === 'kill'
|
||||
? 'run run-control-chat 已标记为 killed'
|
||||
: 'run run-control-chat 当前状态:pending / pending',
|
||||
activityPath: '/tmp/authorized-game/.agent/activity.jsonl',
|
||||
outputPath: '/tmp/authorized-game/.agent/output.jsonl',
|
||||
contextBundlePath: '/tmp/authorized-game/.agent/context.bundle.json',
|
||||
};
|
||||
}
|
||||
if (command === 'read_local_project_file') {
|
||||
return {
|
||||
path: '.agent/run.latest.json',
|
||||
absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`,
|
||||
content: JSON.stringify(trace),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderAppAt('/');
|
||||
|
||||
submitChat('/project /tmp/authorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText('已设置本地项目:/tmp/authorized-game'),
|
||||
).not.toBeNull();
|
||||
|
||||
submitChat('/agent-status');
|
||||
expect(
|
||||
await screen.findByText(/run run-control-chat 当前状态:pending \/ pending/),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
action: 'status',
|
||||
detail: undefined,
|
||||
});
|
||||
|
||||
submitChat('/agent-kill');
|
||||
expect(screen.getByText('agent.kill')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'标记 /tmp/authorized-game/.agent/run.latest.json 为 killed,并写入 activity/output',
|
||||
),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(screen.getByRole('button', { name: '确认' }));
|
||||
expect(
|
||||
await screen.findByText(/run run-control-chat 已标记为 killed/),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('control_agent_run', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
action: 'kill',
|
||||
detail: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('manages long memory from chat through the authorized local project path', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
|
||||
@@ -102,6 +102,9 @@ describe('AI 游戏创作聊天记忆命令', () => {
|
||||
it('requires a project before chat commands write or run local artifacts', () => {
|
||||
expect(needsInitializedChatProject('game.generate_draft')).toBe(true);
|
||||
expect(needsInitializedChatProject('asset.upload')).toBe(true);
|
||||
expect(needsInitializedChatProject('agent.kill')).toBe(true);
|
||||
expect(needsInitializedChatProject('agent.retry')).toBe(true);
|
||||
expect(needsInitializedChatProject('agent.resume')).toBe(true);
|
||||
expect(needsInitializedChatProject('command.run_limited')).toBe(true);
|
||||
expect(needsInitializedChatProject('preview.open')).toBe(true);
|
||||
expect(needsInitializedChatProject('preview.start')).toBe(true);
|
||||
@@ -113,6 +116,21 @@ describe('AI 游戏创作聊天记忆命令', () => {
|
||||
expect(needsInitializedChatProject('canvas.project_open')).toBe(false);
|
||||
});
|
||||
|
||||
it('describes agent run lifecycle controls before confirmation', () => {
|
||||
expect(pendingCommandDetail({ id: 'agent.kill' }, '/tmp/game')).toBe(
|
||||
'标记 /tmp/game/.agent/run.latest.json 为 killed,并写入 activity/output',
|
||||
);
|
||||
expect(pendingCommandDetail({ id: 'agent.retry' }, '/tmp/game')).toBe(
|
||||
'标记 /tmp/game/.agent/run.latest.json 为 pending,等待 runner claim',
|
||||
);
|
||||
expect(
|
||||
pendingCommandDetail(
|
||||
{ id: 'agent.resume', detail: '继续修复输入监听' },
|
||||
'/tmp/game',
|
||||
),
|
||||
).toBe('附加用户说明并标记 /tmp/game/.agent/run.latest.json 为 pending');
|
||||
});
|
||||
|
||||
it('shows the authorized project path in pending write command details', () => {
|
||||
expect(
|
||||
resolvePendingCommandProjectPath(
|
||||
|
||||
Reference in New Issue
Block a user