修复AGC无人值守游戏生成阻断
普通Web工作台默认使用单Supervisor自主生成链路 补齐安全默认返工、结构化诊断与同Run恢复 绑定当前revision、入口摘要和双视口试玩完成凭证 增强Runner跨boot单飞恢复与game-chat来源隔离 新增真实Provider验收入口及确定性回归 同步更新技术方案、开发流程和共享决策
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat",
|
||||
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense",
|
||||
"agent-runtime:supervisor-game-chat-single-main-playable-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test",
|
||||
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
scopedAgentsSuite,
|
||||
steerRunnerKillSuite,
|
||||
supervisorAutonomousPlayableLaneDefenseSuite,
|
||||
supervisorGameChatSingleMainPlayableSuite,
|
||||
supervisorSwarmAutonomousChatSuite,
|
||||
supervisorSwarmCollaborationPolicyMixedRecoverySuite,
|
||||
supervisorSwarmFinalReplyTransientRetrySuite,
|
||||
@@ -70,6 +71,7 @@ export function parseArguments(args) {
|
||||
suite === supervisorSwarmToolPlanHandoffRunnerKillSuite ||
|
||||
suite === supervisorSwarmAutonomousChatSuite ||
|
||||
suite === supervisorAutonomousPlayableLaneDefenseSuite ||
|
||||
suite === supervisorGameChatSingleMainPlayableSuite ||
|
||||
suite === supervisorSwarmStaticIsolatedAutonomousChatSuite ||
|
||||
suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite ||
|
||||
suite === steerRunnerKillSuite ||
|
||||
|
||||
@@ -28,7 +28,10 @@ import {
|
||||
isGoalRuntimeSuite,
|
||||
} from '../suites/goal.mjs';
|
||||
import { isResponseStreamSuite } from '../suites/response-stream.mjs';
|
||||
import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs';
|
||||
import {
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite,
|
||||
isSupervisorGameChatSingleMainPlayableSuite,
|
||||
} from '../suites/supervisor-autonomous-playable.mjs';
|
||||
import {
|
||||
isSupervisorSwarmSuite,
|
||||
supervisorSwarmVerificationFixtureSource,
|
||||
@@ -40,20 +43,26 @@ import { effectiveAgentLlmConfig } from './config.mjs';
|
||||
import { runProcess } from './process.mjs';
|
||||
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
|
||||
export async function checkPrerequisites(config) {
|
||||
const requiredAgents = isUserInputRuntimeSuite()
|
||||
export function requiredAgentIdsForSuite() {
|
||||
return isUserInputRuntimeSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
supervisorSwarmDesignAgentId,
|
||||
supervisorSwarmQualityAgentId,
|
||||
]
|
||||
: isIsolatedRunnerSuite()
|
||||
? [mainAgentId]
|
||||
: [mainAgentId, 'quality-review'];
|
||||
: isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? [projectSupervisorAgentId, mainAgentId]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
supervisorSwarmDesignAgentId,
|
||||
supervisorSwarmQualityAgentId,
|
||||
]
|
||||
: isIsolatedRunnerSuite()
|
||||
? [mainAgentId]
|
||||
: [mainAgentId, 'quality-review'];
|
||||
}
|
||||
|
||||
export async function checkPrerequisites(config) {
|
||||
const requiredAgents = requiredAgentIdsForSuite();
|
||||
const llmConfigured = requiredAgents.every((agentId) => {
|
||||
const effective = effectiveAgentLlmConfig(config, agentId);
|
||||
return ['apiKey', 'baseUrl', 'model'].every(
|
||||
|
||||
@@ -227,6 +227,9 @@ export const supervisorSwarmAutonomousChatSuite =
|
||||
export const supervisorAutonomousPlayableLaneDefenseSuite =
|
||||
'supervisor-autonomous-playable-lane-defense';
|
||||
|
||||
export const supervisorGameChatSingleMainPlayableSuite =
|
||||
'supervisor-game-chat-single-main-playable';
|
||||
|
||||
export const supervisorSwarmStaticIsolatedAutonomousChatSuite =
|
||||
'supervisor-swarm-static-isolated-autonomous-chat';
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../harness/app-data.mjs';
|
||||
import { parseArguments } from '../harness/config.mjs';
|
||||
import { safeProcessFailureDiagnostic } from '../harness/process.mjs';
|
||||
import { requiredAgentIdsForSuite } from '../harness/project.mjs';
|
||||
import { isIsolatedRunnerSuite } from '../harness/reporting.mjs';
|
||||
import { agentConversationPath } from '../harness/runtime.mjs';
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
commandFailureMarker,
|
||||
commandPassedMarker,
|
||||
isolatedAgentJoinClaimSchemaVersion,
|
||||
mainAgentId,
|
||||
projectSupervisorAgentId,
|
||||
providerActionBatchSchemaVersion,
|
||||
repoRoot,
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
supervisorCollaborationPolicySchemaVersion,
|
||||
supervisorCollaborationPolicySnapshotInitialBatchBinding,
|
||||
supervisorCollaborationPolicySnapshotSchemaVersion,
|
||||
supervisorGameChatSingleMainPlayableSuite,
|
||||
supervisorSwarmAutonomousChatSuite,
|
||||
supervisorSwarmCollaborationPolicyMixedRecoverySuite,
|
||||
supervisorSwarmFollowupIsolatedReviews,
|
||||
@@ -63,6 +66,8 @@ import {
|
||||
buildSupervisorAutonomousPlayableStdin,
|
||||
collectPartialSupervisorAutonomousPlayableEvidence,
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite,
|
||||
isSupervisorGameChatSingleMainPlayableSuite,
|
||||
supervisorAutonomousPlayableMode,
|
||||
} from './supervisor-autonomous-playable.mjs';
|
||||
import {
|
||||
buildSupervisorSwarmEvidence,
|
||||
@@ -150,6 +155,69 @@ export async function runAgentRuntimeRealE2eSelfTests() {
|
||||
const shellPackage = JSON.parse(
|
||||
readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
|
||||
);
|
||||
const previousSuiteForGameChatPlayable = state.suite;
|
||||
state.suite = supervisorAutonomousPlayableLaneDefenseSuite;
|
||||
const professionalPlayableMode = supervisorAutonomousPlayableMode();
|
||||
const professionalPlayableRequiredAgents = requiredAgentIdsForSuite();
|
||||
state.suite = supervisorGameChatSingleMainPlayableSuite;
|
||||
const gameChatPlayableProfile = isolatedSuiteAppDataProfile();
|
||||
const gameChatPlayableParsedArguments = parseArguments([
|
||||
'--config-dir',
|
||||
path.resolve('synthetic-game-chat-playable-config'),
|
||||
'--suite',
|
||||
supervisorGameChatSingleMainPlayableSuite,
|
||||
]);
|
||||
const gameChatPlayableMode = supervisorAutonomousPlayableMode();
|
||||
const gameChatPlayableRequiredAgents = requiredAgentIdsForSuite();
|
||||
const gameChatPlayablePackageCommandsRegistered =
|
||||
shellPackage.scripts?.[
|
||||
'agent-runtime:supervisor-game-chat-single-main-playable-real-e2e'
|
||||
] ===
|
||||
'node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable' &&
|
||||
rootPackage.scripts?.[
|
||||
'ai-game-creator-shell:agent-runtime:supervisor-game-chat-single-main-playable-real-e2e'
|
||||
] ===
|
||||
'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-game-chat-single-main-playable-real-e2e --';
|
||||
const gameChatPlayableSuiteRegistered =
|
||||
gameChatPlayableParsedArguments.suite ===
|
||||
supervisorGameChatSingleMainPlayableSuite &&
|
||||
isSupervisorGameChatSingleMainPlayableSuite() &&
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
!isSupervisorSwarmSuite() &&
|
||||
isIsolatedRunnerSuite() &&
|
||||
isolatedSuiteProtectsSourceAppData() &&
|
||||
isolatedSuiteUsesSiblingAppData() &&
|
||||
gameChatPlayableProfile.sentinelName ===
|
||||
supervisorAutonomousPlayableAppDataSentinelFileName &&
|
||||
gameChatPlayableProfile.sentinelSchema ===
|
||||
supervisorAutonomousPlayableAppDataSentinelSchema;
|
||||
const gameChatPlayableModeValidated =
|
||||
professionalPlayableMode.gameChatSingleMain === false &&
|
||||
professionalPlayableMode.rootSource === 'project-supervisor-cli' &&
|
||||
professionalPlayableMode.evidenceAgentId === projectSupervisorAgentId &&
|
||||
professionalPlayableMode.scenario ===
|
||||
'project-supervisor-autonomous-playable-lane-defense' &&
|
||||
professionalPlayableMode.cliFlags.length === 0 &&
|
||||
gameChatPlayableMode.gameChatSingleMain === true &&
|
||||
gameChatPlayableMode.rootSource === 'project-supervisor-game-chat' &&
|
||||
gameChatPlayableMode.evidenceAgentId === mainAgentId &&
|
||||
gameChatPlayableMode.scenario ===
|
||||
'project-supervisor-game-chat-single-main-playable' &&
|
||||
JSON.stringify(gameChatPlayableMode.cliFlags) ===
|
||||
JSON.stringify(['--game-chat-smoke']);
|
||||
const gameChatPlayableRequiredAgentsValidated =
|
||||
JSON.stringify(professionalPlayableRequiredAgents) ===
|
||||
JSON.stringify([projectSupervisorAgentId]) &&
|
||||
JSON.stringify(gameChatPlayableRequiredAgents) ===
|
||||
JSON.stringify([projectSupervisorAgentId, mainAgentId]);
|
||||
state.suite = previousSuiteForGameChatPlayable;
|
||||
assert(
|
||||
gameChatPlayableSuiteRegistered &&
|
||||
gameChatPlayablePackageCommandsRegistered &&
|
||||
gameChatPlayableModeValidated &&
|
||||
gameChatPlayableRequiredAgentsValidated,
|
||||
'agent-runtime-real-e2e-self-test-game-chat-playable-suite-invalid',
|
||||
);
|
||||
const previousSuiteForAutonomousPlayable = state.suite;
|
||||
state.suite = supervisorAutonomousPlayableLaneDefenseSuite;
|
||||
const autonomousPlayableProfile = isolatedSuiteAppDataProfile();
|
||||
@@ -1593,6 +1661,10 @@ export async function runAgentRuntimeRealE2eSelfTests() {
|
||||
collaborationPolicySnapshotBindingStable: true,
|
||||
durableSnapshotEligibilityAndContractBindingValidated: true,
|
||||
sourceEndpointAbsentLifecycleGuardValidated,
|
||||
gameChatPlayableSuiteRegistered,
|
||||
gameChatPlayablePackageCommandsRegistered,
|
||||
gameChatPlayableModeValidated,
|
||||
gameChatPlayableRequiredAgentsValidated,
|
||||
autonomousPlayableSuiteRegistered,
|
||||
autonomousPlayablePackageCommandsRegistered,
|
||||
autonomousPlayableDedicatedPathValidated: true,
|
||||
|
||||
+110
-14
@@ -52,6 +52,7 @@ import {
|
||||
supervisorAutonomousPlayableSafeTurnOutcomes,
|
||||
supervisorAutonomousPlayableSourceFieldMaxChars,
|
||||
supervisorAutonomousPlayableSourceTotalMaxChars,
|
||||
supervisorGameChatSingleMainPlayableSuite,
|
||||
supervisorSwarmSessionId,
|
||||
supervisorSwarmTerminalSidecarCleanupTimeoutMs,
|
||||
} from '../runtime-state.mjs';
|
||||
@@ -65,6 +66,29 @@ import {
|
||||
validateSupervisorSwarmFinalization,
|
||||
} from './supervisor-swarm.mjs';
|
||||
|
||||
const projectSupervisorCliSource = 'project-supervisor-cli';
|
||||
const projectSupervisorGameChatSource = 'project-supervisor-game-chat';
|
||||
const gameChatMainAgentId = 'code-prototype';
|
||||
|
||||
export function supervisorAutonomousPlayableMode() {
|
||||
if (state.suite === supervisorGameChatSingleMainPlayableSuite) {
|
||||
return {
|
||||
gameChatSingleMain: true,
|
||||
rootSource: projectSupervisorGameChatSource,
|
||||
evidenceAgentId: gameChatMainAgentId,
|
||||
scenario: 'project-supervisor-game-chat-single-main-playable',
|
||||
cliFlags: ['--game-chat-smoke'],
|
||||
};
|
||||
}
|
||||
return {
|
||||
gameChatSingleMain: false,
|
||||
rootSource: projectSupervisorCliSource,
|
||||
evidenceAgentId: projectSupervisorAgentId,
|
||||
scenario: 'project-supervisor-autonomous-playable-lane-defense',
|
||||
cliFlags: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSupervisorAutonomousPlayableStdin() {
|
||||
const task = supervisorAutonomousPlayableLaneDefenseTask;
|
||||
assert(
|
||||
@@ -776,6 +800,7 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
residualSidecars,
|
||||
) {
|
||||
const task = supervisorAutonomousPlayableLaneDefenseTask;
|
||||
const mode = supervisorAutonomousPlayableMode();
|
||||
const report = state.supervisorAutonomousPlayable.turnReport;
|
||||
assert(
|
||||
report?.schemaVersion === 'game-creator-swarm-turn-report.v1' &&
|
||||
@@ -814,7 +839,7 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
);
|
||||
assert(
|
||||
parentTask?.task === task &&
|
||||
parentTask.source === 'project-supervisor-cli' &&
|
||||
parentTask.source === mode.rootSource &&
|
||||
parentTask.status === 'completed' &&
|
||||
parentTask.phase === 'completed' &&
|
||||
parentTask.runProfile === autonomousGameBuildRunProfile &&
|
||||
@@ -996,7 +1021,7 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
binding.rootRunId === state.initialRunId &&
|
||||
binding.parentAgentId == null &&
|
||||
binding.parentRunId == null &&
|
||||
binding.source === 'project-supervisor-cli' &&
|
||||
binding.source === mode.rootSource &&
|
||||
binding.profile === autonomousGameBuildRunProfile &&
|
||||
binding.profileFingerprint ===
|
||||
hashJsonValue({
|
||||
@@ -1019,6 +1044,67 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
);
|
||||
const contract = contracts[0];
|
||||
const receipt = receipts[0];
|
||||
let evidenceBinding = binding;
|
||||
let evidenceRunId = state.initialRunId;
|
||||
if (mode.gameChatSingleMain) {
|
||||
const manifest = JSON.parse(
|
||||
decodeUtf8Fatal(
|
||||
await fs.readFile(path.join(state.projectRoot, '.agent/manifest.json')),
|
||||
'supervisor-autonomous-playable-manifest-invalid-utf8',
|
||||
),
|
||||
);
|
||||
const tasks = Array.isArray(manifest.tasks) ? manifest.tasks : [];
|
||||
assert(
|
||||
tasks.length === 1 &&
|
||||
tasks[0]?.id === gameChatMainAgentId &&
|
||||
tasks[0]?.status === 'completed',
|
||||
'supervisor-game-chat-single-main-manifest-invalid',
|
||||
);
|
||||
const rootChildTasks = persistence.taskSnapshot.latest.filter(
|
||||
(candidate) =>
|
||||
candidate.parentAgentId === projectSupervisorAgentId &&
|
||||
candidate.parentRunId === state.initialRunId,
|
||||
);
|
||||
const rootChildRuntimes = persistence.runtimeStates.filter(
|
||||
(candidate) =>
|
||||
candidate.parentAgentId === projectSupervisorAgentId &&
|
||||
candidate.parentRunId === state.initialRunId,
|
||||
);
|
||||
const codeTask = rootChildTasks[0];
|
||||
const codeRuntime = rootChildRuntimes[0];
|
||||
assert(
|
||||
rootChildTasks.length === 1 &&
|
||||
codeTask.agentId === gameChatMainAgentId &&
|
||||
codeTask.status === 'completed' &&
|
||||
codeTask.phase === 'completed' &&
|
||||
codeTask.source === 'agent-ready-task-scheduler' &&
|
||||
rootChildRuntimes.length === 1 &&
|
||||
codeRuntime.agentId === gameChatMainAgentId &&
|
||||
codeRuntime.runId === codeTask.runId &&
|
||||
codeRuntime.phase === 'completed' &&
|
||||
codeRuntime.pendingToolAction == null &&
|
||||
codeRuntime.pendingAction == null,
|
||||
'supervisor-game-chat-single-main-runtime-invalid',
|
||||
);
|
||||
evidenceRunId = codeTask.runId;
|
||||
const codeBindings = runProfileBindings.filter(
|
||||
(candidate) =>
|
||||
candidate.agentId === gameChatMainAgentId &&
|
||||
candidate.runId === evidenceRunId &&
|
||||
candidate.rootAgentId === projectSupervisorAgentId &&
|
||||
candidate.rootRunId === state.initialRunId &&
|
||||
candidate.parentAgentId === projectSupervisorAgentId &&
|
||||
candidate.parentRunId === state.initialRunId &&
|
||||
candidate.profile === autonomousGameBuildRunProfile &&
|
||||
candidate.source === 'agent-ready-task-scheduler' &&
|
||||
candidate.parentBindingFingerprint === binding.bindingFingerprint,
|
||||
);
|
||||
assert(
|
||||
codeBindings.length === 1,
|
||||
'supervisor-game-chat-single-main-binding-invalid',
|
||||
);
|
||||
evidenceBinding = codeBindings[0];
|
||||
}
|
||||
const contractIdentity = {
|
||||
schemaVersion: contract.schemaVersion,
|
||||
projectId: contract.projectId,
|
||||
@@ -1072,23 +1158,22 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
await readSupervisorSwarmJsonDirectory('.agent/runtime/verification')
|
||||
).filter(
|
||||
(gate) =>
|
||||
gate.agentId === projectSupervisorAgentId &&
|
||||
gate.runId === state.initialRunId,
|
||||
gate.agentId === mode.evidenceAgentId && gate.runId === evidenceRunId,
|
||||
);
|
||||
const gate = gates[0];
|
||||
const staticSmokeAudits = persistence.agentDb.filter(
|
||||
(record) =>
|
||||
record.recordType === 'agent.runtime.command.run_limited' &&
|
||||
record.agentId === projectSupervisorAgentId &&
|
||||
record.runId === state.initialRunId &&
|
||||
record.agentId === mode.evidenceAgentId &&
|
||||
record.runId === evidenceRunId &&
|
||||
record.commandId === 'game.static_smoke' &&
|
||||
record.status === 'completed',
|
||||
);
|
||||
assert(
|
||||
gates.length === 1 &&
|
||||
gate.lastVerificationTool === 'game.static_smoke' &&
|
||||
gate.lastVerificationStatus === 'passed' &&
|
||||
gate.verifiedRevision === revision.revision &&
|
||||
gate.staticSmokeVerifiedRevision === revision.revision &&
|
||||
staticSmokeAudits.length >= 1,
|
||||
'supervisor-autonomous-playable-static-smoke-invalid',
|
||||
);
|
||||
@@ -1112,9 +1197,10 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
assert(
|
||||
receipt.schemaVersion === autonomousPlaytestReceiptSchemaVersion &&
|
||||
receipt.projectId === contract.projectId &&
|
||||
receipt.agentId === projectSupervisorAgentId &&
|
||||
receipt.runId === state.initialRunId &&
|
||||
receipt.runProfileBindingFingerprint === binding.bindingFingerprint &&
|
||||
receipt.agentId === mode.evidenceAgentId &&
|
||||
receipt.runId === evidenceRunId &&
|
||||
receipt.runProfileBindingFingerprint ===
|
||||
evidenceBinding.bindingFingerprint &&
|
||||
receipt.revision === revision.revision &&
|
||||
receipt.gameIndex.path === 'game/index.html' &&
|
||||
receipt.gameIndex.sha256 === finalGameIndexSha256 &&
|
||||
@@ -1123,8 +1209,8 @@ export async function validateSupervisorAutonomousPlayableEvidence(
|
||||
receipt.receiptFingerprint === hashJsonValue(receiptIdentity) &&
|
||||
actionReceipts.filter(
|
||||
(record) =>
|
||||
record.agentId === projectSupervisorAgentId &&
|
||||
record.runId === state.initialRunId &&
|
||||
record.agentId === mode.evidenceAgentId &&
|
||||
record.runId === evidenceRunId &&
|
||||
record.actionId === receipt.actionId &&
|
||||
record.actionFingerprint === receipt.actionFingerprint &&
|
||||
record.tool === 'preview.validate' &&
|
||||
@@ -1511,10 +1597,12 @@ export async function runSupervisorAutonomousPlayableLaneDefenseE2e() {
|
||||
state.supervisorAutonomousPlayable.privateValues = [task];
|
||||
state.supervisorSwarm.userTask = task;
|
||||
state.supervisorSwarm.privateValues = [task];
|
||||
const mode = supervisorAutonomousPlayableMode();
|
||||
state.supervisorAutonomousPlayableCliSession = startInteractiveCli([
|
||||
'--swarm-chat',
|
||||
'--init',
|
||||
'--autonomous-game-build',
|
||||
...mode.cliFlags,
|
||||
state.projectRoot,
|
||||
]);
|
||||
state.supervisorAutonomousPlayable.stdinWriteCount = 1;
|
||||
@@ -1546,8 +1634,9 @@ export async function runSupervisorAutonomousPlayableLaneDefenseE2e() {
|
||||
}
|
||||
|
||||
export function emptySupervisorAutonomousPlayableEvidence() {
|
||||
const mode = supervisorAutonomousPlayableMode();
|
||||
return {
|
||||
scenario: 'project-supervisor-autonomous-playable-lane-defense',
|
||||
scenario: mode.scenario,
|
||||
targetAgentId: projectSupervisorAgentId,
|
||||
runProfile: autonomousGameBuildRunProfile,
|
||||
dedicatedZeroInterventionPath: true,
|
||||
@@ -1688,5 +1777,12 @@ export function emptySupervisorAutonomousPlayableEvidence() {
|
||||
}
|
||||
|
||||
export function isSupervisorAutonomousPlayableLaneDefenseSuite() {
|
||||
return state.suite === supervisorAutonomousPlayableLaneDefenseSuite;
|
||||
return (
|
||||
state.suite === supervisorAutonomousPlayableLaneDefenseSuite ||
|
||||
state.suite === supervisorGameChatSingleMainPlayableSuite
|
||||
);
|
||||
}
|
||||
|
||||
export function isSupervisorGameChatSingleMainPlayableSuite() {
|
||||
return state.suite === supervisorGameChatSingleMainPlayableSuite;
|
||||
}
|
||||
|
||||
@@ -259,10 +259,11 @@ pub(crate) fn validate_safe_game_html_runtime(html: &str, label: &str) -> Result
|
||||
|
||||
pub(crate) fn validate_game_html_smoke(html: &str) -> Result<(), String> {
|
||||
let lower_html = html.to_ascii_lowercase();
|
||||
validate_closed_game_script_blocks(&lower_html)?;
|
||||
validate_executable_inline_javascript_syntax(html)?;
|
||||
if !lower_html.contains("<canvas") {
|
||||
return Err("游戏入口必须包含可渲染画布".to_string());
|
||||
}
|
||||
validate_closed_game_script_blocks(&lower_html)?;
|
||||
validate_canvas_rendering_html(html, "游戏入口")?;
|
||||
if !html.contains("requestAnimationFrame") {
|
||||
return Err("游戏入口必须包含游戏主循环".to_string());
|
||||
|
||||
@@ -285,6 +285,79 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
|
||||
)?;
|
||||
return serde_json::to_string(&detail).ok();
|
||||
}
|
||||
if observation.tool == "command.run_limited" {
|
||||
let summary = observation.summary.trim();
|
||||
if !summary.starts_with("game.static_smoke ") {
|
||||
return None;
|
||||
}
|
||||
let passed = observation.status == "ok" && summary == "game.static_smoke 已完成";
|
||||
let failure_code = if passed {
|
||||
None
|
||||
} else {
|
||||
Some(match summary {
|
||||
"game.static_smoke 执行失败" => "static-smoke-failed",
|
||||
"game.static_smoke 无法取得项目验证锁" => "verification-lock-unavailable",
|
||||
"game.static_smoke 无法清除旧验证凭证" => "verification-reset-failed",
|
||||
"game.static_smoke 结果无法形成有效验证凭证" => {
|
||||
"verification-receipt-invalid"
|
||||
}
|
||||
_ => return None,
|
||||
})
|
||||
};
|
||||
let diagnostic_check = observation.detail.as_deref().map_or("none", |detail| {
|
||||
if detail.contains("JavaScript") {
|
||||
"javascript-syntax"
|
||||
} else {
|
||||
"game-static-contract"
|
||||
}
|
||||
});
|
||||
let persisted = observation
|
||||
.detail
|
||||
.as_deref()
|
||||
.and_then(|value| serde_json::from_str::<serde_json::Value>(value).ok())
|
||||
.filter(|value| {
|
||||
value.get("commandId").and_then(serde_json::Value::as_str)
|
||||
== Some("game.static_smoke")
|
||||
});
|
||||
let diagnostic = if let Some(value) = persisted.as_ref() {
|
||||
if value.get("passed").and_then(serde_json::Value::as_bool) != Some(passed)
|
||||
|| value.get("failureCode").and_then(serde_json::Value::as_str) != failure_code
|
||||
{
|
||||
return None;
|
||||
}
|
||||
value
|
||||
.get("diagnostic")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(|value| redact_agent_runtime_error(root, value, 500))
|
||||
} else {
|
||||
observation
|
||||
.detail
|
||||
.as_deref()
|
||||
.map(|value| redact_agent_runtime_error(root, value, 500))
|
||||
};
|
||||
if !passed && diagnostic.is_none() {
|
||||
return None;
|
||||
}
|
||||
if receipt_owner.is_none() {
|
||||
return serde_json::to_string(&serde_json::json!({
|
||||
"commandId": "game.static_smoke",
|
||||
"passed": passed,
|
||||
"failureCode": failure_code,
|
||||
"check": diagnostic_check,
|
||||
"path": AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
return serde_json::to_string(&serde_json::json!({
|
||||
"commandId": "game.static_smoke",
|
||||
"passed": passed,
|
||||
"failureCode": failure_code,
|
||||
"check": diagnostic_check,
|
||||
"path": AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"diagnostic": diagnostic,
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
if observation.tool == "image.inspect" {
|
||||
let detail = serde_json::from_str::<serde_json::Value>(
|
||||
observation.detail.as_deref().unwrap_or_default(),
|
||||
|
||||
@@ -1960,6 +1960,7 @@ mod tests {
|
||||
last_verification_tool: None,
|
||||
last_verification_status: None,
|
||||
static_smoke_verified_revision: None,
|
||||
static_smoke_verified_game_index_sha256: None,
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
};
|
||||
@@ -2001,6 +2002,7 @@ mod tests {
|
||||
last_verification_tool: None,
|
||||
last_verification_status: None,
|
||||
static_smoke_verified_revision: None,
|
||||
static_smoke_verified_game_index_sha256: None,
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
};
|
||||
@@ -2036,6 +2038,7 @@ mod tests {
|
||||
last_verification_tool: None,
|
||||
last_verification_status: None,
|
||||
static_smoke_verified_revision: None,
|
||||
static_smoke_verified_game_index_sha256: None,
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
};
|
||||
|
||||
@@ -157,6 +157,7 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
|
||||
gate.last_verification_tool = None;
|
||||
gate.last_verification_status = None;
|
||||
gate.static_smoke_verified_revision = None;
|
||||
gate.static_smoke_verified_game_index_sha256 = None;
|
||||
gate.failed_playtest_revision = None;
|
||||
gate.updated_at = now;
|
||||
if let Err(error) = write_game_creator_agent_runtime_verification_gate(root, &gate) {
|
||||
@@ -359,8 +360,59 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
|
||||
) -> Result<(), String> {
|
||||
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let revision_unchanged = current_revision.revision == expected_revision.revision;
|
||||
let passed = passed && revision_unchanged;
|
||||
let mut passed = passed && revision_unchanged;
|
||||
let verification_tool = gate.last_verification_tool.clone();
|
||||
let mut static_smoke_game_index_sha256 = None;
|
||||
let mut static_smoke_credential_error = None;
|
||||
if passed && verification_tool.as_deref() == Some("game.static_smoke") {
|
||||
match read_autonomous_evidence_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"game.static_smoke 游戏入口",
|
||||
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
|
||||
) {
|
||||
Ok(Some((digest, bytes))) => {
|
||||
let html = match std::str::from_utf8(&bytes) {
|
||||
Ok(html) => html,
|
||||
Err(error) => {
|
||||
passed = false;
|
||||
static_smoke_credential_error = Some(format!(
|
||||
"game.static_smoke 通过后 game/index.html 不是有效 UTF-8:{error}"
|
||||
));
|
||||
""
|
||||
}
|
||||
};
|
||||
if passed && !html.contains("<html") && !html.contains("<!doctype html") {
|
||||
passed = false;
|
||||
static_smoke_credential_error = Some(
|
||||
"game.static_smoke 通过后 game/index.html 不再是 HTML 文档".to_string(),
|
||||
);
|
||||
}
|
||||
if passed {
|
||||
match validate_game_html_smoke(html) {
|
||||
Ok(()) => static_smoke_game_index_sha256 = Some(digest.sha256),
|
||||
Err(error) => {
|
||||
passed = false;
|
||||
static_smoke_credential_error = Some(format!(
|
||||
"game.static_smoke 通过后 game/index.html 内容已变化且复核失败:{error}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
passed = false;
|
||||
static_smoke_credential_error =
|
||||
Some("game.static_smoke 通过后 game/index.html 缺失".to_string());
|
||||
}
|
||||
Err(error) => {
|
||||
passed = false;
|
||||
static_smoke_credential_error = Some(format!(
|
||||
"game.static_smoke 通过后无法绑定 game/index.html 摘要:{error}"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
gate.verified_revision = passed
|
||||
.then_some(current_revision.revision)
|
||||
.filter(|value| *value > 0);
|
||||
@@ -376,6 +428,8 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
|
||||
}
|
||||
if verification_tool.as_deref() == Some("game.static_smoke") {
|
||||
gate.static_smoke_verified_revision = passed.then_some(current_revision.revision);
|
||||
gate.static_smoke_verified_game_index_sha256 =
|
||||
passed.then_some(static_smoke_game_index_sha256).flatten();
|
||||
}
|
||||
gate.last_verification_status = Some(
|
||||
if passed {
|
||||
@@ -393,6 +447,9 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
|
||||
expected_revision.revision, current_revision.revision
|
||||
));
|
||||
}
|
||||
if let Some(error) = static_smoke_credential_error {
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+10
-3
@@ -2010,8 +2010,7 @@ fn game_chat_fast_path_current_revision_is_verified(
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)?;
|
||||
Ok(revision.revision > 0
|
||||
&& agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision))
|
||||
agent_runtime_static_smoke_passed_for_current_entry_at(root, &gate, revision.revision)
|
||||
}
|
||||
|
||||
fn game_chat_fast_path_is_main_runtime(
|
||||
@@ -2256,7 +2255,11 @@ pub(crate) fn game_chat_fast_path_plan_at(
|
||||
revision.revision,
|
||||
)?;
|
||||
let current_revision_static_smoke_passed = owns_current_mutation
|
||||
&& agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision);
|
||||
&& agent_runtime_static_smoke_passed_for_current_entry_at(
|
||||
root,
|
||||
&gate,
|
||||
revision.revision,
|
||||
)?;
|
||||
let current_revision_failed = owns_current_mutation
|
||||
&& gate.last_verification_status.as_deref()
|
||||
== Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED);
|
||||
@@ -3663,6 +3666,10 @@ mod tests {
|
||||
|
||||
gate.last_verification_tool = Some("game.static_smoke".to_string());
|
||||
gate.static_smoke_verified_revision = Some(1);
|
||||
gate.static_smoke_verified_game_index_sha256 =
|
||||
fs::read(root.join(AGENT_RUNTIME_GAME_INDEX_PATH))
|
||||
.ok()
|
||||
.map(|bytes| format!("{:x}", Sha256::digest(bytes)));
|
||||
write_game_creator_agent_runtime_verification_gate(&root, &gate)
|
||||
.expect("write static smoke gate");
|
||||
assert!(
|
||||
|
||||
@@ -1923,20 +1923,30 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Err(error) = ensure_static_delegate_user_input_wait_at(
|
||||
let waiting_for_user = match ensure_static_delegate_user_input_wait_at(
|
||||
&root,
|
||||
&mut runtime,
|
||||
&deliveries,
|
||||
) {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("Supervisor 用户澄清请求无法安全进入等待态:{error}"),
|
||||
);
|
||||
Ok(waiting) => waiting,
|
||||
Err(error) => {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("Supervisor 用户澄清请求无法安全进入等待态:{error}"),
|
||||
);
|
||||
}
|
||||
};
|
||||
if waiting_for_user {
|
||||
return AgentBackgroundTaskOutcome::WaitingForUserInput;
|
||||
}
|
||||
return AgentBackgroundTaskOutcome::WaitingForUserInput;
|
||||
runtime.phase = "planning".to_string();
|
||||
runtime.current_action =
|
||||
"等待 Project Supervisor 采用安全默认值发起唯一返工".to_string();
|
||||
runtime.waiting_on = "已转换为 needs-repair 的自主构建专业回执".to_string();
|
||||
runtime.next_step = "调用 agent.delegate,并把 repairOfDelegationId 指向原 delivery;返工任务必须采用安全默认值继续,不能再询问用户".to_string();
|
||||
} else if repair_required {
|
||||
runtime.phase = "planning".to_string();
|
||||
runtime.current_action = "等待 Project Supervisor 发起唯一返工".to_string();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -329,6 +329,24 @@ pub(crate) fn ensure_static_delegate_user_input_wait_at(
|
||||
runtime: &mut AgentRuntimeState,
|
||||
deliveries: &[StaticDelegateDeliveryRecord],
|
||||
) -> Result<bool, String> {
|
||||
let parent_task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "needs-user-input 转换缺少当前 parent task".to_string())?;
|
||||
if parent_task.session_id != runtime.session_id
|
||||
|| parent_task.source != runtime.source
|
||||
|| parent_task.run_profile != runtime.run_profile
|
||||
|| parent_task.run_profile_binding_fingerprint != runtime.run_profile_binding_fingerprint
|
||||
{
|
||||
return Err("needs-user-input 转换的 parent task 与 runtime 身份不一致".to_string());
|
||||
}
|
||||
if convert_claimed_game_chat_user_input_deliveries_to_repair_at(root, &parent_task, deliveries)?
|
||||
> 0
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let mut pending_deliveries = deliveries.iter().filter(|delivery| {
|
||||
delivery.structured_result.as_ref().is_some_and(|result| {
|
||||
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|
||||
|
||||
+113
-11
@@ -6145,7 +6145,21 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
};
|
||||
if !agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision) {
|
||||
let static_smoke_matches_current_entry =
|
||||
match agent_runtime_static_smoke_passed_for_current_entry_at(
|
||||
root,
|
||||
&gate,
|
||||
revision.revision,
|
||||
) {
|
||||
Ok(matches) => matches,
|
||||
Err(error) => {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"preview-readiness 无法复核当前 game.static_smoke 入口摘要",
|
||||
error,
|
||||
));
|
||||
}
|
||||
};
|
||||
if !static_smoke_matches_current_entry {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"preview-readiness 尚未通过当前 revision 的 game.static_smoke",
|
||||
format!(
|
||||
@@ -6198,9 +6212,29 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
};
|
||||
if receipt.revision != revision.revision {
|
||||
let current_index = match read_autonomous_evidence_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"preview-playtest 当前游戏入口",
|
||||
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
|
||||
) {
|
||||
Ok(Some((digest, _))) => digest,
|
||||
Ok(None) => {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"preview-playtest 当前游戏入口缺失",
|
||||
"必须重新生成 game/index.html 并在当前 revision 执行 preview.validate。",
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"preview-playtest 当前游戏入口无法安全读取",
|
||||
error,
|
||||
));
|
||||
}
|
||||
};
|
||||
if receipt.revision != revision.revision || receipt.game_index != current_index {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"preview-playtest 浏览器试玩回执不属于当前 revision",
|
||||
"preview-playtest 浏览器试玩回执不属于当前 revision 或入口产物",
|
||||
format!(
|
||||
"receiptRevision={}, currentRevision={}",
|
||||
receipt.revision, revision.revision
|
||||
@@ -6374,13 +6408,13 @@ fn game_chat_main_completion_blocker_at_locked(
|
||||
),
|
||||
));
|
||||
}
|
||||
let current_index_bytes = match read_autonomous_evidence_file_at(
|
||||
let (current_index, current_index_bytes) = match read_autonomous_evidence_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"game-chat 主 Agent 游戏入口",
|
||||
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
|
||||
) {
|
||||
Ok(Some((_, bytes))) => bytes,
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"game-chat code-prototype 尚未生成 game/index.html",
|
||||
@@ -6422,11 +6456,12 @@ fn game_chat_main_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
};
|
||||
if !agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision) {
|
||||
if !agent_runtime_static_smoke_passed_for_entry(&gate, revision.revision, ¤t_index.sha256)
|
||||
{
|
||||
return Some(autonomous_completion_blocker(
|
||||
"game-chat code-prototype 尚未通过当前 revision 的 game.static_smoke",
|
||||
"game-chat code-prototype 尚未通过当前 game/index.html 的 game.static_smoke",
|
||||
format!(
|
||||
"currentRevision={}, staticSmokeVerifiedRevision={}",
|
||||
"path=game/index.html, currentRevision={}, staticSmokeVerifiedRevision={}",
|
||||
revision.revision,
|
||||
gate.static_smoke_verified_revision
|
||||
.map(|value| value.to_string())
|
||||
@@ -6449,9 +6484,9 @@ fn game_chat_main_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
};
|
||||
if receipt.revision != revision.revision {
|
||||
if receipt.revision != revision.revision || receipt.game_index != current_index {
|
||||
return Some(autonomous_completion_blocker(
|
||||
"game-chat code-prototype 试玩回执不属于当前 revision",
|
||||
"game-chat code-prototype 试玩回执不属于当前 revision 或入口产物",
|
||||
format!(
|
||||
"receiptRevision={}, currentRevision={}",
|
||||
receipt.revision, revision.revision
|
||||
@@ -7309,6 +7344,52 @@ pub(in crate::agent) fn javascript_is_syntactically_valid(content: &str, is_modu
|
||||
semantic.diagnostics.is_empty()
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn validate_executable_inline_javascript_syntax(
|
||||
html: &str,
|
||||
) -> Result<(), String> {
|
||||
let lower = html.to_ascii_lowercase();
|
||||
let mut cursor = 0usize;
|
||||
let mut executable_index = 0usize;
|
||||
while let Some(offset) = lower[cursor..].find('<') {
|
||||
let tag_start = cursor + offset;
|
||||
if lower[tag_start..].starts_with("<!--") {
|
||||
cursor = lower[tag_start + 4..]
|
||||
.find("-->")
|
||||
.map(|end| tag_start + 4 + end + 3)
|
||||
.unwrap_or(lower.len());
|
||||
continue;
|
||||
}
|
||||
let Some(tag_after) = html_tag_end(&lower, tag_start) else {
|
||||
break;
|
||||
};
|
||||
let tag = &lower[tag_start..tag_after];
|
||||
if let Some(end) = html_non_executable_container_end(&lower, tag, tag_after - 1) {
|
||||
cursor = end;
|
||||
continue;
|
||||
}
|
||||
if !html_tag_starts_element(tag, "script") {
|
||||
cursor = tag_after;
|
||||
continue;
|
||||
}
|
||||
let Some((close_start, close_end)) =
|
||||
raw_text_html_element_close(&lower, tag_after, "script")
|
||||
else {
|
||||
return Err("游戏入口的 <script> 代码块未闭合,疑似源码被截断".to_string());
|
||||
};
|
||||
if html_script_executes_in_modern_browser(tag) && !html_has_attribute(tag, "src") {
|
||||
executable_index += 1;
|
||||
let body = &html[tag_after..close_start];
|
||||
if !javascript_is_syntactically_valid(body, html_script_is_module(tag)) {
|
||||
return Err(format!(
|
||||
"游戏入口第 {executable_index} 个可执行内联脚本不是有效 JavaScript"
|
||||
));
|
||||
}
|
||||
}
|
||||
cursor = close_end;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct JavascriptLiteralSpanCollector {
|
||||
strings: Vec<std::ops::Range<usize>>,
|
||||
@@ -14689,6 +14770,23 @@ pub(in crate::agent) fn read_autonomous_playtest_receipt(
|
||||
Ok(receipt)
|
||||
}
|
||||
|
||||
fn browser_validation_has_required_passed_viewports(result: &BrowserValidationResult) -> bool {
|
||||
result.viewport_results.len() == 2
|
||||
&& [
|
||||
BrowserValidationViewport::Desktop,
|
||||
BrowserValidationViewport::Mobile,
|
||||
]
|
||||
.iter()
|
||||
.all(|required| {
|
||||
result
|
||||
.viewport_results
|
||||
.iter()
|
||||
.filter(|viewport| viewport.viewport == *required && viewport.passed)
|
||||
.count()
|
||||
== 1
|
||||
})
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn verify_autonomous_playtest_evidence_files_at(
|
||||
root: &Path,
|
||||
receipt: &AgentRuntimeAutonomousPlaytestReceipt,
|
||||
@@ -14722,6 +14820,7 @@ pub(in crate::agent) fn verify_autonomous_playtest_evidence_files_at(
|
||||
.as_ref()
|
||||
.ok_or_else(|| "自主试玩浏览器报告缺少交互试玩结果".to_string())?;
|
||||
if !report.passed
|
||||
|| !browser_validation_has_required_passed_viewports(&report)
|
||||
|| !playtest.passed
|
||||
|| !playtest.matches_scenario_contract()
|
||||
|| playtest.scenario != receipt.playtest_scenario
|
||||
@@ -14753,6 +14852,7 @@ pub(in crate::agent) fn write_autonomous_playtest_receipt_at(
|
||||
let expected_scenario_fingerprint =
|
||||
browser_playtest_scenario_fingerprint(contract.playtest_scenario);
|
||||
if !result.passed
|
||||
|| !browser_validation_has_required_passed_viewports(result)
|
||||
|| !playtest.passed
|
||||
|| !playtest.matches_scenario_contract()
|
||||
|| playtest.scenario != contract.playtest_scenario
|
||||
@@ -14790,6 +14890,7 @@ pub(in crate::agent) fn write_autonomous_playtest_receipt_at(
|
||||
.as_ref()
|
||||
.ok_or_else(|| "持久浏览器报告缺少交互试玩结果".to_string())?;
|
||||
if !persisted_report.passed
|
||||
|| !browser_validation_has_required_passed_viewports(&persisted_report)
|
||||
|| !persisted_playtest.passed
|
||||
|| !persisted_playtest.matches_scenario_contract()
|
||||
|| persisted_playtest.scenario != playtest.scenario
|
||||
@@ -15115,7 +15216,8 @@ pub(in crate::agent) fn autonomous_game_build_completion_blocker_at_locked(
|
||||
));
|
||||
}
|
||||
};
|
||||
if !agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision) {
|
||||
if !agent_runtime_static_smoke_passed_for_entry(&gate, revision.revision, ¤t_index.sha256)
|
||||
{
|
||||
return Some(autonomous_completion_blocker(
|
||||
"当前项目 revision 尚未通过 game.static_smoke",
|
||||
format!(
|
||||
|
||||
+176
-32
@@ -111,7 +111,16 @@ fn game_chat_manifest_seed_projection_starts_only_the_single_main_agent() {
|
||||
}
|
||||
|
||||
fn cropped_spritesheet_game_html() -> &'static str {
|
||||
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);</script></body></html>"
|
||||
"<!doctype html><html><body><p>目标:移动角色收集全部目标,避开障碍并获得胜利;失败后可按 R 重新开始。</p><canvas width=320 height=180></canvas><script>const canvas=document.querySelector('canvas');const context=canvas.getContext('2d');let playerX=0;const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';window.addEventListener('keydown',(event)=>{playerX+=event.key==='ArrowRight'?1:-1;});function draw(){context.clearRect(0,0,canvas.width,canvas.height);context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);context.fillText(String(playerX),8,172);requestAnimationFrame(draw);}requestAnimationFrame(draw);</script></body></html>"
|
||||
}
|
||||
|
||||
fn with_static_smoke_contract(html: &str) -> String {
|
||||
let contract = "<p>目标:移动角色收集全部目标并获得胜利;碰到危险即失败,按 R 重新开始。</p><script>const staticSmokeCanvas=document.querySelector('canvas');const staticSmokeContext=staticSmokeCanvas.getContext('2d');let staticSmokePlayerX=0;window.addEventListener('keydown',(event)=>{if(event.key==='ArrowRight'){staticSmokePlayerX+=1;}if(event.key==='r'||event.key==='R'){staticSmokePlayerX=0;}});function staticSmokeLoop(){staticSmokeContext.fillStyle='#ffffff';staticSmokeContext.fillRect(staticSmokePlayerX,0,1,1);requestAnimationFrame(staticSmokeLoop);}requestAnimationFrame(staticSmokeLoop);</script>";
|
||||
if let Some(body_end) = html.rfind("</body>") {
|
||||
format!("{}{}{}", &html[..body_end], contract, &html[body_end..])
|
||||
} else {
|
||||
format!("{html}{contract}")
|
||||
}
|
||||
}
|
||||
|
||||
fn executable_tetris_game_html() -> String {
|
||||
@@ -5378,11 +5387,10 @@ fn autonomous_preview_manifest_tasks_accept_bound_current_revision_receipts() {
|
||||
let readiness_child =
|
||||
queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-readiness");
|
||||
let readiness_state = agent_runtime_state_from_task_record(&readiness_child);
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&parent_state,
|
||||
"<!doctype html><title>静态检查通过</title><canvas></canvas>",
|
||||
let readiness_html = with_static_smoke_contract(
|
||||
"<!doctype html><html><body><title>静态检查通过</title><canvas></canvas></body></html>",
|
||||
);
|
||||
advance_game_index_revision(&root, &parent_state, &readiness_html);
|
||||
mark_verification_passed(&root, &readiness_state, "game.static_smoke");
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &readiness_state).is_none());
|
||||
|
||||
@@ -5655,6 +5663,50 @@ fn autonomous_playtest_receipt_rejects_previous_scenario_fingerprint() {
|
||||
assert!(error.contains("场景指纹"), "unexpected error: {error}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_playtest_receipt_requires_passed_desktop_and_mobile_viewports() {
|
||||
let (_temporary, root, state, contract) =
|
||||
autonomous_fixture("做一个完整小游戏", "autonomous-required-viewports-run");
|
||||
let revision = advance_game_index_revision(
|
||||
&root,
|
||||
&state,
|
||||
"<!doctype html><title>双视口试玩</title><canvas></canvas>",
|
||||
);
|
||||
let result =
|
||||
browser_result_fixture(&root, &state, revision, BrowserPlaytestScenario::GenericV1);
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "preview.validate".to_string(),
|
||||
reason: Some("验证 desktop/mobile 试玩合同".to_string()),
|
||||
input: serde_json::json!({}),
|
||||
};
|
||||
let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task);
|
||||
let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint);
|
||||
|
||||
let mut duplicate_desktop = result.clone();
|
||||
duplicate_desktop.viewport_results[1].viewport = BrowserValidationViewport::Desktop;
|
||||
write_autonomous_playtest_receipt_at(
|
||||
&root,
|
||||
&contract,
|
||||
&action_id,
|
||||
&action_fingerprint,
|
||||
revision,
|
||||
&duplicate_desktop,
|
||||
)
|
||||
.expect_err("duplicate desktop must not satisfy the mobile viewport gate");
|
||||
|
||||
let mut failed_mobile = result;
|
||||
failed_mobile.viewport_results[1].passed = false;
|
||||
write_autonomous_playtest_receipt_at(
|
||||
&root,
|
||||
&contract,
|
||||
&action_id,
|
||||
&action_fingerprint,
|
||||
revision,
|
||||
&failed_mobile,
|
||||
)
|
||||
.expect_err("failed mobile must not satisfy the dual viewport gate");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_scenario_receipt_reads_as_missing_and_can_be_replaced() {
|
||||
let (_temporary, root, state, contract) = autonomous_fixture(
|
||||
@@ -5849,6 +5901,107 @@ fn game_chat_parent_completion_stops_after_main_agent_smoke_and_dual_viewport_pl
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &state).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_completion_rejects_same_revision_entry_rewrite_after_static_smoke() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
|
||||
"创建一轮植物塔防游戏",
|
||||
"game-chat-static-smoke-entry-rewrite",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
);
|
||||
install_game_chat_existing_art_manifest(&root);
|
||||
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
|
||||
.expect("start the game-chat main agent");
|
||||
let mut main_state = start_game_chat_main_agent(&root, &parent_state);
|
||||
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut main_state);
|
||||
let revision = advance_game_index_revision(&root, &main_state, cropped_spritesheet_game_html());
|
||||
mark_verification_passed(&root, &main_state, "game.static_smoke");
|
||||
|
||||
let rewritten = format!(
|
||||
"{}\n<!-- same-revision bypass rewrite -->",
|
||||
cropped_spritesheet_game_html()
|
||||
);
|
||||
fs::write(root.join(AGENT_RUNTIME_GAME_INDEX_PATH), rewritten)
|
||||
.expect("rewrite game entry without advancing project revision");
|
||||
persist_game_chat_main_playtest_receipt(&root, &parent_state, &main_state, revision);
|
||||
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &main_state)
|
||||
.expect("a static-smoke credential for different entry bytes must not complete");
|
||||
assert_eq!(blocker.tool, "runtime.autonomous_completion");
|
||||
assert!(blocker.summary.contains("game.static_smoke"));
|
||||
assert!(blocker
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("game/index.html")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_prepared_finalization_rejects_same_revision_entry_rewrite() {
|
||||
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
|
||||
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
|
||||
"创建一轮植物塔防游戏",
|
||||
"game-chat-static-smoke-prepared-rewrite",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
);
|
||||
install_game_chat_existing_art_manifest(&root);
|
||||
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
|
||||
.expect("start the game-chat main agent");
|
||||
let mut main_state = start_game_chat_main_agent(&root, &parent_state);
|
||||
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut main_state);
|
||||
let revision = advance_game_index_revision(&root, &main_state, cropped_spritesheet_game_html());
|
||||
mark_verification_passed(&root, &main_state, "game.static_smoke");
|
||||
persist_game_chat_main_playtest_receipt(&root, &parent_state, &main_state, revision);
|
||||
main_state.status = "running".to_string();
|
||||
main_state.phase = "finalizing".to_string();
|
||||
main_state.current_action = "测试恢复 prepared finalization".to_string();
|
||||
append_game_creator_agent_runtime_task(&root, &main_state)
|
||||
.expect("append finalizing game-chat main task");
|
||||
write_game_creator_agent_runtime_state(&root, &main_state)
|
||||
.expect("persist finalizing game-chat main state");
|
||||
|
||||
let response = "不应复用旧静态检查凭证的完成回复";
|
||||
let journal = build_game_creator_agent_runtime_finalization_journal(
|
||||
&root,
|
||||
&main_state,
|
||||
response,
|
||||
revision,
|
||||
)
|
||||
.expect("build prepared game-chat finalization");
|
||||
write_game_creator_agent_runtime_finalization_journal(&root, &journal)
|
||||
.expect("write prepared game-chat finalization");
|
||||
append_game_creator_agent_runtime_finalization_lifecycle_stage(
|
||||
&root,
|
||||
&journal,
|
||||
"prepared",
|
||||
journal.prepared_at,
|
||||
)
|
||||
.expect("write prepared game-chat lifecycle");
|
||||
fs::write(
|
||||
root.join(AGENT_RUNTIME_GAME_INDEX_PATH),
|
||||
format!(
|
||||
"{}\n<!-- prepared same-revision bypass rewrite -->",
|
||||
cropped_spritesheet_game_html()
|
||||
),
|
||||
)
|
||||
.expect("rewrite game entry after prepared finalization");
|
||||
|
||||
assert_eq!(
|
||||
resume_game_creator_agent_finalization_for_test_at(&root, &main_state.agent_id)
|
||||
.expect("resume game-chat prepared finalization"),
|
||||
"not-found"
|
||||
);
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(&main_state.agent_id),
|
||||
Some(&main_state.session_id),
|
||||
)
|
||||
.expect("read game-chat main conversation");
|
||||
assert!(!conversation
|
||||
.messages
|
||||
.iter()
|
||||
.any(|message| message.role == "assistant" && message.content == response));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_schedule_ready_tool_cannot_bypass_the_single_round_publish_boundary() {
|
||||
let (_temporary, root, state, _contract) = autonomous_fixture_with_source(
|
||||
@@ -6111,11 +6264,10 @@ fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
|
||||
.expect("mark code prototype running");
|
||||
let mut code_state = start_game_chat_main_agent(&root, &parent_state);
|
||||
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut code_state);
|
||||
let revision = advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
let pure_code_html = with_static_smoke_contract(
|
||||
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
|
||||
);
|
||||
let revision = advance_game_index_revision(&root, &code_state, &pure_code_html);
|
||||
mark_verification_passed(&root, &code_state, "game.static_smoke");
|
||||
persist_game_chat_main_playtest_receipt(&root, &parent_state, &code_state, revision);
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
|
||||
@@ -6131,7 +6283,8 @@ fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
|
||||
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const sheet=new Image();sheet.src='../assets/art-spritesheet.png';const context=document.querySelector('canvas').getContext('2d');context.drawImage(sheet,0,0,32,32,0,0,64,64);</script></body></html>",
|
||||
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';function neverDraw(){context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);}if(false) neverDraw();</script></body></html>",
|
||||
] {
|
||||
let revision = advance_game_index_revision(&root, &code_state, html);
|
||||
let html = with_static_smoke_contract(html);
|
||||
let revision = advance_game_index_revision(&root, &code_state, &html);
|
||||
mark_verification_passed(&root, &code_state, "game.static_smoke");
|
||||
persist_game_chat_main_playtest_receipt(&root, &parent_state, &code_state, revision);
|
||||
assert!(
|
||||
@@ -6140,11 +6293,10 @@ fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
|
||||
);
|
||||
}
|
||||
|
||||
let overwritten_revision = advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
let overwritten_html = with_static_smoke_contract(
|
||||
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';playerArt.src='data:image/png;base64,overridden';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);</script></body></html>",
|
||||
);
|
||||
let overwritten_revision = advance_game_index_revision(&root, &code_state, &overwritten_html);
|
||||
mark_verification_passed(&root, &code_state, "game.static_smoke");
|
||||
persist_game_chat_main_playtest_receipt(
|
||||
&root,
|
||||
@@ -6262,11 +6414,10 @@ fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_co
|
||||
let code_record =
|
||||
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
|
||||
let code_state = agent_runtime_state_from_task_record(&code_record);
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
let art_spec_only_html = with_static_smoke_contract(
|
||||
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
|
||||
);
|
||||
advance_game_index_revision(&root, &code_state, &art_spec_only_html);
|
||||
mark_verification_passed(&root, &code_state, "game.static_smoke");
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
|
||||
.expect("CLI must still require the art spritesheet");
|
||||
@@ -6370,11 +6521,10 @@ fn game_chat_code_prototype_fails_closed_without_generated_spritesheet() {
|
||||
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut code_state);
|
||||
fs::remove_file(root.join("assets/art-spritesheet.png"))
|
||||
.expect("remove required art-spritesheet file");
|
||||
let revision = advance_game_index_revision(
|
||||
&root,
|
||||
&code_state,
|
||||
let pure_code_html = with_static_smoke_contract(
|
||||
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
|
||||
);
|
||||
let revision = advance_game_index_revision(&root, &code_state, &pure_code_html);
|
||||
mark_verification_passed(&root, &code_state, "game.static_smoke");
|
||||
persist_game_chat_main_playtest_receipt(&root, &parent_state, &code_state, revision);
|
||||
|
||||
@@ -7768,11 +7918,8 @@ fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest(
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("game/index.html(initial-placeholder)")));
|
||||
|
||||
let revision = advance_game_index_revision(
|
||||
&root,
|
||||
&state,
|
||||
"<!doctype html><title>可玩塔防</title><canvas></canvas>",
|
||||
);
|
||||
let valid_game = cropped_spritesheet_game_html().to_string();
|
||||
let revision = advance_game_index_revision(&root, &state, &valid_game);
|
||||
mark_verification_passed(&root, &state, "project.verify");
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state)
|
||||
.expect("project.verify cannot replace static smoke");
|
||||
@@ -7839,11 +7986,8 @@ fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest(
|
||||
.expect("persist autonomous playtest receipt");
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &state).is_none());
|
||||
|
||||
advance_game_index_revision(
|
||||
&root,
|
||||
&state,
|
||||
"<!doctype html><title>试玩后又修改</title><canvas></canvas>",
|
||||
);
|
||||
let changed_game = format!("{valid_game}\n<!-- 试玩后又修改 -->");
|
||||
advance_game_index_revision(&root, &state, &changed_game);
|
||||
mark_verification_passed(&root, &state, "game.static_smoke");
|
||||
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state)
|
||||
.expect("stale playtest must block completion");
|
||||
@@ -7856,11 +8000,10 @@ fn autonomous_prepared_finalization_without_playtest_is_discarded_before_assista
|
||||
"做一个植物大战僵尸式塔防游戏",
|
||||
"autonomous-prepared-finalization-run",
|
||||
);
|
||||
let revision = advance_game_index_revision(
|
||||
&root,
|
||||
&state,
|
||||
"<!doctype html><title>尚未试玩的塔防</title><canvas></canvas>",
|
||||
let unplayed_html = with_static_smoke_contract(
|
||||
"<!doctype html><html><body><title>尚未试玩的塔防</title><canvas></canvas></body></html>",
|
||||
);
|
||||
let revision = advance_game_index_revision(&root, &state, &unplayed_html);
|
||||
mark_verification_passed(&root, &state, "game.static_smoke");
|
||||
state.status = "running".to_string();
|
||||
state.phase = "finalizing".to_string();
|
||||
@@ -7920,6 +8063,7 @@ fn autonomous_playtest_liveness_only_enforces_the_latest_preview_result() {
|
||||
last_verification_tool: Some("game.static_smoke".to_string()),
|
||||
last_verification_status: Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string()),
|
||||
static_smoke_verified_revision: Some(2),
|
||||
static_smoke_verified_game_index_sha256: Some("a".repeat(64)),
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
};
|
||||
|
||||
@@ -286,6 +286,11 @@ pub(crate) struct AgentRuntimeVerificationGate {
|
||||
/// proof that the exact same revision passed `game.static_smoke`.
|
||||
#[serde(default)]
|
||||
pub(crate) static_smoke_verified_revision: Option<u64>,
|
||||
/// Bind the durable static-smoke credential to the exact entry bytes.
|
||||
/// A project revision alone is insufficient because files can be changed
|
||||
/// outside the Runtime mutation path without advancing that revision.
|
||||
#[serde(default)]
|
||||
pub(crate) static_smoke_verified_game_index_sha256: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) failed_playtest_revision: Option<u64>,
|
||||
pub(crate) updated_at: u64,
|
||||
|
||||
@@ -49,6 +49,7 @@ pub(in crate::agent) fn default_agent_runtime_verification_gate(
|
||||
last_verification_tool: None,
|
||||
last_verification_status: None,
|
||||
static_smoke_verified_revision: None,
|
||||
static_smoke_verified_game_index_sha256: None,
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
})
|
||||
@@ -125,6 +126,20 @@ pub(in crate::agent) fn validate_agent_runtime_verification_gate(
|
||||
"Agent Runtime verification gate 的 failedPlaytestRevision 必须大于 0".to_string(),
|
||||
);
|
||||
}
|
||||
if gate
|
||||
.static_smoke_verified_game_index_sha256
|
||||
.as_deref()
|
||||
.is_some_and(|sha256| !is_lowercase_sha256(sha256))
|
||||
{
|
||||
return Err("Agent Runtime verification gate 的 static smoke 入口摘要无效".to_string());
|
||||
}
|
||||
if gate.static_smoke_verified_revision.is_none()
|
||||
&& gate.static_smoke_verified_game_index_sha256.is_some()
|
||||
{
|
||||
return Err(
|
||||
"Agent Runtime verification gate 的 static smoke 入口摘要缺少 revision".to_string(),
|
||||
);
|
||||
}
|
||||
if gate
|
||||
.failed_playtest_revision
|
||||
.zip(gate.mutation_revision)
|
||||
@@ -195,11 +210,35 @@ pub(in crate::agent) fn validate_agent_runtime_verification_gate(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn agent_runtime_static_smoke_passed_for_revision(
|
||||
pub(in crate::agent) fn agent_runtime_static_smoke_passed_for_entry(
|
||||
gate: &AgentRuntimeVerificationGate,
|
||||
revision: u64,
|
||||
game_index_sha256: &str,
|
||||
) -> bool {
|
||||
revision > 0 && gate.static_smoke_verified_revision == Some(revision)
|
||||
revision > 0
|
||||
&& gate.static_smoke_verified_revision == Some(revision)
|
||||
&& gate.static_smoke_verified_game_index_sha256.as_deref() == Some(game_index_sha256)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn agent_runtime_static_smoke_passed_for_current_entry_at(
|
||||
root: &Path,
|
||||
gate: &AgentRuntimeVerificationGate,
|
||||
revision: u64,
|
||||
) -> Result<bool, String> {
|
||||
let Some((game_index, _)) = read_autonomous_evidence_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"game.static_smoke 当前游戏入口",
|
||||
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
|
||||
)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(agent_runtime_static_smoke_passed_for_entry(
|
||||
gate,
|
||||
revision,
|
||||
&game_index.sha256,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn read_game_creator_agent_runtime_project_revision(
|
||||
|
||||
@@ -35,7 +35,11 @@ pub(in crate::agent) use run_status::*;
|
||||
pub(in crate::agent) use task_ops::*;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) use delivery::build_static_delegate_result_for_child_at;
|
||||
pub(crate) use delivery::{
|
||||
build_static_delegate_result_for_child_at,
|
||||
convert_game_chat_child_user_input_to_safe_default_repair,
|
||||
trusted_game_chat_autonomous_root_parent_at,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(crate) use media::validate_agent_runtime_canvas_replacement_authorization_at;
|
||||
|
||||
@@ -51,7 +55,9 @@ pub(crate) use delegation::{
|
||||
};
|
||||
pub(crate) use delivery::{
|
||||
agent_runtime_delegation_id, dispatch_isolated_agent_join_at,
|
||||
game_chat_safe_default_repair_replacement, game_chat_safe_default_repair_task_instruction,
|
||||
game_creator_agent_runtime_terminal_status, publish_game_creator_agent_delegate_result,
|
||||
reconcile_claimed_game_chat_safe_default_half_states_at,
|
||||
reconcile_game_creator_agent_delegate_receipts_at,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
|
||||
@@ -286,18 +286,18 @@ fn validate_game_chat_main_art_delegation_at(
|
||||
acceptance_criteria: &[String],
|
||||
expected_artifacts: &[String],
|
||||
repair_of_delegation_id: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<bool, String> {
|
||||
let Some(binding) =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, parent_agent_id, parent_run_id)?
|
||||
else {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
};
|
||||
let may_be_game_chat = binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
|| (binding.source == "agent-ready-task-scheduler"
|
||||
&& binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID);
|
||||
if !may_be_game_chat {
|
||||
return Ok(());
|
||||
return Ok(false);
|
||||
}
|
||||
let root_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
@@ -320,13 +320,32 @@ fn validate_game_chat_main_art_delegation_at(
|
||||
);
|
||||
}
|
||||
if !is_game_chat_main {
|
||||
return Ok(());
|
||||
}
|
||||
if repair_of_delegation_id.is_some() {
|
||||
return Err(
|
||||
"game-chat 主 Agent 美术委派不允许发起返工链;请认领当前回执后重新审计".to_string(),
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
let safe_default_repair = if let Some(original_delegation_id) = repair_of_delegation_id {
|
||||
let parent_task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 安全默认返工缺少 code-prototype 父任务".to_string())?;
|
||||
let original = read_static_delegate_delivery_at(root, original_delegation_id)?
|
||||
.ok_or_else(|| "game-chat 安全默认返工引用的原 delivery 不存在".to_string())?;
|
||||
if original.repair_of_delegation_id.is_some()
|
||||
|| original.target_agent_id != target_agent_id
|
||||
|| original.acceptance_criteria != acceptance_criteria
|
||||
|| original.expected_artifacts != expected_artifacts
|
||||
|| !trusted_game_chat_safe_default_repair_delivery_at(root, &parent_task, &original)?
|
||||
{
|
||||
return Err(
|
||||
"game-chat code-prototype 只能对同一安全默认 delivery 发起唯一、同合同的一层返工"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if !matches!(target_agent_id, "art-director" | "art-asset-plan") {
|
||||
return Err(
|
||||
"game-chat code-prototype 只能按审计结果委派 art-director 或 art-asset-plan"
|
||||
@@ -387,13 +406,14 @@ fn validate_game_chat_main_art_delegation_at(
|
||||
&& delivery.parent_run_id == parent_run_id
|
||||
&& delivery.parent_action_id == action_identity
|
||||
&& delivery.target_agent_id == target_agent_id
|
||||
&& delivery.repair_of_delegation_id.is_none()
|
||||
&& delivery.repair_of_delegation_id.as_deref() == repair_of_delegation_id
|
||||
&& delivery.status != StaticDelegateDeliveryStatus::Suppressed
|
||||
});
|
||||
if static_delegate_target_agent_ids_at(root, parent_agent_id, parent_run_id)?
|
||||
.iter()
|
||||
.any(|existing_target| existing_target == target_agent_id)
|
||||
&& !same_action_replay
|
||||
&& !safe_default_repair
|
||||
{
|
||||
return Err(format!(
|
||||
"game-chat 每个审计缺口最多委派一次:target={target_agent_id}"
|
||||
@@ -404,7 +424,7 @@ fn validate_game_chat_main_art_delegation_at(
|
||||
{
|
||||
return Err("game-chat code-prototype 同一时刻最多保留一个活跃美术委派".to_string());
|
||||
}
|
||||
Ok(())
|
||||
Ok(safe_default_repair)
|
||||
}
|
||||
|
||||
pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
@@ -528,7 +548,68 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
if repair_of_delegation_id.is_some() && agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
let mut safe_default_repair_instruction = None;
|
||||
let game_chat_safe_default_repair = if let Some(original_delegation_id) =
|
||||
repair_of_delegation_id.as_deref()
|
||||
{
|
||||
let parent_task = match read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
agent_id,
|
||||
parent_run_id,
|
||||
) {
|
||||
Ok(parent_task) => parent_task,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
let original = match read_static_delegate_delivery_at(root, original_delegation_id) {
|
||||
Ok(original) => original,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
match (parent_task.as_ref(), original.as_ref()) {
|
||||
(Some(parent_task), Some(original)) => {
|
||||
match trusted_game_chat_safe_default_repair_delivery_at(root, parent_task, original)
|
||||
{
|
||||
Ok(authorized) => {
|
||||
if authorized {
|
||||
safe_default_repair_instruction = original
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.and_then(game_chat_safe_default_repair_task_instruction);
|
||||
}
|
||||
authorized
|
||||
}
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if repair_of_delegation_id.is_some()
|
||||
&& agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& !game_chat_safe_default_repair
|
||||
{
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
@@ -573,7 +654,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
if let Err(error) = validate_game_chat_main_art_delegation_at(
|
||||
let game_chat_safe_default_repair = match validate_game_chat_main_art_delegation_at(
|
||||
root,
|
||||
agent_id,
|
||||
parent_run_id,
|
||||
@@ -583,13 +664,16 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
&expected_artifacts,
|
||||
repair_of_delegation_id.as_deref(),
|
||||
) {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
Ok(safe_default_repair) => safe_default_repair,
|
||||
Err(error) => {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "agent.delegate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: redact_agent_runtime_project_paths(root, &error, 240),
|
||||
detail: None,
|
||||
};
|
||||
}
|
||||
};
|
||||
if let Err(error) =
|
||||
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, parent_run_id)
|
||||
{
|
||||
@@ -609,8 +693,11 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
&target_agent_id,
|
||||
delegation_action_identity,
|
||||
);
|
||||
let delegated_task_text = safe_default_repair_instruction
|
||||
.map(|instruction| format!("{instruction}\n\n{task}"))
|
||||
.unwrap_or(task);
|
||||
let delegated_task = match render_static_delegate_task_contract(
|
||||
&task,
|
||||
&delegated_task_text,
|
||||
agent_id,
|
||||
parent_run_id,
|
||||
&delegation_id,
|
||||
@@ -638,6 +725,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
||||
&& binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
});
|
||||
debug_assert!(!game_chat_safe_default_repair || main_art_delegation);
|
||||
let run_id = if run_id_input.trim().is_empty() {
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || main_art_delegation {
|
||||
format!("delegated-{delegation_id}")
|
||||
|
||||
@@ -1,5 +1,432 @@
|
||||
use super::*;
|
||||
|
||||
const GAME_CHAT_SAFE_DEFAULT_REPAIR_SCHEMA_VERSION: &str = "game-chat-safe-default-repair.v1";
|
||||
const GAME_CHAT_SAFE_DEFAULT_REPAIR_CODE: &str = "child-needs-user-input";
|
||||
const GAME_CHAT_SAFE_DEFAULT_REPAIR_STRATEGY: &str = "continue-with-safe-defaults";
|
||||
const GAME_CHAT_SAFE_DEFAULT_REASON_PREFERENCE: &str = "preference-clarification";
|
||||
const GAME_CHAT_SAFE_DEFAULT_REASON_SENSITIVE: &str = "sensitive-or-permission-request";
|
||||
const GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT: &str = "use-safe-default";
|
||||
const GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED: &str = "skip-denied";
|
||||
const GAME_CHAT_SAFE_DEFAULT_REPAIR_SUMMARY: &str =
|
||||
"专业 Agent 请求了用户补充信息;自主构建必须采用安全默认值继续,并由 Supervisor 发起唯一返工。";
|
||||
const GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY: &str =
|
||||
"自主构建已将专业 Agent 的补充信息请求转换为安全默认返工。";
|
||||
|
||||
fn game_chat_safe_default_request_allows_preference_default(
|
||||
result: &StaticDelegateStructuredResult,
|
||||
) -> bool {
|
||||
let serialized = serde_json::to_string(&result.user_input_questions).unwrap_or_default();
|
||||
let lower = serialized.to_ascii_lowercase();
|
||||
if redact_secret_tokens(&serialized) != serialized
|
||||
|| redact_absolute_path_tokens(&serialized) != serialized
|
||||
|| [
|
||||
".env",
|
||||
"api key",
|
||||
"api_key",
|
||||
"apikey",
|
||||
"token",
|
||||
"secret",
|
||||
"password",
|
||||
"credential",
|
||||
"authorization",
|
||||
"cookie",
|
||||
"bearer ",
|
||||
"权限",
|
||||
"授权",
|
||||
"删除",
|
||||
"发布",
|
||||
"支付",
|
||||
"外部副作用",
|
||||
"permission",
|
||||
"authorize",
|
||||
"delete",
|
||||
"publish",
|
||||
"payment",
|
||||
]
|
||||
.iter()
|
||||
.any(|marker| lower.contains(marker))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
[
|
||||
"偏好",
|
||||
"视觉",
|
||||
"配色",
|
||||
"色彩",
|
||||
"颜色",
|
||||
"风格",
|
||||
"主题",
|
||||
"布局",
|
||||
"难度",
|
||||
"节奏",
|
||||
"preference",
|
||||
"visual",
|
||||
"palette",
|
||||
"color",
|
||||
"style",
|
||||
"theme",
|
||||
"layout",
|
||||
"difficulty",
|
||||
"pace",
|
||||
]
|
||||
.iter()
|
||||
.any(|marker| lower.contains(marker))
|
||||
}
|
||||
|
||||
fn game_chat_safe_default_repair_error(result: &StaticDelegateStructuredResult) -> String {
|
||||
let use_preference_default = game_chat_safe_default_request_allows_preference_default(result);
|
||||
serde_json::json!({
|
||||
"schemaVersion": GAME_CHAT_SAFE_DEFAULT_REPAIR_SCHEMA_VERSION,
|
||||
"code": GAME_CHAT_SAFE_DEFAULT_REPAIR_CODE,
|
||||
"strategy": GAME_CHAT_SAFE_DEFAULT_REPAIR_STRATEGY,
|
||||
"reasonCode": if use_preference_default {
|
||||
GAME_CHAT_SAFE_DEFAULT_REASON_PREFERENCE
|
||||
} else {
|
||||
GAME_CHAT_SAFE_DEFAULT_REASON_SENSITIVE
|
||||
},
|
||||
"defaultDecision": if use_preference_default {
|
||||
GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT
|
||||
} else {
|
||||
GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED
|
||||
},
|
||||
"requestSha256": result.user_input_questions_sha256,
|
||||
"summary": GAME_CHAT_SAFE_DEFAULT_REPAIR_SUMMARY,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn game_chat_safe_default_repair_marker(
|
||||
result: &StaticDelegateStructuredResult,
|
||||
) -> Option<serde_json::Value> {
|
||||
if result.contract_status != StaticDelegateContractStatus::NeedsRepair
|
||||
|| !result.user_input_questions.is_empty()
|
||||
|| result.user_input_questions_sha256.is_some()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let marker = serde_json::from_str::<serde_json::Value>(result.error.as_deref()?).ok()?;
|
||||
let reason = marker.get("reasonCode")?.as_str()?;
|
||||
let decision = marker.get("defaultDecision")?.as_str()?;
|
||||
let request_sha256 = marker.get("requestSha256")?.as_str()?;
|
||||
(marker.get("schemaVersion")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_SCHEMA_VERSION
|
||||
&& marker.get("code")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_CODE
|
||||
&& marker.get("strategy")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_STRATEGY
|
||||
&& marker.get("summary")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_SUMMARY
|
||||
&& matches!(
|
||||
(reason, decision),
|
||||
(
|
||||
GAME_CHAT_SAFE_DEFAULT_REASON_PREFERENCE,
|
||||
GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT
|
||||
) | (
|
||||
GAME_CHAT_SAFE_DEFAULT_REASON_SENSITIVE,
|
||||
GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED
|
||||
)
|
||||
)
|
||||
&& request_sha256.len() == 64
|
||||
&& request_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()))
|
||||
.then_some(marker)
|
||||
}
|
||||
|
||||
pub(crate) fn game_chat_safe_default_repair_result_is_valid(
|
||||
result: &StaticDelegateStructuredResult,
|
||||
) -> bool {
|
||||
game_chat_safe_default_repair_marker(result).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn game_chat_safe_default_repair_task_instruction(
|
||||
result: &StaticDelegateStructuredResult,
|
||||
) -> Option<&'static str> {
|
||||
let marker = game_chat_safe_default_repair_marker(result)?;
|
||||
match marker.get("defaultDecision")?.as_str()? {
|
||||
GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT => Some(
|
||||
"Runtime 安全默认决策:use-safe-default。仅采用无需用户补充、无需新增权限的普通偏好默认值继续;不得再次询问用户。",
|
||||
),
|
||||
GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED => Some(
|
||||
"Runtime 安全默认决策:skip-denied。跳过被拒绝、敏感、越权或无法证明安全的输入与动作,仅在原合同和现有权限内继续;不得再次询问用户。",
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn game_chat_safe_default_repair_replacement(
|
||||
result: &StaticDelegateStructuredResult,
|
||||
) -> Option<(String, StaticDelegateStructuredResult)> {
|
||||
let mut replacement = result.clone();
|
||||
if replacement.contract_status == StaticDelegateContractStatus::NeedsUserInput {
|
||||
convert_game_chat_child_user_input_to_safe_default_repair(&mut replacement);
|
||||
} else if !game_chat_safe_default_repair_result_is_valid(&replacement) {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY.to_string(),
|
||||
replacement,
|
||||
))
|
||||
}
|
||||
|
||||
fn trusted_game_chat_autonomous_root_binding(
|
||||
parent_task: &AgentRuntimeTaskRecord,
|
||||
binding: &AgentRuntimeRunProfileBinding,
|
||||
) -> bool {
|
||||
parent_task.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& parent_task.run_id == binding.run_id
|
||||
&& parent_task.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
&& parent_task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& parent_task.parent_agent_id.is_none()
|
||||
&& parent_task.parent_run_id.is_none()
|
||||
&& binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& binding.root_agent_id == binding.agent_id
|
||||
&& binding.root_run_id == binding.run_id
|
||||
&& binding.parent_agent_id.is_none()
|
||||
&& binding.parent_run_id.is_none()
|
||||
&& binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
&& binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& parent_task.run_profile_binding_fingerprint == binding.binding_fingerprint
|
||||
}
|
||||
|
||||
fn trusted_game_chat_autonomous_parent_chain_at(
|
||||
root: &Path,
|
||||
parent_task: &AgentRuntimeTaskRecord,
|
||||
) -> Result<Option<AgentRuntimeRunProfileBinding>, String> {
|
||||
let Some(parent_binding) = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&parent_task.agent_id,
|
||||
&parent_task.run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if parent_task.agent_id != parent_binding.agent_id
|
||||
|| parent_task.run_id != parent_binding.run_id
|
||||
|| parent_task.source != parent_binding.source
|
||||
|| parent_task.run_profile != parent_binding.profile
|
||||
|| parent_task.parent_agent_id != parent_binding.parent_agent_id
|
||||
|| parent_task.parent_run_id != parent_binding.parent_run_id
|
||||
|| parent_task.run_profile_binding_fingerprint != parent_binding.binding_fingerprint
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
if trusted_game_chat_autonomous_root_binding(parent_task, &parent_binding) {
|
||||
return Ok(Some(parent_binding));
|
||||
}
|
||||
if parent_task.agent_id != "code-prototype"
|
||||
|| parent_task.source != "agent-ready-task-scheduler"
|
||||
|| parent_task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| parent_task.delegation_id.is_some()
|
||||
|| parent_binding.parent_agent_id.as_deref()
|
||||
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
|| parent_binding.parent_run_id.as_deref() != Some(parent_binding.root_run_id.as_str())
|
||||
|| parent_binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| parent_binding.parent_binding_fingerprint.is_none()
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&parent_binding.root_agent_id,
|
||||
&parent_binding.root_run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(root_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&root_binding.agent_id,
|
||||
&root_binding.run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !trusted_game_chat_autonomous_root_binding(&root_task, &root_binding)
|
||||
|| parent_binding.parent_binding_fingerprint.as_deref()
|
||||
!= Some(root_binding.binding_fingerprint.as_str())
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(parent_binding))
|
||||
}
|
||||
|
||||
pub(crate) fn trusted_game_chat_autonomous_root_parent_at(
|
||||
root: &Path,
|
||||
parent_task: &AgentRuntimeTaskRecord,
|
||||
) -> Result<bool, String> {
|
||||
if parent_task.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| parent_task.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
|| parent_task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| parent_task.parent_agent_id.is_some()
|
||||
|| parent_task.parent_run_id.is_some()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let Some(binding) = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&parent_task.agent_id,
|
||||
&parent_task.run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(trusted_game_chat_autonomous_root_binding(
|
||||
parent_task,
|
||||
&binding,
|
||||
))
|
||||
}
|
||||
|
||||
fn trusted_game_chat_autonomous_child_delivery_at(
|
||||
root: &Path,
|
||||
parent_task: &AgentRuntimeTaskRecord,
|
||||
delivery: &StaticDelegateDeliveryRecord,
|
||||
supplied_child_task: Option<&AgentRuntimeTaskRecord>,
|
||||
) -> Result<bool, String> {
|
||||
let Some(parent_binding) = trusted_game_chat_autonomous_parent_chain_at(root, parent_task)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
if delivery.parent_agent_id != parent_task.agent_id
|
||||
|| delivery.parent_session_id != parent_task.session_id
|
||||
|| delivery.parent_run_id != parent_task.run_id
|
||||
|| agent_runtime_delegation_id(
|
||||
&delivery.parent_agent_id,
|
||||
&delivery.parent_run_id,
|
||||
&delivery.target_agent_id,
|
||||
&delivery.parent_action_id,
|
||||
) != delivery.delegation_id
|
||||
|| !matches!(
|
||||
delivery.target_agent_id.as_str(),
|
||||
"code-prototype" | "art-director" | "art-asset-plan"
|
||||
)
|
||||
|| (parent_task.agent_id == "code-prototype"
|
||||
&& !matches!(
|
||||
delivery.target_agent_id.as_str(),
|
||||
"art-director" | "art-asset-plan"
|
||||
))
|
||||
{
|
||||
return Err("game-chat 安全默认返工 delivery 与可信父责任链不一致".to_string());
|
||||
}
|
||||
let child_task = match supplied_child_task {
|
||||
Some(task) => task.clone(),
|
||||
None => read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&delivery.target_agent_id,
|
||||
&delivery.target_run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 安全默认返工缺少专业 child task".to_string())?,
|
||||
};
|
||||
validate_static_delegate_delivery_for_child_result(delivery, parent_task, &child_task)?;
|
||||
let child_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&child_task.agent_id,
|
||||
&child_task.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 安全默认返工缺少专业 child binding".to_string())?;
|
||||
if child_task.source != "agent-delegate"
|
||||
|| child_task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| child_task.run_profile_binding_fingerprint != child_binding.binding_fingerprint
|
||||
|| child_binding.agent_id != delivery.target_agent_id
|
||||
|| child_binding.run_id != delivery.target_run_id
|
||||
|| child_binding.source != "agent-delegate"
|
||||
|| child_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
|| child_binding.root_agent_id != parent_binding.root_agent_id
|
||||
|| child_binding.root_run_id != parent_binding.root_run_id
|
||||
|| child_binding.parent_agent_id.as_deref() != Some(parent_task.agent_id.as_str())
|
||||
|| child_binding.parent_run_id.as_deref() != Some(parent_task.run_id.as_str())
|
||||
|| child_binding.parent_binding_fingerprint.as_deref()
|
||||
!= Some(parent_binding.binding_fingerprint.as_str())
|
||||
{
|
||||
return Err("game-chat 安全默认返工专业 child binding/link 身份不一致".to_string());
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn convert_game_chat_child_user_input_to_safe_default_repair(
|
||||
result: &mut StaticDelegateStructuredResult,
|
||||
) -> bool {
|
||||
if result.contract_status != StaticDelegateContractStatus::NeedsUserInput {
|
||||
return false;
|
||||
}
|
||||
let error = game_chat_safe_default_repair_error(result);
|
||||
result.contract_status = StaticDelegateContractStatus::NeedsRepair;
|
||||
result.user_input_questions.clear();
|
||||
result.user_input_questions_sha256 = None;
|
||||
result.error = Some(error);
|
||||
true
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn convert_claimed_game_chat_user_input_deliveries_to_repair_at(
|
||||
root: &Path,
|
||||
parent_task: &AgentRuntimeTaskRecord,
|
||||
deliveries: &[StaticDelegateDeliveryRecord],
|
||||
) -> Result<usize, String> {
|
||||
if trusted_game_chat_autonomous_parent_chain_at(root, parent_task)?.is_none() {
|
||||
return Ok(0);
|
||||
}
|
||||
let mut converted = 0_usize;
|
||||
for expected in deliveries.iter().filter(|delivery| {
|
||||
delivery.parent_agent_id == parent_task.agent_id
|
||||
&& delivery.parent_session_id == parent_task.session_id
|
||||
&& delivery.parent_run_id == parent_task.run_id
|
||||
&& delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
&& delivery.clarification_request_id.is_none()
|
||||
&& delivery.clarification_answers_sha256.is_none()
|
||||
&& delivery.structured_result.as_ref().is_some_and(|result| {
|
||||
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|
||||
})
|
||||
}) {
|
||||
if !trusted_game_chat_autonomous_child_delivery_at(root, parent_task, expected, None)? {
|
||||
continue;
|
||||
}
|
||||
replace_claimed_static_delegate_result_for_game_chat_safe_default_at(root, expected)?;
|
||||
converted = converted.saturating_add(1);
|
||||
}
|
||||
Ok(converted)
|
||||
}
|
||||
|
||||
pub(crate) fn reconcile_claimed_game_chat_safe_default_half_states_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
parent_run_id: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(parent_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
)?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
if trusted_game_chat_autonomous_parent_chain_at(root, &parent_task)?.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
for delivery in claimed_static_delegate_deliveries_at(root, parent_agent_id, parent_run_id)? {
|
||||
if delivery.structured_result.as_ref().is_some_and(|result| {
|
||||
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|
||||
|| game_chat_safe_default_repair_result_is_valid(result)
|
||||
}) {
|
||||
if !trusted_game_chat_autonomous_child_delivery_at(root, &parent_task, &delivery, None)?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
replace_claimed_static_delegate_result_for_game_chat_safe_default_at(root, &delivery)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn trusted_game_chat_safe_default_repair_delivery_at(
|
||||
root: &Path,
|
||||
parent_task: &AgentRuntimeTaskRecord,
|
||||
delivery: &StaticDelegateDeliveryRecord,
|
||||
) -> Result<bool, String> {
|
||||
Ok(
|
||||
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
&& delivery.result_summary.as_deref() == Some(GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY)
|
||||
&& delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.is_some_and(game_chat_safe_default_repair_result_is_valid)
|
||||
&& trusted_game_chat_autonomous_child_delivery_at(root, parent_task, delivery, None)?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn agent_runtime_delegation_id(
|
||||
parent_agent_id: &str,
|
||||
parent_run_id: &str,
|
||||
@@ -334,7 +761,22 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
|
||||
¤t_task.run_id,
|
||||
)?;
|
||||
let mut state = state;
|
||||
ensure_static_delegate_user_input_wait_at(root, &mut state, &deliveries)?;
|
||||
if !ensure_static_delegate_user_input_wait_at(root, &mut state, &deliveries)? {
|
||||
let state = advance_game_creator_agent_runtime_turn_at(
|
||||
root,
|
||||
state,
|
||||
"planning",
|
||||
"自主构建澄清已转换为安全默认返工",
|
||||
"专业 Agent 回执已按安全默认策略进入 needs-repair,恢复同一父 run。",
|
||||
)?;
|
||||
let root = root.to_path_buf();
|
||||
let agent_id = current_task.agent_id.clone();
|
||||
let task = current_task.task.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let _runtime_lock = runtime_lock;
|
||||
drain_game_creator_agent_background_tasks(root, agent_id, task, state).await;
|
||||
});
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
let state = advance_game_creator_agent_runtime_turn_at(
|
||||
@@ -889,8 +1331,8 @@ pub(crate) fn publish_game_creator_agent_delegate_result(
|
||||
return;
|
||||
}
|
||||
let was_dispatched = existing_delivery.status == StaticDelegateDeliveryStatus::Dispatched;
|
||||
let safe_result_summary = truncate_agent_runtime_text(&result_detail, 140);
|
||||
let structured_result = match build_static_delegate_result_for_child_at(
|
||||
let mut safe_result_summary = truncate_agent_runtime_text(&result_detail, 140);
|
||||
let mut structured_result = match build_static_delegate_result_for_child_at(
|
||||
root,
|
||||
&existing_delivery,
|
||||
child_task,
|
||||
@@ -918,6 +1360,24 @@ pub(crate) fn publish_game_creator_agent_delegate_result(
|
||||
return;
|
||||
}
|
||||
};
|
||||
match trusted_game_chat_autonomous_child_delivery_at(
|
||||
root,
|
||||
&parent_task,
|
||||
&existing_delivery,
|
||||
Some(child_task),
|
||||
) {
|
||||
Ok(true) => {
|
||||
if convert_game_chat_child_user_input_to_safe_default_repair(&mut structured_result)
|
||||
{
|
||||
safe_result_summary = GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY.to_string();
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(error) => {
|
||||
record_game_creator_agent_delegate_result_failure(root, child_task, &error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let delivery = match mark_static_delegate_delivery_ready_with_result_at(
|
||||
root,
|
||||
&child_task.agent_id,
|
||||
|
||||
@@ -24,6 +24,7 @@ pub(crate) enum CliCommand {
|
||||
parent_agent_id: String,
|
||||
initialize: bool,
|
||||
run_profile: String,
|
||||
supervisor_source: &'static str,
|
||||
},
|
||||
AgentEnqueue {
|
||||
project_path: PathBuf,
|
||||
@@ -687,7 +688,7 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
}));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--swarm-chat") {
|
||||
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] <本地项目绝对路径> [parentAgentId]";
|
||||
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--game-chat-smoke] <本地项目绝对路径> [parentAgentId]";
|
||||
let mut rest = args[1..].to_vec();
|
||||
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
|
||||
rest.remove(index);
|
||||
@@ -711,9 +712,36 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
}
|
||||
_ => return Err(USAGE.to_string()),
|
||||
};
|
||||
let game_chat_smoke = match rest
|
||||
.iter()
|
||||
.filter(|arg| arg.as_str() == "--game-chat-smoke")
|
||||
.count()
|
||||
{
|
||||
0 => false,
|
||||
1 => {
|
||||
let index = rest
|
||||
.iter()
|
||||
.position(|arg| arg == "--game-chat-smoke")
|
||||
.expect("counted game-chat smoke flag");
|
||||
rest.remove(index);
|
||||
true
|
||||
}
|
||||
_ => return Err(USAGE.to_string()),
|
||||
};
|
||||
if !(1..=2).contains(&rest.len()) || rest.iter().any(|value| value.trim().is_empty()) {
|
||||
return Err(USAGE.to_string());
|
||||
}
|
||||
if game_chat_smoke
|
||||
&& (!autonomous_game_build
|
||||
|| rest.get(1).is_some_and(|parent| {
|
||||
parent.trim() != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
}))
|
||||
{
|
||||
return Err(
|
||||
"--game-chat-smoke 仅允许 project-supervisor 的 --autonomous-game-build 受限验收入口"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(Some(CliCommand::SwarmChat {
|
||||
project_path: PathBuf::from(&rest[0]),
|
||||
parent_agent_id: rest
|
||||
@@ -726,6 +754,11 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
|
||||
} else {
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
|
||||
},
|
||||
supervisor_source: if game_chat_smoke {
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
} else {
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
|
||||
},
|
||||
}));
|
||||
}
|
||||
if args.first().map(String::as_str) == Some("--agent-task") {
|
||||
@@ -984,11 +1017,17 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
|
||||
parent_agent_id,
|
||||
initialize,
|
||||
run_profile,
|
||||
supervisor_source,
|
||||
} => {
|
||||
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
|
||||
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
|
||||
initialize_cli_agent_project(&project_path, initialize)?;
|
||||
run_game_creator_swarm_chat_at(&project_path, &parent_agent_id, &run_profile)
|
||||
run_game_creator_swarm_chat_at(
|
||||
&project_path,
|
||||
&parent_agent_id,
|
||||
&run_profile,
|
||||
supervisor_source,
|
||||
)
|
||||
}
|
||||
CliCommand::AgentEnqueue {
|
||||
project_path,
|
||||
@@ -1869,6 +1908,7 @@ mod tests {
|
||||
parent_agent_id: "code-prototype".to_string(),
|
||||
initialize: true,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
|
||||
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
}
|
||||
);
|
||||
assert!(command.requires_external_agent_runner());
|
||||
@@ -1948,6 +1988,7 @@ mod tests {
|
||||
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||||
initialize: false,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
|
||||
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1970,10 +2011,57 @@ mod tests {
|
||||
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||||
initialize: false,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
|
||||
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_chat_game_chat_smoke_flag_is_restricted_and_selects_trusted_source() {
|
||||
let project_path = std::env::current_dir().expect("current directory");
|
||||
let command = parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--init".to_string(),
|
||||
"--autonomous-game-build".to_string(),
|
||||
"--game-chat-smoke".to_string(),
|
||||
project_path.display().to_string(),
|
||||
])
|
||||
.expect("parse game-chat smoke")
|
||||
.expect("game-chat smoke command");
|
||||
assert_eq!(
|
||||
command,
|
||||
CliCommand::SwarmChat {
|
||||
project_path,
|
||||
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||||
initialize: true,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
|
||||
supervisor_source: AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
}
|
||||
);
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--game-chat-smoke".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--autonomous-game-build".to_string(),
|
||||
"--game-chat-smoke".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
"code-prototype".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
assert!(parse_cli_command(&[
|
||||
"--swarm-chat".to_string(),
|
||||
"--autonomous-game-build".to_string(),
|
||||
"--game-chat-smoke".to_string(),
|
||||
"--game-chat-smoke".to_string(),
|
||||
"/tmp/game-project".to_string(),
|
||||
])
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swarm_chat_rejects_missing_or_extra_arguments() {
|
||||
assert!(parse_cli_command(&["--swarm-chat".to_string()]).is_err());
|
||||
|
||||
@@ -419,6 +419,7 @@ pub(crate) fn static_delegate_completion_barrier_at(
|
||||
) -> Result<StaticDelegateCompletionBarrier, String> {
|
||||
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
|
||||
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
|
||||
reconcile_claimed_game_chat_safe_default_half_states_at(root, parent_agent_id, parent_run_id)?;
|
||||
let claims = list_static_delegate_claims_at(root)?
|
||||
.into_iter()
|
||||
.filter(|claim| {
|
||||
@@ -1677,6 +1678,141 @@ fn write_static_delegate_delivery_at(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn replace_claimed_static_delegate_result_for_game_chat_safe_default_at(
|
||||
root: &Path,
|
||||
expected: &StaticDelegateDeliveryRecord,
|
||||
) -> Result<StaticDelegateDeliveryRecord, String> {
|
||||
validate_static_delegate_delivery_record(expected)?;
|
||||
let expected_result = expected
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.ok_or_else(|| "game-chat 安全默认返工 delivery 缺少 structuredResult".to_string())?;
|
||||
let (result_summary, structured_result) =
|
||||
game_chat_safe_default_repair_replacement(expected_result)
|
||||
.ok_or_else(|| "game-chat 安全默认返工转换状态无效".to_string())?;
|
||||
let expected_is_original =
|
||||
expected_result.contract_status == StaticDelegateContractStatus::NeedsUserInput;
|
||||
let expected_is_converted = expected.result_summary.as_deref() == Some(result_summary.as_str())
|
||||
&& expected_result == &structured_result;
|
||||
if expected.status != StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
|| expected.clarification_request_id.is_some()
|
||||
|| expected.clarification_answers_sha256.is_some()
|
||||
|| (!expected_is_original && !expected_is_converted)
|
||||
{
|
||||
return Err("game-chat 安全默认返工转换状态无效".to_string());
|
||||
}
|
||||
validate_static_delegate_structured_result(
|
||||
&structured_result,
|
||||
expected.terminal_status.as_deref().unwrap_or_default(),
|
||||
&expected.expected_artifacts,
|
||||
)?;
|
||||
let claim_action_id = expected
|
||||
.claimed_by_action_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "game-chat 安全默认返工 delivery 缺少 claim actionId".to_string())?;
|
||||
let _claim_lock = acquire_static_delegate_claim_lock_at(
|
||||
root,
|
||||
&expected.parent_agent_id,
|
||||
&expected.parent_run_id,
|
||||
claim_action_id,
|
||||
)?;
|
||||
let _delivery_locks =
|
||||
acquire_static_delegate_delivery_locks_at(root, vec![expected.delegation_id.clone()])?;
|
||||
let mut delivery = read_static_delegate_delivery_at(root, &expected.delegation_id)?
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"game-chat 安全默认返工 delivery 不存在:{}",
|
||||
expected.delegation_id
|
||||
)
|
||||
})?;
|
||||
validate_static_delegate_delivery_identity(&delivery, expected)?;
|
||||
if delivery.status != StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
|| delivery.claimed_by_action_id.as_deref() != Some(claim_action_id)
|
||||
|| delivery.clarification_request_id.is_some()
|
||||
|| delivery.clarification_answers_sha256.is_some()
|
||||
{
|
||||
return Err("game-chat 安全默认返工 delivery 当前状态无效".to_string());
|
||||
}
|
||||
let mut claim = read_static_delegate_claim_at(
|
||||
root,
|
||||
&expected.parent_agent_id,
|
||||
&expected.parent_run_id,
|
||||
claim_action_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 安全默认返工缺少原 claim".to_string())?;
|
||||
let receipt_index = claim
|
||||
.receipts
|
||||
.iter()
|
||||
.position(|receipt| receipt.delegation_id == expected.delegation_id)
|
||||
.ok_or_else(|| "game-chat 安全默认返工 claim 缺少原 delivery".to_string())?;
|
||||
let receipt = &claim.receipts[receipt_index];
|
||||
if receipt.target_agent_id != delivery.target_agent_id
|
||||
|| receipt.status != delivery.terminal_status.as_deref().unwrap_or_default()
|
||||
|| receipt.acceptance_criteria != delivery.acceptance_criteria
|
||||
|| receipt.expected_artifacts != delivery.expected_artifacts
|
||||
|| receipt.repair_of_delegation_id != delivery.repair_of_delegation_id
|
||||
{
|
||||
return Err("game-chat 安全默认返工 claim 与 delivery 身份冲突".to_string());
|
||||
}
|
||||
|
||||
let delivery_is_original = delivery.structured_result.as_ref().is_some_and(|result| {
|
||||
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|
||||
});
|
||||
let delivery_is_converted = delivery.result_summary.as_deref() == Some(result_summary.as_str())
|
||||
&& delivery.structured_result.as_ref() == Some(&structured_result);
|
||||
let receipt_is_original = receipt.structured_result.as_ref().is_some_and(|result| {
|
||||
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|
||||
});
|
||||
let receipt_is_converted = receipt.summary == result_summary
|
||||
&& receipt.structured_result.as_ref() == Some(&structured_result);
|
||||
if (!delivery_is_original && !delivery_is_converted)
|
||||
|| (!receipt_is_original && !receipt_is_converted)
|
||||
{
|
||||
return Err("game-chat 安全默认返工 delivery/claim 含非唯一转换差异".to_string());
|
||||
}
|
||||
|
||||
if delivery_is_original {
|
||||
let original = delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.expect("original delivery result exists");
|
||||
let (_, converted) = game_chat_safe_default_repair_replacement(original)
|
||||
.ok_or_else(|| "game-chat 安全默认返工原 delivery 无法转换".to_string())?;
|
||||
if converted != structured_result
|
||||
|| (receipt_is_original
|
||||
&& (receipt.summary != delivery.result_summary.as_deref().unwrap_or_default()
|
||||
|| receipt.structured_result.as_ref() != Some(original)))
|
||||
{
|
||||
return Err("game-chat 安全默认返工 claim 与原 delivery 结果冲突".to_string());
|
||||
}
|
||||
} else if receipt_is_original {
|
||||
let original = receipt
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.expect("original receipt result exists");
|
||||
let (_, converted) = game_chat_safe_default_repair_replacement(original)
|
||||
.ok_or_else(|| "game-chat 安全默认返工原 claim 无法转换".to_string())?;
|
||||
if converted != structured_result {
|
||||
return Err("game-chat 安全默认返工 converted delivery 与原 claim 冲突".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !delivery_is_converted {
|
||||
delivery.result_summary = Some(result_summary.clone());
|
||||
delivery.structured_result = Some(structured_result.clone());
|
||||
delivery.updated_at = unix_timestamp();
|
||||
write_static_delegate_delivery_at(root, &delivery)?;
|
||||
}
|
||||
if !receipt_is_converted {
|
||||
let receipt = &mut claim.receipts[receipt_index];
|
||||
receipt.summary = result_summary;
|
||||
receipt.structured_result = Some(structured_result);
|
||||
claim.updated_at = unix_timestamp();
|
||||
write_static_delegate_claim_at(root, &claim)?;
|
||||
}
|
||||
Ok(delivery)
|
||||
}
|
||||
|
||||
fn read_static_delegate_claim_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
@@ -2271,6 +2407,140 @@ mod tests {
|
||||
assert!(error.contains("user.input_request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_safe_default_replacement_recovers_both_persisted_half_states() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-safe-default-half-state-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system time after unix epoch")
|
||||
.as_nanos()
|
||||
));
|
||||
init_local_game_project_at(&root, "project-1", "安全默认半状态恢复测试")
|
||||
.expect("project init");
|
||||
let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
|
||||
let parent_run_id = "safe-default-half-state-parent-run";
|
||||
bind_supervisor_collaboration_policy_snapshot_at(
|
||||
&root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
&SupervisorCollaborationPolicy::default(),
|
||||
"legacy-current-project-policy",
|
||||
)
|
||||
.expect("bind collaboration snapshot");
|
||||
|
||||
for (suffix, convert_delivery_first) in [("delivery", true), ("claim", false)] {
|
||||
let action_id = format!("safe-default-half-state-action-{suffix}");
|
||||
let delegation_id = format!("safe-default-half-state-delegation-{suffix}");
|
||||
let mut delivery = new_static_delegate_delivery_with_contract(
|
||||
parent_agent_id,
|
||||
"safe-default-half-state-session",
|
||||
parent_run_id,
|
||||
&format!("safe-default-half-state-parent-action-{suffix}"),
|
||||
&delegation_id,
|
||||
"code-prototype",
|
||||
&format!("safe-default-half-state-child-session-{suffix}"),
|
||||
&format!("safe-default-half-state-child-run-{suffix}"),
|
||||
&["采用安全默认值继续".to_string()],
|
||||
&[],
|
||||
None,
|
||||
);
|
||||
create_or_read_static_delegate_delivery_at(&root, &delivery).expect("create delivery");
|
||||
let original = build_static_delegate_structured_result_at(
|
||||
&root,
|
||||
"completed",
|
||||
&[],
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(&format!(
|
||||
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
|
||||
serde_json::json!({
|
||||
"questions": [{
|
||||
"id": "visual_style",
|
||||
"header": "风格",
|
||||
"question": "请选择视觉风格",
|
||||
"options": [
|
||||
{"label": "明亮", "description": "使用明亮配色"},
|
||||
{"label": "柔和", "description": "使用柔和配色"}
|
||||
]
|
||||
}]
|
||||
})
|
||||
)),
|
||||
)
|
||||
.expect("build needs-user-input result");
|
||||
mark_static_delegate_delivery_ready_with_result_at(
|
||||
&root,
|
||||
&delivery.target_agent_id,
|
||||
&delivery.target_session_id,
|
||||
&delivery.target_run_id,
|
||||
&delivery.delegation_id,
|
||||
"completed",
|
||||
"需要用户选择",
|
||||
original.clone(),
|
||||
)
|
||||
.expect("mark delivery ready");
|
||||
claim_ready_static_delegate_receipts_at(
|
||||
&root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
&action_id,
|
||||
)
|
||||
.expect("claim delivery");
|
||||
delivery = read_static_delegate_delivery_at(&root, &delegation_id)
|
||||
.expect("read delivery")
|
||||
.expect("delivery exists");
|
||||
let (summary, converted) = game_chat_safe_default_repair_replacement(&original)
|
||||
.expect("convert safe default result");
|
||||
if convert_delivery_first {
|
||||
delivery.result_summary = Some(summary.clone());
|
||||
delivery.structured_result = Some(converted.clone());
|
||||
delivery.updated_at = unix_timestamp();
|
||||
write_static_delegate_delivery_at(&root, &delivery).expect("write delivery half");
|
||||
} else {
|
||||
let mut claim = read_static_delegate_claim_at(
|
||||
&root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
&action_id,
|
||||
)
|
||||
.expect("read claim")
|
||||
.expect("claim exists");
|
||||
let receipt = claim
|
||||
.receipts
|
||||
.iter_mut()
|
||||
.find(|receipt| receipt.delegation_id == delegation_id)
|
||||
.expect("claim receipt");
|
||||
receipt.summary = summary.clone();
|
||||
receipt.structured_result = Some(converted.clone());
|
||||
claim.updated_at = unix_timestamp();
|
||||
write_static_delegate_claim_at(&root, &claim).expect("write claim half");
|
||||
}
|
||||
|
||||
let reconciled = replace_claimed_static_delegate_result_for_game_chat_safe_default_at(
|
||||
&root, &delivery,
|
||||
)
|
||||
.expect("reconcile half state");
|
||||
assert_eq!(reconciled.result_summary.as_deref(), Some(summary.as_str()));
|
||||
assert_eq!(reconciled.structured_result.as_ref(), Some(&converted));
|
||||
let claim =
|
||||
read_static_delegate_claim_at(&root, parent_agent_id, parent_run_id, &action_id)
|
||||
.expect("read reconciled claim")
|
||||
.expect("reconciled claim exists");
|
||||
let receipt = claim
|
||||
.receipts
|
||||
.iter()
|
||||
.find(|receipt| receipt.delegation_id == delegation_id)
|
||||
.expect("reconciled receipt");
|
||||
assert_eq!(receipt.summary, summary);
|
||||
assert_eq!(receipt.structured_result.as_ref(), Some(&converted));
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_prepared_claim_snapshot_cannot_downgrade_observed_claim() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -33,5 +33,39 @@ pub(crate) use server::{
|
||||
bind_loopback_listener_with_linux_fallback, run_external_agent_runner_server,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn simulate_external_agent_runner_cross_boot_owner_claim_for_test(
|
||||
root: &std::path::Path,
|
||||
config_dir: &std::path::Path,
|
||||
previous_boot_id: &str,
|
||||
current_boot_id: &str,
|
||||
) -> Result<(std::path::PathBuf, Option<String>), String> {
|
||||
let previous = project_owner::acquire_external_agent_runner_project_execution_owner(
|
||||
root,
|
||||
previous_boot_id,
|
||||
protocol::EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
)?;
|
||||
drop(previous);
|
||||
let state = state::ExternalAgentRunnerServerState::new(
|
||||
config_dir.join(protocol::EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
|
||||
protocol::ExternalAgentRunnerEndpoint {
|
||||
protocol_version: protocol::EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
pid: std::process::id(),
|
||||
boot_id: current_boot_id.to_string(),
|
||||
port: 41009,
|
||||
token: "owner-provider-token-owner-provider-token".to_string(),
|
||||
heartbeat_at: 1_725_000_000_000,
|
||||
executable_fingerprint: Some("a".repeat(64)),
|
||||
process_start_identity: None,
|
||||
},
|
||||
);
|
||||
let claim = state.claim_project_execution_owner(root)?;
|
||||
let result = (claim.root, claim.recovered_from_boot_id);
|
||||
let second = state.claim_project_execution_owner(root)?;
|
||||
if second.recovered_from_boot_id.is_some() {
|
||||
return Err("同 boot execution owner 重入重复触发恢复".to_string());
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -368,10 +368,12 @@ pub(super) fn dispatch_external_agent_runner_wake_pending_request(
|
||||
pub(super) fn external_agent_runner_response_is_cacheable(
|
||||
response: &ExternalAgentRunnerResponse,
|
||||
) -> bool {
|
||||
response
|
||||
.error
|
||||
.as_ref()
|
||||
.is_none_or(|error| error.code != EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE)
|
||||
response.error.as_ref().is_none_or(|error| {
|
||||
!matches!(
|
||||
error.code.as_str(),
|
||||
EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE | "project-execution-owned"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn cache_external_agent_runner_response_if_cacheable(
|
||||
@@ -392,8 +394,77 @@ pub(super) fn cache_external_agent_runner_response_if_cacheable(
|
||||
pub(super) fn dispatch_external_agent_runner_runtime_request(
|
||||
request: &ExternalAgentRunnerRequest,
|
||||
state: &ExternalAgentRunnerServerState,
|
||||
) -> ExternalAgentRunnerResponse {
|
||||
dispatch_external_agent_runner_runtime_request_with_owner_claim(request, state, |root| {
|
||||
state.claim_project_execution_owner(root)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
|
||||
request: &ExternalAgentRunnerRequest,
|
||||
state: &ExternalAgentRunnerServerState,
|
||||
claim_owner: impl Fn(&Path) -> Result<ExternalAgentRunnerProjectExecutionOwnerClaim, String>,
|
||||
) -> ExternalAgentRunnerResponse {
|
||||
let fingerprint = external_agent_runner_request_fingerprint(request);
|
||||
{
|
||||
let cache = lock_unpoisoned(&state.write_request_cache);
|
||||
if let Some(cached) = cache.find(&request.request_id) {
|
||||
if cached.fingerprint == fingerprint {
|
||||
return cached.response.clone();
|
||||
}
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"request-id-conflict",
|
||||
"同一 requestId 不能用于不同请求",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let requires_project_execution_owner = matches!(
|
||||
request.method.as_str(),
|
||||
"runtime.wake_pending"
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.interrupt_for_steer_decision"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runtime.compact"
|
||||
);
|
||||
if requires_project_execution_owner && state.draining.load(Ordering::Acquire) {
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"runner-draining",
|
||||
"Agent Runner 正在排空并准备退出,拒绝新的写请求",
|
||||
);
|
||||
}
|
||||
|
||||
let token = state.endpoint_snapshot().token;
|
||||
let claimed_project_root = if requires_project_execution_owner {
|
||||
let root = match external_agent_runner_request_root(request) {
|
||||
Ok(root) => root,
|
||||
Err(error) => {
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"invalid-params",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
match claim_owner(&root) {
|
||||
Ok(claim) => Some(claim.root),
|
||||
Err(error) => {
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"project-execution-owned",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut cache = lock_unpoisoned(&state.write_request_cache);
|
||||
if let Some(cached) = cache.find(&request.request_id) {
|
||||
if cached.fingerprint == fingerprint {
|
||||
@@ -406,26 +477,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request(
|
||||
);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
request.method.as_str(),
|
||||
"runtime.wake_pending"
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.interrupt_for_steer_decision"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runtime.compact"
|
||||
) && state.draining.load(Ordering::Acquire)
|
||||
{
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"runner-draining",
|
||||
"Agent Runner 正在排空并准备退出,拒绝新的写请求",
|
||||
);
|
||||
}
|
||||
|
||||
let token = state.endpoint_snapshot().token;
|
||||
let response = match request.method.as_str() {
|
||||
"runtime.wake_pending"
|
||||
| "runtime.resume"
|
||||
@@ -435,26 +486,8 @@ pub(super) fn dispatch_external_agent_runner_runtime_request(
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runtime.compact" => {
|
||||
let root = match external_agent_runner_request_root(request) {
|
||||
Ok(root) => root,
|
||||
Err(error) => {
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"invalid-params",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let root = match state.claim_project_execution_owner(&root) {
|
||||
Ok(root) => root,
|
||||
Err(error) => {
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
&request.request_id,
|
||||
"project-execution-owned",
|
||||
error,
|
||||
);
|
||||
}
|
||||
};
|
||||
let root = claimed_project_root
|
||||
.expect("runtime write request must claim its project execution owner");
|
||||
if request.method == "runtime.wake_pending" {
|
||||
dispatch_external_agent_runner_wake_pending_request(request, &root, &token)
|
||||
} else {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user