diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs index c38fa40f9..be2bf6c95 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs @@ -63,6 +63,10 @@ const webSearchSuite = 'web-search'; const contextCompactionSuite = 'context-compaction'; const mcpRuntimeSuite = 'mcp-runtime'; const userInputRuntimeSuite = 'user-input-runtime'; +const runtimeContextBundleSchemaVersion = + 'game-creator-runtime-context-bundle.v5'; +const providerRequestLifecycleSchemaVersion = + 'game-creator-provider-request-lifecycle.v2'; const mcpFixtureScript = path.join( appRoot, 'src-tauri/test-fixtures/mcp-server.mjs', @@ -96,6 +100,9 @@ const goalInitialMarker = `GENARRATIVE_GOAL_REVISION_ONE_${randomUUID() const goalFinalMarker = `GENARRATIVE_GOAL_REVISION_TWO_${randomUUID() .replaceAll('-', '') .slice(0, 16)}`; +const goalFailureEvidenceCanary = `GENARRATIVE_GOAL_FAILURE_EVIDENCE_${randomUUID() + .replaceAll('-', '') + .slice(0, 16)}`; const goalInitialPayload = { outcome: `在当前 disposable 项目的 ${goalDeliveryPath} 中交付一份新的变更证据,文件完整内容必须是唯一标记“${goalInitialMarker}”和一个结尾换行。基于仓库真实状态维护计划、完成变更并审阅实际结果,证据不足时不要结束。`, constraints: [ @@ -111,6 +118,7 @@ const goalEditedPayload = { outcome: `前一版目标已经废止,项目交付文件不得保留标记“${goalInitialMarker}”。改为修复当前 disposable 项目唯一的真实验收失败,让项目声明的验收通过,并让 ${goalDeliveryPath} 的完整内容只包含唯一标记“${goalFinalMarker}”和一个结尾换行。基于真实运行反馈维护计划并审阅完整结果,证据不足时不要结束。`, constraints: [ '保留既有可见内容、非空动画画布和仓库安全边界,只落地完成当前目标所需的原子变更。', + `失败证据门禁 ${goalFailureEvidenceCanary}:任何修复动作前必须先运行项目清单声明的原始验收并观察到真实非零退出;只读源码或验收脚本得出的推断不能替代失败证据。`, '不得读取、提交或转述敏感诱饵、配置密钥、Runtime 私有正文或项目绝对路径。', `任何项目交付文件都不得出现已废止标记“${goalInitialMarker}”。`, ], @@ -318,6 +326,7 @@ const state = { editedGoalSnapshotFingerprint: null, initialMarkerAbsenceCheckCount: 0, monitoredWriteActionIds: new Set(), + preFailureDeliveryActionIds: new Set(), runner: isolatedRunnerState, }, responseStream: { @@ -1208,10 +1217,10 @@ async function runGoalRuntimeE2e() { const editedPending = await waitForGoalRevisionPendingAction({ revision: state.goal.editedRevision, - marker: goalFinalMarker, codePrefix: 'goal-edited', minimumPlanRevision: initialPending.plan.revision + 1, requiredCompletedStepHashes: state.goal.initialCompletedStepHashes, + pendingMatcher: goalPendingMatchesRevisionTwoRepair, }); state.goal.editedPending = summarizeGoalPending(editedPending.pending); @@ -4609,6 +4618,7 @@ function goalPrivateBodyValues() { ...goalEditedPayload.verification, goalInitialMarker, goalFinalMarker, + goalFailureEvidenceCanary, ]; } @@ -4622,6 +4632,7 @@ function goalPublicBodyValues() { ...goalEditedPayload.verification, goalInitialMarker, goalFinalMarker, + goalFailureEvidenceCanary, ]; } @@ -5163,7 +5174,7 @@ function inspectStructuredPlanSnapshot(runtime, codePrefix) { function assertStructuredPlanContextSnapshot(runtime, contextBundle, code) { assert( - contextBundle.schemaVersion === 'game-creator-runtime-context-bundle.v3' && + contextBundle.schemaVersion === runtimeContextBundleSchemaVersion && contextBundle.agentId === runtime.agentId && contextBundle.taskId === runtime.taskId && contextBundle.sessionId === runtime.sessionId && @@ -5258,16 +5269,17 @@ function assertGoalMutationIdentity(mutation, expectedRevision, codePrefix) { } function goalSnapshotFingerprint(goal) { + // serde_json::Map uses lexicographically sorted keys without preserve_order. return hashValue( JSON.stringify({ - projectId: goal.projectId, - goalId: goal.goalId, agentId: goal.agentId, - sessionId: goal.sessionId, - runId: goal.runId, - revision: goal.revision, - outcome: goal.outcome, constraints: goal.constraints, + goalId: goal.goalId, + outcome: goal.outcome, + projectId: goal.projectId, + revision: goal.revision, + runId: goal.runId, + sessionId: goal.sessionId, verification: goal.verification, }), ); @@ -5291,7 +5303,7 @@ async function readGoalStatus() { function assertGoalContextSnapshot(runtime, contextBundle, goal, codePrefix) { const expectedGoalSnapshotFingerprint = goalSnapshotFingerprint(goal); assert( - contextBundle.schemaVersion === 'game-creator-runtime-context-bundle.v4' && + contextBundle.schemaVersion === runtimeContextBundleSchemaVersion && contextBundle.projectId === goal.projectId && contextBundle.agentId === runtime.agentId && contextBundle.taskId === runtime.taskId && @@ -5366,13 +5378,39 @@ function goalPendingMatchesDelivery(pending, marker) { ); } -function validateGoalPendingAction( - pending, - plan, - revision, - marker, - codePrefix, -) { +function goalPendingIsStandaloneDeliveryMutation(pending, marker) { + if (!goalPendingMatchesDelivery(pending, marker)) return false; + const action = pending?.record?.action ?? pending?.action; + return ( + action?.tool === 'file.write' || + (action?.tool === 'project.patchset' && action.input?.changes?.length === 1) + ); +} + +function goalPendingMatchesRevisionTwoRepair(pending) { + const action = pending?.record?.action ?? pending?.action; + const input = action?.input; + if (!action || !input || typeof input !== 'object') return false; + if (action.tool === 'file.patch') { + return input.path === 'game/index.html'; + } + if (action.tool === 'file.write') { + return ( + input.path === 'game/index.html' || input.path === patchsetCreatedPath + ); + } + return ( + action.tool === 'project.patchset' && + Array.isArray(input.changes) && + input.changes.some( + (change) => + change?.path === 'game/index.html' || + change?.path === patchsetCreatedPath, + ) + ); +} + +function validateGoalPendingAction(pending, plan, revision, codePrefix) { const record = pending.record; const expectedGoalSnapshotFingerprint = goalSnapshotFingerprint(plan.goal); assert( @@ -5391,8 +5429,7 @@ function validateGoalPendingAction( record.actionId === pending.actionId && record.action?.tool === pending.tool && ['pending', 'pending-confirmation'].includes(record.status) && - Number.isSafeInteger(record.plannedSteerCursor) && - goalPendingMatchesDelivery(pending, marker), + Number.isSafeInteger(record.plannedSteerCursor), `${codePrefix}-pending-action-invalid`, ); if (revision === state.goal.initialRevision) { @@ -5416,11 +5453,20 @@ function validateGoalPendingAction( async function waitForGoalRevisionPendingAction({ revision, - marker, codePrefix, minimumPlanRevision = 1, requiredCompletedStepHashes = [], + marker = null, + pendingMatcher = null, }) { + const matchesPending = + pendingMatcher ?? + ((pending) => goalPendingMatchesDelivery(pending, marker)); + assert( + typeof matchesPending === 'function' && + (pendingMatcher || isNonEmptyString(marker)), + `${codePrefix}-pending-matcher-invalid`, + ); const deadline = Date.now() + runTimeoutMs; let lastError = null; while (Date.now() < deadline) { @@ -5437,52 +5483,45 @@ async function waitForGoalRevisionPendingAction({ throw codedError(`${codePrefix}-runtime-terminal-before-pending`); } + let plan = null; let targetPending = null; try { - const plan = await readVerifiedGoalPlanSnapshot(`${codePrefix}-plan`); const pendingActions = (await findPendingActions()).filter( (pending) => pending.agentId === mainAgentId && pending.runId === state.initialRunId, ); - targetPending = pendingActions.find((pending) => - goalPendingMatchesDelivery(pending, marker), - ); - if (targetPending) { - validateGoalPendingAction( - targetPending, - plan, - revision, - marker, - codePrefix, - ); - assert( - plan.revision >= minimumPlanRevision && - plan.completedStepHashes.length > 0 && - plan.incompleteStepCount > 0 && - requiredCompletedStepHashes.every((stepHash) => - plan.completedStepHashes.includes(stepHash), - ), - `${codePrefix}-partial-plan-missing`, - ); - const messages = await readOptionalJsonl( - agentConversationPath(mainAgentId, state.initialSessionId), - ); - assert( - messages.filter((message) => message.role === 'assistant').length === - 0, - `${codePrefix}-assistant-before-control-point`, - ); - return { pending: targetPending, plan }; - } + targetPending = pendingActions.find(matchesPending); + plan = await readVerifiedGoalPlanSnapshot(`${codePrefix}-plan`); } catch (error) { + if (targetPending) { + throw codedError(`${codePrefix}-pending-contract-invalid`, error); + } lastError = error; } - await confirmPendingActions( - null, - (pending) => !goalPendingMatchesDelivery(pending, marker), - ); + if (targetPending) { + validateGoalPendingAction(targetPending, plan, revision, codePrefix); + assert( + plan.revision >= minimumPlanRevision && + plan.completedStepHashes.length > 0 && + plan.incompleteStepCount > 0 && + requiredCompletedStepHashes.every((stepHash) => + plan.completedStepHashes.includes(stepHash), + ), + `${codePrefix}-partial-plan-missing`, + ); + const messages = await readOptionalJsonl( + agentConversationPath(mainAgentId, state.initialSessionId), + ); + assert( + messages.filter((message) => message.role === 'assistant').length === 0, + `${codePrefix}-assistant-before-control-point`, + ); + return { pending: targetPending, plan }; + } + + await confirmPendingActions(null, (pending) => !matchesPending(pending)); await sleep(pollIntervalMs); } throw codedError(`${codePrefix}-pending-action-timeout`, lastError); @@ -5567,8 +5606,7 @@ async function waitForGoalOldActionBlocked(initialPending) { runtime.sessionId === state.initialSessionId && runtime.goalId === state.goal.goalId && runtime.goalRevision === state.goal.editedRevision && - contextBundle.schemaVersion === - 'game-creator-runtime-context-bundle.v4' && + contextBundle.schemaVersion === runtimeContextBundleSchemaVersion && contextBundle.goalId === state.goal.goalId && contextBundle.goalRevision === state.goal.editedRevision && contextBundle.goalSnapshotFingerprint === @@ -5651,15 +5689,26 @@ async function waitForGoalRevisionTwoAgentVerificationFailure() { (pending) => pending.agentId === mainAgentId && pending.runId === state.initialRunId, ); - const earlyWrite = pendingActions.find((pending) => - goalProjectWriteTools.has(pending.tool), + const unsupportedEarlyWrite = pendingActions.find( + (pending) => + goalProjectWriteTools.has(pending.tool) && + !goalPendingIsStandaloneDeliveryMutation(pending, goalFinalMarker), + ); + assert( + !unsupportedEarlyWrite, + 'goal-revision-two-repair-before-failed-verification', ); - assert(!earlyWrite, 'goal-revision-two-write-before-failed-verification'); await confirmPendingActions( - new Set(['project.checkpoint', 'project.verify']), + new Set([ + 'project.checkpoint', + 'project.verify', + 'file.write', + 'project.patchset', + ]), (pending) => pending.tool === 'project.checkpoint' || - pending.tool === 'project.verify', + pending.tool === 'project.verify' || + goalPendingIsStandaloneDeliveryMutation(pending, goalFinalMarker), ); lastError = codedError('goal-revision-two-agent-failure-not-yet-observed'); await sleep(pollIntervalMs); @@ -5724,17 +5773,21 @@ function findGoalRevisionTwoAgentVerificationFailure(records) { ) { continue; } - const earlyWrite = records + const unsupportedEarlyWrite = records .slice(state.goal.revisionTwoEditAgentDbBoundary, observationIndex + 1) .some( (record) => goalProjectWriteTools.has(record.tool) && + !state.goal.preFailureDeliveryActionIds.has(record.actionId) && [ 'agent.runtime.tool_action.executing', 'agent.runtime.tool_confirmation.approved', ].includes(record.recordType), ); - assert(!earlyWrite, 'goal-revision-two-write-executed-before-failure'); + assert( + !unsupportedEarlyWrite, + 'goal-revision-two-repair-executed-before-failure', + ); return { audit, auditIndex, observationIndex, startIndex }; } return null; @@ -6522,6 +6575,7 @@ function validateResponseStreamProviderLifecycle(agentDb, stream) { 'requestId', 'requestKind', 'requestSlot', + 'webSearchEnabled', 'status', 'schemaVersion', 'updatedAt', @@ -6529,11 +6583,11 @@ function validateResponseStreamProviderLifecycle(agentDb, stream) { assert(records.length === 2, 'response-stream-final-lifecycle-count-invalid'); for (const record of records) { assert( - record.auditSchemaVersion === - 'game-creator-provider-request-lifecycle.v1' && + record.auditSchemaVersion === providerRequestLifecycleSchemaVersion && record.taskId === stream.taskId && record.sessionId === state.initialSessionId && record.requestKind === 'final-reply' && + record.webSearchEnabled === false && record.requestSlot === stream.requestSlot && isNonEmptyString(record.requestId) && Number.isSafeInteger(record.updatedAt) && @@ -7135,8 +7189,7 @@ function validateWebSearchProviderLifecycle(agentDb, runtimeState) { ); for (const record of records) { assert( - record.auditSchemaVersion === - 'game-creator-provider-request-lifecycle.v2' && + record.auditSchemaVersion === providerRequestLifecycleSchemaVersion && record.taskId === runtimeState.taskId && record.sessionId === state.initialSessionId && ['tool-plan', 'final-reply'].includes(record.requestKind) && @@ -7987,8 +8040,7 @@ async function captureGoalPausedSnapshot(expectedPending, codePrefix) { latest?.status === 'paused' && latest?.phase === 'paused' && JSON.stringify(targetRuns) === JSON.stringify([state.initialRunId]) && - contextBundle.schemaVersion === - 'game-creator-runtime-context-bundle.v4' && + contextBundle.schemaVersion === runtimeContextBundleSchemaVersion && contextBundle.goalId === state.goal.goalId && contextBundle.goalRevision === state.goal.editedRevision && contextBundle.goalStatus === 'active' && @@ -8381,11 +8433,19 @@ async function confirmPendingActions( const monitorsGoalWrite = isGoalRuntimeSuite() && goalProjectWriteTools.has(pending.tool); if (monitorsGoalWrite) { + const allowedRevisionTwoDelivery = + state.goal.editedRevision > 0 && + state.goal.revisionTwoFailureObserved !== true && + goalPendingIsStandaloneDeliveryMutation(pending, goalFinalMarker); assert( state.goal.editedRevision === 0 || - state.goal.revisionTwoFailureObserved === true, + state.goal.revisionTwoFailureObserved === true || + allowedRevisionTwoDelivery, 'goal-write-confirmed-before-revision-two-failure', ); + if (allowedRevisionTwoDelivery) { + state.goal.preFailureDeliveryActionIds.add(pending.actionId); + } await assertGoalInitialMarkerAbsent( 'goal-write-before-confirm', pending.actionId, @@ -10133,7 +10193,7 @@ async function validateLandedEvidence() { const runtimeStatePath = mainRuntimeStatePath(); const runtimeState = await readJson(runtimeStatePath); assert( - contextBundle.schemaVersion === 'game-creator-runtime-context-bundle.v3' && + contextBundle.schemaVersion === runtimeContextBundleSchemaVersion && contextBundle.agentId === mainAgentId && contextBundle.runId === state.initialRunId && typeof contextBundle.repositoryContextFingerprint === 'string' && @@ -12032,9 +12092,12 @@ async function validateGoalRuntimeEvidence() { Buffer.from(JSON.stringify(records)), goalPublicBodyValues(), ); + assert( + goalPublicBodyLeakCounts[surface] === 0, + `goal-body-public-${surface}-leak-detected`, + ); } const goalPublicBodyLeakCount = sumObjectValues(goalPublicBodyLeakCounts); - assert(goalPublicBodyLeakCount === 0, 'goal-body-public-leak-detected'); const projectPathPublicLeakCounts = validateProjectRootPublicLeakBoundary( publicSurfaces, 'goal-public', @@ -12088,6 +12151,8 @@ async function validateGoalRuntimeEvidence() { goalInitialMarkerAbsenceCheckCount: state.goal.initialMarkerAbsenceCheckCount, goalMonitoredWriteActionCount: state.goal.monitoredWriteActionIds.size, + goalPreFailureDeliveryActionCount: + state.goal.preFailureDeliveryActionIds.size, goalFinalStatus: goal.status, goalEditProviderInterrupted: state.goal.editProviderInterrupted, goalPauseProviderInterrupted: state.goal.pauseProviderInterrupted, @@ -12356,12 +12421,14 @@ function validateGoalProviderRequestLifecycle(agentDb) { const byRequest = new Map(); for (const record of records) { assert( - record.auditSchemaVersion === - 'game-creator-provider-request-lifecycle.v1' && + record.auditSchemaVersion === providerRequestLifecycleSchemaVersion && record.taskId && record.sessionId === state.initialSessionId && ['tool-plan', 'final-reply'].includes(record.requestKind) && typeof record.requestSlot === 'string' && + typeof record.webSearchEnabled === 'boolean' && + (record.requestKind !== 'final-reply' || + record.webSearchEnabled === false) && !['prompt', 'response', 'error', 'baseUrl', 'model'].some((key) => Object.hasOwn(record, key), ), @@ -13108,6 +13175,7 @@ function emptyGoalEvidence() { goalSnapshotFingerprintChanged: false, goalInitialMarkerAbsenceCheckCount: 0, goalMonitoredWriteActionCount: 0, + goalPreFailureDeliveryActionCount: 0, goalFinalStatus: null, goalEditProviderInterrupted: false, goalPauseProviderInterrupted: false, @@ -13629,6 +13697,8 @@ async function collectPartialGoalEvidence() { goalInitialMarkerAbsenceCheckCount: state.goal.initialMarkerAbsenceCheckCount, goalMonitoredWriteActionCount: state.goal.monitoredWriteActionIds.size, + goalPreFailureDeliveryActionCount: + state.goal.preFailureDeliveryActionIds.size, goalFinalStatus: goal?.status ?? runtime?.goalStatus ?? null, goalRunnerKillMethod: state.goal.runner.pidfdClaimCount > 0 ? 'linux-pidfd' : null, @@ -15326,21 +15396,27 @@ function validateToolActionReplays(records) { 'tool-action-replay-identity-missing', ); const attempt = { + recordType: record.recordType, agentId: record.agentId, runId: record.runId, actionId: record.actionId, actionFingerprint: record.actionFingerprint, tool: record.tool, + status: record.status ?? null, + rawInputSummary: record.inputSummary ?? null, inputSummary: canonicalAuditInputSummary(record.inputSummary), }; const existing = attemptsByActionId.get(attempt.actionId); if (existing) { - assert( + const sameIdentity = existing.agentId === attempt.agentId && - existing.runId === attempt.runId && - existing.actionFingerprint === attempt.actionFingerprint && - existing.tool === attempt.tool && - existing.inputSummary === attempt.inputSummary, + existing.runId === attempt.runId && + existing.actionFingerprint === attempt.actionFingerprint && + existing.tool === attempt.tool && + existing.inputSummary === attempt.inputSummary; + assert( + sameIdentity || + goalStaleActionReceiptMatchesOriginalAction(existing, attempt), 'tool-action-replay-identity-conflict', ); } else { @@ -15392,6 +15468,26 @@ function validateToolActionReplays(records) { }; } +function goalStaleActionReceiptMatchesOriginalAction(original, receipt) { + if ( + !isGoalRuntimeSuite() || + receipt.recordType !== 'agent.runtime.action_receipt' || + receipt.tool !== 'runtime.goal' || + receipt.status !== 'blocked' || + !goalProjectWriteTools.has(original.tool) || + !isNonEmptyString(original.rawInputSummary) || + original.agentId !== receipt.agentId || + original.runId !== receipt.runId || + original.actionFingerprint !== receipt.actionFingerprint + ) { + return false; + } + const expectedReceiptSummary = canonicalAuditInputSummary( + `inputSummarySha256=${hashValue(original.rawInputSummary)} · inputSummaryChars=${[...original.rawInputSummary].length}`, + ); + return receipt.inputSummary === expectedReceiptSummary; +} + function canonicalAuditInputSummary(summary) { if (summary == null || summary === '') return '[empty]'; assert(typeof summary === 'string', 'audit-input-summary-invalid'); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 32d182ee6..c98083b5f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -4174,6 +4174,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( }; runtime.pending_tool_action = Some(pending.summary()); runtime.updated_at = unix_timestamp(); + let public_observation_detail = agent_runtime_public_observation_detail(&root, &observation); let persisted = append_game_creator_agent_runtime_task_projection_once(&root, &runtime, &pending.action_id) .and_then(|_| refresh_game_creator_agent_runtime_task_queue(&root, &mut runtime)) @@ -4186,7 +4187,7 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action( "running", "observation", &observation_summary, - agent_runtime_public_observation_detail(&observation), + public_observation_detail.as_deref(), &pending.action_id, ) }) @@ -4375,7 +4376,7 @@ fn mark_game_creator_agent_runtime_tool_observation_needs_reconciliation_at( Some(&pending.action_id), ); complete_agent_runtime_active_plan_step(runtime, "failed", &observation_summary); - let mut error = agent_runtime_public_observation_detail(observation) + let mut error = agent_runtime_public_observation_detail(root, observation) .filter(|detail| !detail.trim().is_empty()) .map(|detail| format!("{observation_summary};{detail}")) .unwrap_or(observation_summary); @@ -6355,6 +6356,8 @@ async fn run_game_creator_agent_background_task_pass_with_context( }; } runtime.updated_at = unix_timestamp(); + let public_observation_detail = + agent_runtime_public_observation_detail(&root, &observation); let persistence = append_game_creator_agent_runtime_task_projection_once( &root, &runtime, @@ -6377,7 +6380,7 @@ async fn run_game_creator_agent_background_task_pass_with_context( "observation" }, observation_summary.as_str(), - agent_runtime_public_observation_detail(&observation), + public_observation_detail.as_deref(), observation_action_identity .as_ref() .map(|identity| identity.0.as_str()) @@ -11643,7 +11646,7 @@ impl AgentRuntimeToolObservation { } } -fn agent_runtime_public_observation_detail( +fn agent_runtime_local_observation_detail( observation: &AgentRuntimeToolObservation, ) -> Option<&str> { if matches!( @@ -11659,6 +11662,18 @@ fn agent_runtime_public_observation_detail( } } +fn agent_runtime_public_observation_detail( + root: &Path, + observation: &AgentRuntimeToolObservation, +) -> Option { + if observation.tool == "agent.action_history" && observation.status == "ok" { + let detail = observation.detail.as_deref()?; + validate_agent_runtime_pending_serialized_content(root, detail).ok()?; + return Some(detail.to_string()); + } + agent_runtime_action_receipt_safe_detail(root, observation) +} + pub(crate) fn advance_agent_runtime_project_revision_locked(root: &Path) -> Result { let mut revision = read_game_creator_agent_runtime_project_revision(root)?; let next_revision = revision @@ -12505,7 +12520,7 @@ pub(crate) fn append_agent_runtime_tool_call_record( .map(|value| sanitize_agent_runtime_text(value, 240)) .filter(|value| !value.trim().is_empty()), summary: sanitize_agent_runtime_text(&observation.summary, 240), - detail: agent_runtime_public_observation_detail(observation) + detail: agent_runtime_local_observation_detail(observation) .map(|value| sanitize_agent_runtime_text(value, 500)) .filter(|value| !value.trim().is_empty()), updated_at: unix_timestamp(), @@ -17496,6 +17511,7 @@ fn observe_agent_runtime_memory_write( write_isolated_agent_private_memory_at(root, &target_agent_id, &next_content) }) .and_then(|path| { + let relative_path = normalize_relative_path(&path)?; append_agent_db_record( root, serde_json::json!({ @@ -17503,7 +17519,7 @@ fn observe_agent_runtime_memory_write( "agentId": agent_id, "targetAgentId": target_agent_id, "scope": "agent", - "path": path, + "path": relative_path, "mode": if overwrite { "overwrite" } else { "append" }, "memoryLane": "isolated-instance-private", }), @@ -17518,6 +17534,7 @@ fn observe_agent_runtime_memory_write( write_local_agent_memory_at(root, &target_agent_id, &next_content) }) .and_then(|memory| { + let relative_path = relative_project_path(root, Path::new(&memory.path))?; append_agent_db_record( root, serde_json::json!({ @@ -17525,7 +17542,7 @@ fn observe_agent_runtime_memory_write( "agentId": agent_id, "targetAgentId": target_agent_id, "scope": "agent", - "path": memory.path, + "path": relative_path, "mode": if overwrite { "overwrite" } else { "append" }, }), ) @@ -17553,13 +17570,14 @@ fn observe_agent_runtime_memory_write( write_local_game_memory_at(root, game_scope, &next_content) }) .and_then(|memory| { + let relative_path = relative_project_path(root, Path::new(&memory.path))?; append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.memory.write", "agentId": agent_id, "scope": memory.scope, - "path": memory.path, + "path": relative_path, "mode": if overwrite { "overwrite" } else { "append" }, }), ) @@ -18942,6 +18960,26 @@ fn observe_agent_runtime_file_write( detail: None, }; }; + if content.trim().is_empty() { + return AgentRuntimeToolObservation { + tool: "file.write".to_string(), + status: "failed".to_string(), + summary: "缺少非空 content".to_string(), + detail: None, + }; + } + let content_chars = content.chars().count(); + if content_chars > AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS { + return AgentRuntimeToolObservation { + tool: "file.write".to_string(), + status: "failed".to_string(), + summary: format!( + "content 不能超过 {} 字符", + AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS + ), + detail: None, + }; + } let _lock = match acquire_project_write_lock(root, "file.write") { Ok(lock) => lock, Err(error) => { @@ -18958,18 +18996,17 @@ fn observe_agent_runtime_file_write( { return agent_runtime_mutation_gate_failure_observation(root, "file.write", &error); } - let content = truncate_agent_runtime_text( + let observation_content = truncate_agent_runtime_text( sanitize_prompt_context(content).as_str(), AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS, ); - let result = write_local_project_file_at(root, path, &content).and_then(|written| { + let result = write_local_project_file_at(root, path, content).and_then(|written| { append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.file.write", "agentId": agent_id, "path": written.path, - "absolutePath": written.absolute_path, }), ) .map(|()| written) @@ -18979,7 +19016,7 @@ fn observe_agent_runtime_file_write( tool: "file.write".to_string(), status: "ok".to_string(), summary: format!("已写入 {}", written.path), - detail: Some(content), + detail: Some(observation_content), }, Err(error) => AgentRuntimeToolObservation { tool: "file.write".to_string(), @@ -27113,6 +27150,9 @@ fn append_game_creator_agent_runtime_event_with_action( && candidate.phase == event.phase }) { + let legacy_detail_is_stricter_projection_compatible = event.event_type == "observation" + && event.detail.is_none() + && existing.detail.is_some(); if existing.agent_id != event.agent_id || existing.task_id != event.task_id || existing.session_id != event.session_id @@ -27121,7 +27161,8 @@ fn append_game_creator_agent_runtime_event_with_action( || existing.status != event.status || existing.phase != event.phase || existing.summary != event.summary - || existing.detail != event.detail + || (existing.detail != event.detail + && !legacy_detail_is_stricter_projection_compatible) { return Err(format!( "Agent Runtime action event 幂等身份冲突:actionId={action_id}" diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 303f41d92..ff83eb962 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -11538,7 +11538,11 @@ async fn runtime_v11_closure_isolated_child_memory_is_instance_private() { }, ) .await; - assert_eq!(write.status, "ok", "{}", instance.instance_id); + assert_eq!( + write.status, "ok", + "{}: {}", + instance.instance_id, write.summary + ); } for (instance, own_marker, sibling_marker) in [ @@ -13510,11 +13514,21 @@ async fn background_agent_runtime_can_write_memory_and_project_files() { .content .contains("项目主题是月光厨房对抗暗影厨具。")); let notes = fs::read_to_string(root.join("game/agent-notes.md")).expect("notes"); - assert!(notes.contains("月光厨房:收集食材,躲避暗影厨具。")); + assert_eq!( + notes, + "# Agent Notes\n\n月光厨房:收集食材,躲避暗影厨具。\n" + ); + let public_events = + fs::read_to_string(root.join(".agent/runtime/events/design-director.jsonl")) + .expect("public runtime events"); + assert!(!public_events.contains("# Agent Notes")); + assert!(!public_events.contains("月光厨房:收集食材,躲避暗影厨具。")); let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); assert!(agent_db.contains("\"recordType\":\"agent.runtime.memory.write\"")); assert!(agent_db.contains("\"recordType\":\"agent.runtime.file.write\"")); assert!(agent_db.contains("\"path\":\"game/agent-notes.md\"")); + assert!(!agent_db.contains("\"absolutePath\"")); + assert!(!agent_db.contains(root.to_string_lossy().as_ref())); let records = read_agent_db_records_for_test(&root); let executing_records = records .iter() @@ -16890,7 +16904,7 @@ async fn background_agent_runtime_requires_verification_after_project_mutation() .any(|item| item.contains("project.verify:ok · check:agent 已通过"))); assert_eq!( fs::read_to_string(root.join("game/revision-gate.txt")).expect("read gated file"), - "revision=1" + "revision=1\n" ); fs::remove_dir_all(root).ok(); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 49604cea6..8f259ad0a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4648,6 +4648,14 @@ - 客户端:Project Supervisor 主聊天、启动器开发 Agent 聊天和项目内 Agent 弹窗复用同一问题卡;等待时普通输入/steer 禁用,卡片不随 Runtime 详情折叠,失败重试保持同一 responseId。 - 真实验收:2026-07-16 正式 `openai_chat / gpt-5.5` 的 `user-input-runtime` suite PASS。Project Supervisor 自主提出 1 题/2 选项,Runner pidfd 强杀换 boot 后 Provider started 保持 `1 -> 1`,回答后同 Agent/Session/run 完成唯一最终 assistant;会话问题/答案各 1,重复 message、公共正文、API Key、项目/配置路径和报告泄漏均为 0,隔离现场已清理。 +## 2026-07-16 AI 游戏创作 Agent Runtime V1.18 真实 Goal Provider 验收收口 + +- 真实结论:正式 AppData 的 `openai_chat / gpt-5.5` 路由通过隔离 `goal-runtime` suite。Goal revision 1 的旧待确认写动作在 revision 2 形成唯一 `runtime.goal / blocked` receipt 且零执行/零重放;Agent 取得真实退出码 1 后用一个 patchset 修复,暂停、Linux pidfd 强杀、Runner 换 boot 与显式 resume 全部保持原 Agent/Session/run,稳定窗口中 task/plan/conversation/Provider/action 零推进。 +- 完成证据:最终代码快照复跑的结构化计划 revision 11 的 8 步全部完成,11 组 Provider lifecycle 均唯一闭合,finalization v3 四阶段与两层 completed projection 完整,Session 只有 1 条 assistant;3 个副作用无重放,重复 action/message/receipt、Goal 正文和失败证据 canary、API Key、诱饵、项目/配置绝对路径以及报告泄漏均为 0。当前 context bundle 生产 schema 为 v5,Provider lifecycle 为 v2。 +- 同轮修正:`file.write` 只校验非空和上限,合法正文按原字符落盘,不能再经 prompt 清洗或静默截断末尾换行;旧 Goal action 的 `runtime.goal / blocked` receipt 是合法终态转换,验收器必须核对同 actionId/指纹及原输入摘要哈希,不能误判为重放身份冲突;公共 event 的 observation detail 复用安全 receipt 元数据,不保存 `file.read / project.diff` 正文;file/memory 专用审计只保存项目内相对路径,不写 `absolutePath` 或项目根。 +- 恢复兼容:公开 observation event 收紧为安全 receipt 元数据后,恢复旧 action event 时只允许“旧 detail 存在、当前投影省略 detail”这一种向更严格投影迁移;Agent/Task/Session/Run/action、状态、阶段和摘要仍须完全一致。其他 payload 差异继续按幂等身份冲突失败关闭,避免旧项目因脱敏升级永久停在 `needs-reconciliation`。 +- 验收约束:revision 2 的 Goal 明确要求任何修复前先运行项目声明的原始验收并观察非零退出,不指定命令或修复配方;一次性失败证据 canary 按 event/Agent DB/receipt/activity/output 分面扫描。保留现场完成二次核对后,隔离 Runner、AppData 和 disposable 项目均按 sentinel 清理。 + ## 2026-07-13 普通微信支付 V3 退款使用统一观察事务闭环 - 背景:普通微信支付 V3 的退款申请响应、退款结果回调、主动查单和商户平台手工退款发现可能重复、乱序或只出现其中一种;原充值订单只有单一终态,无法表达多次部分退款、权益回收欠款和会员人工处理。 diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md index ae662441d..72b3c3b82 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md @@ -775,13 +775,13 @@ V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任 ### 暂停、恢复与清理 -- pause 写 durable request,并通过 typed Runner `runtime.pause` 中断当前 planning/final Provider;已进入工具的动作允许返回后再停。Provider 注册、durable Goal control/cancel 二次复核与 `started` 审计使用同一项目写锁形成线性化边界:锁内同时核对 cancel tombstone、queued steer、Goal 状态和 task 绑定的 Goal revision,edit 已提交新 revision 但 steer 尚未落盘时也不得轮询旧 Provider 或写伪 `started`;请求先提交时才属于可中断或等待安全边界的在途调用。Provider 中断或返回边界先把可恢复 continuation 写入 v4 context bundle,其中恢复快照按 `active` Goal 语义保存,随后 Runtime 才在 LLM/工具/observation/finalization 安全边界把同一 run 收束为 `paused`。Runner 重启时先处理 cancel / Goal control,再进入 process reconciliation、finalization 和 pending action;`pause-requested` 会先收束成 `paused`,`paused` 直接保持休眠,不生成 assistant、不创建新 run、不调用 Provider。 +- pause 写 durable request,并通过 typed Runner `runtime.pause` 中断当前 planning/final Provider;已进入工具的动作允许返回后再停。Provider 注册、durable Goal control/cancel 二次复核与 `started` 审计使用同一项目写锁形成线性化边界:锁内同时核对 cancel tombstone、queued steer、Goal 状态和 task 绑定的 Goal revision,edit 已提交新 revision 但 steer 尚未落盘时也不得轮询旧 Provider 或写伪 `started`;请求先提交时才属于可中断或等待安全边界的在途调用。Provider 中断或返回边界先把可恢复 continuation 写入当前 v5 context bundle(V1.18 初版为 v4,V1.21 增加压缩状态后升级),其中恢复快照按 `active` Goal 语义保存,随后 Runtime 才在 LLM/工具/observation/finalization 安全边界把同一 run 收束为 `paused`。Runner 重启时先处理 cancel / Goal control,再进入 process reconciliation、finalization 和 pending action;`pause-requested` 会先收束成 `paused`,`paused` 直接保持休眠,不生成 assistant、不创建新 run、不调用 Provider。 - 暂停父 Agent 不撤销已经 durable 投递的专业 child;child 可以把结果写成 ready,但父 run 在显式 resume 前不能认领或继续 Provider。Runner-owned process session 在暂停提交前终止,恢复后由 Agent 根据 observation 重规划,禁止按 PID 重连或重放未知 start。 -- resume 的有效状态迁移只接受 `paused -> active`,先清理同一 run 遗留的 cancel tombstone,再把原 Agent/Session/run 重新投影为 pending 并唤醒 External Runner;不创建 retry run。若进程在 Goal sidecar 已写成 `active`、Runtime 投影或 Runner 唤醒尚未完成时失败,重复 resume 必须识别同一 run 的半提交并继续补齐,不能把 `active` 单独当成恢复成功或直接返回。pending confirmation 仍回到确认态,普通 planning 从 v4 context bundle 继续。clear 对活跃 Goal 复用取消 tombstone,待安全取消后把 Goal 写为 cleared;paused/completed Goal 可直接清理。clear 不删除历史 conversation、task、event 或 Goal history。 +- resume 的有效状态迁移只接受 `paused -> active`,先清理同一 run 遗留的 cancel tombstone,再把原 Agent/Session/run 重新投影为 pending 并唤醒 External Runner;不创建 retry run。若进程在 Goal sidecar 已写成 `active`、Runtime 投影或 Runner 唤醒尚未完成时失败,重复 resume 必须识别同一 run 的半提交并继续补齐,不能把 `active` 单独当成恢复成功或直接返回。pending confirmation 仍回到确认态,普通 planning 从当前 v5 context bundle 继续。clear 对活跃 Goal 复用取消 tombstone,待安全取消后把 Goal 写为 cleared;paused/completed Goal 可直接清理。clear 不删除历史 conversation、task、event 或 Goal history。 ### 恢复与完成门禁 -- context bundle 升级为 `game-creator-runtime-context-bundle.v4`,绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`;v3 在现有身份、计划和 verification 校验通过后从 Runtime/Goal sidecar 补齐,v2 继续先按 V1.17 迁移计划再补 Goal。v4 任一 Goal 身份、revision 或快照不一致都失败关闭。当前 Agent/Session/run 的 Goal sidecar 无法读取、损坏或身份冲突时,即使 legacy/半写 Runtime 尚无 `goalId` 投影也必须进入 `needs-reconciliation`,不得退化成无 Goal run 或绕过完成门禁。 +- V1.18 首次把 context bundle 升级为 v4 并绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`;当前生产 schema 已由 V1.21 扩展为 `game-creator-runtime-context-bundle.v5`。v4 在原 Goal/计划/verification 身份通过后补入当前压缩字段并写回 v5,v3 先补 Goal,v2 继续先按 V1.17 迁移计划再补 Goal;任一当前 Goal 身份、revision 或快照不一致都失败关闭。当前 Agent/Session/run 的 Goal sidecar 无法读取、损坏或身份冲突时,即使 legacy/半写 Runtime 尚无 `goalId` 投影也必须进入 `needs-reconciliation`,不得退化成无 Goal run 或绕过完成门禁。 - pending action 升级为 `game-creator-pending-action.v5`,在既有 project revision、verification gate、repository context fingerprint 和 steer cursor 外,固定绑定 `goalId / goalRevision / goalSnapshotFingerprint`。旧 v1-v4 一律失败关闭,不能从当前 Goal 猜回缺失绑定;Goal edit 后,无论动作原为自动还是待确认,都把旧记录收束成稳定 `blocked` observation 并在同一 run 重规划。 - finalization journal 升级为 `game-creator-runtime-finalization.v3`,把 Goal 快照指纹纳入 finalizationId。prepared 回复必须绑定当前 active Goal、同一 run/revision、全部完成的结构化计划、清空的 process/join/delegate 屏障和现有 verification gate。Goal 编辑、暂停、清理或 revision 漂移会让未提交 assistant 的旧 finalization 失效并回到同 run;assistant 已提交后只允许按 journal 原快照补齐 Runtime 与 Goal completed,不能重新请求 Provider。 - assistant 持久化后,finalization 先写 Runtime completed task/state,再把规范 Goal 写为 completed,并补写携带 completed Goal 状态的 task/state projection;两层投影均可靠后,journal 才推进 `runtime-completed` 并删除。完成证据由系统从最终结构化计划、verification gate、run/session 身份和 response fingerprint 生成,不保存模型 thinking 或原始私有 observation。Goal 未完成、paused、clearing、sidecar 缺失或 revision 不匹配时,response 不能绕过完成门禁。 @@ -794,13 +794,13 @@ V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任 - background planning / final reply 的专用 Provider 客户端强制 `max_retries=0`;每个 `agent.runtime.provider_request.lifecycle` 从 `started` 到唯一 `completed / failed / interrupted` 最多对应一次物理请求。`Timeout / Connectivity / Transport / EmptyResponse / 408 / 429 / 5xx` 以及无法证明请求未被上游接收的其它错误,不得在同一 lifecycle 内自动原样重放;只记录 error kind、SHA-256、字符数或脱敏摘要。显式 steer、Goal resume 或人工 reconciliation 决定再次调用时,必须使用新的 request slot/lifecycle;格式修复同样使用 `loop--repair-` 新 slot,不能伪装成底层 retry。 - 每次 request snapshot 固定绑定 `projectId / agentId / taskId / sessionId / runId / source / goalId / goalRevision / goalSnapshotFingerprint / appliedSteerCursor / requestKind / requestSlot`,requestId 从该闭集稳定派生。真正进入 Provider future 前,Runtime 在同一项目写锁内重读 task/Runtime 身份、queued steer、cancel tombstone、规范 Goal 状态与快照;已生效控制只返回未启动,不得写伪 `started`。Provider lifecycle 的生产字段闭集只允许 `recordType / auditSchemaVersion / agentId / taskId / sessionId / runId / source / requestId / requestKind / requestSlot / status`,持久层只可再添加统一 `schemaVersion / updatedAt` envelope;不包含 prompt、工具输入、URL、模型、回复或错误正文。 - 启动新请求前必须在 Agent DB 锁内全量扫描同 Agent/run 的 Provider lifecycle,不依赖 recent tail。只要发现 `started` 后没有可信唯一终态,就把原 run/task/state 收束到 `needs-reconciliation` orphan barrier,阻断后续 Provider、工具和 finalization;同 request 多终态、缺 started、物理顺序倒置、重复阶段、身份/字段冲突或额外生产字段同样失败关闭,禁止自动补发。paused Runner 重启窗口必须以 started 数量零增长证明没有暗中请求,不能只看 plan/action 是否落盘。 -- 确定性验收覆盖:单 Session 单 Goal、跨 Agent/Session 隔离、expectedRevision 幂等/冲突、活跃 Goal 阻止新 run、Provider in-flight pause、工具返回后 pause、paused 重启不自启、同 run resume、pending confirmation 恢复、编辑触发 steer 与旧动作失效、clear/cancel 竞态、v4/v3/v2 恢复、v3 finalization、assistant 后崩溃补齐 completed,以及 Goal 元数据零 project revision/policy 变化。 +- 确定性验收覆盖:单 Session 单 Goal、跨 Agent/Session 隔离、expectedRevision 幂等/冲突、活跃 Goal 阻止新 run、Provider in-flight pause、工具返回后 pause、paused 重启不自启、同 run resume、pending confirmation 恢复、编辑触发 steer 与旧动作失效、clear/cancel 竞态、v5/v4/v3/v2 恢复、v3 finalization、assistant 后崩溃补齐 completed,以及 Goal 元数据零 project revision/policy 变化。 - 真实 Provider 使用现有 `agent-runtime-real-e2e.mjs` 的独立 `goal-runtime` suite 和一次性项目证明,不新增平行验收器。suite 只要求 AppData 中 `code-prototype` 的真实 LLM 配置,不要求 Chrome 或 External Editor API。revision 1 必须先形成至少三步、已有 completed 且仍有未完成步骤的计划,并停在一个绑定 Goal revision 1 的 `game-creator-pending-action.v5` 写动作;编辑到 revision 2 后,该旧动作必须形成 `runtime.goal / blocked` observation 且旧标记从未落盘,同一 run 再形成绑定 revision 2 的 v5 写动作。 - `goal-runtime` 不复用正在运行的正式 AppData Runner。验收器在用户提供的 AppData 下创建 `0700` 专用子目录和带随机 owner token/PID/时间的 sentinel;主配置与可选 local 配置只以普通文件 hardlink 复用,结束时复核 source/link 的 device、inode 与 SHA-256,全程不复制或输出 API Key。所有 Goal/Runner CLI 统一附加该专用 `runtimeConfigDir`。SIGKILL 前必须同时核对 sentinel、endpoint、Runner boot/PID/port、实际 CLI 路径、`--agent-runner --config-dir` argv 和 OS 启动指纹;endpoint 在已认领后异常消失时,只允许按先前同一启动指纹回收。成功或失败都先停止自有 Runner,再按 sentinel 和受限目录前缀删除专用 AppData;身份不一致时保留现场并失败,禁止猜测或清理其它 Runner。 - revision 2 验证 fixture 只在 revision 1 待确认动作与隔离证明完成后注入,并先由宿主真实执行一次失败命令形成不可伪造的失败边界。失败报告对 task、event、Agent DB 和 conversation 分面容错读取,截断尾行保留已完成 JSONL 记录并单独报告读取错误;不得把某一分面损坏折算成其它分面全为零。 -- revision 2 写动作待确认时执行 pause;命令返回、Goal status 与 Runtime state 都必须是 durable `paused`。随后记录 Goal、`game-creator-runtime-context-bundle.v4`、pending v5、计划、task/event/Agent DB、conversation 和副作用计数,SIGKILL Runner 并使用全局 `--agent-resume` 启动新 boot;至少两个稳定采样窗口内上述运行证据不得推进,不得新增 Provider plan、工具执行、assistant 或主 run。只有显式 `--agent-goal-resume` 后才允许确认 revision 2 动作并继续。 -- 最终 Goal、Runtime 和最新 task 必须在原 Agent/Session/run 上 completed,结构化计划全部完成,Goal completion evidence 必须精确匹配当前 Goal/plan/verification/run/session;同一 finalizationId 必须按严格七槽物理顺序形成完整记录,finalization sidecar 最终不残留,目标 Session 只允许一个 assistant。Goal sidecar、task JSONL、Runtime state、v4 context 和 conversation 属于本地私有执行事实,可包含完成目标所需正文;event、Agent DB、receipt、activity、output 与最终报告不得保存 task、Goal/steer、委派任务、verify 命令或 error 正文,只允许身份/状态、SHA-256、字符/字节/条目计数和经 URL、项目根、其它绝对路径及凭据清洗的有界摘要。公共 task 统一不保留正文;委派只保留 `taskSha256 / taskChars`,verify 只保留脚本安全标识、`expectedCommandSha256 / expectedCommandChars`、timeout 和结果计数,error 只保留 kind/fingerprint/chars 或脱敏摘要,禁止任何正文、preview、head 或 tail。验收必须扫描完整 Goal/编辑/委派/verify/error canary、已加载密钥和一次性项目绝对路径在全部公共持久面泄漏为 0,并确认动作、消息、receipt 和 Provider lifecycle 均无重复。 -- 截至 2026-07-15,V1.18 真实 Provider 门禁尚未通过。最新保留现场在首轮 planning、`planRevision=0`、零 pending action/observation 时由对端关闭长 TLS 连接;同一发布配置、模型和 Rust native-tls 客户端的最小单 Agent 请求在 25.2 秒成功,证明基础鉴权与短请求通道可用,但不能外推为工具 planning 或 Goal 长链路 PASS。Provider 长请求恢复后仍需完整执行上一条一次性项目验收。 +- revision 2 写动作待确认时执行 pause;命令返回、Goal status 与 Runtime state 都必须是 durable `paused`。随后记录 Goal、`game-creator-runtime-context-bundle.v5`、pending v5、计划、task/event/Agent DB、conversation 和副作用计数,SIGKILL Runner 并使用全局 `--agent-resume` 启动新 boot;至少两个稳定采样窗口内上述运行证据不得推进,不得新增 Provider plan、工具执行、assistant 或主 run。只有显式 `--agent-goal-resume` 后才允许确认 revision 2 动作并继续。 +- 最终 Goal、Runtime 和最新 task 必须在原 Agent/Session/run 上 completed,结构化计划全部完成,Goal completion evidence 必须精确匹配当前 Goal/plan/verification/run/session;同一 finalizationId 必须按严格七槽物理顺序形成完整记录,finalization sidecar 最终不残留,目标 Session 只允许一个 assistant。Goal sidecar、task JSONL、Runtime state、v5 context 和 conversation 属于本地私有执行事实,可包含完成目标所需正文;event、Agent DB、receipt、activity、output 与最终报告不得保存 task、Goal/steer、委派任务、verify 命令或 error 正文,只允许身份/状态、SHA-256、字符/字节/条目计数和经 URL、项目根、其它绝对路径及凭据清洗的有界摘要。公共 task 统一不保留正文;委派只保留 `taskSha256 / taskChars`,verify 只保留脚本安全标识、`expectedCommandSha256 / expectedCommandChars`、timeout 和结果计数,error 只保留 kind/fingerprint/chars 或脱敏摘要,禁止任何正文、preview、head 或 tail。验收必须扫描完整 Goal/编辑/委派/verify/error canary、已加载密钥和一次性项目绝对路径在全部公共持久面泄漏为 0,并确认动作、消息、receipt 和 Provider lifecycle 均无重复。 +- 2026-07-16 使用正式 AppData 的 `openai_chat / gpt-5.5` 路由执行隔离 `goal-runtime` suite,V1.18 真实 Provider 门禁 **PASS**。同一 Agent/Session/run 从 Goal revision 1 编辑到 revision 2,保留 1 个已完成计划步骤;旧写动作形成 1 条 `runtime.goal / blocked` receipt,执行与重放均为 0。Agent 先取得真实退出码 1,再用 1 个 patchset 修复并通过 revision 2 verification;暂停前后、Runner pidfd 强杀换 boot 后的稳定窗口中 task/plan/conversation/Provider/action 均零推进,显式 resume 后恢复原 execution owner 和原 run。最终代码快照复跑的计划 revision 11 的 8 步全部完成,11 组 Provider lifecycle 均唯一闭合,finalization v3 四阶段和两层 completed projection 完整,Session 只有 1 条 assistant;3 个副作用无重放,重复 action/message/receipt、Goal 正文、失败证据 canary、API Key、诱饵和项目绝对路径公共泄漏均为 0。验收过程同时修正 `file.write` 静默裁剪末尾换行、旧 Goal action receipt 的合法 tool/摘要转换、file/project diff 正文进入公共 event、file/memory 审计保存绝对路径,以及旧 event detail 在新脱敏投影下被误判幂等冲突五类真实缺陷;隔离 Runner、AppData 和 disposable 项目均已按 sentinel 清理。 ## V1.19 后台 Agent 真流式最终回复 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 42eaed9c3..d4ac754dc 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -62,9 +62,9 @@ V1.17 同时把 finalization journal 升级为 v2 并绑定最终完整计划快 2026-07-15 V1.17 收口时,确定性回归已通过,但真实 `gpt-5.5` `llm-runtime` 连续三轮都在首个 planning POST 返回前遇到相同 TLS record-layer failure,未形成结构化计划或工具动作,因此继续保持“真实 Provider 未 PASS”。首轮严格泄漏扫描另发现开发 CLI 的 `runtimeJson` 直接带出 `sessionPath / eventPath / taskPath`;CLI 输出现已移除这三个绝对存储路径,第三轮 transcript/report 路径泄漏计数归零。Provider 恢复后仍需完整重跑 Runner kill + same-run steer 套件,不能以短 `/models` 鉴权成功或确定性测试代替。 -2026-07-15 起,同一 Runtime 文档的“V1.18 单 Agent 持久 Goal mode”作为开发侧长任务目标的新事实源。Goal 规范 sidecar 使用 `.agent/runtime/goals/current//.json` 与 `.agent/runtime/goals/history//.json`,绑定一个 Agent Session 和同一 run;context bundle 升级为 v4,pending action 升级为 v5 并绑定 Goal ID、revision 与快照指纹。Goal edit 让旧自动/待确认动作变成 `blocked` observation 后 same-run 重规划;暂停重启在 finalization/pending 前保持休眠,显式恢复只续接原 run。resume 在 sidecar 已 `active` 但 Runtime/Runner 尚未完成时可重试补齐;当前 run 的 Goal sidecar 损坏或身份冲突时,即使 legacy Runtime 没有 `goalId` 也必须失败关闭。 +2026-07-15 起,同一 Runtime 文档的“V1.18 单 Agent 持久 Goal mode”作为开发侧长任务目标的新事实源。Goal 规范 sidecar 使用 `.agent/runtime/goals/current//.json` 与 `.agent/runtime/goals/history//.json`,绑定一个 Agent Session 和同一 run;V1.18 引入的 context bundle v4 已随 V1.21 升级为当前 v5,pending action 为 v5 并绑定 Goal ID、revision 与快照指纹。Goal edit 让旧自动/待确认动作变成 `blocked` observation 后 same-run 重规划;暂停重启在 finalization/pending 前保持休眠,显式恢复只续接原 run。resume 在 sidecar 已 `active` 但 Runtime/Runner 尚未完成时可重试补齐;当前 run 的 Goal sidecar 损坏或身份冲突时,即使 legacy Runtime 没有 `goalId` 也必须失败关闭。 -V1.18 开发窗口把模式扩展为 `执行 / 聊天 / 目标`,Goal 创建和编辑使用独立弹层,正式用户窗口不暴露该开发控制面。assistant 写入后必须先可靠投影 Runtime completed,再提交 Goal completed 并补齐 Goal 终态 projection。截至 2026-07-15 尚未记录 V1.18 真实 Provider PASS,确定性回归不能替代完整真实链路验收。 +V1.18 开发窗口把模式扩展为 `执行 / 聊天 / 目标`,Goal 创建和编辑使用独立弹层,正式用户窗口不暴露该开发控制面。assistant 写入后必须先可靠投影 Runtime completed,再提交 Goal completed 并补齐 Goal 终态 projection。2026-07-16 正式 `openai_chat / gpt-5.5` 的隔离 `goal-runtime` 已完成 edit、真实失败回灌、pause、Runner pidfd 强杀、重启静默、显式同 run resume、唯一 assistant 和零重放/零公共泄漏全链路,V1.18 真实 Provider 门禁已 PASS。 2026-07-12 真实验收:发布 AppData 中的真实 `gpt-5.5` 已通过最终安全收紧后的 `llm-runtime` 套件,覆盖 Runner 强杀恢复且 run/session 身份稳定、仓库上下文、checkpoint/精确修改、失败命令诊断与修复复验、6 套确认生命周期、项目验证、桌面与移动非空画布证据、3 个隔离实例并行和唯一 all-join;95 条 task、161 条 event、137 条 Agent DB、13 条合法工具协议、副作用判重、终态投影、assistant audit、消息、回执和密钥泄露均以结构化落盘事实验收。`full` 套件仍要求 External Editor API 配置,缺失时必须返回 `BLOCKED(editorApi)`,不得记为通过。 @@ -140,8 +140,8 @@ Agent Runtime 负责: - 2026-07-11 补充:开发验证可用 `npm run ai-game-creator-shell:agent-task -- [--init] ` 无 UI 启动单 Agent 后台任务。CLI 只负责可选初始化、调用现有 Runtime、按 runId 轮询终态并打印 `status / phase / replyText / pendingActionId`,不复制 planning 或工具执行逻辑;默认 10 分钟轮询上限。`waiting-for-confirmation` 会以非零状态退出并要求转到开发窗口确认,CLI 不提供跳过项目权限的自动确认参数。该入口用于真实 provider 的可重复端到端验收,不进入普通用户界面。 - 2026-07-11 补充,2026-07-13 调整:后台工具规划与最终回复的 LLM 请求新增可恢复错误重试:`LlmError::EmptyResponse` 原样自动重试最多 3 次;`Timeout / Connectivity / Transport` 及上游 `408 / 429 / 5xx` 最多额外重试 5 次并按 `500ms / 1000ms / 1500ms / 2000ms / 2500ms` 退避。配置、请求、流能力、反序列化错误及其他 `4xx` 不重试。重试发生在工具计划被解析和执行前,或最终回复尚未落盘时,因此不会重复执行已经落盘的工具副作用;重试耗尽后仍写入原有 `error / turn.failed` 事件并把失败消息追加到当前 Agent 会话。 - 2026-07-11 调整,2026-07-12 更新,2026-07-15 由 V1.21 澄清:后台单 Agent planning loop 每 6 轮形成一个进度 checkpoint,每轮最多 3 个工具动作;6 轮是停滞检测窗口,不是单个 run 的固定上限,也不是上下文压缩触发器。`loopIteration` 在同一 run 内连续递增,`maxLoopIterations` 指向当前 checkpoint 的结束轮次;待确认或重启恢复后按 context bundle 的 `nextLoopIndex` 在同一 run 继续。checkpoint 产生新的独立观察时继续下一窗口,最近 6 轮没有独立进展或相邻窗口指纹重复时才写入 `failed / budget-exhausted` 和 `loop-budget-exhausted`,不生成总结伪装完成;真正的旧历史摘要只由 V1.21 token 阈值或显式 `/compact` 触发。这只调整后台单 Agent Runtime;游戏草案 Generator/Evaluator 仍保持独立的 3 轮修复预算。旧摘要中“后台最多 3 轮”“整个 run 最多 6 轮”或“每 6 轮自动压缩”的描述不再有效。 -- 2026-07-12 补充并冻结,2026-07-13 调整容量,2026-07-15 由 V1.18 升级:后台 Agent 每个 run 的可恢复 planning 上下文通过临时文件替换原子写入 `.agent/runtime/context-bundles//.json`;当前 schema 为 `game-creator-runtime-context-bundle.v4`。除 Agent/Task/Session/Run、任务正文、project revision、verification gate、窗口、fallback response、压缩 observation、`contextStalled` 与完整结构化计划快照外,v4 新增 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`,任一当前 Goal 身份、状态、revision 或指纹不一致都失败关闭。 -- v3 在原身份、任务、project revision、verification gate 和结构化计划校验通过后,从当前 Runtime/Goal sidecar 补齐 Goal 快照并继续;v2 先按 V1.17 规则补齐结构化计划,再补 Goal;后续 checkpoint 统一写 v4。v1、缺失既有 gate 关联或无法证明 Goal 快照的记录不自动迁移。Provider pause 中断/返回边界先持久化 continuation;该恢复快照把 `goalStatus` 设为恢复后的 `active`,避免 resume 后用 paused 上下文自相矛盾。 +- 2026-07-12 补充并冻结,2026-07-13 调整容量,2026-07-15 由 V1.18 增加 Goal 快照,随后由 V1.21 升级压缩状态:后台 Agent 每个 run 的可恢复 planning 上下文通过临时文件替换原子写入 `.agent/runtime/context-bundles//.json`;当前 schema 为 `game-creator-runtime-context-bundle.v5`。除 Agent/Task/Session/Run、任务正文、project revision、verification gate、窗口、fallback response、压缩 observation、`contextStalled` 与完整结构化计划快照外,v4 新增 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`,v5 再绑定 compaction revision、source/summary fingerprint 和已压缩消息/observation 计数;任一当前 Goal 身份、状态、revision、指纹或压缩快照不一致都失败关闭。 +- v4 在压缩状态校验通过后迁移为当前格式;v3 在原身份、任务、project revision、verification gate 和结构化计划校验通过后,从当前 Runtime/Goal sidecar 补齐 Goal 快照并继续;v2 先按 V1.17 规则补齐结构化计划,再补 Goal;后续 checkpoint 统一写 v5。v1、缺失既有 gate 关联或无法证明 Goal 快照的记录不自动迁移。Provider pause 中断/返回边界先持久化 continuation;该恢复快照把 `goalStatus` 设为恢复后的 `active`,避免 resume 后用 paused 上下文自相矛盾。 - stale continuation 必须清空旧 actions 与 fallback response,保留 blocker、loop 位置、窗口进度和结构化计划;`contextStalled` 一旦成立,同 run 重规划和重启不得清除。动态 revision 数字、时间戳与验证命令输出不构成独立进展;重复 stale 最迟在相邻窗口指纹重复时以 `loop-budget-exhausted` 终止。单文件最多 128 KiB、最多 12 条 observation;写入前统一限长并过滤敏感内容和项目绝对路径。revision 与验证资格仍以锁内独立文件为准,bundle 只是 Runtime 私有恢复上下文,不等同于根级 `.agent/context.bundle.json`,不得由通用文件工具暴露。 - 2026-07-11 调整:后台任务的可执行正文上限统一为 4,000 字符。入队 JSONL、启动后的 `currentTask/currentGoal`、planning prompt、待确认动作 task context、确认续跑和重启恢复都保留同一份正文;对话仍保存用户原始消息。状态事件、列表卡片和 `agent.db` 摘要可继续使用较短安全预览,但不能再反向作为后续 LLM 执行输入。这样长任务末尾的验收标记和输出格式要求不会在队列边界被 180 字符截断。 - 2026-07-11 调整,2026-07-12 由 Runtime V1.2 更新:后台 planning 使用 4,000 输出 token,最终回复使用 2,400,并继续叠加最多 3 次 EmptyResponse 重试。推理档位不再硬编码为 `low`:planning、普通单 Agent 聊天和最终回复统一使用解析后的 `llm.reasoningEffort`,`agentLlm..reasoningEffort` 有值时覆盖全局、缺省时继承全局;取值只允许 `default / low / medium / high`,发布默认 `high`,`default` 表示不向 Provider 发送推理档位。 @@ -170,7 +170,7 @@ Agent Runtime 负责: - 2026-07-10 补充:同一 Agent 的前台直接聊天、流式聊天和后台任务统一使用 `.agent/runtime/locks/.lock` OS 文件锁。前台聊天不再在整个 LLM 请求期间占用项目级写锁;同 Agent 后台任务在前台运行时只入队,前台成功或失败后把当前 Agent 锁直接移交给 drain,不重新抢锁,也不允许 drain 启动异常把已经完成的聊天结果改判为失败。不同 Agent 继续并行,真实项目写工具只在副作用执行期间短暂申请项目写锁。 - 2026-07-10 补充:默认 `agent.resume=confirm` 时,客户端自动恢复命令只做 auto gate 并返回待确认错误;主工作区和独立开发 Agent 聊天窗口在首次读取项目 Runtime 时都必须显示 `agent.resume` 确认条,确认对象绑定发起时的项目路径,切换项目会取消旧确认,异步返回后也不得把旧项目 Runtime 合并到新项目 UI。开发者确认后调用独立 `confirm_resume_game_creator_agent_runtime_tasks`,该命令仍执行 deny-only 权限检查后才接回 durable queue。临时调用失败不锁死项目路径,允许后续刷新重试;明确 deny 或取消都不恢复任务。 - 2026-07-10 补充,2026-07-12 更新,2026-07-15 增加 V1.17 完成门禁并由 V1.21 澄清:后台 Agent 返回空 `actions` 后,只有不存在 `project.verify` 等既有 blocker,且当前结构化计划的全部必要步骤均为 `completed`,才视为 loop 已收束。工具 action 序号和成功 observation 不会自动推进结构化计划;未完成时 Runtime 返回 `runtime.plan_update` blocker,在同一 run 要求 Agent 按真实进度更新。每 6 轮只做进度 checkpoint 与停滞检测;有新的独立 observation 时继续同一 run,最近 6 轮没有独立进展或相邻 checkpoint 重复时终态才写为 `status=failed / phase=budget-exhausted`,error 使用 `loop-budget-exhausted` 机器可读前缀,不再调用 final reply 后写 completed 审计。上下文摘要只由 token 阈值或显式 `/compact` 触发。解析阶段保留过滤后的 action 总数,每轮超过 3 个 action 时写入 `runtime.tool_budget` observation 并只执行前三个,要求下一轮重新排序。Runtime 默认 `allowedTools` 直接由实际可执行工具白名单派生,避免 UI 观测与执行边界漂移。 -- 2026-07-15 V1.18 补充:开发单 Agent 对话框使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑在独立弹层完成,并可查看状态、revision、完成标准以及暂停/恢复/清理;正式用户窗口不展示 Goal 管理控件。Provider 中断边界先持久化可恢复的 v4 context;Runner 重启先收束 Goal control,`paused` 在 finalization/pending action 前直接保持休眠。resume 只从 `paused` 续接,先删除同一 run 旧 cancel tombstone;finalization v3 在 assistant 后先投影 Runtime completed,再写 Goal completed 并补 Goal 终态投影。 +- 2026-07-15 V1.18 补充:开发单 Agent 对话框使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑在独立弹层完成,并可查看状态、revision、完成标准以及暂停/恢复/清理;正式用户窗口不展示 Goal 管理控件。Provider 中断边界先持久化可恢复的当前 v5 context;Runner 重启先收束 Goal control,`paused` 在 finalization/pending action 前直接保持休眠。resume 只从 `paused` 续接,先删除同一 run 旧 cancel tombstone;finalization v3 在 assistant 后先投影 Runtime completed,再写 Goal completed 并补 Goal 终态投影。 - 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。 - 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、项目级黑板 `memory/blackboard.md` 和角色私有记忆 `memory/agents//.md`;黑板用于共享重要跨 agent 记忆,角色私有记忆只给对应角色 brief 读取和追加。最近 project / agent conversation 会作为短期 prompt 上下文读取,不替代正式 memory 文件。 - 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;普通聊天写 `.agent/conversations/project.jsonl`,进入单个 agent 后只写对应 `.agent/conversations/agents/.jsonl`,不把原始对话混进项目黑板或角色私有记忆。 @@ -326,7 +326,7 @@ game-project/ - `npm run ai-game-creator-shell:check`:覆盖壳 typecheck、聊天命令单测、用户 / 开发窗口 UI 边界 smoke、主窗口命令按钮复用 `/help` 命令列表、主窗口项目摘要从 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令且未选项目时不显示、灵感草稿只填充输入框不提交、能力按钮复用 `/capabilities`、LLM状态按钮复用 `/llm-status` 且结果回填 Agent 状态列表、聊天侧 `/agents` 汇总和单 Agent 对话的 provider / 模型 / 流式 / API Key 读取状态、开发日志面板只读读取 `.agent/logs/command.log` / `preview.log` / `agent.log`、项目状态按钮复用 `/status`、权限按钮复用 `/policy` 且策略草稿按钮只填入 `/policy-confirm project.index` / `/policy-confirm asset.register` / `/policy-confirm memory.write` / `/policy-confirm preview.start` / `/policy-confirm preview.open` / `/policy-confirm preview.stop` / `/policy-confirm agent.run_status` / `/policy-confirm conversation.read` / `/policy-confirm conversation.write`、审计按钮复用 `/audit`、资产按钮复用 `/assets` 且资产结果可一键复用 `/read`、任务按钮复用 `/tasks`、聊天侧 `/agents` 汇总每个 Agent 的当前状态、聊天侧 `/agent-conversations` 列出 Agent 对话读取命令、聊天侧 `/agent-memories` 列出 Agent 私有记忆读取命令、Trace 按钮复用 `/trace`、文件按钮复用 `/files` 且文件结果可一键复用 `/read`、索引按钮复用 `/index`、记忆 / 短期记忆 / 黑板按钮复用 `/memory long|short|blackboard`、快照按钮复用 `/checkpoint`、快照列表按钮复用 `/checkpoints` 且 checkpoint 结果可一键复用 `/diff` / `/restore`、历史按钮复用 `/history`、受限命令白名单按钮复用 `/commands` 且无需项目初始化、静态自检快捷按钮复用 `/smoke`、预览状态快捷按钮复用 `/preview-status`、主窗口运行时配置面板读写 Tauri 配置目录中的 `game-creator.config.json`、支持全局与每个 agent 单独选择 LLM Provider 且不把 API Key 写入聊天、单 Agent 对话面板可手动追加私有记忆且走 `memory.write` 策略、主窗口提供音效登记、画板音频导入和常用生成产物读取草稿入口,聊天侧 `/art` 可盘点美术素材且不直接触发平台生成或画板同步,聊天侧 `/context` 可盘点生成上下文来源且不直接读取上下文文件,聊天侧 `/timeline` 可汇总项目活动时间线且不直接读取日志或 trace 文件,聊天侧 `/artifacts` 可列出常用生成产物读取命令,聊天侧 `/run-artifacts` 可列出最近 run 产物读取命令,聊天侧 `/run-files` 可列出 Agent 运行辅助文件读取命令,聊天侧 `/logs` 可列出固定日志读取命令且不直接读取日志,聊天侧 `/brief` 只基于当前已加载的 manifest / 最近 run trace / 预览状态 / 资产数量 / 最近命令生成项目简报,聊天侧 `/goal` 只基于当前 manifest.goal / 最近 run goal / taskGraph.goal 汇总创作目标来源,提供 `/next` 或 `/agent-resume 细化目标:` 后续草稿且不直接触发 Tauri 读写、文件读取、预览启动或新增面板,聊天侧 `/mvp` 只基于当前 manifest / 最近 run trace / preview / 任务 / 资产 / 最近命令汇总本轮最小可玩范围,提供 `/run` 等后续草稿且不直接触发 Tauri 读写、文件读取、预览启动、导出或新增面板,聊天侧 `/audience` 只基于当前 manifest / 最近 run trace / preview 准备首批试玩对象和观察重点且不直接触发 Tauri 读写、预览启动、导出或继续 run,聊天侧 `/feedback` 只基于当前 manifest / 最近 run trace / preview 准备试玩反馈模板和修改说明草稿且不直接触发 Tauri 读写、预览启动或继续 run,聊天侧 `/next` 基于当前已加载的 manifest / 最近 run trace 输出下一步建议和 `/goal` / `/mvp` / `/accessibility` / `/performance` / `/tasks` / `/criteria` / `/groups` / `/budget` / `/qa` / `/changes` / `/trace` / `/run` / `/open-preview` / `/test-plan` / `/audience` / `/feedback` / `/assets` / `/art` / `/context` / `/timeline` / `/artifacts` / `/run-artifacts` / `/run-files` / `/logs` / `/agent-resume ` 等安全命令草稿方向,提供一个首选草稿且不直接触发 Tauri 读写、预览启动或文件读取,聊天侧 `/capabilities` 展示标准 Agent 能力清单且不打开开发面板、聊天侧 `/audit` 从 manifest / 本地文件 / `.agent/run.latest.json` 分别汇总用户面、6 组任务配置、6 组协作证据、任务编排、loop、记忆、本地产物、HTTP 预览、画板回流和权限日志证据且不打开开发面板;未生成 `.agent/run.latest.json` 前,`/audit` 只能标记任务配置通过,不能把 6 组协作证据误判为通过;trace 已存在但状态为 `failed`、`needs-revision`、`running`、`max-passes-exhausted` 或缺少 `Evaluator passed` 步骤时,`/audit` 不能把 loop 误判为通过。聊天侧 `/llm-status` 只显示 base_url / model / API Key 已读取状态且不泄露密钥本体、聊天侧长期记忆查看 / 追加 / 覆盖 / 删除的授权本地项目路径、上传资产写入后的 manifest 刷新和 `/assets` 聊天可见性、`/smoke` 聊天侧确认后只通过授权本地项目路径执行白名单 `game.static_smoke`、`/run` 聊天侧确认后通过授权本地项目路径执行 `game.static_smoke`、启动 `127.0.0.1` 本地预览并交给外部浏览器、`/preview` 聊天侧确认后通过授权本地项目路径启动 `127.0.0.1` 本地预览并交给外部浏览器、`/status` 聊天侧项目 / 任务 / 资产 / 预览 / 最近命令汇总、`/files` 聊天侧本地项目文件列表、`/read` 聊天侧文件读取的授权本地项目路径、`/tasks` 聊天侧任务拆分与下一步专业组展示的授权本地项目路径、聊天确认生成后实时展示 Planner / Orchestrator / 角色 brief / Generator / Evaluator / 写盘 / 自检进度,并自动读取 `.agent/run.latest.json` 在普通聊天消息里展示 Run、LLM 对话、loop 轮次、工具调用、active / carry-over 任务、返工焦点、编排轮次、最近步骤、画板同步建议命令和本地产物快照、`/trace` 聊天侧读取 `.agent/run.latest.json` 并展示 loop 轮次 / active 任务 / 返工路线 / dependency waves 的授权本地项目路径、`platform-agent` 编排测试、共享契约测试、Tauri 本地能力测试和无密钥本地 provider 端到端 smoke;用于证明独立 App、真实 LLM-compatible loop、本地落盘、自检和 HTTP 预览闭环,并覆盖 loop 跑满 3 轮失败时不会写入最终游戏产物。 - V1.17 单 Agent 持久计划验收:Rust 定向用例覆盖 native function 显式 `planUpdate`、文本 JSON omission 兼容、输入上限、单调 revision、外层 failed / budget-exhausted 保留最后可信进度、终态保留、工具 action 下标零推进、未完成步骤阻止 final、损坏状态失败关闭、context bundle v3/v2 恢复、finalization v2 计划快照与 assistant 已落盘后的 state 丢失恢复,以及 thinking / legacy plan / repair 公共审计零正文;`appSurface.test.ts` 覆盖开发 UI 刷新后完整 8 步仍在,以及普通用户 Supervisor 只显示完成数、当前步骤、等待、下一步和协作数量。恢复/steer 专项还必须证明 Runner 重启和 same-run steer 后身份不变、旧动作零执行、终态步骤不丢、revision 不回退;真实 Provider 必须按 Runtime V1.17 章节完成无配方验收后才能记 PASS,当前状态为未验收。 -- V1.18 单 Agent 持久 Goal mode 验收:Rust/Runner 定向用例覆盖 Goal CAS 生命周期、当前 Session/run 隔离、Provider 中断安全边界、paused 重启不自启、同 run resume、旧 cancel tombstone 清理、v4 context、v5 pending action 的 Goal 快照门禁、旧 v1-v4 失败关闭、Goal edit 后自动/确认动作转 `blocked` 并重规划、finalization v3 以及 assistant 后 Runtime/Goal completed 顺序;`appSurface.test.ts` 覆盖 `执行 / 聊天 / 目标`、独立 Goal 弹层、完整控制动作和正式用户界面隔离。真实 Provider 仍须证明 edit + pause + Runner 强杀 + 显式 resume 全程保持同 run 且只写一个 assistant;截至 2026-07-15 该门禁未 PASS。 +- V1.18 单 Agent 持久 Goal mode 验收:Rust/Runner 定向用例覆盖 Goal CAS 生命周期、当前 Session/run 隔离、Provider 中断安全边界、paused 重启不自启、同 run resume、旧 cancel tombstone 清理、当前 v5 context、v5 pending action 的 Goal 快照门禁、旧 schema 失败关闭、Goal edit 后自动/确认动作转 `blocked` 并重规划、finalization v3 以及 assistant 后 Runtime/Goal completed 顺序;`appSurface.test.ts` 覆盖 `执行 / 聊天 / 目标`、独立 Goal 弹层、完整控制动作和正式用户界面隔离。2026-07-16 真实 `openai_chat / gpt-5.5` 已完成 revision 1 -> 2、旧动作 blocked 且零执行、真实失败后 patchset 修复、pause、Runner pidfd 强杀换 boot、重启零推进、显式同 run resume;最终代码快照复跑有 11 组 Provider lifecycle 闭合、计划 revision 11 八步完成、唯一 assistant、零副作用重放和零 Goal/密钥/路径公共泄漏,门禁状态为 PASS。 - `file.delete` 的 Runtime 验收必须覆盖:删除普通文件与缺失文件的幂等结果、缺少路径、目录、绝对路径、父目录、反斜杠、有效与悬空符号链接和整个 `.agent/**` 控制面拒绝、独立 `confirm / deny` 策略、确认前无副作用、确认期间全局 revision 漂移失败关闭、durable action ledger 的 approved / executing / observed 恢复边界、`agent.runtime.file.delete` 审计,以及删除前 revision 推进、删除后必须通过当前 revision 的 `project.verify` 或 `game.static_smoke` 才能收束。另用完整后台 loop 和开发 CLI 真实任务证明 Agent 能自主选择删除并完成验证;普通用户窗口继续没有文件写入或删除入口。 - `/risks` 聊天入口由 `appSurface.test.ts` 的主窗口 smoke 覆盖:只基于当前已加载的 manifest / trace / 预览 / 任务 / 资产 / 最近命令生成风险摘要,提供首个风险处理草稿,不触发 Tauri 读写、文件读取、预览启动或新增普通用户面板。 - `/goal` 聊天入口由 `appSurface.test.ts` 主窗口 smoke 覆盖:只基于当前已加载 manifest.goal、最近 run goal 和 taskGraph.goal 汇总创作目标来源,提供 `/agent-resume 细化目标:` 或 `/next` 草稿,不触发 Tauri 读写、不读取 spec / 上下文 / trace 文件、不新增普通用户目标面板。