完善自主 Swarm 无干预可玩交付
固定首批程序实现与质量只读职责,并在计划执行前阻断质量 Agent 项目写入 要求非只读专业 Agent 完成本人修改和对应 revision 验证后再交付 补齐 Provider 瞬态重试、最终回复和 Swarm CLI 终态收敛 升级 Provider action batch v3 并兼容 v2/v1 持久恢复 扩展确定性与真实外部 Provider 塔防验收、前端状态和项目文档
This commit is contained in:
@@ -116,10 +116,57 @@ function parseChildReport(result) {
|
||||
}
|
||||
}
|
||||
|
||||
function responsibilityContractEvidence(stats) {
|
||||
const qualityPlanningCount =
|
||||
stats?.byAgent?.['quality-review']?.planning ?? null;
|
||||
const evidence = {
|
||||
delegationContractCount: stats?.delegationContractCount ?? null,
|
||||
delegationContractViolationCount:
|
||||
stats?.delegationContractViolationCount ?? null,
|
||||
codePrototypeDelegationCount: stats?.codePrototypeDelegationCount ?? null,
|
||||
codePrototypeExpectedArtifactCount:
|
||||
stats?.codePrototypeExpectedArtifactCount ?? null,
|
||||
codePrototypeGameIndexArtifactDelegationCount:
|
||||
stats?.codePrototypeGameIndexArtifactDelegationCount ?? null,
|
||||
codePrototypeProjectMutationCount:
|
||||
stats?.byAgent?.['code-prototype']?.projectMutation ?? null,
|
||||
qualityReviewDelegationCount: stats?.qualityReviewDelegationCount ?? null,
|
||||
qualityReviewReadOnlyDelegationCount:
|
||||
stats?.qualityReviewReadOnlyDelegationCount ?? null,
|
||||
qualityReviewExpectedArtifactCount:
|
||||
stats?.qualityReviewExpectedArtifactCount ?? null,
|
||||
qualityReviewProjectMutationCount:
|
||||
stats?.byAgent?.['quality-review']?.projectMutation ?? null,
|
||||
qualityPlanningCount,
|
||||
qualityRevisionReplanCount: stats?.qualityRevisionReplanCount ?? null,
|
||||
};
|
||||
return {
|
||||
...evidence,
|
||||
codeOwnsGameIndex:
|
||||
evidence.codePrototypeDelegationCount === 2 &&
|
||||
evidence.codePrototypeExpectedArtifactCount === 2 &&
|
||||
evidence.codePrototypeGameIndexArtifactDelegationCount === 2,
|
||||
qualityIsReadOnly:
|
||||
evidence.qualityReviewDelegationCount === 1 &&
|
||||
evidence.qualityReviewReadOnlyDelegationCount === 1 &&
|
||||
evidence.qualityReviewExpectedArtifactCount === 0 &&
|
||||
evidence.qualityReviewProjectMutationCount === 0,
|
||||
qualityIndependentOfCodeTiming:
|
||||
(qualityPlanningCount === 1 || qualityPlanningCount === 2) &&
|
||||
evidence.qualityRevisionReplanCount === qualityPlanningCount - 1,
|
||||
contractViolationFree:
|
||||
evidence.delegationContractCount === 3 &&
|
||||
evidence.delegationContractViolationCount === 0,
|
||||
};
|
||||
}
|
||||
|
||||
function expectedProviderStats(stats) {
|
||||
const responsibility = responsibilityContractEvidence(stats);
|
||||
const qualityPlanningCount = responsibility.qualityPlanningCount;
|
||||
return (
|
||||
stats.requestCount === 17 &&
|
||||
stats.planningRequestCount === 17 &&
|
||||
(qualityPlanningCount === 1 || qualityPlanningCount === 2) &&
|
||||
stats.requestCount === 15 + qualityPlanningCount &&
|
||||
stats.planningRequestCount === 15 + qualityPlanningCount &&
|
||||
stats.finalReplyRequestCount === 0 &&
|
||||
stats.initialDelegationCount === 2 &&
|
||||
stats.followupDelegationCount === 1 &&
|
||||
@@ -133,10 +180,73 @@ function expectedProviderStats(stats) {
|
||||
Object.keys(stats.rejectionCodes ?? {}).length === 0 &&
|
||||
stats.byAgent?.['project-supervisor']?.planning === 9 &&
|
||||
stats.byAgent?.['project-supervisor']?.finalReply === 0 &&
|
||||
stats.byAgent?.['project-supervisor']?.projectMutation === 1 &&
|
||||
stats.byAgent?.['code-prototype']?.planning === 6 &&
|
||||
stats.byAgent?.['code-prototype']?.finalReply === 0 &&
|
||||
stats.byAgent?.['quality-review']?.planning === 2 &&
|
||||
stats.byAgent?.['quality-review']?.finalReply === 0
|
||||
stats.byAgent?.['code-prototype']?.projectMutation === 2 &&
|
||||
stats.byAgent?.['quality-review']?.finalReply === 0 &&
|
||||
responsibility.codeOwnsGameIndex &&
|
||||
responsibility.qualityIsReadOnly &&
|
||||
responsibility.qualityIndependentOfCodeTiming &&
|
||||
responsibility.contractViolationFree
|
||||
);
|
||||
}
|
||||
|
||||
const requiredZeroChildEvidenceFields = Object.freeze([
|
||||
'activeRunnerKillCount',
|
||||
'approveInputCount',
|
||||
'answerInputCount',
|
||||
'steerInputCount',
|
||||
'turnReportWaitingForConfirmationCount',
|
||||
'turnReportWaitingForUserInputCount',
|
||||
'turnReportReconciliationAgentCount',
|
||||
'providerLifecycleFailedCount',
|
||||
'openProviderLifecycleCount',
|
||||
'pendingActionCount',
|
||||
'confirmationSidecarCount',
|
||||
'userInputSidecarCount',
|
||||
'providerActionBatchSidecarCount',
|
||||
'providerRetrySidecarCount',
|
||||
'providerHandoffSidecarCount',
|
||||
'toolPlanHandoffSidecarCount',
|
||||
'finalizationJournalCount',
|
||||
'reconciliationResidueCount',
|
||||
]);
|
||||
|
||||
function expectedChildReport(report, options, providerStats) {
|
||||
const evidence = report?.evidence;
|
||||
const providerRequestCount = providerStats?.requestCount;
|
||||
return (
|
||||
report?.status === 'PASS' &&
|
||||
report?.suite === suite &&
|
||||
report?.errorCount === 0 &&
|
||||
Array.isArray(report?.blocked) &&
|
||||
report.blocked.length === 0 &&
|
||||
report?.cleanup?.performed === !options.keepProject &&
|
||||
report?.cleanup?.kept === options.keepProject &&
|
||||
evidence?.evidenceCompleteness === 'complete' &&
|
||||
evidence?.dedicatedZeroInterventionPath === true &&
|
||||
evidence?.stdinTaskCount === 1 &&
|
||||
evidence?.stdinEndedAfterTask === true &&
|
||||
evidence?.turnReportOutcome === 'settled' &&
|
||||
evidence?.parentTaskStatus === 'completed' &&
|
||||
evidence?.parentRuntimeStatus === 'idle' &&
|
||||
evidence?.parentRuntimePhase === 'completed' &&
|
||||
evidence?.laneDefensePlaytestPassed === true &&
|
||||
evidence?.laneDefenseAssertionCount === 37 &&
|
||||
evidence?.laneDefensePassedAssertionCount === 37 &&
|
||||
evidence?.browserValidationPassed === true &&
|
||||
evidence?.staticSmokePassed === true &&
|
||||
evidence?.gameIndexChanged === true &&
|
||||
Number.isInteger(evidence?.projectRevisionDelta) &&
|
||||
evidence.projectRevisionDelta > 0 &&
|
||||
evidence?.finalSupervisorAssistantCount === 1 &&
|
||||
evidence?.professionalAssistantCount === 3 &&
|
||||
evidence?.providerRequestIdentityCount === providerRequestCount &&
|
||||
evidence?.providerLifecycleStartedCount === providerRequestCount &&
|
||||
evidence?.providerLifecycleTerminalCount === providerRequestCount &&
|
||||
evidence?.providerLifecycleCompletedCount === providerRequestCount &&
|
||||
requiredZeroChildEvidenceFields.every((field) => evidence?.[field] === 0)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,6 +270,13 @@ function responseFunctionNames(response) {
|
||||
);
|
||||
}
|
||||
|
||||
function responseFunctionCalls(response) {
|
||||
return response.choices[0].message.tool_calls.map((call) => ({
|
||||
name: call.function.name,
|
||||
arguments: JSON.parse(call.function.arguments),
|
||||
}));
|
||||
}
|
||||
|
||||
async function runSelfTest() {
|
||||
const html = deterministicLaneDefenseInitialHtml();
|
||||
assert([...html].length <= 8_000, 'self-test-html-source-budget-invalid');
|
||||
@@ -189,17 +306,41 @@ async function runSelfTest() {
|
||||
authorization: `Bearer ${apiKey}`,
|
||||
payload: syntheticPayload(agentId, runId, tools, extraContext),
|
||||
});
|
||||
const initialDelegationResponse = route('project-supervisor', 'parent-run');
|
||||
const initialDelegationCalls = responseFunctionCalls(
|
||||
initialDelegationResponse,
|
||||
);
|
||||
assert(
|
||||
responseFunctionNames(route('project-supervisor', 'parent-run')).join(
|
||||
',',
|
||||
) === 'runtime_tool_agent_delegate,runtime_tool_agent_delegate',
|
||||
initialDelegationCalls.map((call) => call.name).join(',') ===
|
||||
'runtime_tool_agent_delegate,runtime_tool_agent_delegate',
|
||||
'self-test-initial-delegation-invalid',
|
||||
);
|
||||
const initialCodeDelegation = initialDelegationCalls[0]?.arguments?.input;
|
||||
const initialQualityDelegation = initialDelegationCalls[1]?.arguments?.input;
|
||||
assert(
|
||||
initialCodeDelegation?.agentId === 'code-prototype' &&
|
||||
JSON.stringify(initialCodeDelegation.expectedArtifacts) ===
|
||||
JSON.stringify(['game/index.html']) &&
|
||||
initialQualityDelegation?.agentId === 'quality-review' &&
|
||||
initialQualityDelegation.task.includes('只读验收') &&
|
||||
initialQualityDelegation.task.includes('不要修改任何文件') &&
|
||||
initialQualityDelegation.task.includes(
|
||||
'不要读取或依赖并行 code-prototype',
|
||||
) &&
|
||||
Array.isArray(initialQualityDelegation.expectedArtifacts) &&
|
||||
initialQualityDelegation.expectedArtifacts.length === 0,
|
||||
'self-test-initial-delegation-contract-invalid',
|
||||
);
|
||||
const qualityPlan =
|
||||
'计划进度:\n- #1 [in_progress] 核对完整玩法\n- #2 [pending] 回传结论\n工具策略:auto=无';
|
||||
const builderPlan =
|
||||
'计划进度:\n- #1 [completed] 生成入口\n- #2 [completed] 静态验证\n- #3 [in_progress] 回传结论\n工具策略:auto=无';
|
||||
route('quality-review', 'quality-run', allTools, qualityPlan);
|
||||
assert(
|
||||
responseFunctionNames(
|
||||
route('quality-review', 'quality-run', allTools, qualityPlan),
|
||||
).join(',') === 'update_agent_plan,respond_to_user',
|
||||
'self-test-quality-read-only-invalid',
|
||||
);
|
||||
route('code-prototype', 'initial-code-run');
|
||||
route('code-prototype', 'initial-code-run');
|
||||
assert(
|
||||
@@ -222,15 +363,21 @@ async function runSelfTest() {
|
||||
'runtime_tool_file_patch',
|
||||
'self-test-forbidden-parent-mutation-invalid',
|
||||
);
|
||||
const followupDelegationResponse = route(
|
||||
'project-supervisor',
|
||||
'parent-run',
|
||||
['runtime_tool_agent_delegate'],
|
||||
'当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate',
|
||||
);
|
||||
const followupDelegationCall = responseFunctionCalls(
|
||||
followupDelegationResponse,
|
||||
)[0];
|
||||
assert(
|
||||
responseFunctionNames(
|
||||
route(
|
||||
'project-supervisor',
|
||||
'parent-run',
|
||||
['runtime_tool_agent_delegate'],
|
||||
'当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate',
|
||||
),
|
||||
)[0] === 'runtime_tool_agent_delegate',
|
||||
followupDelegationCall?.name === 'runtime_tool_agent_delegate' &&
|
||||
followupDelegationCall.arguments?.input?.agentId === 'code-prototype' &&
|
||||
JSON.stringify(
|
||||
followupDelegationCall.arguments.input.expectedArtifacts,
|
||||
) === JSON.stringify(['game/index.html']),
|
||||
'self-test-followup-delegation-invalid',
|
||||
);
|
||||
route('code-prototype', 'repair-code-run');
|
||||
@@ -250,6 +397,78 @@ async function runSelfTest() {
|
||||
route('project-supervisor', 'parent-run');
|
||||
const stats = router.getStats();
|
||||
assert(expectedProviderStats(stats), 'self-test-provider-stats-invalid');
|
||||
const responsibilityContract = responsibilityContractEvidence(stats);
|
||||
assert(
|
||||
responsibilityContract.codeOwnsGameIndex &&
|
||||
responsibilityContract.qualityIsReadOnly &&
|
||||
responsibilityContract.qualityIndependentOfCodeTiming &&
|
||||
responsibilityContract.contractViolationFree,
|
||||
'self-test-responsibility-contract-invalid',
|
||||
);
|
||||
const singlePassQualityStats = structuredClone(stats);
|
||||
singlePassQualityStats.requestCount -= 1;
|
||||
singlePassQualityStats.planningRequestCount -= 1;
|
||||
singlePassQualityStats.qualityRevisionReplanCount = 0;
|
||||
singlePassQualityStats.byAgent['quality-review'].planning = 1;
|
||||
assert(
|
||||
expectedProviderStats(singlePassQualityStats),
|
||||
'self-test-quality-single-pass-timing-invalid',
|
||||
);
|
||||
|
||||
const syntheticChildReport = {
|
||||
status: 'PASS',
|
||||
suite,
|
||||
errorCount: 0,
|
||||
blocked: [],
|
||||
cleanup: { performed: true, kept: false },
|
||||
evidence: {
|
||||
...Object.fromEntries(
|
||||
requiredZeroChildEvidenceFields.map((field) => [field, 0]),
|
||||
),
|
||||
evidenceCompleteness: 'complete',
|
||||
dedicatedZeroInterventionPath: true,
|
||||
stdinTaskCount: 1,
|
||||
stdinEndedAfterTask: true,
|
||||
turnReportOutcome: 'settled',
|
||||
parentTaskStatus: 'completed',
|
||||
parentRuntimeStatus: 'idle',
|
||||
parentRuntimePhase: 'completed',
|
||||
laneDefensePlaytestPassed: true,
|
||||
laneDefenseAssertionCount: 37,
|
||||
laneDefensePassedAssertionCount: 37,
|
||||
browserValidationPassed: true,
|
||||
staticSmokePassed: true,
|
||||
gameIndexChanged: true,
|
||||
projectRevisionDelta: 2,
|
||||
finalSupervisorAssistantCount: 1,
|
||||
professionalAssistantCount: 3,
|
||||
providerRequestIdentityCount: stats.requestCount,
|
||||
providerLifecycleStartedCount: stats.requestCount,
|
||||
providerLifecycleTerminalCount: stats.requestCount,
|
||||
providerLifecycleCompletedCount: stats.requestCount,
|
||||
},
|
||||
};
|
||||
const incompletePlaytestReport = structuredClone(syntheticChildReport);
|
||||
incompletePlaytestReport.evidence.laneDefensePassedAssertionCount = 36;
|
||||
const manualInputReport = structuredClone(syntheticChildReport);
|
||||
manualInputReport.evidence.approveInputCount = 1;
|
||||
const residualSidecarReport = structuredClone(syntheticChildReport);
|
||||
residualSidecarReport.evidence.providerHandoffSidecarCount = 1;
|
||||
assert(
|
||||
expectedChildReport(syntheticChildReport, { keepProject: false }, stats) &&
|
||||
!expectedChildReport(
|
||||
incompletePlaytestReport,
|
||||
{ keepProject: false },
|
||||
stats,
|
||||
) &&
|
||||
!expectedChildReport(manualInputReport, { keepProject: false }, stats) &&
|
||||
!expectedChildReport(
|
||||
residualSidecarReport,
|
||||
{ keepProject: false },
|
||||
stats,
|
||||
),
|
||||
'self-test-child-hard-gates-invalid',
|
||||
);
|
||||
|
||||
const rootPackage = JSON.parse(
|
||||
await fs.readFile(path.join(repoRoot, 'package.json'), 'utf8'),
|
||||
@@ -273,6 +492,9 @@ async function runSelfTest() {
|
||||
providerUsed: false,
|
||||
htmlChars: [...html].length,
|
||||
providerStats: stats,
|
||||
responsibilityContract,
|
||||
qualityTimingOrdersValidated: ['before-code', 'after-code'],
|
||||
childHardGatesValidated: true,
|
||||
packageCommandsRegistered: true,
|
||||
};
|
||||
}
|
||||
@@ -359,12 +581,10 @@ async function runE2e(options) {
|
||||
childResult?.code === 0 &&
|
||||
childResult?.signal === null &&
|
||||
!childResult?.error &&
|
||||
childReport?.status === 'PASS' &&
|
||||
childReport?.suite === suite &&
|
||||
childReport?.cleanup?.performed === !options.keepProject &&
|
||||
childReport?.cleanup?.kept === options.keepProject;
|
||||
expectedChildReport(childReport, options, providerStats);
|
||||
const providerPassed =
|
||||
providerStats?.stopped === true && expectedProviderStats(providerStats);
|
||||
const responsibilityContract = responsibilityContractEvidence(providerStats);
|
||||
const status =
|
||||
!failureCode && childPassed && providerPassed && configRemoved
|
||||
? 'PASS'
|
||||
@@ -383,6 +603,7 @@ async function runE2e(options) {
|
||||
delegatedSuite: suite,
|
||||
child: childReport,
|
||||
provider: providerStats,
|
||||
responsibilityContract,
|
||||
cleanup: {
|
||||
providerStopped: providerStats?.stopped === true,
|
||||
configRemoved,
|
||||
|
||||
@@ -187,17 +187,51 @@ function chatTextResponse(sequence, model, content) {
|
||||
};
|
||||
}
|
||||
|
||||
function delegateCall(agentId, task, acceptanceCriteria) {
|
||||
function delegateCall(agentId, task, acceptanceCriteria, expectedArtifacts) {
|
||||
return nativeAction('agent.delegate', `把独立工作交给 ${agentId}`, {
|
||||
agentId,
|
||||
task,
|
||||
acceptanceCriteria,
|
||||
expectedArtifacts: ['game/index.html'],
|
||||
expectedArtifacts,
|
||||
repairOfDelegationId: null,
|
||||
runId: null,
|
||||
});
|
||||
}
|
||||
|
||||
const projectMutationFunctionNames = new Set(
|
||||
[
|
||||
'blackboard.write',
|
||||
'canvas.asset_generate',
|
||||
'command.exec',
|
||||
'command.start',
|
||||
'file.delete',
|
||||
'file.patch',
|
||||
'file.write',
|
||||
'memory.write',
|
||||
'project.git_commit',
|
||||
'project.patchset',
|
||||
'project.restore',
|
||||
'task.create',
|
||||
'task.update',
|
||||
].map(runtimeFunction),
|
||||
);
|
||||
|
||||
function isGameIndexArtifactContract(expectedArtifacts) {
|
||||
return (
|
||||
Array.isArray(expectedArtifacts) &&
|
||||
expectedArtifacts.length === 1 &&
|
||||
expectedArtifacts[0] === 'game/index.html'
|
||||
);
|
||||
}
|
||||
|
||||
function isExplicitReadOnlyQualityTask(task) {
|
||||
return (
|
||||
typeof task === 'string' &&
|
||||
task.includes('只读验收') &&
|
||||
task.includes('不要修改任何文件')
|
||||
);
|
||||
}
|
||||
|
||||
function staticSmokeCall(reason) {
|
||||
return nativeAction('command.run_limited', reason, {
|
||||
commandId: 'game.static_smoke',
|
||||
@@ -262,6 +296,15 @@ export function createDeterministicLaneDefenseRouter({
|
||||
staticSmokeCount: 0,
|
||||
previewValidationCount: 0,
|
||||
supervisorDirectMutationAttemptCount: 0,
|
||||
delegationContractCount: 0,
|
||||
delegationContractViolationCount: 0,
|
||||
codePrototypeDelegationCount: 0,
|
||||
codePrototypeExpectedArtifactCount: 0,
|
||||
codePrototypeGameIndexArtifactDelegationCount: 0,
|
||||
qualityReviewDelegationCount: 0,
|
||||
qualityReviewReadOnlyDelegationCount: 0,
|
||||
qualityReviewExpectedArtifactCount: 0,
|
||||
qualityRevisionReplanCount: 0,
|
||||
unexpectedRequestCount: 0,
|
||||
rejectionCodes: {},
|
||||
rejections: [],
|
||||
@@ -272,10 +315,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
let initialBuilderRunId = null;
|
||||
let repairBuilderRunId = null;
|
||||
|
||||
function recordAgent(agentId, kind) {
|
||||
const current = agentCounts.get(agentId) ?? { planning: 0, finalReply: 0 };
|
||||
current[kind] += 1;
|
||||
agentCounts.set(agentId, current);
|
||||
function publishAgentCounts() {
|
||||
stats.byAgent = Object.fromEntries(
|
||||
[...agentCounts.entries()].sort(([left], [right]) =>
|
||||
left.localeCompare(right),
|
||||
@@ -283,8 +323,70 @@ export function createDeterministicLaneDefenseRouter({
|
||||
);
|
||||
}
|
||||
|
||||
function callsResponse(tools, calls) {
|
||||
function getAgentCounts(agentId) {
|
||||
return (
|
||||
agentCounts.get(agentId) ?? {
|
||||
planning: 0,
|
||||
finalReply: 0,
|
||||
projectMutation: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function recordAgent(agentId, kind) {
|
||||
const current = getAgentCounts(agentId);
|
||||
current[kind] += 1;
|
||||
agentCounts.set(agentId, current);
|
||||
publishAgentCounts();
|
||||
}
|
||||
|
||||
function recordEmittedCalls(agentId, calls) {
|
||||
const current = getAgentCounts(agentId);
|
||||
current.projectMutation += calls.filter((call) =>
|
||||
projectMutationFunctionNames.has(call.name),
|
||||
).length;
|
||||
agentCounts.set(agentId, current);
|
||||
publishAgentCounts();
|
||||
|
||||
for (const call of calls) {
|
||||
if (call.name !== runtimeFunction('agent.delegate')) continue;
|
||||
stats.delegationContractCount += 1;
|
||||
const input = call.arguments?.input;
|
||||
const hasExplicitExpectedArtifacts = Array.isArray(
|
||||
input?.expectedArtifacts,
|
||||
);
|
||||
const expectedArtifacts = hasExplicitExpectedArtifacts
|
||||
? input.expectedArtifacts
|
||||
: [];
|
||||
if (input?.agentId === 'code-prototype') {
|
||||
stats.codePrototypeDelegationCount += 1;
|
||||
stats.codePrototypeExpectedArtifactCount += expectedArtifacts.length;
|
||||
if (isGameIndexArtifactContract(expectedArtifacts)) {
|
||||
stats.codePrototypeGameIndexArtifactDelegationCount += 1;
|
||||
} else {
|
||||
stats.delegationContractViolationCount += 1;
|
||||
}
|
||||
} else if (input?.agentId === 'quality-review') {
|
||||
stats.qualityReviewDelegationCount += 1;
|
||||
stats.qualityReviewExpectedArtifactCount += expectedArtifacts.length;
|
||||
if (
|
||||
hasExplicitExpectedArtifacts &&
|
||||
expectedArtifacts.length === 0 &&
|
||||
isExplicitReadOnlyQualityTask(input.task)
|
||||
) {
|
||||
stats.qualityReviewReadOnlyDelegationCount += 1;
|
||||
} else {
|
||||
stats.delegationContractViolationCount += 1;
|
||||
}
|
||||
} else {
|
||||
stats.delegationContractViolationCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function callsResponse(agentId, tools, calls) {
|
||||
requireAdvertised(tools, calls);
|
||||
recordEmittedCalls(agentId, calls);
|
||||
responseSequence += 1;
|
||||
return chatToolResponse(responseSequence, model, calls);
|
||||
}
|
||||
@@ -302,7 +404,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
}
|
||||
parentStage = 'verify-repair';
|
||||
stats.followupDelegationCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
delegateCall(
|
||||
'code-prototype',
|
||||
'修复最近一次真实浏览器验证证明的 canvas 不可见问题;保持现有 lane-defense-v1 交互、状态面、关卡推进和重开行为不变,并在修改后通过静态自检。',
|
||||
@@ -311,6 +413,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
'lane-defense-v1 全部固定试玩断言继续通过',
|
||||
'修改后的当前 revision 通过 game.static_smoke',
|
||||
],
|
||||
['game/index.html'],
|
||||
),
|
||||
]);
|
||||
}
|
||||
@@ -319,7 +422,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
case 'initial-delegation': {
|
||||
parentStage = 'claim-initial';
|
||||
stats.initialDelegationCount += 2;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
delegateCall(
|
||||
'code-prototype',
|
||||
'生成一个紧凑但完整的植物塔防网页游戏:支持开始、选择植物、放置防御单位、敌人移动和受伤、胜利、下一关与重开;写入 game/index.html,并在修改后通过静态自检。',
|
||||
@@ -328,51 +431,60 @@ export function createDeterministicLaneDefenseRouter({
|
||||
'敌人会移动并受伤,关卡可胜利、进入下一关并重开',
|
||||
'修改后的当前 revision 通过 game.static_smoke',
|
||||
],
|
||||
['game/index.html'],
|
||||
),
|
||||
delegateCall(
|
||||
'quality-review',
|
||||
'独立核对塔防交付合同,确认必须覆盖植物选择、战斗、胜利、下一关、重开、桌面和移动视口;只返回验收重点,不修改项目。',
|
||||
['验收结论覆盖完整可玩循环和双视口', '不修改项目文件'],
|
||||
'执行只读验收:独立核对塔防交付合同是否覆盖植物选择、战斗、胜利、下一关、重开、桌面和移动视口;只返回合同验收重点,不要修改任何文件,也不要读取或依赖并行 code-prototype 尚未完成的项目产物。',
|
||||
[
|
||||
'验收结论覆盖完整可玩循环和双视口',
|
||||
'只读返回合同重点且项目 mutation 为零',
|
||||
],
|
||||
[],
|
||||
),
|
||||
]);
|
||||
}
|
||||
case 'claim-initial':
|
||||
parentStage = 'static-initial';
|
||||
stats.runStatusCount += 1;
|
||||
return callsResponse(tools, [runStatusCall('认领两份首轮专业回执')]);
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
runStatusCall('认领两份首轮专业回执'),
|
||||
]);
|
||||
case 'static-initial':
|
||||
parentStage = 'preview-initial';
|
||||
stats.staticSmokeCount += 1;
|
||||
return callsResponse(tools, [staticSmokeCall('验证初版当前 revision')]);
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
staticSmokeCall('验证初版当前 revision'),
|
||||
]);
|
||||
case 'preview-initial':
|
||||
parentStage = 'attempt-forbidden-repair';
|
||||
stats.previewValidationCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
previewCall('真实试玩并检查桌面与移动视口'),
|
||||
]);
|
||||
case 'attempt-forbidden-repair':
|
||||
parentStage = 'await-delegation-repair';
|
||||
stats.supervisorDirectMutationAttemptCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
repairPatchCall('根据浏览器失败诊断直接修复不可见 canvas'),
|
||||
]);
|
||||
case 'verify-repair':
|
||||
parentStage = 'verify-repair-after-claim';
|
||||
stats.runStatusCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
runStatusCall('先认领返工后的专业交付,再验证最新 revision'),
|
||||
]);
|
||||
case 'verify-repair-after-claim':
|
||||
parentStage = 'respond';
|
||||
stats.staticSmokeCount += 1;
|
||||
stats.previewValidationCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
staticSmokeCall('验证返工后的当前 revision'),
|
||||
previewCall('重跑完整真实试玩和双视口检查'),
|
||||
]);
|
||||
case 'respond':
|
||||
parentStage = 'done';
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('project-supervisor', tools, [
|
||||
...completedPlanCallsForContext(
|
||||
context,
|
||||
'可试玩项目和全部验证已经完成',
|
||||
@@ -405,7 +517,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
if (phase === 0) {
|
||||
runPhases.set(runId, 1);
|
||||
stats.sourceWriteCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('code-prototype', tools, [
|
||||
nativeAction('file.write', '写入完整可玩的塔防初版', {
|
||||
path: 'game/index.html',
|
||||
content: deterministicLaneDefenseInitialHtml(),
|
||||
@@ -415,11 +527,13 @@ export function createDeterministicLaneDefenseRouter({
|
||||
if (phase === 1) {
|
||||
runPhases.set(runId, 2);
|
||||
stats.staticSmokeCount += 1;
|
||||
return callsResponse(tools, [staticSmokeCall('验证初版源码与状态面')]);
|
||||
return callsResponse('code-prototype', tools, [
|
||||
staticSmokeCall('验证初版源码与状态面'),
|
||||
]);
|
||||
}
|
||||
if (phase === 2) {
|
||||
runPhases.set(runId, 3);
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('code-prototype', tools, [
|
||||
...completedPlanCallsForContext(
|
||||
context,
|
||||
'初版实现和静态验证已经完成',
|
||||
@@ -432,18 +546,20 @@ export function createDeterministicLaneDefenseRouter({
|
||||
if (phase === 0) {
|
||||
runPhases.set(runId, 1);
|
||||
stats.sourcePatchCount += 1;
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('code-prototype', tools, [
|
||||
repairPatchCall('修复真实浏览器发现的 canvas 可见性'),
|
||||
]);
|
||||
}
|
||||
if (phase === 1) {
|
||||
runPhases.set(runId, 2);
|
||||
stats.staticSmokeCount += 1;
|
||||
return callsResponse(tools, [staticSmokeCall('验证返工后的源码')]);
|
||||
return callsResponse('code-prototype', tools, [
|
||||
staticSmokeCall('验证返工后的源码'),
|
||||
]);
|
||||
}
|
||||
if (phase === 2) {
|
||||
runPhases.set(runId, 3);
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('code-prototype', tools, [
|
||||
...completedPlanCallsForContext(
|
||||
context,
|
||||
'画布可见性返工和静态验证已经完成',
|
||||
@@ -462,7 +578,7 @@ export function createDeterministicLaneDefenseRouter({
|
||||
if (phase === 0) {
|
||||
runKinds.set(runId, 'quality-review');
|
||||
runPhases.set(runId, 1);
|
||||
return callsResponse(tools, [
|
||||
return callsResponse('quality-review', tools, [
|
||||
...completedPlanCallsForContext(context, '独立验收重点已经核对'),
|
||||
nativeResponse(
|
||||
'验收必须以真实植物选择、放置、战斗推进、胜利、下一关、重开和双视口可见性为准。',
|
||||
@@ -471,7 +587,8 @@ export function createDeterministicLaneDefenseRouter({
|
||||
}
|
||||
if (phase === 1) {
|
||||
runPhases.set(runId, 2);
|
||||
return callsResponse(tools, [
|
||||
stats.qualityRevisionReplanCount += 1;
|
||||
return callsResponse('quality-review', tools, [
|
||||
...completedPlanCallsForContext(
|
||||
context,
|
||||
'项目 revision 更新后已重新核对独立验收重点',
|
||||
|
||||
+458
-18
File diff suppressed because it is too large
Load Diff
+94
-1
@@ -73,7 +73,7 @@ pub(in crate::agent) struct AgentRuntimeParallelReadBatch {
|
||||
pub(in crate::agent) updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct AgentRuntimeProviderActionBatch {
|
||||
pub(crate) schema_version: String,
|
||||
@@ -102,6 +102,79 @@ pub(crate) struct AgentRuntimeProviderActionBatch {
|
||||
pub(crate) updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct AgentRuntimeProviderActionBatchWire {
|
||||
schema_version: String,
|
||||
batch_id: String,
|
||||
project_id: String,
|
||||
agent_id: String,
|
||||
task_id: String,
|
||||
session_id: String,
|
||||
run_id: String,
|
||||
source: String,
|
||||
#[serde(default = "default_agent_runtime_run_profile")]
|
||||
run_profile: String,
|
||||
#[serde(default)]
|
||||
run_profile_binding_fingerprint: String,
|
||||
loop_iteration: u32,
|
||||
planned_steer_cursor: u64,
|
||||
status: String,
|
||||
next_action_index: u32,
|
||||
plan: AgentRuntimeToolPlan,
|
||||
actions: Vec<AgentRuntimePendingToolAction>,
|
||||
#[serde(default)]
|
||||
collaboration_contract: Option<SupervisorCollaborationContract>,
|
||||
project_revision_before: AgentRuntimeProjectRevision,
|
||||
planned_repository_context_fingerprint: String,
|
||||
created_at: u64,
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for AgentRuntimeProviderActionBatch {
|
||||
fn deserialize<Deserializer>(deserializer: Deserializer) -> Result<Self, Deserializer::Error>
|
||||
where
|
||||
Deserializer: serde::Deserializer<'de>,
|
||||
{
|
||||
let wire = AgentRuntimeProviderActionBatchWire::deserialize(deserializer)?;
|
||||
let batch = Self {
|
||||
schema_version: wire.schema_version,
|
||||
batch_id: wire.batch_id,
|
||||
project_id: wire.project_id,
|
||||
agent_id: wire.agent_id,
|
||||
task_id: wire.task_id,
|
||||
session_id: wire.session_id,
|
||||
run_id: wire.run_id,
|
||||
source: wire.source,
|
||||
run_profile: wire.run_profile,
|
||||
run_profile_binding_fingerprint: wire.run_profile_binding_fingerprint,
|
||||
loop_iteration: wire.loop_iteration,
|
||||
planned_steer_cursor: wire.planned_steer_cursor,
|
||||
status: wire.status,
|
||||
next_action_index: wire.next_action_index,
|
||||
plan: wire.plan,
|
||||
actions: wire.actions,
|
||||
collaboration_contract: wire.collaboration_contract,
|
||||
project_revision_before: wire.project_revision_before,
|
||||
planned_repository_context_fingerprint: wire.planned_repository_context_fingerprint,
|
||||
created_at: wire.created_at,
|
||||
updated_at: wire.updated_at,
|
||||
};
|
||||
if batch.schema_version == AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
&& batch.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& batch.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& batch
|
||||
.collaboration_contract
|
||||
.as_ref()
|
||||
.is_some_and(|contract| contract.initial_wave)
|
||||
{
|
||||
validate_agent_runtime_autonomous_initial_collaboration_contract(&batch.plan)
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
}
|
||||
Ok(batch)
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentRuntimePendingToolAction {
|
||||
pub(crate) fn summary(&self) -> AgentRuntimePendingToolActionSummary {
|
||||
AgentRuntimePendingToolActionSummary {
|
||||
@@ -290,6 +363,26 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch(
|
||||
} else {
|
||||
(None, None, SupervisorCollaborationPreflight::default())
|
||||
};
|
||||
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& collaboration_preflight
|
||||
.contract
|
||||
.as_ref()
|
||||
.is_some_and(|contract| contract.initial_wave)
|
||||
{
|
||||
if let Err(error) =
|
||||
validate_agent_runtime_autonomous_initial_collaboration_contract(&batch_plan)
|
||||
{
|
||||
return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked(
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "runtime.collaboration_policy".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "Project Supervisor 首批协作不满足 autonomous-game-build 合同"
|
||||
.to_string(),
|
||||
detail: Some(error),
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(violation) = collaboration_preflight.violation {
|
||||
return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked(
|
||||
AgentRuntimeToolObservation {
|
||||
|
||||
+65
-8
@@ -39,7 +39,7 @@ pub(in crate::agent) fn agent_runtime_provider_action_batch_id_v1(
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(in crate::agent) fn agent_runtime_provider_action_batch_id(
|
||||
pub(in crate::agent) fn agent_runtime_provider_action_batch_id_v2(
|
||||
project_id: &str,
|
||||
agent_id: &str,
|
||||
task_id: &str,
|
||||
@@ -79,6 +79,37 @@ pub(in crate::agent) fn agent_runtime_provider_action_batch_id(
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(in crate::agent) fn agent_runtime_provider_action_batch_id(
|
||||
project_id: &str,
|
||||
agent_id: &str,
|
||||
task_id: &str,
|
||||
session_id: &str,
|
||||
run_id: &str,
|
||||
loop_iteration: u32,
|
||||
planned_steer_cursor: u64,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
project_revision_before: &AgentRuntimeProjectRevision,
|
||||
planned_repository_context_fingerprint: &str,
|
||||
actions: &[AgentRuntimePendingToolAction],
|
||||
collaboration_contract: Option<&SupervisorCollaborationContract>,
|
||||
) -> Result<String, String> {
|
||||
agent_runtime_provider_action_batch_id_v2(
|
||||
project_id,
|
||||
agent_id,
|
||||
task_id,
|
||||
session_id,
|
||||
run_id,
|
||||
loop_iteration,
|
||||
planned_steer_cursor,
|
||||
plan,
|
||||
project_revision_before,
|
||||
planned_repository_context_fingerprint,
|
||||
actions,
|
||||
collaboration_contract,
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batch(
|
||||
root: &Path,
|
||||
batch: &AgentRuntimeProviderActionBatch,
|
||||
@@ -97,6 +128,7 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
batch.schema_version.as_str(),
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
| AGENT_RUNTIME_PROVIDER_ACTION_BATCH_PREVIOUS_SCHEMA_VERSION
|
||||
| AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION
|
||||
) {
|
||||
return Err(format!(
|
||||
"不支持的 Agent Runtime Provider action 批次版本:{}",
|
||||
@@ -132,7 +164,7 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
));
|
||||
}
|
||||
let minimum_action_count = if batch.schema_version
|
||||
== AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
!= AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION
|
||||
&& batch.collaboration_contract.is_some()
|
||||
{
|
||||
1
|
||||
@@ -298,8 +330,8 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
};
|
||||
let state = read_supervisor_collaboration_state_at(root, &batch.agent_id, &batch.run_id)?;
|
||||
if let Some(contract) = batch.collaboration_contract.as_ref() {
|
||||
if batch.schema_version != AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION {
|
||||
return Err("旧版 Provider action 批次不能携带协作合同".to_string());
|
||||
if batch.schema_version == AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION {
|
||||
return Err("v1 Provider action 批次不能携带协作合同".to_string());
|
||||
}
|
||||
let pristine = next_action_index == 0
|
||||
&& batch.actions.iter().all(|pending| {
|
||||
@@ -315,6 +347,12 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
contract,
|
||||
pristine.then_some(!state.has_collaboration()),
|
||||
)?;
|
||||
if batch.schema_version == AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION
|
||||
&& batch.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& contract.initial_wave
|
||||
{
|
||||
validate_agent_runtime_autonomous_initial_collaboration_contract(&batch.plan)?;
|
||||
}
|
||||
} else {
|
||||
let preflight = preflight_supervisor_collaboration_plan(
|
||||
&batch.agent_id,
|
||||
@@ -335,8 +373,8 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
} else if batch.collaboration_contract.is_some() {
|
||||
return Err("非 Project Supervisor Provider 批次不能携带协作合同".to_string());
|
||||
}
|
||||
let expected_batch_id =
|
||||
if batch.schema_version == AGENT_RUNTIME_PROVIDER_ACTION_BATCH_PREVIOUS_SCHEMA_VERSION {
|
||||
let expected_batch_id = match batch.schema_version.as_str() {
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION => {
|
||||
agent_runtime_provider_action_batch_id_v1(
|
||||
&batch.project_id,
|
||||
&batch.agent_id,
|
||||
@@ -350,7 +388,24 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
&batch.planned_repository_context_fingerprint,
|
||||
&batch.actions,
|
||||
)?
|
||||
} else {
|
||||
}
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_PREVIOUS_SCHEMA_VERSION => {
|
||||
agent_runtime_provider_action_batch_id_v2(
|
||||
&batch.project_id,
|
||||
&batch.agent_id,
|
||||
&batch.task_id,
|
||||
&batch.session_id,
|
||||
&batch.run_id,
|
||||
batch.loop_iteration,
|
||||
batch.planned_steer_cursor,
|
||||
&batch.plan,
|
||||
&batch.project_revision_before,
|
||||
&batch.planned_repository_context_fingerprint,
|
||||
&batch.actions,
|
||||
batch.collaboration_contract.as_ref(),
|
||||
)?
|
||||
}
|
||||
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION => {
|
||||
agent_runtime_provider_action_batch_id(
|
||||
&batch.project_id,
|
||||
&batch.agent_id,
|
||||
@@ -365,7 +420,9 @@ pub(in crate::agent) fn validate_game_creator_agent_runtime_provider_action_batc
|
||||
&batch.actions,
|
||||
batch.collaboration_contract.as_ref(),
|
||||
)?
|
||||
};
|
||||
}
|
||||
_ => unreachable!("Provider action batch schema was validated above"),
|
||||
};
|
||||
if batch.batch_id != expected_batch_id {
|
||||
return Err("Agent Runtime Provider action 批次身份指纹已变化".to_string());
|
||||
}
|
||||
|
||||
+33
-11
@@ -1,5 +1,19 @@
|
||||
use super::*;
|
||||
|
||||
fn normalize_game_creator_agent_background_final_reply_response(
|
||||
mut response: platform_llm::LlmRunResponse,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
) -> Result<platform_llm::LlmRunResponse, platform_llm::LlmError> {
|
||||
response.text = redact_agent_runtime_private_process_output_from_response(
|
||||
&strip_llm_thinking_blocks(&response.text),
|
||||
observations,
|
||||
);
|
||||
if response.text.trim().is_empty() {
|
||||
return Err(platform_llm::LlmError::EmptyResponse);
|
||||
}
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -120,6 +134,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
let fallback_response = fallback_response.filter(|response| !response.trim().is_empty());
|
||||
let stream_response = llm.stream;
|
||||
let request_stream_snapshot = stream_snapshot.clone();
|
||||
let response_observations = observations;
|
||||
let response_result =
|
||||
request_game_creator_agent_runtime_llm_with_persisted_transient_retry_using(
|
||||
root,
|
||||
@@ -146,8 +161,19 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
publisher.handoff();
|
||||
Ok(response)
|
||||
match normalize_game_creator_agent_background_final_reply_response(
|
||||
response,
|
||||
response_observations,
|
||||
) {
|
||||
Ok(response) => {
|
||||
publisher.handoff();
|
||||
Ok(response)
|
||||
}
|
||||
Err(error) => {
|
||||
publisher.failed();
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
publisher.failed();
|
||||
@@ -155,18 +181,14 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
}
|
||||
}
|
||||
} else {
|
||||
client.run(attempt_request).await
|
||||
normalize_game_creator_agent_background_final_reply_response(
|
||||
client.run(attempt_request).await?,
|
||||
response_observations,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
|response| {
|
||||
let mut response = response.clone();
|
||||
response.text = redact_agent_runtime_private_process_output_from_response(
|
||||
&strip_llm_thinking_blocks(&response.text),
|
||||
observations,
|
||||
);
|
||||
response
|
||||
},
|
||||
|response| response.clone(),
|
||||
)
|
||||
.await;
|
||||
if response_result.is_err() {
|
||||
|
||||
+115
-6
@@ -160,8 +160,13 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
} else {
|
||||
AGENT_RUNTIME_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS
|
||||
};
|
||||
let read_only_delivery = run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
let task_text_requires_read_only_delivery = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_runtime_task_requires_read_only_delivery(agent_id, task);
|
||||
let read_only_delivery = run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_runtime_task_requires_read_only_delivery_at(
|
||||
root, agent_id, session_id, run_id, task,
|
||||
)?;
|
||||
let verified_delivery = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
let verification_gate =
|
||||
read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
|
||||
@@ -273,6 +278,28 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
)
|
||||
.and_then(|parsed| {
|
||||
let source_payload = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
let verification_gate = read_game_creator_agent_runtime_verification_gate(
|
||||
root, agent_id, run_id,
|
||||
)
|
||||
.map_err(|error| {
|
||||
AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
validate_agent_runtime_autonomous_specialist_response_delivery(
|
||||
agent_id,
|
||||
run_id,
|
||||
read_only_delivery,
|
||||
&verification_gate,
|
||||
&parsed.plan,
|
||||
)
|
||||
.map_err(|error| {
|
||||
AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
let source_payload = validate_agent_runtime_autonomous_source_payload(&parsed.plan)
|
||||
.map_err(|error| {
|
||||
AgentRuntimeToolPlanProtocolError::new(
|
||||
@@ -280,6 +307,16 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
validate_agent_runtime_autonomous_read_only_delivery_plan(
|
||||
read_only_delivery,
|
||||
&parsed.plan,
|
||||
)
|
||||
.map_err(|error| {
|
||||
AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
if autonomous_scaffold_repair_active
|
||||
&& source_payload.max_field_chars
|
||||
> AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS
|
||||
@@ -368,7 +405,23 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
&collaboration_policy,
|
||||
&collaboration_state,
|
||||
)?;
|
||||
if let Some(violation) = preflight.violation {
|
||||
let autonomous_initial_collaboration = if run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& preflight
|
||||
.contract
|
||||
.as_ref()
|
||||
.is_some_and(|contract| contract.initial_wave)
|
||||
{
|
||||
validate_agent_runtime_autonomous_initial_collaboration_contract(&parsed.plan)
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
if let Err(error) = autonomous_initial_collaboration {
|
||||
Err(AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
error,
|
||||
))
|
||||
} else if let Some(violation) = preflight.violation {
|
||||
Err(AgentRuntimeToolPlanProtocolError::new(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
format!("{}:{}", violation.summary, violation.detail),
|
||||
@@ -415,6 +468,20 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
mut normalized_text_chars,
|
||||
mut normalized_text_sha256,
|
||||
} = parsed;
|
||||
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& read_only_delivery
|
||||
&& !task_text_requires_read_only_delivery
|
||||
&& !plan.response.trim().is_empty()
|
||||
{
|
||||
let runtime = read_game_creator_agent_runtime_for_session_at(
|
||||
root,
|
||||
agent_id,
|
||||
Some(session_id),
|
||||
)?
|
||||
.state;
|
||||
plan.plan_update =
|
||||
agent_runtime_read_only_delivery_completion_plan_update(&runtime);
|
||||
}
|
||||
if let Some((count, source_chars, source_sha256)) =
|
||||
response_handoff.thinking_normalization_metadata()
|
||||
{
|
||||
@@ -548,8 +615,25 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& protocol_error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX)
|
||||
&& !request.function_tools.is_empty();
|
||||
let force_autonomous_read_only_delivery =
|
||||
force_autonomous_pre_mutation && read_only_delivery;
|
||||
let force_autonomous_read_only_delivery = read_only_delivery
|
||||
&& (force_autonomous_pre_mutation
|
||||
|| protocol_error
|
||||
.starts_with(AGENT_RUNTIME_AUTONOMOUS_READ_ONLY_MUTATION_ERROR_PREFIX));
|
||||
let force_autonomous_specialist_mutation_only = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& !read_only_delivery
|
||||
&& agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& (force_autonomous_pre_mutation
|
||||
|| protocol_error.starts_with(
|
||||
AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_MUTATION_ONLY_REPAIR_ERROR_PREFIX,
|
||||
))
|
||||
&& !request.function_tools.is_empty();
|
||||
let force_autonomous_specialist_verification_only = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& protocol_error.starts_with(
|
||||
AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX,
|
||||
)
|
||||
&& !request.function_tools.is_empty();
|
||||
let force_autonomous_pending_verification = run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& protocol_error.starts_with(
|
||||
@@ -607,6 +691,8 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
&protocol_error,
|
||||
) && !request.function_tools.is_empty();
|
||||
if force_supervisor_initial_collaboration
|
||||
|| force_autonomous_specialist_mutation_only
|
||||
|| force_autonomous_specialist_verification_only
|
||||
|| force_autonomous_response_plan_completion
|
||||
|| force_autonomous_delegated_playtest_repair
|
||||
|| force_autonomous_failed_playtest
|
||||
@@ -616,6 +702,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
|| force_autonomous_preview_after_static
|
||||
|| force_autonomous_verified_delivery
|
||||
|| force_autonomous_truncated_scaffold
|
||||
|| force_autonomous_read_only_delivery
|
||||
|| force_autonomous_pre_mutation
|
||||
{
|
||||
request.function_tools =
|
||||
@@ -623,8 +710,30 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
}
|
||||
if force_supervisor_initial_collaboration {
|
||||
restrict_agent_runtime_supervisor_collaboration_repair_tools(&mut request)?;
|
||||
let instruction = if run_profile
|
||||
== AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
{
|
||||
format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须在同一响应一次性建立完整首批合同:code-prototype 必须是非只读实现任务且 expectedArtifacts 包含 game/index.html;quality-review 的 task 必须显式声明只读、不得修改项目,且 expectedArtifacts 必须为 []。两者都使用 agent.delegate,repairOfDelegationId=null、runId=null;如当前 policy 还要求 isolated,再在同批补齐 agent.spawn_isolated。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须根据当前 Project Supervisor 协作策略,在同一响应中一次性调用完整的 agent.delegate / agent.spawn_isolated 批次,使首批协作合同全部成立。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
|
||||
)
|
||||
};
|
||||
request.messages.push(LlmMessage::user(instruction));
|
||||
} else if force_autonomous_specialist_mutation_only {
|
||||
autonomous_scaffold_repair_active = true;
|
||||
restrict_agent_runtime_autonomous_specialist_mutation_repair_tools(
|
||||
&mut request,
|
||||
)?;
|
||||
request.messages.push(LlmMessage::user(format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须根据当前 Project Supervisor 协作策略,在同一响应中一次性调用完整的 agent.delegate / agent.spawn_isolated 批次,使首批协作合同全部成立。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。"
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前是 autonomous-game-build 的非只读专业任务,本次修复的原生工具目录只保留项目 mutation 工具。必须立即完成本人 run 的实际项目修改;不得 respond_to_user、验证、更新计划、读取、搜索、查询状态或委派。首次源码字段不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符,完整写入 game/index.html 时必须保持 HTML 与 script 闭合。不要解释,不要 markdown,不要代码围栏。"
|
||||
)));
|
||||
} else if force_autonomous_specialist_verification_only {
|
||||
restrict_agent_runtime_autonomous_reverification_repair_tools(&mut request)?;
|
||||
request.messages.push(LlmMessage::user(format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前是 autonomous-game-build 的非只读专业任务,且本人 run 已有 mutation。本次修复的原生工具目录只保留 project.verify 与 command.run_limited;必须立即验证本人 run 的最新 mutation revision,通过后才能 respond_to_user。不得继续修改项目、更新计划、读取、搜索、查询状态、委派或解释。不要 markdown,不要代码围栏。"
|
||||
)));
|
||||
} else if force_autonomous_response_plan_completion {
|
||||
restrict_agent_runtime_autonomous_response_plan_repair_tools(&mut request)?;
|
||||
@@ -636,7 +745,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
|
||||
&mut request,
|
||||
)?;
|
||||
request.messages.push(LlmMessage::user(format!(
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已有 ready 未认领回执或 3 个 active delivery,本次修复的原生工具目录只保留 agent.run_status。必须立即以 agentId=null、scope=all、delegationId=null 查询状态并原子认领 readyDelegateReceipts;不得创建第四次 agent.delegate、更新计划、读取、搜索、修改项目、重复验证或 respond_to_user。认领完成后再依据最新 project revision 重新规划验证。不要解释,不要 markdown,不要代码围栏。"
|
||||
"上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已有 ready 未认领回执、尚未 observed 的持久 claim,或 3 个 active delivery;本次修复的原生工具目录只保留 agent.run_status。必须立即以 agentId=null、scope=all、delegationId=null 查询状态并原子认领、观察 readyDelegateReceipts;不得创建第四次 agent.delegate、更新计划、读取、搜索、修改项目、重复验证或 respond_to_user。收敛完成后再依据最新 project revision 重新规划验证或 repair。不要解释,不要 markdown,不要代码围栏。"
|
||||
)));
|
||||
} else if force_autonomous_preview_after_static {
|
||||
restrict_agent_runtime_autonomous_preview_after_static_repair_tools(
|
||||
|
||||
@@ -50,8 +50,10 @@ pub(super) const AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_EXECUTING: &str = "exe
|
||||
pub(super) const AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED: &str = "observed";
|
||||
pub(super) const AGENT_RUNTIME_PARALLEL_READ_BATCH_SIDECAR_MAX_BYTES: usize = 4 * 1024 * 1024;
|
||||
pub(crate) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION: &str =
|
||||
"game-creator-provider-action-batch.v2";
|
||||
"game-creator-provider-action-batch.v3";
|
||||
pub(super) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_PREVIOUS_SCHEMA_VERSION: &str =
|
||||
"game-creator-provider-action-batch.v2";
|
||||
pub(super) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_LEGACY_SCHEMA_VERSION: &str =
|
||||
"game-creator-provider-action-batch.v1";
|
||||
pub(super) const AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_WAITING_CONFIRMATION: &str =
|
||||
"waiting-confirmation";
|
||||
|
||||
@@ -10,13 +10,18 @@ pub(super) fn game_creator_agent_background_final_reply_fallback(
|
||||
let response = strip_llm_thinking_blocks(plan_response);
|
||||
return (!response.trim().is_empty()).then_some(response);
|
||||
}
|
||||
(run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
.then(|| {
|
||||
if run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||
return None;
|
||||
}
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
Some(
|
||||
format!(
|
||||
"项目已完成生成,并通过当前 revision {response_revision} 的静态检查和桌面、移动端交互试玩验证。"
|
||||
)
|
||||
})
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Some("当前专业任务已完成,执行结果与验证证据已记录。".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_TOOL_PLAN: &str = "tool-plan-failed";
|
||||
|
||||
@@ -180,14 +180,17 @@ fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_agent_empty_plan_has_no_deterministic_final_reply_fallback() {
|
||||
assert!(game_creator_agent_background_final_reply_fallback(
|
||||
"",
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
"code-prototype",
|
||||
7,
|
||||
)
|
||||
.is_none());
|
||||
fn autonomous_specialist_empty_plan_uses_internal_completion_fallback() {
|
||||
assert_eq!(
|
||||
game_creator_agent_background_final_reply_fallback(
|
||||
"",
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
"code-prototype",
|
||||
7,
|
||||
)
|
||||
.as_deref(),
|
||||
Some("当前专业任务已完成,执行结果与验证证据已记录。")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -201,6 +204,17 @@ fn standard_supervisor_empty_plan_has_no_deterministic_final_reply_fallback() {
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_specialist_empty_plan_has_no_deterministic_final_reply_fallback() {
|
||||
assert!(game_creator_agent_background_final_reply_fallback(
|
||||
"",
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
"code-prototype",
|
||||
7,
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_response_precedes_autonomous_supervisor_deterministic_fallback() {
|
||||
assert_eq!(
|
||||
@@ -314,13 +328,26 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac
|
||||
.collect::<Vec<_>>(),
|
||||
vec![fallback.as_str()]
|
||||
);
|
||||
let stream = read_game_creator_agent_runtime_response_stream_at(
|
||||
let mut stream = read_game_creator_agent_runtime_response_stream_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read autonomous fallback response stream")
|
||||
.expect("autonomous fallback response stream exists");
|
||||
for _ in 0..250 {
|
||||
if stream.status == "committed" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
stream = read_game_creator_agent_runtime_response_stream_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("poll autonomous fallback response stream")
|
||||
.expect("autonomous fallback response stream remains present");
|
||||
}
|
||||
assert_eq!(stream.status, "committed");
|
||||
assert_eq!(stream.accumulated_text, fallback);
|
||||
assert!(read_game_creator_agent_runtime_finalization_journal(
|
||||
@@ -377,5 +404,11 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac
|
||||
})
|
||||
.filter_map(|record| record["status"].as_str().map(str::to_string))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(final_reply_lifecycle_statuses, vec!["started", "failed"]);
|
||||
assert!(
|
||||
final_reply_lifecycle_statuses.len() >= 2 && final_reply_lifecycle_statuses.len() % 2 == 0,
|
||||
"final reply retry chain must contain complete started/failed pairs"
|
||||
);
|
||||
assert!(final_reply_lifecycle_statuses
|
||||
.chunks_exact(2)
|
||||
.all(|pair| pair == ["started", "failed"]));
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ pub(crate) use steering::{
|
||||
consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_steer_ledger_path,
|
||||
interrupt_game_creator_agent_runtime_provider_request_at,
|
||||
render_game_creator_agent_runtime_steers_for_prompt, steer_game_creator_agent_runtime_task_at,
|
||||
steer_game_creator_agent_runtime_task_for_profile_at,
|
||||
validate_game_creator_agent_runtime_steer_notification_at,
|
||||
};
|
||||
pub(crate) use verification::{
|
||||
|
||||
@@ -34,6 +34,20 @@ pub(in crate::agent) fn game_creator_agent_runtime_transient_provider_error_kind
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 400, ..
|
||||
} if retry_autonomous_upstream_400 => Some("upstream-400"),
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 408, ..
|
||||
} => Some("upstream-408"),
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 429, ..
|
||||
} => Some("upstream-429"),
|
||||
platform_llm::LlmError::Upstream { status_code, .. }
|
||||
if (500..=599).contains(status_code) =>
|
||||
{
|
||||
Some("upstream-5xx")
|
||||
}
|
||||
platform_llm::LlmError::EmptyResponse => Some("empty-response"),
|
||||
platform_llm::LlmError::Deserialize(_) => Some("deserialize"),
|
||||
platform_llm::LlmError::StreamUnavailable => Some("stream-unavailable"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1317,3 +1331,105 @@ pub(crate) fn append_game_creator_agent_runtime_provider_lifecycle_for_test(
|
||||
)?;
|
||||
Ok(request_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn provider_retry_error_classification_accepts_durable_failures_and_rejects_client_faults() {
|
||||
let retryable = vec![
|
||||
(platform_llm::LlmError::Timeout { attempts: 1 }, "timeout"),
|
||||
(
|
||||
platform_llm::LlmError::Connectivity {
|
||||
attempts: 1,
|
||||
message: "temporary connection failure".to_string(),
|
||||
},
|
||||
"connectivity",
|
||||
),
|
||||
(
|
||||
platform_llm::LlmError::Transport("temporary transport failure".to_string()),
|
||||
"transport",
|
||||
),
|
||||
(
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 408,
|
||||
message: "request timeout".to_string(),
|
||||
},
|
||||
"upstream-408",
|
||||
),
|
||||
(
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 429,
|
||||
message: "rate limited".to_string(),
|
||||
},
|
||||
"upstream-429",
|
||||
),
|
||||
(
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 500,
|
||||
message: "server failure".to_string(),
|
||||
},
|
||||
"upstream-5xx",
|
||||
),
|
||||
(
|
||||
platform_llm::LlmError::Upstream {
|
||||
status_code: 599,
|
||||
message: "server failure".to_string(),
|
||||
},
|
||||
"upstream-5xx",
|
||||
),
|
||||
(platform_llm::LlmError::EmptyResponse, "empty-response"),
|
||||
(
|
||||
platform_llm::LlmError::Deserialize("temporary invalid response".to_string()),
|
||||
"deserialize",
|
||||
),
|
||||
(
|
||||
platform_llm::LlmError::StreamUnavailable,
|
||||
"stream-unavailable",
|
||||
),
|
||||
];
|
||||
for (error, expected_kind) in retryable {
|
||||
assert_eq!(
|
||||
game_creator_agent_runtime_transient_provider_error_kind(&error, false),
|
||||
Some(expected_kind),
|
||||
"unexpected retry classification for {error:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let upstream_400 = platform_llm::LlmError::Upstream {
|
||||
status_code: 400,
|
||||
message: "autonomous gateway failure".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
game_creator_agent_runtime_transient_provider_error_kind(&upstream_400, true),
|
||||
Some("upstream-400")
|
||||
);
|
||||
assert_eq!(
|
||||
game_creator_agent_runtime_transient_provider_error_kind(&upstream_400, false),
|
||||
None
|
||||
);
|
||||
|
||||
for status_code in [400, 401, 403, 404, 422, 499, 600] {
|
||||
let error = platform_llm::LlmError::Upstream {
|
||||
status_code,
|
||||
message: "non-retryable upstream response".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
game_creator_agent_runtime_transient_provider_error_kind(&error, false),
|
||||
None,
|
||||
"upstream {status_code} must not enter durable retry"
|
||||
);
|
||||
}
|
||||
for error in [
|
||||
platform_llm::LlmError::InvalidConfig("invalid config".to_string()),
|
||||
platform_llm::LlmError::InvalidRequest("invalid request".to_string()),
|
||||
] {
|
||||
assert_eq!(
|
||||
game_creator_agent_runtime_transient_provider_error_kind(&error, false),
|
||||
None,
|
||||
"client fault must not enter durable retry: {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +473,28 @@ pub(crate) fn steer_game_creator_agent_runtime_task_at(
|
||||
steer_id: &str,
|
||||
instruction: &str,
|
||||
accepted_via: &str,
|
||||
) -> Result<AgentRuntimeSteerResult, String> {
|
||||
steer_game_creator_agent_runtime_task_for_profile_at(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
steer_id,
|
||||
instruction,
|
||||
None,
|
||||
accepted_via,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn steer_game_creator_agent_runtime_task_for_profile_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
run_id: &str,
|
||||
steer_id: &str,
|
||||
instruction: &str,
|
||||
expected_run_profile: Option<&str>,
|
||||
accepted_via: &str,
|
||||
) -> Result<AgentRuntimeSteerResult, String> {
|
||||
validate_project_root(root)?;
|
||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||
@@ -495,6 +517,25 @@ pub(crate) fn steer_game_creator_agent_runtime_task_at(
|
||||
if state.run_id != run_id || state.session_id != session_id {
|
||||
return Err("追加指令与当前 Agent 的 session/run 身份不匹配".to_string());
|
||||
}
|
||||
if let Some(expected_run_profile) = expected_run_profile {
|
||||
let expected_run_profile = normalize_agent_runtime_run_profile(Some(expected_run_profile))?;
|
||||
let (persisted_run_profile, persisted_binding_fingerprint) =
|
||||
agent_runtime_run_profile_identity_at(
|
||||
root,
|
||||
&agent_id,
|
||||
run_id,
|
||||
Some(&state.run_profile),
|
||||
Some(&state.run_profile_binding_fingerprint),
|
||||
)?;
|
||||
if persisted_run_profile != expected_run_profile
|
||||
|| state.run_profile != persisted_run_profile
|
||||
|| state.run_profile_binding_fingerprint != persisted_binding_fingerprint
|
||||
{
|
||||
return Err(format!(
|
||||
"追加指令请求的 Run Profile 与目标 run 不匹配:expected={expected_run_profile} actual={persisted_run_profile}"
|
||||
));
|
||||
}
|
||||
}
|
||||
validate_agent_runtime_steer_target_state(&state)?;
|
||||
if game_creator_agent_runtime_finalization_path(root, &agent_id, run_id).exists() {
|
||||
return Err("当前 Agent run 已进入最终持久化,不再接受追加指令".to_string());
|
||||
|
||||
@@ -637,18 +637,20 @@ pub(crate) fn steer_game_creator_agent_runtime_task(
|
||||
run_id: String,
|
||||
steer_id: String,
|
||||
instruction: String,
|
||||
run_profile: Option<String>,
|
||||
) -> Result<AgentRuntimeSteerResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
enforce_project_permission_policy(root, "conversation.write")?;
|
||||
enforce_project_permission_policy(root, "agent.run_status")?;
|
||||
let mut result = steer_game_creator_agent_runtime_task_at(
|
||||
let mut result = steer_game_creator_agent_runtime_task_for_profile_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.trim(),
|
||||
run_id.trim(),
|
||||
steer_id.trim(),
|
||||
instruction.trim(),
|
||||
run_profile.as_deref(),
|
||||
"tauri",
|
||||
)?;
|
||||
if !result.provider_interrupted
|
||||
|
||||
@@ -98,17 +98,31 @@ pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||||
}
|
||||
|
||||
if runtimes_are_busy(&existing_runtimes) {
|
||||
let active_conversation =
|
||||
read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
|
||||
let active_session_id = active_conversation
|
||||
.session_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
|
||||
let matching_parent_is_busy = swarm_parent_runtime(
|
||||
parent_agent_id,
|
||||
active_session_id,
|
||||
run_profile,
|
||||
&existing_runtimes,
|
||||
)
|
||||
.is_some_and(runtime_is_busy);
|
||||
if matching_parent_is_busy {
|
||||
writeln!(output, "[恢复] 检测到未收束 Runtime,继续观察现有任务。")
|
||||
.map_err(|error| format!("写入终端失败:{error}"))?;
|
||||
let before = read_local_conversation_for_session_at(root, Some(parent_agent_id), None)?;
|
||||
let before = active_conversation;
|
||||
let session_id = before
|
||||
.session_id
|
||||
.as_deref()
|
||||
.ok_or_else(|| "父 Agent 当前 Session 缺失".to_string())?;
|
||||
let parent_run_id = swarm_parent_runtime(parent_agent_id, session_id, &existing_runtimes)
|
||||
.map(|runtime| runtime.state.run_id.as_str())
|
||||
.unwrap_or_default();
|
||||
let parent_run_id =
|
||||
swarm_parent_runtime(parent_agent_id, session_id, run_profile, &existing_runtimes)
|
||||
.map(|runtime| runtime.state.run_id.as_str())
|
||||
.unwrap_or_default();
|
||||
let mut conversation_baseline =
|
||||
new_swarm_turn_conversation_baseline(before.messages.len(), parent_run_id);
|
||||
capture_recovered_swarm_assistant_at(
|
||||
@@ -122,6 +136,7 @@ pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||||
root,
|
||||
parent_agent_id,
|
||||
session_id,
|
||||
run_profile,
|
||||
conversation_baseline,
|
||||
input,
|
||||
output,
|
||||
@@ -172,6 +187,7 @@ pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||||
root,
|
||||
parent_agent_id,
|
||||
&observation.session_id,
|
||||
run_profile,
|
||||
conversation_baseline,
|
||||
input,
|
||||
output,
|
||||
@@ -214,6 +230,7 @@ pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||||
goal.run_id.clone(),
|
||||
steer_id.clone(),
|
||||
message,
|
||||
Some(run_profile.to_string()),
|
||||
)?;
|
||||
writeln!(
|
||||
output,
|
||||
@@ -229,6 +246,7 @@ pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||||
root,
|
||||
parent_agent_id,
|
||||
&session_id,
|
||||
run_profile,
|
||||
conversation_baseline,
|
||||
input,
|
||||
output,
|
||||
@@ -307,6 +325,7 @@ pub(super) fn run_game_creator_swarm_chat_with_input<W: Write>(
|
||||
root,
|
||||
parent_agent_id,
|
||||
&session_id,
|
||||
run_profile,
|
||||
conversation_baseline,
|
||||
input,
|
||||
output,
|
||||
|
||||
@@ -24,10 +24,13 @@ pub(super) struct SwarmTerminalFailureScan {
|
||||
pub(super) fn swarm_parent_runtime<'a>(
|
||||
parent_agent_id: &str,
|
||||
session_id: &str,
|
||||
run_profile: &str,
|
||||
runtimes: &'a [AgentRuntimeResult],
|
||||
) -> Option<&'a AgentRuntimeResult> {
|
||||
runtimes.iter().find(|runtime| {
|
||||
runtime.state.agent_id == parent_agent_id && runtime.state.session_id == session_id
|
||||
runtime.state.agent_id == parent_agent_id
|
||||
&& runtime.state.session_id == session_id
|
||||
&& runtime.state.run_profile == run_profile
|
||||
})
|
||||
}
|
||||
|
||||
@@ -131,10 +134,12 @@ pub(super) fn scan_swarm_terminal_failures_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
session_id: &str,
|
||||
run_profile: &str,
|
||||
runtimes: &[AgentRuntimeResult],
|
||||
) -> SwarmTerminalFailureScan {
|
||||
let mut scan = SwarmTerminalFailureScan::default();
|
||||
let Some(parent) = swarm_parent_runtime(parent_agent_id, session_id, runtimes) else {
|
||||
let Some(parent) = swarm_parent_runtime(parent_agent_id, session_id, run_profile, runtimes)
|
||||
else {
|
||||
return scan;
|
||||
};
|
||||
let claimed_deliveries = match claimed_static_delegate_deliveries_at(
|
||||
|
||||
@@ -146,6 +146,107 @@ fn same_run_steer_preserves_bound_autonomous_profile() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cross_profile_steer_is_rejected_before_persistent_side_effects() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"swarm-cli-profile-mismatch-{}-{}",
|
||||
std::process::id(),
|
||||
unix_millis()
|
||||
));
|
||||
init_local_game_project_at(&root, "project-swarm-mismatch", "Swarm Profile Mismatch")
|
||||
.expect("initialize profile mismatch project");
|
||||
let run_id = "swarm-profile-standard-run";
|
||||
let state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"等待开发者确认的标准任务",
|
||||
run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
"准备标准模式运行",
|
||||
vec!["等待确认".to_string()],
|
||||
)
|
||||
.expect("start standard supervisor runtime");
|
||||
assert_eq!(
|
||||
state.run_profile, AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
"fixture must stay standard"
|
||||
);
|
||||
let conversation_before = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
Some(&state.session_id),
|
||||
)
|
||||
.expect("read conversation before rejected steer");
|
||||
|
||||
let error = steer_game_creator_agent_runtime_task_for_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&state.session_id,
|
||||
run_id,
|
||||
"swarm-profile-mismatch-steer",
|
||||
"切换为自主构建并继续",
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
"swarm-cli",
|
||||
)
|
||||
.expect_err("cross-profile steer must fail closed");
|
||||
assert!(error.contains("Run Profile"), "unexpected error: {error}");
|
||||
assert!(!game_creator_agent_runtime_steer_ledger_path(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
)
|
||||
.exists());
|
||||
let conversation_after = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
Some(&state.session_id),
|
||||
)
|
||||
.expect("read conversation after rejected steer");
|
||||
assert_eq!(conversation_after.messages, conversation_before.messages);
|
||||
let persisted =
|
||||
read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
.expect("read runtime after rejected steer");
|
||||
assert_eq!(persisted.state.run_id, run_id);
|
||||
assert_eq!(
|
||||
persisted.state.run_profile,
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD
|
||||
);
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_runtime_matching_is_scoped_to_requested_profile() {
|
||||
let mut standard = runtime("running", "planning", 0);
|
||||
standard.state.agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string();
|
||||
standard.state.session_id = "session-profile-match".to_string();
|
||||
standard.state.run_id = "run-standard".to_string();
|
||||
standard.state.run_profile = AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string();
|
||||
let mut autonomous = standard.clone();
|
||||
autonomous.state.run_id = "run-autonomous".to_string();
|
||||
autonomous.state.run_profile = AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string();
|
||||
let runtimes = vec![standard, autonomous];
|
||||
|
||||
assert_eq!(
|
||||
swarm_parent_runtime(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"session-profile-match",
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
&runtimes,
|
||||
)
|
||||
.map(|runtime| runtime.state.run_id.as_str()),
|
||||
Some("run-standard")
|
||||
);
|
||||
assert_eq!(
|
||||
swarm_parent_runtime(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"session-profile-match",
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
&runtimes,
|
||||
)
|
||||
.map(|runtime| runtime.state.run_id.as_str()),
|
||||
Some("run-autonomous")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_chat_commands_without_stealing_normal_messages() {
|
||||
assert_eq!(parse_swarm_chat_input(" "), None);
|
||||
@@ -427,6 +528,7 @@ fn active_turn_eof_keeps_observing_until_parent_completes() {
|
||||
&root,
|
||||
parent_agent_id,
|
||||
&session_id,
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
conversation_baseline,
|
||||
&rx,
|
||||
&mut output,
|
||||
@@ -801,6 +903,7 @@ fn observer_failure_scan_waits_for_original_repair_and_fails_repair_child() {
|
||||
&root,
|
||||
&parent.state.agent_id,
|
||||
&parent.state.session_id,
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
&[parent.clone(), child.clone()],
|
||||
);
|
||||
assert!(original_scan.failed_agents.is_empty());
|
||||
@@ -827,6 +930,7 @@ fn observer_failure_scan_waits_for_original_repair_and_fails_repair_child() {
|
||||
&root,
|
||||
&parent.state.agent_id,
|
||||
&parent.state.session_id,
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
&[parent.clone(), child],
|
||||
);
|
||||
assert_eq!(repair_scan.failed_agents, vec!["code-prototype:failed"]);
|
||||
|
||||
@@ -13,6 +13,7 @@ pub(super) fn wait_for_swarm_turn<W: Write>(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
session_id: &str,
|
||||
run_profile: &str,
|
||||
conversation_baseline: SwarmTurnConversationBaseline,
|
||||
input: &Receiver<SwarmInputEvent>,
|
||||
output: &mut W,
|
||||
@@ -91,8 +92,13 @@ pub(super) fn wait_for_swarm_turn<W: Write>(
|
||||
pending_interactions,
|
||||
);
|
||||
}
|
||||
let failure_scan =
|
||||
scan_swarm_terminal_failures_at(root, parent_agent_id, session_id, &runtimes);
|
||||
let failure_scan = scan_swarm_terminal_failures_at(
|
||||
root,
|
||||
parent_agent_id,
|
||||
session_id,
|
||||
run_profile,
|
||||
&runtimes,
|
||||
);
|
||||
if !failure_scan.reconciliation_agents.is_empty() {
|
||||
observer.close_response_line(output)?;
|
||||
return build_reconciliation_turn_outcome(
|
||||
@@ -169,7 +175,8 @@ pub(super) fn wait_for_swarm_turn<W: Write>(
|
||||
session_id,
|
||||
&conversation_baseline,
|
||||
)?;
|
||||
let parent_runtime = swarm_parent_runtime(parent_agent_id, session_id, &runtimes);
|
||||
let parent_runtime =
|
||||
swarm_parent_runtime(parent_agent_id, session_id, run_profile, &runtimes);
|
||||
let completion_blockers = parent_runtime
|
||||
.map(|parent| swarm_parent_completion_contract_blockers_at(root, parent))
|
||||
.unwrap_or_else(|| vec!["parent-runtime-missing".to_string()]);
|
||||
@@ -275,6 +282,8 @@ pub(super) fn wait_for_swarm_turn<W: Write>(
|
||||
SwarmChatInput::Message(message) => {
|
||||
if let Some(parent) = runtimes.iter().find(|runtime| {
|
||||
runtime.state.agent_id == parent_agent_id
|
||||
&& runtime.state.session_id == session_id
|
||||
&& runtime.state.run_profile == run_profile
|
||||
&& matches!(runtime.state.status.as_str(), "pending" | "running")
|
||||
}) {
|
||||
let steer_id = format!("swarm-steer-{}", unix_millis());
|
||||
@@ -285,6 +294,7 @@ pub(super) fn wait_for_swarm_turn<W: Write>(
|
||||
parent.state.run_id.clone(),
|
||||
steer_id.clone(),
|
||||
message,
|
||||
Some(run_profile.to_string()),
|
||||
)?;
|
||||
writeln!(
|
||||
output,
|
||||
|
||||
+495
-2
File diff suppressed because it is too large
Load Diff
@@ -1495,6 +1495,60 @@ fn spawn_mock_llm_upstream_400_then_raw_response(
|
||||
base_url
|
||||
}
|
||||
|
||||
fn spawn_mock_llm_http_failure_then_response(
|
||||
failed_status_line: &'static str,
|
||||
failed_body: String,
|
||||
recovered_content: String,
|
||||
request_notice_sender: Option<mpsc::Sender<()>>,
|
||||
) -> String {
|
||||
let listener = bind_test_tcp_listener("mock durable Provider classification bind");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr"));
|
||||
std::thread::spawn(move || {
|
||||
let (mut failed_stream, _) = listener
|
||||
.accept()
|
||||
.expect("mock durable Provider failure accept");
|
||||
drop(read_mock_http_request(&mut failed_stream));
|
||||
if let Some(sender) = request_notice_sender.as_ref() {
|
||||
let _ = sender.send(());
|
||||
}
|
||||
let failed_response = format!(
|
||||
"HTTP/1.1 {failed_status_line}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
failed_body.len(),
|
||||
failed_body
|
||||
);
|
||||
failed_stream
|
||||
.write_all(failed_response.as_bytes())
|
||||
.expect("mock durable Provider failure response");
|
||||
|
||||
let (mut recovered_stream, _) = listener
|
||||
.accept()
|
||||
.expect("mock durable Provider recovery accept");
|
||||
drop(read_mock_http_request(&mut recovered_stream));
|
||||
if let Some(sender) = request_notice_sender.as_ref() {
|
||||
let _ = sender.send(());
|
||||
}
|
||||
let recovered_body = serde_json::json!({
|
||||
"id": "chatcmpl_durable_provider_recovered",
|
||||
"model": "mock-game-model",
|
||||
"choices": [{
|
||||
"message": { "content": recovered_content },
|
||||
"finish_reason": "stop"
|
||||
}],
|
||||
"usage": { "prompt_tokens": 11, "completion_tokens": 22, "total_tokens": 33 }
|
||||
})
|
||||
.to_string();
|
||||
let recovered_response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
recovered_body.len(),
|
||||
recovered_body
|
||||
);
|
||||
recovered_stream
|
||||
.write_all(recovered_response.as_bytes())
|
||||
.expect("mock durable Provider recovery response");
|
||||
});
|
||||
base_url
|
||||
}
|
||||
|
||||
fn spawn_mock_llm_tool_plan_then_transient_final_reply(
|
||||
planning_response: String,
|
||||
final_response: String,
|
||||
|
||||
@@ -3425,14 +3425,14 @@ async fn background_agent_runtime_resumes_approved_auto_action_once_without_llm_
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_does_not_replay_empty_provider_response() {
|
||||
async fn provider_retry_empty_response_closes_then_stable_retry_succeeds() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![
|
||||
String::new(),
|
||||
final_tool_plan_response("不应发起第二次请求"),
|
||||
final_tool_plan_response("空响应持久重试后已恢复"),
|
||||
],
|
||||
Some(sender),
|
||||
);
|
||||
@@ -3443,57 +3443,217 @@ async fn background_agent_runtime_does_not_replay_empty_provider_response() {
|
||||
"apiKey": "design-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "design-runtime-model",
|
||||
"apiKind": "openai_chat"
|
||||
"apiKind": "openai_chat",
|
||||
"stream": false,
|
||||
"maxRetries": 1,
|
||||
"retryBackoffMs": 30000
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
let run_id = "design-empty-response-retry-run";
|
||||
let runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"验证后台 Agent 空响应不会自动重放",
|
||||
"design-empty-response-no-replay-run",
|
||||
"验证后台 Agent 空响应进入持久重试",
|
||||
run_id,
|
||||
"agent-background-task",
|
||||
"等待 Provider 规划",
|
||||
vec!["请求并验证工具计划".to_string()],
|
||||
)
|
||||
.expect("start background task");
|
||||
.expect("start empty-response retry runtime");
|
||||
|
||||
let request = receiver
|
||||
let waiting = request_game_creator_agent_background_tool_plan_waiting_retry_for_test(
|
||||
&root,
|
||||
"design-director",
|
||||
&runtime.session_id,
|
||||
run_id,
|
||||
&runtime.current_task,
|
||||
&[],
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.expect("empty response enters durable retry wait");
|
||||
let first_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("capture only empty-response Provider request");
|
||||
assert!(request.contains("POST /chat/completions HTTP/1.1"));
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(250)).is_err());
|
||||
let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read empty-response runtime")
|
||||
.state;
|
||||
for _ in 0..250 {
|
||||
if runtime.phase == "failed" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("poll empty-response runtime")
|
||||
.state;
|
||||
}
|
||||
assert_eq!(runtime.phase, "failed");
|
||||
assert!(runtime
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("kind=empty-response")));
|
||||
.expect("capture empty-response Provider request");
|
||||
assert!(first_request.contains("POST /chat/completions HTTP/1.1"));
|
||||
assert_eq!(waiting.error_kind, "empty-response");
|
||||
assert_eq!(waiting.next_attempt, 1);
|
||||
assert_eq!(waiting.max_retries, 1);
|
||||
assert!(provider_retry::remaining_ms(&waiting) > 0);
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
provider_retry::force_provider_retry_due_for_test_at(&root, &waiting.identity)
|
||||
.expect("force empty-response retry due");
|
||||
|
||||
let plan = request_game_creator_agent_background_tool_plan_for_test(
|
||||
&root,
|
||||
"design-director",
|
||||
&runtime.session_id,
|
||||
run_id,
|
||||
&runtime.current_task,
|
||||
&[],
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.expect("empty-response retry succeeds")
|
||||
.expect("recovered tool plan");
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("capture recovered Provider request");
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
assert_eq!(plan.response, "空响应持久重试后已恢复");
|
||||
assert!(
|
||||
provider_retry::read_for_run_at(&root, "design-director", run_id)
|
||||
.expect("read cleared empty-response retry")
|
||||
.is_none()
|
||||
);
|
||||
let lifecycle = read_agent_db_records_for_test(&root)
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.lifecycle"
|
||||
&& record["runId"] == "design-empty-response-no-replay-run"
|
||||
&& record["runId"] == run_id
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(lifecycle.len(), 2);
|
||||
assert_eq!(lifecycle.len(), 4);
|
||||
assert_eq!(lifecycle[0]["status"], "started");
|
||||
assert_eq!(lifecycle[1]["status"], "failed");
|
||||
assert_eq!(lifecycle[0]["requestId"], lifecycle[1]["requestId"]);
|
||||
assert_eq!(lifecycle[2]["status"], "started");
|
||||
assert_eq!(lifecycle[3]["status"], "completed");
|
||||
assert_eq!(lifecycle[2]["requestId"], lifecycle[3]["requestId"]);
|
||||
assert_ne!(lifecycle[0]["requestId"], lifecycle[2]["requestId"]);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_retry_http_and_deserialize_failures_recover_through_durable_sidecar() {
|
||||
let cases = vec![
|
||||
(
|
||||
"rate-limit",
|
||||
"429 Too Many Requests",
|
||||
serde_json::json!({"error": {"message": "rate limited"}}).to_string(),
|
||||
"upstream-429",
|
||||
),
|
||||
(
|
||||
"server-error",
|
||||
"503 Service Unavailable",
|
||||
serde_json::json!({"error": {"message": "temporarily unavailable"}}).to_string(),
|
||||
"upstream-5xx",
|
||||
),
|
||||
("deserialize", "200 OK", "{".to_string(), "deserialize"),
|
||||
];
|
||||
|
||||
for (case_id, status_line, failed_body, expected_kind) in cases {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"project-provider-classification",
|
||||
"Provider 持久分类恢复测试",
|
||||
)
|
||||
.expect("provider classification project init");
|
||||
let recovered_text = format!("{case_id} 持久重试后已恢复");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let base_url = spawn_mock_llm_http_failure_then_response(
|
||||
status_line,
|
||||
failed_body,
|
||||
final_tool_plan_response(&recovered_text),
|
||||
Some(sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "provider-classification-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "provider-classification-model",
|
||||
"apiKind": "openai_chat",
|
||||
"stream": false,
|
||||
"maxRetries": 1,
|
||||
"retryBackoffMs": 30000
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let run_id = format!("provider-classification-{case_id}-run");
|
||||
let runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"验证 Provider 错误进入同一 durable retry",
|
||||
&run_id,
|
||||
"agent-background-task",
|
||||
"等待 Provider 规划",
|
||||
vec!["请求并验证工具计划".to_string()],
|
||||
)
|
||||
.expect("start provider classification runtime");
|
||||
|
||||
let waiting = request_game_creator_agent_background_tool_plan_waiting_retry_for_test(
|
||||
&root,
|
||||
"design-director",
|
||||
&runtime.session_id,
|
||||
&run_id,
|
||||
&runtime.current_task,
|
||||
&[],
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.expect("Provider failure enters durable retry wait");
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("capture failed Provider request");
|
||||
assert_eq!(waiting.error_kind, expected_kind, "case={case_id}");
|
||||
assert_eq!(waiting.next_attempt, 1, "case={case_id}");
|
||||
assert_eq!(waiting.max_retries, 1, "case={case_id}");
|
||||
assert!(provider_retry::remaining_ms(&waiting) > 0, "case={case_id}");
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
provider_retry::force_provider_retry_due_for_test_at(&root, &waiting.identity)
|
||||
.expect("force classified Provider retry due");
|
||||
|
||||
let plan = request_game_creator_agent_background_tool_plan_for_test(
|
||||
&root,
|
||||
"design-director",
|
||||
&runtime.session_id,
|
||||
&run_id,
|
||||
&runtime.current_task,
|
||||
&[],
|
||||
1,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.expect("classified Provider retry succeeds")
|
||||
.expect("classified Provider retry returns plan");
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("capture recovered Provider request");
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(100)).is_err());
|
||||
assert_eq!(plan.response, recovered_text, "case={case_id}");
|
||||
assert!(
|
||||
provider_retry::read_for_run_at(&root, "design-director", &run_id)
|
||||
.expect("read cleared classified Provider retry")
|
||||
.is_none()
|
||||
);
|
||||
let lifecycle = read_agent_db_records_for_test(&root)
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.lifecycle"
|
||||
&& record["runId"] == run_id
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(lifecycle.len(), 4, "case={case_id}");
|
||||
assert_eq!(lifecycle[0]["status"], "started", "case={case_id}");
|
||||
assert_eq!(lifecycle[1]["status"], "failed", "case={case_id}");
|
||||
assert_eq!(lifecycle[2]["status"], "started", "case={case_id}");
|
||||
assert_eq!(lifecycle[3]["status"], "completed", "case={case_id}");
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_transient_retry_transport_failure_closes_then_stable_retry_succeeds() {
|
||||
let root = unique_project_path();
|
||||
@@ -4564,8 +4724,8 @@ async fn provider_transient_retry_autonomous_upstream_400_retries_with_bounded_b
|
||||
"reason": "委派独立质量评审",
|
||||
"input": {
|
||||
"agentId": "quality-review",
|
||||
"task": "评审可玩性与闯关闭环",
|
||||
"acceptanceCriteria": ["给出阻塞试玩的问题和验收结论"],
|
||||
"task": "只读评审可玩性与闯关闭环,不要修改任何项目文件",
|
||||
"acceptanceCriteria": ["只读给出阻塞试玩的问题和验收结论"],
|
||||
"expectedArtifacts": [],
|
||||
"repairOfDelegationId": null,
|
||||
"runId": null
|
||||
|
||||
@@ -608,7 +608,7 @@ async fn response_stream_private_process_output_is_never_published_or_committed_
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn response_stream_final_failure_is_single_attempt_and_commits_planning_fallback() {
|
||||
async fn response_stream_final_failure_with_retry_disabled_commits_planning_fallback() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
@@ -632,7 +632,7 @@ async fn response_stream_final_failure_is_single_attempt_and_commits_planning_fa
|
||||
"model": "response-stream-retry-model",
|
||||
"apiKind": "openai_responses",
|
||||
"stream": true,
|
||||
"maxRetries": 7,
|
||||
"maxRetries": 0,
|
||||
"retryBackoffMs": 1
|
||||
}}
|
||||
}}
|
||||
@@ -642,7 +642,7 @@ async fn response_stream_final_failure_is_single_attempt_and_commits_planning_fa
|
||||
let started = start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"验证 final reply 失败不在 lifecycle 内重试",
|
||||
"验证关闭 retry 时 final reply 失败只发起一次请求",
|
||||
run_id,
|
||||
)
|
||||
.expect("start final stream single-attempt task");
|
||||
@@ -704,7 +704,11 @@ async fn response_stream_final_failure_is_single_attempt_and_commits_planning_fa
|
||||
);
|
||||
|
||||
let requests = mock.stop_and_collect();
|
||||
assert_eq!(requests.len(), 2, "final stream must not retry physically");
|
||||
assert_eq!(
|
||||
requests.len(),
|
||||
2,
|
||||
"disabled retry must not issue a second final stream"
|
||||
);
|
||||
assert_eq!(
|
||||
mock_http_request_json(&requests[0])["stream"],
|
||||
Value::Bool(false)
|
||||
@@ -757,6 +761,187 @@ async fn response_stream_final_failure_is_single_attempt_and_commits_planning_fa
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_retry_final_reply_thinking_only_response_retries_before_handoff() {
|
||||
const PRIVATE_THINKING: &str = "FINAL_REPLY_PRIVATE_THINKING_MUST_NOT_PERSIST";
|
||||
const FINAL_RESPONSE: &str = "去除 thinking 后为空的最终回复已通过持久重试恢复。";
|
||||
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"project-final-reply-empty-normalization",
|
||||
"最终回复规范化为空持久重试项目",
|
||||
)
|
||||
.expect("thinking-only final reply project init");
|
||||
let (request_sender, request_receiver) = mpsc::channel();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![
|
||||
final_tool_plan_response(""),
|
||||
format!("<think>{PRIVATE_THINKING}</think>"),
|
||||
FINAL_RESPONSE.to_string(),
|
||||
],
|
||||
Some(request_sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "final-reply-empty-normalization-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "final-reply-empty-normalization-model",
|
||||
"apiKind": "openai_responses",
|
||||
"stream": false,
|
||||
"maxRetries": 1,
|
||||
"retryBackoffMs": 30000
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let run_id = "provider-retry-final-reply-empty-normalization-run";
|
||||
let started = start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"验证 final reply 在 durable handoff 前完成去 thinking 和非空校验",
|
||||
run_id,
|
||||
)
|
||||
.expect("start thinking-only final reply task");
|
||||
|
||||
request_receiver
|
||||
.recv_timeout(Duration::from_secs(5))
|
||||
.expect("tool-plan Provider request");
|
||||
request_receiver
|
||||
.recv_timeout(Duration::from_secs(5))
|
||||
.expect("thinking-only final-reply Provider request");
|
||||
let mut retry = None;
|
||||
for _ in 0..250 {
|
||||
retry = crate::provider_retry::read_for_run_at(&root, "design-director", run_id)
|
||||
.expect("read thinking-only final reply retry");
|
||||
if retry.is_some()
|
||||
&& game_creator_agent_runtime_task_lock_is_available(&root, "design-director")
|
||||
.expect("probe thinking-only final reply lane")
|
||||
{
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
let retry = retry.expect("thinking-only final reply must persist retry sidecar");
|
||||
assert_eq!(retry.identity.request_kind, "final-reply");
|
||||
assert_eq!(retry.error_kind, "empty-response");
|
||||
assert_eq!(retry.next_attempt, 1);
|
||||
assert!(retry.next_request_slot.ends_with("-transient-1"));
|
||||
assert!(crate::provider_retry::remaining_ms(&retry) > 0);
|
||||
assert!(
|
||||
provider_handoff::read_for_run_at(&root, "design-director", run_id)
|
||||
.expect("read absent thinking-only final reply handoff")
|
||||
.is_none(),
|
||||
"normalized empty final reply must fail before durable handoff"
|
||||
);
|
||||
|
||||
let before_retry_records = read_agent_db_records_for_test(&root);
|
||||
let before_retry_lifecycle = before_retry_records
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.lifecycle"
|
||||
&& record["runId"] == run_id
|
||||
&& record["requestKind"] == "final-reply"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(before_retry_lifecycle.len(), 2);
|
||||
assert_eq!(before_retry_lifecycle[0]["status"], "started");
|
||||
assert_eq!(before_retry_lifecycle[1]["status"], "failed");
|
||||
assert!(before_retry_lifecycle
|
||||
.iter()
|
||||
.all(|record| record["status"] != "completed"));
|
||||
let before_retry_conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some("design-director"),
|
||||
Some(&started.state.session_id),
|
||||
)
|
||||
.expect("read conversation before final reply retry");
|
||||
assert!(before_retry_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.all(|message| message.role != "assistant"));
|
||||
|
||||
crate::provider_retry::force_provider_retry_due_for_test_at(&root, &retry.identity)
|
||||
.expect("force thinking-only final reply retry due");
|
||||
resume_game_creator_agent_background_tasks_at(&root)
|
||||
.expect("resume thinking-only final reply retry");
|
||||
request_receiver
|
||||
.recv_timeout(Duration::from_secs(5))
|
||||
.expect("recovered final-reply Provider request");
|
||||
assert!(request_receiver
|
||||
.recv_timeout(Duration::from_millis(100))
|
||||
.is_err());
|
||||
|
||||
let completed = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(completed.phase, "completed");
|
||||
assert_eq!(completed.last_response.as_deref(), Some(FINAL_RESPONSE));
|
||||
assert!(
|
||||
crate::provider_retry::read_for_run_at(&root, "design-director", run_id)
|
||||
.expect("read cleared thinking-only final reply retry")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
provider_handoff::read_for_run_at(&root, "design-director", run_id)
|
||||
.expect("read cleared thinking-only final reply handoff")
|
||||
.is_none()
|
||||
);
|
||||
let committed =
|
||||
wait_for_response_stream_status(&root, "design-director", run_id, "committed", 1);
|
||||
assert_eq!(committed.accumulated_text, FINAL_RESPONSE);
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some("design-director"),
|
||||
Some(&started.state.session_id),
|
||||
)
|
||||
.expect("read recovered final reply conversation");
|
||||
assert_eq!(
|
||||
conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.role == "assistant")
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![FINAL_RESPONSE]
|
||||
);
|
||||
|
||||
let records = read_agent_db_records_for_test(&root);
|
||||
let final_reply_lifecycle = records
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.lifecycle"
|
||||
&& record["runId"] == run_id
|
||||
&& record["requestKind"] == "final-reply"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(final_reply_lifecycle.len(), 4);
|
||||
assert_eq!(final_reply_lifecycle[0]["status"], "started");
|
||||
assert_eq!(final_reply_lifecycle[1]["status"], "failed");
|
||||
assert_eq!(final_reply_lifecycle[2]["status"], "started");
|
||||
assert_eq!(final_reply_lifecycle[3]["status"], "completed");
|
||||
let retry_audits = records
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.retry"
|
||||
&& record["runId"] == run_id
|
||||
&& record["requestKind"] == "final-reply"
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(retry_audits.len(), 1);
|
||||
assert_eq!(retry_audits[0]["errorKind"], "empty-response");
|
||||
let persisted = format!(
|
||||
"{}\n{}\n{}",
|
||||
serde_json::to_string(&completed).expect("serialize completed Runtime"),
|
||||
fs::read_to_string(root.join(".agent/runtime/events/design-director.jsonl"))
|
||||
.expect("read final reply events"),
|
||||
fs::read_to_string(root.join(".agent/agent.db")).expect("read final reply Agent DB"),
|
||||
);
|
||||
assert!(!persisted.contains(PRIVATE_THINKING));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visual_specialist_finalization_requires_existing_registered_canvas_image() {
|
||||
for (agent_id, local_path, kind) in [
|
||||
|
||||
+710
-99
File diff suppressed because it is too large
Load Diff
@@ -14,13 +14,12 @@ pub(super) use super::super::{
|
||||
append_auto_tool_action_audit_pair_for_test, assert_auto_tool_action_audit_pair,
|
||||
assert_pending_runtime_decision_revalidates_after_lock, assert_task_status,
|
||||
fake_llm_game_draft, final_tool_plan_response, mock_http_request_json,
|
||||
native_agent_tool_plan_chat_response, native_agent_tool_plan_chat_response_with_calls,
|
||||
pending_tool_action_for_test, persist_needs_reconciliation_runtime_for_test,
|
||||
persist_project_verification_for_test, read_agent_db_records_for_test,
|
||||
register_canvas_visual_asset_fixture, spawn_barrier_mock_llm_server,
|
||||
spawn_interruptible_mock_llm_server_with_capture, spawn_mock_llm_raw_responses_with_capture,
|
||||
spawn_mock_llm_server, spawn_mock_llm_server_responses,
|
||||
spawn_mock_llm_server_responses_with_capture,
|
||||
native_agent_tool_plan_chat_response, pending_tool_action_for_test,
|
||||
persist_needs_reconciliation_runtime_for_test, persist_project_verification_for_test,
|
||||
read_agent_db_records_for_test, register_canvas_visual_asset_fixture,
|
||||
spawn_barrier_mock_llm_server, spawn_interruptible_mock_llm_server_with_capture,
|
||||
spawn_mock_llm_raw_responses_with_capture, spawn_mock_llm_server,
|
||||
spawn_mock_llm_server_responses, spawn_mock_llm_server_responses_with_capture,
|
||||
spawn_releasable_mock_llm_server_responses_with_capture,
|
||||
spawn_releasable_mock_llm_server_responses_with_capture_at, start_agent_runtime_steer_fixture,
|
||||
ui_prototype_assessment_fixture, unique_project_path, use_test_runtime_config_dir,
|
||||
|
||||
@@ -667,6 +667,9 @@ export function matchingAgentRuntimeForSteer(
|
||||
runtimes: Array<AgentRuntimeState | null | undefined>,
|
||||
agentId: string,
|
||||
sessionId: string | null,
|
||||
requestedRunProfile: NonNullable<
|
||||
AgentRuntimeState['runProfile']
|
||||
> = 'standard',
|
||||
) {
|
||||
if (!sessionId) {
|
||||
return null;
|
||||
@@ -676,6 +679,7 @@ export function matchingAgentRuntimeForSteer(
|
||||
(runtime) =>
|
||||
runtime?.agentId === agentId &&
|
||||
runtime.sessionId === sessionId &&
|
||||
(runtime.runProfile ?? 'standard') === requestedRunProfile &&
|
||||
isAgentRuntimeSteerableState(runtime),
|
||||
) ?? null
|
||||
);
|
||||
@@ -772,6 +776,7 @@ export async function submitProjectSupervisorRuntimeTask({
|
||||
[runtime],
|
||||
PROJECT_SUPERVISOR_AGENT_ID,
|
||||
sessionId,
|
||||
runProfile,
|
||||
);
|
||||
if (steerRuntime) {
|
||||
const steer = await invoke<AgentRuntimeSteerResult>(
|
||||
@@ -783,6 +788,7 @@ export async function submitProjectSupervisorRuntimeTask({
|
||||
runId: steerRuntime.runId,
|
||||
steerId: createAgentChatRunId('project-supervisor-steer'),
|
||||
instruction: prompt,
|
||||
runProfile,
|
||||
},
|
||||
);
|
||||
return { mode: 'steer' as const, runtimeResult: steer.runtime };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user