From c607451edd2270d7e20626b455ff46b2d6eb281f Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 17 Jul 2026 16:37:26 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E6=80=BB=E6=8E=A7=E9=9D=99?= =?UTF-8?q?=E6=80=81=E4=B8=8E=E9=9A=94=E7=A6=BB=E6=B7=B7=E5=90=88=E5=8D=8F?= =?UTF-8?q?=E4=BD=9C=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增同一 Supervisor Session/run 下 static delegate 与 isolated all-join 的真实 Provider 门禁。 补齐首批确认、并行重叠、强杀恢复、持久化身份和零泄漏审计。 增加双向完成屏障回归、命令入口及 Runtime/开发文档记录。 修正原生计划元调用与业务 action 数量的验收边界。 --- apps/ai-game-creator-shell/package.json | 1 + .../scripts/agent-runtime-real-e2e.mjs | 2256 +++++++++++++++-- .../src-tauri/src/agent.rs | 2 +- .../src-tauri/src/tests.rs | 193 ++ .../shared-memory/decision-log.md | 8 + .../shared-memory/development-workflow.md | 11 + ...案】AI游戏创作Agent Runtime V1.1-2026-07-12.md | 17 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 1 + package.json | 1 + 9 files changed, 2298 insertions(+), 192 deletions(-) diff --git a/apps/ai-game-creator-shell/package.json b/apps/ai-game-creator-shell/package.json index 08b398a11..3f9952635 100644 --- a/apps/ai-game-creator-shell/package.json +++ b/apps/ai-game-creator-shell/package.json @@ -15,6 +15,7 @@ "agent-run": "node scripts/run-cli-with-config.mjs --agent-run", "agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs", "agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs", + "agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat", "agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat", "agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry", "agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs", diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs index 0237f9fc1..f9a4eff21 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs @@ -75,6 +75,10 @@ const supervisorSwarmAutonomousChatAppDataSentinelFileName = '.agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-appdata.json'; const supervisorSwarmAutonomousChatAppDataSentinelSchema = 'genarrative-agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-appdata.v1'; +const supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName = + '.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-appdata.json'; +const supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema = + 'genarrative-agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-appdata.v1'; const mainAgentId = 'code-prototype'; const projectSupervisorAgentId = 'project-supervisor'; const requestedRunId = `real-e2e-${Date.now()}-${randomUUID().slice(0, 8)}`; @@ -106,6 +110,8 @@ const parallelReadSuite = 'parallel-read'; const supervisorSwarmSuite = 'supervisor-swarm'; const supervisorSwarmTransientRetrySuite = 'supervisor-swarm-transient-retry'; const supervisorSwarmAutonomousChatSuite = 'supervisor-swarm-autonomous-chat'; +const supervisorSwarmStaticIsolatedAutonomousChatSuite = + 'supervisor-swarm-static-isolated-autonomous-chat'; const runtimeContextBundleSchemaVersion = 'game-creator-runtime-context-bundle.v5'; const providerRequestLifecycleSchemaVersion = @@ -114,6 +120,13 @@ const staticDelegateDeliverySchemaVersion = 'game-creator-static-delegate-delivery.v1'; const staticDelegateClaimSchemaVersion = 'game-creator-static-delegate-claim.v1'; +const isolatedAgentGroupSchemaVersion = 'game-creator-isolated-agent-group.v1'; +const isolatedAgentInstanceSchemaVersion = + 'game-creator-isolated-agent-instance.v1'; +const isolatedAgentResultSchemaVersion = + 'game-creator-isolated-agent-result.v1'; +const isolatedAgentJoinDeliverySchemaVersion = + 'game-creator-isolated-agent-join-delivery.v1'; const mcpFixtureScript = path.join( appRoot, 'src-tauri/test-fixtures/mcp-server.mjs', @@ -169,6 +182,11 @@ const parallelReadBetaQuery = `PARALLEL_READ_BETA_${randomUUID() const parallelReadCorpusFileCount = 420; const supervisorSwarmDesignAgentId = 'design-director'; const supervisorSwarmQualityAgentId = 'quality-review'; +const supervisorSwarmRequiredAgentIds = [ + projectSupervisorAgentId, + supervisorSwarmDesignAgentId, + supervisorSwarmQualityAgentId, +]; const supervisorSwarmSessionId = `agent-session-${projectSupervisorAgentId}`; const supervisorSwarmDesignPath = 'game/supervisor-swarm-design.txt'; const supervisorSwarmQualityPath = 'game/supervisor-swarm-quality.txt'; @@ -190,6 +208,31 @@ const supervisorSwarmDeniedMutationTools = [ 'project.patchset', 'project.restore', ]; +const supervisorSwarmProjectMutationTools = new Set([ + 'asset.register', + 'asset.upload', + 'canvas.asset_generate', + 'canvas.asset_import', + 'canvas.export_import', + 'canvas.project_open', + 'canvas.project_sync', + 'command.exec', + 'command.run_limited', + 'command.start', + 'command.stdin', + 'command.terminate', + 'file.delete', + 'file.patch', + 'file.write', + 'game.generate_draft', + 'game.run_local', + 'mcp.call', + 'project.create', + 'project.export_package', + 'project.git_commit', + 'project.patchset', + 'project.restore', +]); const supervisorSwarmPolicyConfirmCommands = [ 'agent.kill', 'agent.resume', @@ -236,7 +279,7 @@ const supervisorSwarmConfirmedTools = [ 'project.verify', ]; const supervisorSwarmAutonomousTask = - '请把这个试玩项目推进到可以交给首批玩家体验的状态。以仓库现有要求和实际验证结果为准,完成后简短说明交付内容、验证结论和仍需关注的问题。'; + '请把这个试玩项目推进到可以交给首批玩家体验的状态。以仓库现有的正式交付要求、临时检查要求和实际验证结果为准,完成后简短说明交付内容、验证结论和仍需关注的问题。'; const supervisorSwarmAutonomousRoutingTerms = [ supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId, @@ -255,6 +298,32 @@ const supervisorSwarmAutonomousRoutingTerms = [ 'actionId', 'delegationId', ]; +const supervisorSwarmIsolatedReviews = [ + { + path: 'e2e/isolated-a/evidence.txt', + scope: 'e2e/isolated-a/**', + content: 'isolated-a seeded evidence\n', + boundaryTerms: ['onboarding', 'first-experience'], + requirement: + 'player onboarding readiness and whether the evidence gives a clear first-experience signal', + }, + { + path: 'e2e/isolated-b/evidence.txt', + scope: 'e2e/isolated-b/**', + content: 'isolated-b seeded evidence\n', + boundaryTerms: ['release risk', 'safe player handoff'], + requirement: + 'release risk readiness and whether the evidence supports a safe player handoff', + }, + { + path: 'e2e/isolated-c/evidence.txt', + scope: 'e2e/isolated-c/**', + content: 'isolated-c seeded evidence\n', + boundaryTerms: ['observability', 'follow-up'], + requirement: + 'post-handoff observability and whether the evidence supports follow-up decisions', + }, +]; const webSearchBaselineApiUrl = 'https://api.github.com/repos/nodejs/node/releases/latest'; const goalSessionId = `agent-session-${mainAgentId}`; @@ -610,6 +679,7 @@ const state = { effectiveRequestTimeoutMs: null, effectiveMaxRetries: null, effectiveRetryBackoffMs: null, + effectiveDefaultPolicy: null, effectiveAgentPolicies: {}, privateValues: [], userTask: null, @@ -629,6 +699,7 @@ const state = { repairPendingIdentity: null, repairPendingTool: null, preKillProviderStartedCount: 0, + preKillProviderStartedIdentities: [], preKillDeliveryIdentities: [], preKillClaimIdentities: [], preKillTaskIdentities: [], @@ -646,6 +717,11 @@ const state = { autonomousRepositoryRecipeFree: false, interactiveCliUsed: false, turnReport: null, + mixedSpawnActionId: null, + mixedSpawnRequestHash: null, + staticIsolatedProviderRequestIds: [], + staticIsolatedProviderOverlapObserved: false, + preKillMixedIdentity: null, }, confirmedActionIds: new Set(), cleanupPerformed: false, @@ -795,7 +871,7 @@ try { } userInputCliSession = null; } - if (isSupervisorSwarmAutonomousChatSuite() && supervisorSwarmCliSession) { + if (isSupervisorSwarmInteractiveChatSuite() && supervisorSwarmCliSession) { try { await closeInteractiveCli(supervisorSwarmCliSession); } catch (error) { @@ -5762,6 +5838,27 @@ async function collectPartialParallelReadEvidence() { } function supervisorSwarmRepositoryInstructions() { + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + return `# Player-ready repository contract + +- SUPERVISOR_SWARM_REPOSITORY_CONTEXT: a player-ready handoff requires both deliverables below to satisfy their business meaning and repository verification. +- The experience specification is ${supervisorSwarmDesignPath}; its complete content must be exactly: + DESIGN_STATUS=ready + DESIGN_MARKER=${supervisorSwarmDesignMarker} +- The release quality record is ${supervisorSwarmQualityPath}; its required final content must be exactly: + QUALITY_STATUS=ready + QUALITY_MARKER=${supervisorSwarmQualityMarker} +- A status label is not semantic proof. Before the release quality record is changed, its current content must be independently audited against this contract; any correction remains bound to the same acceptance criteria and artifact. +- ${supervisorSwarmIsolatedReviews[0].path} requires a temporary read-only review limited to ${supervisorSwarmIsolatedReviews[0].requirement}. +- ${supervisorSwarmIsolatedReviews[1].path} requires a temporary read-only review limited to ${supervisorSwarmIsolatedReviews[1].requirement}. +- ${supervisorSwarmIsolatedReviews[2].path} requires a temporary read-only review limited to ${supervisorSwarmIsolatedReviews[2].requirement}. +- The review areas must remain non-overlapping, must leave project files unchanged, and their conclusions must be included in the player-ready judgment. +- Any work that changes a deliverable must read package.json, complete its declared verification, and return evidence before the player-ready handoff is accepted. +- The user receives one concise handoff only after all required evidence is accepted. Intermediate professional work stays internal. +- Do not use Git, MCP, or external generation for this repository contract. +- Never read or expose .env, ${configFileName}, .agent/private-secret.txt, credentials, private runtime payloads, or absolute paths. +`; + } if (isSupervisorSwarmAutonomousChatSuite()) { return `# Player-ready repository contract @@ -5800,7 +5897,7 @@ function supervisorSwarmRepositoryInstructions() { function assertSupervisorSwarmAutonomousRepositoryInstructions( repositoryInstructions, ) { - if (!isSupervisorSwarmAutonomousChatSuite()) return; + if (!isSupervisorSwarmInteractiveChatSuite()) return; for (const forbidden of supervisorSwarmAutonomousRoutingTerms) { assert( !repositoryInstructions.includes(forbidden), @@ -5815,6 +5912,34 @@ function assertSupervisorSwarmAutonomousRepositoryInstructions( repositoryInstructions.includes('independently audited'), 'supervisor-swarm-autonomous-repository-contract-missing', ); + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + for (const review of supervisorSwarmIsolatedReviews) { + assert( + repositoryInstructions.includes(review.path) && + repositoryInstructions.includes(review.requirement), + 'supervisor-swarm-mixed-repository-review-contract-missing', + ); + } + for (const forbidden of [ + mainAgentId, + 'agent.spawn_isolated', + 'joinMode', + 'templateAgentId', + 'writeScopes', + 'static Agent', + 'isolated child', + 'parallel', + 'delegate', + 'spawn', + 'all-join', + 'runId', + ]) { + assert( + !repositoryInstructions.includes(forbidden), + 'supervisor-swarm-mixed-repository-recipe-leak', + ); + } + } state.supervisorSwarm.autonomousRepositoryRecipeFree = true; } @@ -5904,14 +6029,14 @@ async function seedSupervisorSwarmDisposableProject() { } function buildSupervisorSwarmTaskPrompt() { - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { return supervisorSwarmAutonomousTask; } return `请把 ${supervisorSwarmDesignPath} 与 ${supervisorSwarmQualityPath} 两项专业交付推进到仓库规范规定的 ready 状态。设计方向交给 ${supervisorSwarmDesignAgentId},质量方向交给 ${supervisorSwarmQualityAgentId};第一轮必须在同一个 planning 轮次同时安排两个方向。收到内部证据后由总控逐项验收,质量首轮未达标时只安排一次返工;下一步执行条件已经齐全时直接行动,不要反复只更新计划。全部证据成立后再由总控向用户简短汇报。不要转述仓库指令、内部标记、私有运行信息或绝对路径。`; } function assertSupervisorSwarmTaskPrompt(task) { - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { assert( task === supervisorSwarmAutonomousTask && !task.includes('\n'), 'supervisor-swarm-autonomous-task-invalid', @@ -5989,6 +6114,10 @@ async function readSupervisorSwarmPersistence() { runtimeStates, contextBundles, legacyConversation, + isolatedGroups, + isolatedInstances, + isolatedResults, + isolatedJoinDeliveries, ] = await Promise.all([ readTaskSnapshot(), readAllRuntimeEvents(), @@ -6002,6 +6131,14 @@ async function readSupervisorSwarmPersistence() { readOptionalJsonl( path.join(state.projectRoot, '.agent/conversations/project.jsonl'), ), + readSupervisorSwarmJsonDirectory('.agent/runtime/isolated-agents/groups'), + readSupervisorSwarmJsonDirectory( + '.agent/runtime/isolated-agents/instances', + ), + readSupervisorSwarmJsonDirectory('.agent/runtime/isolated-agents/results'), + readSupervisorSwarmJsonDirectory( + '.agent/runtime/isolated-agents/join-deliveries', + ), ]); const supervisorConversation = await readOptionalJsonl( agentConversationPath(projectSupervisorAgentId, supervisorSwarmSessionId), @@ -6029,6 +6166,24 @@ async function readSupervisorSwarmPersistence() { ); professionalConversations.push({ ...session, messages }); } + const isolatedConversations = []; + for (const instance of isolatedInstances) { + if ( + !isNonEmptyString(instance.instanceId) || + !isNonEmptyString(instance.sessionId) + ) { + continue; + } + const messages = await readOptionalJsonl( + agentConversationPath(instance.instanceId, instance.sessionId), + ); + isolatedConversations.push({ + agentId: instance.instanceId, + sessionId: instance.sessionId, + runId: instance.runId, + messages, + }); + } return { taskSnapshot, events, @@ -6042,6 +6197,11 @@ async function readSupervisorSwarmPersistence() { legacyConversation, supervisorConversation, professionalConversations, + isolatedGroups, + isolatedInstances, + isolatedResults, + isolatedJoinDeliveries, + isolatedConversations, }; } @@ -6095,6 +6255,126 @@ function supervisorSwarmTaskIdentity(task) { }; } +function supervisorSwarmIsolatedGroupIdentity(group) { + return { + schemaVersion: group.schemaVersion, + parentAgentId: group.parentAgentId, + parentSessionId: group.parentSessionId, + parentRunId: group.parentRunId, + parentActionId: group.parentActionId, + delegationGroupId: group.delegationGroupId, + joinRunId: group.joinRunId, + depth: group.depth, + joinMode: group.joinMode, + requestSha256: hashJsonValue(group.request ?? null), + instanceIds: [...(group.instanceIds ?? [])].sort(), + createdAt: group.createdAt, + }; +} + +function supervisorSwarmIsolatedInstanceIdentity(instance) { + return { + schemaVersion: instance.schemaVersion, + parentAgentId: instance.parentAgentId, + parentSessionId: instance.parentSessionId, + parentRunId: instance.parentRunId, + parentActionId: instance.parentActionId, + delegationGroupId: instance.delegationGroupId, + delegationId: instance.delegationId, + childIndex: instance.childIndex, + instanceId: instance.instanceId, + templateAgentId: instance.templateAgentId, + sessionId: instance.sessionId, + runId: instance.runId, + depth: instance.depth, + taskSha256: hashValue(instance.task ?? ''), + acceptanceCriteriaSha256: hashValue( + JSON.stringify(instance.acceptanceCriteria ?? []), + ), + expectedArtifacts: instance.expectedArtifacts, + writeScopes: instance.writeScopes, + createdAt: instance.createdAt, + }; +} + +function supervisorSwarmIsolatedResultIdentity(record) { + const result = record.result ?? {}; + return { + schemaVersion: record.schemaVersion, + delegationGroupId: record.delegationGroupId, + childIndex: record.childIndex, + delegationId: result.delegationId, + instanceId: result.instanceId, + templateAgentId: result.templateAgentId, + runId: result.runId, + status: result.status, + summarySha256: hashValue(result.summary ?? ''), + artifacts: result.artifacts, + evidenceSha256: hashValue(JSON.stringify(result.evidence ?? [])), + verifiedRevision: result.verifiedRevision ?? null, + errorSha256: result.error == null ? null : hashValue(result.error), + recordedAt: record.recordedAt, + }; +} + +function supervisorSwarmIsolatedJoinIdentity(delivery) { + return { + schemaVersion: delivery.schemaVersion, + parentAgentId: delivery.parentAgentId, + parentRunId: delivery.parentRunId, + delegationGroupId: delivery.delegationGroupId, + joinRunId: delivery.joinRunId, + status: delivery.status, + deliveryTarget: isolatedJoinDeliveryTarget(delivery), + queuedRunId: delivery.queuedRunId ?? null, + claimedByActionId: delivery.claimedByActionId ?? null, + updatedAt: delivery.updatedAt, + }; +} + +function supervisorSwarmParentIsolatedRecords(persistence) { + const groups = (persistence.isolatedGroups ?? []).filter( + (group) => + group.parentAgentId === projectSupervisorAgentId && + group.parentSessionId === supervisorSwarmSessionId && + group.parentRunId === state.initialRunId, + ); + const groupIds = new Set(groups.map((group) => group.delegationGroupId)); + const instances = (persistence.isolatedInstances ?? []).filter( + (instance) => + instance.parentAgentId === projectSupervisorAgentId && + instance.parentSessionId === supervisorSwarmSessionId && + instance.parentRunId === state.initialRunId && + groupIds.has(instance.delegationGroupId), + ); + const results = (persistence.isolatedResults ?? []).filter((record) => + groupIds.has(record.delegationGroupId), + ); + const joinDeliveries = (persistence.isolatedJoinDeliveries ?? []).filter( + (delivery) => groupIds.has(delivery.delegationGroupId), + ); + return { groups, instances, results, joinDeliveries }; +} + +function supervisorSwarmEffectiveAgentPolicy(agentId, isolatedInstances = []) { + const templateAgentId = isolatedInstances.find( + (instance) => instance.instanceId === agentId, + )?.templateAgentId; + return ( + state.supervisorSwarm.effectiveAgentPolicies[templateAgentId ?? agentId] ?? + state.supervisorSwarm.effectiveDefaultPolicy + ); +} + +function supervisorSwarmReportAgentPolicies() { + return Object.fromEntries( + supervisorSwarmRequiredAgentIds.flatMap((agentId) => { + const policy = state.supervisorSwarm.effectiveAgentPolicies[agentId]; + return policy ? [[agentId, policy]] : []; + }), + ); +} + function supervisorSwarmPendingIdentity(pending) { const record = pending.record; assert( @@ -6169,71 +6449,238 @@ function assertSupervisorSwarmContractShape(input, expectedPath, codePrefix) { ); } +function supervisorSwarmIsolatedWriteScopeRoots(children) { + const roots = children.map((child) => { + assert( + Array.isArray(child.writeScopes) && + child.writeScopes.length === 1 && + isNonEmptyString(child.writeScopes[0]) && + child.writeScopes[0].endsWith('/**'), + 'supervisor-swarm-mixed-child-write-scope-invalid', + ); + const root = child.writeScopes[0].slice(0, -3); + assert( + isNonEmptyString(root) && + !path.posix.isAbsolute(root) && + path.posix.normalize(root) === root && + !root.split('/').includes('..') && + !['.agent', '.git'].some( + (privateRoot) => + root === privateRoot || root.startsWith(`${privateRoot}/`), + ), + 'supervisor-swarm-mixed-child-write-scope-unsafe', + ); + return root; + }); + for (let leftIndex = 0; leftIndex < roots.length; leftIndex += 1) { + for ( + let rightIndex = leftIndex + 1; + rightIndex < roots.length; + rightIndex += 1 + ) { + const left = roots[leftIndex]; + const right = roots[rightIndex]; + assert( + left !== right && + !left.startsWith(`${right}/`) && + !right.startsWith(`${left}/`), + 'supervisor-swarm-mixed-child-write-scopes-overlap', + ); + } + } + return roots; +} + +function assertSupervisorSwarmIsolatedChildBusinessContract(child, review) { + const contract = [child.task, ...(child.acceptanceCriteria ?? [])] + .join('\n') + .toLowerCase(); + assert( + review.boundaryTerms.some((term) => contract.includes(term)) && + /read[- ]?only|audit[- ]?only|do not (?:change|modify|write)|leave [^\n]* unchanged|只读|不得修改|不要修改/iu.test( + contract, + ), + 'supervisor-swarm-mixed-child-business-boundary-invalid', + ); +} + function validateSupervisorSwarmInitialProviderBatch(batch) { + const mixed = isSupervisorSwarmStaticIsolatedAutonomousChatSuite(); + const expectedActionCount = mixed ? 3 : 2; assert( batch?.schemaVersion === 'game-creator-provider-action-batch.v1' && isNonEmptyString(batch.batchId) && batch.agentId === projectSupervisorAgentId && + isNonEmptyString(batch.taskId) && batch.sessionId === supervisorSwarmSessionId && batch.runId === state.initialRunId && Number.isSafeInteger(batch.loopIteration) && Number.isSafeInteger(batch.plannedSteerCursor) && - ['ready', 'completed'].includes(batch.status) && + ['ready', 'waiting-confirmation', 'completed'].includes(batch.status) && Number.isSafeInteger(batch.nextActionIndex) && batch.nextActionIndex >= 0 && batch.nextActionIndex <= batch.actions?.length && Array.isArray(batch.actions) && - batch.actions.length === 2 && + batch.actions.length === expectedActionCount && Array.isArray(batch.plan?.actions) && - batch.plan.actions.length === 2, + batch.plan.actions.length === expectedActionCount, 'supervisor-swarm-initial-provider-batch-invalid', ); + if (mixed) { + assert( + batch.status === 'waiting-confirmation' && batch.nextActionIndex === 0, + 'supervisor-swarm-mixed-initial-confirmation-gate-missing', + ); + } const agentIds = new Set(); const actionIds = new Set(); + const delegateActionIds = new Set(); + let spawnActionId = null; for (const [index, pending] of batch.actions.entries()) { const action = pending.action; - const expectedPath = - action?.input?.agentId === supervisorSwarmDesignAgentId - ? supervisorSwarmDesignPath - : supervisorSwarmQualityPath; + const confirmationExpected = + mixed && action?.tool === 'agent.spawn_isolated'; assert( pending.agentId === projectSupervisorAgentId && pending.sessionId === supervisorSwarmSessionId && pending.runId === state.initialRunId && pending.actionIndex === index && isNonEmptyString(pending.actionId) && + /^[0-9a-f]{64}$/u.test(pending.actionFingerprint ?? '') && !actionIds.has(pending.actionId) && - pending.executionMode === 'auto' && - ['approved', 'executing', 'observed-approved'].includes( - pending.status, - ) && - action?.tool === 'agent.delegate' && - batch.plan.actions[index]?.tool === 'agent.delegate' && + pending.executionMode === + (confirmationExpected ? 'confirmation' : 'auto') && + (confirmationExpected + ? [ + 'pending-confirmation', + 'approved', + 'executing', + 'observed-approved', + ].includes(pending.status) + : ['approved', 'executing', 'observed-approved'].includes( + pending.status, + )) && + action?.tool === batch.plan.actions[index]?.tool && JSON.stringify(action.input) === JSON.stringify(batch.plan.actions[index].input), 'supervisor-swarm-initial-provider-batch-action-invalid', ); - assertSupervisorSwarmContractShape( - action.input, - expectedPath, - 'supervisor-swarm-initial-provider-batch', - ); - assert( - [supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId].includes( - action.input.agentId, - ), - 'supervisor-swarm-initial-provider-batch-target-invalid', - ); - agentIds.add(action.input.agentId); + if (action.tool === 'agent.delegate') { + const expectedPath = + action.input?.agentId === supervisorSwarmDesignAgentId + ? supervisorSwarmDesignPath + : supervisorSwarmQualityPath; + assertSupervisorSwarmContractShape( + action.input, + expectedPath, + 'supervisor-swarm-initial-provider-batch', + ); + assert( + [supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId].includes( + action.input.agentId, + ), + 'supervisor-swarm-initial-provider-batch-target-invalid', + ); + agentIds.add(action.input.agentId); + delegateActionIds.add(pending.actionId); + } else { + assert( + mixed && + action.tool === 'agent.spawn_isolated' && + spawnActionId == null && + isPlainObject(action.input) && + JSON.stringify(Object.keys(action.input).sort()) === + JSON.stringify(['children', 'joinMode'].sort()) && + action.input.joinMode === 'all' && + Array.isArray(action.input.children) && + action.input.children.length === + supervisorSwarmIsolatedReviews.length, + 'supervisor-swarm-mixed-spawn-action-invalid', + ); + const expectedPaths = new Set( + supervisorSwarmIsolatedReviews.map((review) => review.path), + ); + for (const child of action.input.children) { + const review = supervisorSwarmIsolatedReviews.find( + (candidate) => candidate.path === child?.expectedArtifacts?.[0], + ); + assert( + isPlainObject(child) && + JSON.stringify(Object.keys(child).sort()) === + JSON.stringify( + [ + 'templateAgentId', + 'task', + 'acceptanceCriteria', + 'expectedArtifacts', + 'writeScopes', + ].sort(), + ), + 'supervisor-swarm-mixed-child-shape-invalid', + ); + assert( + isNonEmptyString(child.templateAgentId), + 'supervisor-swarm-mixed-child-template-invalid', + ); + assert( + isNonEmptyString(child.task) && + Array.isArray(child.acceptanceCriteria) && + child.acceptanceCriteria.length > 0 && + child.acceptanceCriteria.every(isNonEmptyString), + 'supervisor-swarm-mixed-child-task-contract-invalid', + ); + assert( + Array.isArray(child.expectedArtifacts) && + child.expectedArtifacts.length === 1 && + review != null && + expectedPaths.delete(child.expectedArtifacts[0]), + 'supervisor-swarm-mixed-child-artifact-contract-invalid', + ); + assert( + Array.isArray(child.writeScopes) && child.writeScopes.length === 1, + 'supervisor-swarm-mixed-child-write-scope-count-invalid', + ); + assertSupervisorSwarmIsolatedChildBusinessContract(child, review); + } + supervisorSwarmIsolatedWriteScopeRoots(action.input.children); + assert( + expectedPaths.size === 0, + 'supervisor-swarm-mixed-child-boundaries-incomplete', + ); + spawnActionId = pending.actionId; + state.supervisorSwarm.mixedSpawnRequestHash = hashJsonValue(action.input); + } actionIds.add(pending.actionId); } assert( - agentIds.size === 2, + agentIds.size === 2 && (!mixed || isNonEmptyString(spawnActionId)), 'supervisor-swarm-initial-provider-batch-target-count-invalid', ); + assert( + !mixed || + batch.actions.filter( + (pending) => + pending.actionId === spawnActionId && + pending.executionMode === 'confirmation' && + pending.status === 'pending-confirmation', + ).length === 1, + 'supervisor-swarm-initial-provider-batch-waiting-state-invalid', + ); + state.supervisorSwarm.mixedSpawnActionId = spawnActionId; state.supervisorSwarm.initialProviderBatch = { batchId: batch.batchId, + taskId: batch.taskId, + sessionId: batch.sessionId, + runId: batch.runId, actionIds: [...actionIds], + delegateActionIds: [...delegateActionIds], + actions: batch.actions.map((pending) => ({ + actionIndex: pending.actionIndex, + actionId: pending.actionId, + actionFingerprint: pending.actionFingerprint, + tool: pending.action.tool, + executionMode: pending.executionMode, + })), loopIteration: batch.loopIteration, snapshotHash: hashValue( JSON.stringify({ @@ -6243,7 +6690,13 @@ function validateSupervisorSwarmInitialProviderBatch(batch) { runId: batch.runId, loopIteration: batch.loopIteration, plannedSteerCursor: batch.plannedSteerCursor, - actionIds: [...actionIds], + actions: batch.actions.map((pending) => ({ + actionIndex: pending.actionIndex, + actionId: pending.actionId, + actionFingerprint: pending.actionFingerprint, + tool: pending.action.tool, + executionMode: pending.executionMode, + })), }), ), }; @@ -6263,23 +6716,29 @@ async function captureSupervisorSwarmInitialProviderBatch() { if (error?.code === 'ENOENT') return null; throw error; }); - const autonomousDelegateBatch = - isSupervisorSwarmAutonomousChatSuite() && - Array.isArray(batch?.actions) && - batch.actions.length === 2 && - batch.actions.every( - (pending) => pending?.action?.tool === 'agent.delegate', - ); + const batchTools = Array.isArray(batch?.actions) + ? batch.actions.map((pending) => pending?.action?.tool) + : []; + const interactiveCollaborationBatch = + (isSupervisorSwarmAutonomousChatSuite() && + batchTools.length === 2 && + batchTools.every((tool) => tool === 'agent.delegate')) || + (isSupervisorSwarmStaticIsolatedAutonomousChatSuite() && + batchTools.length === 3 && + batchTools.filter((tool) => tool === 'agent.delegate').length === 2 && + batchTools.filter((tool) => tool === 'agent.spawn_isolated').length === + 1); if ( batch && - (!isSupervisorSwarmAutonomousChatSuite() || autonomousDelegateBatch) + (!isSupervisorSwarmInteractiveChatSuite() || + interactiveCollaborationBatch) ) { validateSupervisorSwarmInitialProviderBatch(batch); return; } pollCount += 1; if (pollCount % 20 === 0) { - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { await confirmSupervisorSwarmPendingActions( new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), ); @@ -6318,42 +6777,54 @@ async function captureSupervisorSwarmInitialProviderBatch() { throw codedError('supervisor-swarm-initial-provider-batch-timeout'); } +function supervisorSwarmProviderIntervals(agentDb, agentId, runId) { + const records = agentDb + .map((record, index) => ({ record, index })) + .filter( + ({ record }) => + record.recordType === 'agent.runtime.provider_request.lifecycle' && + record.agentId === agentId && + record.runId === runId, + ); + return records + .filter( + ({ record }) => + record.status === 'started' && record.requestKind === 'tool-plan', + ) + .map((started) => ({ + started, + terminal: records.find( + ({ record, index }) => + index > started.index && + record.requestId === started.record.requestId && + ['completed', 'failed', 'interrupted'].includes(record.status), + ), + })) + .filter((interval) => Boolean(interval.terminal)); +} + +function supervisorSwarmProviderIntervalsOverlap(left, right) { + return ( + Math.max(left.started.index, right.started.index) < + Math.min(left.terminal.index, right.terminal.index) + ); +} + function observeSupervisorSwarmInitialProviderOverlap(agentDb, deliveries) { if (deliveries.length !== 2) return false; - const intervalsByDelivery = []; - for (const delivery of deliveries) { - const records = agentDb - .map((record, index) => ({ record, index })) - .filter( - ({ record }) => - record.recordType === 'agent.runtime.provider_request.lifecycle' && - record.agentId === delivery.targetAgentId && - record.runId === delivery.targetRunId, - ); - const intervals = records - .filter( - ({ record }) => - record.status === 'started' && record.requestKind === 'tool-plan', - ) - .map((started) => ({ - started, - terminal: records.find( - ({ record, index }) => - index > started.index && - record.requestId === started.record.requestId && - ['completed', 'failed', 'interrupted'].includes(record.status), - ), - })) - .filter((interval) => Boolean(interval.terminal)); - if (intervals.length === 0) return false; - intervalsByDelivery.push(intervals); + const intervalsByDelivery = deliveries.map((delivery) => + supervisorSwarmProviderIntervals( + agentDb, + delivery.targetAgentId, + delivery.targetRunId, + ), + ); + if (intervalsByDelivery.some((intervals) => intervals.length === 0)) { + return false; } for (const left of intervalsByDelivery[0]) { for (const right of intervalsByDelivery[1]) { - if ( - Math.max(left.started.index, right.started.index) < - Math.min(left.terminal.index, right.terminal.index) - ) { + if (supervisorSwarmProviderIntervalsOverlap(left, right)) { state.supervisorSwarm.initialProviderRequestIds = [ left.started.record.requestId, right.started.record.requestId, @@ -6366,6 +6837,46 @@ function observeSupervisorSwarmInitialProviderOverlap(agentDb, deliveries) { return false; } +function observeSupervisorSwarmStaticIsolatedProviderOverlap( + agentDb, + deliveries, + instances, +) { + if (!isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) return false; + for (const delivery of deliveries) { + const staticIntervals = supervisorSwarmProviderIntervals( + agentDb, + delivery.targetAgentId, + delivery.targetRunId, + ); + for (const instance of instances) { + const isolatedIntervals = supervisorSwarmProviderIntervals( + agentDb, + instance.instanceId, + instance.runId, + ); + for (const staticInterval of staticIntervals) { + for (const isolatedInterval of isolatedIntervals) { + if ( + supervisorSwarmProviderIntervalsOverlap( + staticInterval, + isolatedInterval, + ) + ) { + state.supervisorSwarm.staticIsolatedProviderRequestIds = [ + staticInterval.started.record.requestId, + isolatedInterval.started.record.requestId, + ]; + state.supervisorSwarm.staticIsolatedProviderOverlapObserved = true; + return true; + } + } + } + } + } + return false; +} + function supervisorSwarmParentDeliveries(deliveries) { return deliveries.filter( (delivery) => @@ -6552,12 +7063,15 @@ function validateSupervisorSwarmClaimedContractRecovery( return attempts; } -function supervisorSwarmRelevantRunKeys(deliveries) { +function supervisorSwarmRelevantRunKeys(deliveries, isolatedInstances = []) { return new Set([ `${projectSupervisorAgentId}\0${state.initialRunId}`, ...deliveries.map( (delivery) => `${delivery.targetAgentId}\0${delivery.targetRunId}`, ), + ...isolatedInstances.map( + (instance) => `${instance.instanceId}\0${instance.runId}`, + ), ]); } @@ -6592,7 +7106,7 @@ function assertSupervisorSwarmDeliveryContract( function assertSupervisorSwarmInitialDeliveries(deliveries) { const batchActionIds = [ - ...(state.supervisorSwarm.initialProviderBatch?.actionIds ?? []), + ...(state.supervisorSwarm.initialProviderBatch?.delegateActionIds ?? []), ].sort(); const deliveryActionIds = deliveries .map((delivery) => delivery.parentActionId) @@ -6705,6 +7219,252 @@ function supervisorSwarmArtifactHash(delivery, expectedPath) { return artifact?.sha256 ?? null; } +function validateSupervisorSwarmMixedIsolatedPersistence(persistence) { + assert( + isSupervisorSwarmStaticIsolatedAutonomousChatSuite(), + 'supervisor-swarm-mixed-validation-outside-suite', + ); + const { groups, instances, results, joinDeliveries } = + supervisorSwarmParentIsolatedRecords(persistence); + assert( + groups.length === 1 && + persistence.isolatedGroups.length === 1 && + groups[0].schemaVersion === isolatedAgentGroupSchemaVersion && + groups[0].parentAgentId === projectSupervisorAgentId && + groups[0].parentSessionId === supervisorSwarmSessionId && + groups[0].parentRunId === state.initialRunId && + groups[0].parentActionId === state.supervisorSwarm.mixedSpawnActionId && + groups[0].joinMode === 'all' && + groups[0].depth === 1 && + Array.isArray(groups[0].instanceIds) && + groups[0].instanceIds.length === supervisorSwarmIsolatedReviews.length && + hashJsonValue(groups[0].request) === + state.supervisorSwarm.mixedSpawnRequestHash, + 'supervisor-swarm-mixed-group-invalid', + ); + const group = groups[0]; + supervisorSwarmIsolatedWriteScopeRoots(group.request.children); + const expectedByPath = new Map( + supervisorSwarmIsolatedReviews.map((review) => [review.path, review]), + ); + assert( + instances.length === supervisorSwarmIsolatedReviews.length && + persistence.isolatedInstances.length === instances.length && + new Set(instances.map((instance) => instance.instanceId)).size === + instances.length && + new Set(instances.map((instance) => instance.delegationId)).size === + instances.length && + new Set(instances.map((instance) => instance.sessionId)).size === + instances.length && + new Set(instances.map((instance) => instance.runId)).size === + instances.length && + JSON.stringify( + instances.map((instance) => instance.childIndex).sort((a, b) => a - b), + ) === JSON.stringify([0, 1, 2]), + 'supervisor-swarm-mixed-instance-cardinality-invalid', + ); + for (const instance of instances) { + const expectedPath = instance.expectedArtifacts?.[0]; + const review = expectedByPath.get(expectedPath); + const requestChild = group.request?.children?.[instance.childIndex]; + assert( + review && + instance.schemaVersion === isolatedAgentInstanceSchemaVersion && + instance.parentAgentId === projectSupervisorAgentId && + instance.parentSessionId === supervisorSwarmSessionId && + instance.parentRunId === state.initialRunId && + instance.parentActionId === state.supervisorSwarm.mixedSpawnActionId && + instance.delegationGroupId === group.delegationGroupId && + group.instanceIds.includes(instance.instanceId) && + isNonEmptyString(instance.templateAgentId) && + isNonEmptyString(instance.task) && + instance.depth === 1 && + JSON.stringify(instance.acceptanceCriteria) === + JSON.stringify(requestChild?.acceptanceCriteria) && + JSON.stringify(instance.expectedArtifacts) === + JSON.stringify([review.path]) && + JSON.stringify(requestChild?.expectedArtifacts) === + JSON.stringify(instance.expectedArtifacts) && + JSON.stringify(requestChild?.writeScopes) === + JSON.stringify(instance.writeScopes) && + requestChild?.templateAgentId === instance.templateAgentId && + requestChild?.task === instance.task, + 'supervisor-swarm-mixed-instance-contract-invalid', + ); + assertSupervisorSwarmIsolatedChildBusinessContract(requestChild, review); + } + assert( + results.length === instances.length && + persistence.isolatedResults.length === results.length && + new Set(results.map((record) => record.result?.instanceId)).size === + results.length, + 'supervisor-swarm-mixed-result-cardinality-invalid', + ); + for (const record of results) { + const instance = instances.find( + (candidate) => candidate.instanceId === record.result?.instanceId, + ); + const review = expectedByPath.get(instance?.expectedArtifacts?.[0]); + const artifact = record.result?.artifacts?.find( + (candidate) => candidate.path === review?.path, + ); + assert( + instance && + review && + record.schemaVersion === isolatedAgentResultSchemaVersion && + record.delegationGroupId === group.delegationGroupId && + record.childIndex === instance.childIndex && + record.result.delegationId === instance.delegationId && + record.result.templateAgentId === instance.templateAgentId && + record.result.runId === instance.runId && + record.result.status === 'completed' && + isNonEmptyString(record.result.summary) && + Array.isArray(record.result.artifacts) && + JSON.stringify(record.result.artifacts) === + JSON.stringify([ + { path: review.path, sha256: hashValue(review.content) }, + ]) && + artifact?.sha256 === hashValue(review.content) && + Array.isArray(record.result.evidence) && + (record.result.verifiedRevision == null || + (Number.isSafeInteger(record.result.verifiedRevision) && + record.result.verifiedRevision >= 0)) && + record.result.error == null, + 'supervisor-swarm-mixed-result-invalid', + ); + } + assert( + joinDeliveries.length === 1 && + persistence.isolatedJoinDeliveries.length === 1 && + joinDeliveries[0].schemaVersion === + isolatedAgentJoinDeliverySchemaVersion && + joinDeliveries[0].parentAgentId === projectSupervisorAgentId && + joinDeliveries[0].parentRunId === state.initialRunId && + joinDeliveries[0].delegationGroupId === group.delegationGroupId && + joinDeliveries[0].joinRunId === group.joinRunId && + joinDeliveries[0].status === 'claimed-by-parent' && + isolatedJoinDeliveryTarget(joinDeliveries[0]) === 'parent-wake' && + joinDeliveries[0].queuedRunId == null && + isNonEmptyString(joinDeliveries[0].claimedByActionId), + 'supervisor-swarm-mixed-join-delivery-invalid', + ); + const joinDelivery = joinDeliveries[0]; + const isolatedTasks = persistence.taskSnapshot.latest.filter((task) => + instances.some( + (instance) => + task.agentId === instance.instanceId && + task.sessionId === instance.sessionId && + task.runId === instance.runId && + task.delegationId === instance.delegationId, + ), + ); + const continuationTasks = persistence.taskSnapshot.all.filter( + (task) => + task.source === 'agent-isolated-join' && task.runId === group.joinRunId, + ); + assert( + isolatedTasks.length === instances.length && + isolatedTasks.every( + (task) => + task.source === 'agent-isolated-child' && + task.parentAgentId === projectSupervisorAgentId && + task.parentRunId === state.initialRunId && + task.status === 'completed' && + task.phase === 'completed', + ) && + continuationTasks.length === 0, + 'supervisor-swarm-mixed-child-or-continuation-task-invalid', + ); + const spawnAudits = persistence.agentDb.filter( + (record) => + record.recordType === 'agent.runtime.agent.spawn_isolated' && + record.agentId === projectSupervisorAgentId && + record.sessionId === supervisorSwarmSessionId && + record.runId === state.initialRunId && + record.actionId === state.supervisorSwarm.mixedSpawnActionId && + record.delegationGroupId === group.delegationGroupId && + record.joinRunId === group.joinRunId, + ); + const parentWakeAudits = persistence.agentDb.filter( + (record) => + record.recordType === + 'agent.runtime.agent.isolated_join.parent_wake.dispatched' && + record.agentId === projectSupervisorAgentId && + record.sessionId === supervisorSwarmSessionId && + record.parentRunId === state.initialRunId && + record.parentActionId === state.supervisorSwarm.mixedSpawnActionId && + record.delegationGroupId === group.delegationGroupId && + record.joinRunId === group.joinRunId, + ); + const continuationAudits = persistence.agentDb.filter( + (record) => + record.recordType === 'agent.runtime.agent.isolated_join.dispatched' && + record.parentRunId === state.initialRunId && + record.delegationGroupId === group.delegationGroupId && + record.joinRunId === group.joinRunId, + ); + const claimAudits = persistence.agentDb + .map((record, index) => ({ record, index })) + .filter( + ({ record }) => + record.recordType === + 'agent.runtime.agent.isolated_join.claimed_by_parent' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId && + record.parentActionId === state.supervisorSwarm.mixedSpawnActionId && + record.delegationGroupId === group.delegationGroupId && + record.joinRunId === group.joinRunId, + ); + const claimObservations = persistence.agentDb + .map((record, index) => ({ record, index })) + .filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_observation' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId && + record.tool === 'agent.run_status' && + record.status === 'ok' && + String(record.summary ?? '').includes('ready all-join'), + ); + assert( + spawnAudits.length === 1 && + Array.isArray(spawnAudits[0].children) && + spawnAudits[0].children.length === instances.length && + parentWakeAudits.length === 1 && + continuationAudits.length === 0 && + claimAudits.length === 1 && + claimObservations.length === 1 && + claimAudits[0].record.actionId === joinDelivery.claimedByActionId && + claimObservations[0].record.actionId === joinDelivery.claimedByActionId && + claimAudits[0].index < claimObservations[0].index, + 'supervisor-swarm-mixed-spawn-or-claim-audit-invalid', + ); + return { + group, + instances, + results, + joinDelivery, + isolatedTasks, + continuationTasks, + continuationAudits, + claimAuditIndex: claimAudits[0].index, + claimObservationIndex: claimObservations[0].index, + identity: { + group: supervisorSwarmIsolatedGroupIdentity(group), + instances: instances + .map(supervisorSwarmIsolatedInstanceIdentity) + .sort((left, right) => left.instanceId.localeCompare(right.instanceId)), + results: results + .map(supervisorSwarmIsolatedResultIdentity) + .sort((left, right) => left.instanceId.localeCompare(right.instanceId)), + joinDelivery: supervisorSwarmIsolatedJoinIdentity(joinDelivery), + tasks: isolatedTasks + .map(supervisorSwarmTaskIdentity) + .sort((left, right) => left.agentId.localeCompare(right.agentId)), + }, + }; +} + function assertSupervisorSwarmWeakQualityDelivery(delivery) { assert( delivery.status === 'claimed-by-parent' && @@ -6721,7 +7481,10 @@ function assertSupervisorSwarmWeakQualityDelivery(delivery) { function assertSupervisorSwarmRuntimeHealthy(persistence) { const deliveries = supervisorSwarmParentDeliveries(persistence.deliveries); - const relevantRuns = supervisorSwarmRelevantRunKeys(deliveries); + const relevantRuns = supervisorSwarmRelevantRunKeys( + deliveries, + supervisorSwarmParentIsolatedRecords(persistence).instances, + ); for (const task of persistence.taskSnapshot.latest) { if (!relevantRuns.has(`${task.agentId}\0${task.runId}`)) continue; if (isFailedTask(task)) { @@ -6741,8 +7504,11 @@ function assertSupervisorSwarmRuntimeHealthy(persistence) { function assertSupervisorSwarmProviderFailureRecoverable(persistence) { const now = Math.floor(Date.now() / 1_000); + const isolatedInstances = + supervisorSwarmParentIsolatedRecords(persistence).instances; const relevantRuns = supervisorSwarmRelevantRunKeys( supervisorSwarmParentDeliveries(persistence.deliveries ?? []), + isolatedInstances, ); const lifecycle = persistence.agentDb.filter( (record) => @@ -6760,7 +7526,10 @@ function assertSupervisorSwarmProviderFailureRecoverable(persistence) { const retry = retries.find( (record) => record.requestId === failed.requestId, ); - const policy = state.supervisorSwarm.effectiveAgentPolicies[failed.agentId]; + const policy = supervisorSwarmEffectiveAgentPolicy( + failed.agentId, + isolatedInstances, + ); const maxRetries = Math.min(policy?.maxRetries ?? 0, 3); const attemptMatch = failed.requestSlot?.match(/-transient-(\d+)$/u); const attempt = attemptMatch ? Number(attemptMatch[1]) : 0; @@ -6845,9 +7614,21 @@ async function confirmSupervisorSwarmPendingActions( ) { const before = state.confirmedActionIds.size; const allowedTools = new Set(supervisorSwarmConfirmedTools); + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + allowedTools.add('agent.spawn_isolated'); + } await confirmPendingActions(allowedTools, (pending) => { const runKey = `${pending.agentId}\0${pending.runId}`; const deferred = deferredRunKeys.has(runKey); + if (pending.tool === 'agent.spawn_isolated') { + assert( + isSupervisorSwarmStaticIsolatedAutonomousChatSuite() && + pending.agentId === projectSupervisorAgentId && + pending.runId === state.initialRunId && + pending.actionId === state.supervisorSwarm.mixedSpawnActionId, + 'supervisor-swarm-unexpected-spawn-confirmation', + ); + } assert( confirmRunKeys.has(runKey) || deferred, 'supervisor-swarm-unexpected-pending-run', @@ -6857,7 +7638,11 @@ async function confirmSupervisorSwarmPendingActions( pending.agentId === projectSupervisorAgentId && pending.runId === state.initialRunId; assert( - !isParentRun || pending.tool === 'project.verify', + !isParentRun || + pending.tool === 'project.verify' || + (isSupervisorSwarmStaticIsolatedAutonomousChatSuite() && + pending.tool === 'agent.spawn_isolated' && + pending.actionId === state.supervisorSwarm.mixedSpawnActionId), 'supervisor-swarm-parent-pending-tool-invalid', ); return true; @@ -6939,11 +7724,37 @@ function supervisorSwarmRepairProviderLifecycle(agentDb, repair) { }; } +function supervisorSwarmProviderStartedIdentities(agentDb, relevantRuns) { + return agentDb + .filter( + (record) => + record.recordType === 'agent.runtime.provider_request.lifecycle' && + record.status === 'started' && + relevantRuns.has(`${record.agentId}\0${record.runId}`), + ) + .map((record) => ({ + auditSchemaVersion: record.auditSchemaVersion, + agentId: record.agentId, + taskId: record.taskId, + sessionId: record.sessionId, + runId: record.runId, + source: record.source, + requestId: record.requestId, + requestKind: record.requestKind, + requestSlot: record.requestSlot, + webSearchEnabled: record.webSearchEnabled, + })) + .sort((left, right) => left.requestId.localeCompare(right.requestId)); +} + async function restartSupervisorSwarmRunnerAtRepairBoundary( persistence, repair, repairPending, ) { + const mixedState = isSupervisorSwarmStaticIsolatedAutonomousChatSuite() + ? validateSupervisorSwarmMixedIsolatedPersistence(persistence) + : null; const initial = supervisorSwarmParentDeliveries( persistence.deliveries, ).filter((delivery) => delivery.repairOfDelegationId == null); @@ -6976,6 +7787,11 @@ async function restartSupervisorSwarmRunnerAtRepairBoundary( (delivery) => task.agentId === delivery.targetAgentId && task.runId === delivery.targetRunId, + ) || + (mixedState?.instances ?? []).some( + (instance) => + task.agentId === instance.instanceId && + task.runId === instance.runId, ), ) .map(supervisorSwarmTaskIdentity) @@ -6993,9 +7809,26 @@ async function restartSupervisorSwarmRunnerAtRepairBoundary( .map(supervisorSwarmClaimIdentity) .sort((left, right) => left.actionId.localeCompare(right.actionId)); assert( - preKillTaskIdentities.length === 4 && preKillClaimIdentities.length === 1, + preKillTaskIdentities.length === (mixedState ? 7 : 4) && + preKillClaimIdentities.length === 1, 'supervisor-swarm-pre-kill-work-or-claim-identity-invalid', ); + const relevantRuns = supervisorSwarmRelevantRunKeys( + parentDeliveries, + mixedState?.instances ?? [], + ); + const preKillProviderStartedIdentities = + supervisorSwarmProviderStartedIdentities(persistence.agentDb, relevantRuns); + const preKillParentContext = persistence.contextBundles.find( + (bundle) => + bundle.agentId === projectSupervisorAgentId && + bundle.sessionId === supervisorSwarmSessionId && + bundle.runId === state.initialRunId, + ); + assert( + preKillParentContext && preKillProviderStartedIdentities.length > 0, + 'supervisor-swarm-pre-kill-context-or-provider-identity-missing', + ); const ownerBefore = await readSupervisorSwarmExecutionOwner(); const currentRunner = await verifyOwnedRunnerForKill(); assert( @@ -7010,11 +7843,19 @@ async function restartSupervisorSwarmRunnerAtRepairBoundary( supervisorSwarmPendingIdentity(repairPending); state.supervisorSwarm.repairPendingTool = repairPending.tool; state.supervisorSwarm.preKillProviderStartedCount = provider.startedCount; + state.supervisorSwarm.preKillProviderStartedIdentities = + preKillProviderStartedIdentities; state.supervisorSwarm.preKillDeliveryIdentities = parentDeliveries .map(supervisorSwarmDeliveryIdentity) .sort((left, right) => left.delegationId.localeCompare(right.delegationId)); state.supervisorSwarm.preKillClaimIdentities = preKillClaimIdentities; state.supervisorSwarm.preKillTaskIdentities = preKillTaskIdentities; + state.supervisorSwarm.preKillMixedIdentity = mixedState + ? { + isolated: mixedState.identity, + parentContextSha256: hashValue(JSON.stringify(preKillParentContext)), + } + : null; state.supervisorSwarm.runnerKillBoundaryObserved = true; await killRunnerOnce(); @@ -7065,6 +7906,11 @@ async function restartSupervisorSwarmRunnerAtRepairBoundary( delivery.parentRunId === state.initialRunId && task.agentId === delivery.targetAgentId && task.runId === delivery.targetRunId, + ) || + supervisorSwarmParentIsolatedRecords(after).instances.some( + (instance) => + task.agentId === instance.instanceId && + task.runId === instance.runId, ), ) .map(supervisorSwarmTaskIdentity) @@ -7088,6 +7934,24 @@ async function restartSupervisorSwarmRunnerAtRepairBoundary( candidate.tool === repairPending.tool, ); if (recoveredPending) { + const currentMixedState = + isSupervisorSwarmStaticIsolatedAutonomousChatSuite() + ? validateSupervisorSwarmMixedIsolatedPersistence(after) + : null; + const currentParentContext = after.contextBundles.find( + (bundle) => + bundle.agentId === projectSupervisorAgentId && + bundle.sessionId === supervisorSwarmSessionId && + bundle.runId === state.initialRunId, + ); + const currentProviderStartedIdentities = + supervisorSwarmProviderStartedIdentities( + after.agentDb, + supervisorSwarmRelevantRunKeys( + supervisorSwarmParentDeliveries(after.deliveries), + currentMixedState?.instances ?? [], + ), + ); assert( JSON.stringify(currentIdentities) === JSON.stringify(state.supervisorSwarm.preKillDeliveryIdentities) && @@ -7098,7 +7962,19 @@ async function restartSupervisorSwarmRunnerAtRepairBoundary( JSON.stringify(supervisorSwarmPendingIdentity(recoveredPending)) === JSON.stringify(state.supervisorSwarm.repairPendingIdentity) && repairLifecycle.length === - state.supervisorSwarm.preKillProviderStartedCount, + state.supervisorSwarm.preKillProviderStartedCount && + JSON.stringify(currentProviderStartedIdentities) === + JSON.stringify( + state.supervisorSwarm.preKillProviderStartedIdentities, + ) && + (!currentMixedState || + (JSON.stringify(currentMixedState.identity) === + JSON.stringify( + state.supervisorSwarm.preKillMixedIdentity?.isolated, + ) && + hashValue(JSON.stringify(currentParentContext)) === + state.supervisorSwarm.preKillMixedIdentity + ?.parentContextSha256)), 'supervisor-swarm-recovery-identity-or-provider-replay-invalid', ); state.identityStable = true; @@ -7122,19 +7998,56 @@ async function driveSupervisorSwarmToRepairKillBoundary() { (delivery) => delivery.repairOfDelegationId == null, ); assert(initial.length <= 2, 'supervisor-swarm-extra-initial-delivery'); + if ( + isSupervisorSwarmStaticIsolatedAutonomousChatSuite() && + isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) && + !state.confirmedActionIds.has(state.supervisorSwarm.mixedSpawnActionId) + ) { + const pending = await findPendingActions(); + if ( + pending.some( + (candidate) => + candidate.agentId === projectSupervisorAgentId && + candidate.runId === state.initialRunId && + candidate.actionId === state.supervisorSwarm.mixedSpawnActionId && + candidate.tool === 'agent.spawn_isolated', + ) + ) { + await confirmSupervisorSwarmPendingActions( + new Set([`${projectSupervisorAgentId}\0${state.initialRunId}`]), + new Set( + initial.map( + (delivery) => + `${delivery.targetAgentId}\0${delivery.targetRunId}`, + ), + ), + ); + } + } if (initial.length === 2) { const { quality } = assertSupervisorSwarmInitialDeliveries(initial); observeSupervisorSwarmInitialProviderOverlap( persistence.agentDb, initial, ); - if (state.supervisorSwarm.initialProviderOverlapObserved) { + const isolatedInstances = + supervisorSwarmParentIsolatedRecords(persistence).instances; + observeSupervisorSwarmStaticIsolatedProviderOverlap( + persistence.agentDb, + initial, + isolatedInstances, + ); + if ( + state.supervisorSwarm.initialProviderOverlapObserved && + (!isSupervisorSwarmStaticIsolatedAutonomousChatSuite() || + state.supervisorSwarm.staticIsolatedProviderOverlapObserved) + ) { const confirmRunKeys = new Set( initial.map( (delivery) => `${delivery.targetAgentId}\0${delivery.targetRunId}`, ), ); - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { confirmRunKeys.add( `${projectSupervisorAgentId}\0${state.initialRunId}`, ); @@ -7226,6 +8139,9 @@ async function driveSupervisorSwarmToRepairKillBoundary() { supervisorSwarmConfirmedTools.includes(repairPending.tool), `supervisor-swarm-repair-pending-tool-invalid:${repairPending.tool}`, ); + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + validateSupervisorSwarmMixedIsolatedPersistence(persistence); + } await restartSupervisorSwarmRunnerAtRepairBoundary( persistence, repair, @@ -7295,8 +8211,13 @@ async function driveSupervisorSwarmRuntimeToCompletion() { assertSupervisorSwarmProviderFailureRecoverable(persistence); assertSupervisorSwarmRuntimeHealthy(persistence); const deliveries = supervisorSwarmParentDeliveries(persistence.deliveries); - const knownRunKeys = supervisorSwarmRelevantRunKeys(deliveries); - await confirmSupervisorSwarmPendingActions(knownRunKeys); + const confirmRunKeys = new Set([ + `${projectSupervisorAgentId}\0${state.initialRunId}`, + ...deliveries.map( + (delivery) => `${delivery.targetAgentId}\0${delivery.targetRunId}`, + ), + ]); + await confirmSupervisorSwarmPendingActions(confirmRunKeys); assert( state.supervisorSwarm.confirmedActionCount <= 16, 'supervisor-swarm-confirmation-count-exceeded', @@ -7318,6 +8239,25 @@ async function driveSupervisorSwarmRuntimeToCompletion() { delivery.delegationId === task.delegationId, ), ); + const isolatedRecords = supervisorSwarmParentIsolatedRecords(persistence); + observeSupervisorSwarmStaticIsolatedProviderOverlap( + persistence.agentDb, + deliveries.filter((delivery) => delivery.repairOfDelegationId == null), + isolatedRecords.instances, + ); + const mixedReady = + !isSupervisorSwarmStaticIsolatedAutonomousChatSuite() || + (isolatedRecords.groups.length === 1 && + isolatedRecords.instances.length === + supervisorSwarmIsolatedReviews.length && + isolatedRecords.results.length === + supervisorSwarmIsolatedReviews.length && + isolatedRecords.results.every( + (record) => record.result?.status === 'completed', + ) && + isolatedRecords.joinDeliveries.length === 1 && + isolatedRecords.joinDeliveries[0].status === 'claimed-by-parent' && + validateSupervisorSwarmMixedIsolatedPersistence(persistence)); const completed = deliveries.length === 3 && deliveries.every((delivery) => delivery.status === 'claimed-by-parent') && @@ -7325,6 +8265,7 @@ async function driveSupervisorSwarmRuntimeToCompletion() { childTasks.every( (task) => task.status === 'completed' && task.phase === 'completed', ) && + Boolean(mixedReady) && parentRuntime?.runId === state.initialRunId && parentRuntime?.sessionId === supervisorSwarmSessionId && parentRuntime?.status === 'idle' && @@ -7647,7 +8588,7 @@ async function runSupervisorSwarmE2e() { sha256: hashValue(task), }; state.isolatedRunner.launchAttempted = true; - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { supervisorSwarmCliSession = startInteractiveCli([ '--swarm-chat', '--init', @@ -7694,7 +8635,7 @@ async function runSupervisorSwarmE2e() { await captureSupervisorSwarmTransientRetryCheckpoint(); await driveSupervisorSwarmToRepairKillBoundary(); await driveSupervisorSwarmRuntimeToCompletion(); - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { await captureSupervisorSwarmAutonomousTurnReport(); writeInteractiveCliLine(supervisorSwarmCliSession, '/quit'); await waitForInteractiveCliExit(supervisorSwarmCliSession, 30_000); @@ -7769,8 +8710,15 @@ async function captureSupervisorSwarmAutonomousTurnReport() { state.supervisorSwarm.turnReport = report; } -function validateSupervisorSwarmProviderLifecycle(agentDb, deliveries) { - const relevantRuns = supervisorSwarmRelevantRunKeys(deliveries); +function validateSupervisorSwarmProviderLifecycle( + agentDb, + deliveries, + isolatedInstances = [], +) { + const relevantRuns = supervisorSwarmRelevantRunKeys( + deliveries, + isolatedInstances, + ); const lifecycle = agentDb.filter( (record) => record.recordType === 'agent.runtime.provider_request.lifecycle' && @@ -7852,8 +8800,10 @@ function validateSupervisorSwarmProviderLifecycle(agentDb, deliveries) { const failedGroup = byRequest.get(retry.requestId); const failedStarted = failedGroup?.[0]; const failedTerminal = failedGroup?.[1]; - const retryPolicy = - state.supervisorSwarm.effectiveAgentPolicies[retry.agentId]; + const retryPolicy = supervisorSwarmEffectiveAgentPolicy( + retry.agentId, + isolatedInstances, + ); const currentAttemptMatch = retry.requestSlot?.match(/-transient-(\d+)$/u); const expectedRetryAttempt = currentAttemptMatch ? Number(currentAttemptMatch[1]) + 1 @@ -8030,7 +8980,10 @@ function validateSupervisorSwarmProviderLifecycle(agentDb, deliveries) { }; } -function validateSupervisorSwarmNativeProtocol(agentDb) { +function validateSupervisorSwarmNativeProtocol( + agentDb, + isolatedInstances = [], +) { const protocols = agentDb.filter( (record) => record.recordType === 'agent.runtime.tool_plan.protocol', ); @@ -8046,7 +8999,12 @@ function validateSupervisorSwarmNativeProtocol(agentDb) { delivery.targetRunId === record.runId, ) || (record.agentId === state.supervisorSwarm.repairTargetAgentId && - record.runId === state.supervisorSwarm.repairTargetRunId), + record.runId === state.supervisorSwarm.repairTargetRunId) || + isolatedInstances.some( + (instance) => + instance.instanceId === record.agentId && + instance.runId === record.runId, + ), ); assert( relevant.length > 0 && @@ -8061,26 +9019,47 @@ function validateSupervisorSwarmNativeProtocol(agentDb) { ), 'supervisor-swarm-native-tool-protocol-required', ); - const initialMultiCall = protocols.filter( - (record) => + const mixed = isSupervisorSwarmStaticIsolatedAutonomousChatSuite(); + const expectedActionFunctionCount = mixed ? 3 : 2; + const allowedInitialFunctionNames = new Set([ + 'update_agent_plan', + 'runtime_tool_agent_delegate', + ...(mixed ? ['runtime_tool_agent_spawn_isolated'] : []), + ]); + const initialMultiCall = protocols.filter((record) => { + if (!Array.isArray(record.functionNames)) return false; + const actionFunctionNames = record.functionNames.filter((name) => + name.startsWith('runtime_tool_'), + ); + return ( record.agentId === projectSupervisorAgentId && record.sessionId === supervisorSwarmSessionId && record.runId === state.initialRunId && record.loopIteration === state.supervisorSwarm.initialProviderBatch?.loopIteration && record.protocol === 'native_runtime_tools' && - Array.isArray(record.functionNames) && - record.functionNames.filter( + actionFunctionNames.length === expectedActionFunctionCount && + actionFunctionNames.filter( (name) => name === 'runtime_tool_agent_delegate', ).length === 2 && + actionFunctionNames.filter( + (name) => name === 'runtime_tool_agent_spawn_isolated', + ).length === (mixed ? 1 : 0) && + record.functionNames.every((name) => + allowedInitialFunctionNames.has(name), + ) && + record.functionNames.filter((name) => name === 'update_agent_plan') + .length <= 1 && + Number.isSafeInteger(record.functionCallCount) && record.functionCallCount === record.functionNames.length && Array.isArray(record.callIds) && record.callIds.length === record.functionCallCount && - new Set(record.callIds).size === record.callIds.length, - ); + new Set(record.callIds).size === record.callIds.length + ); + }); assert( initialMultiCall.length === 1, - 'supervisor-swarm-native-dual-delegate-plan-count-invalid', + 'supervisor-swarm-native-collaboration-plan-count-invalid', ); return { toolPlanProtocolCount: relevant.filter( @@ -8106,6 +9085,10 @@ function validateSupervisorSwarmNativeProtocol(agentDb) { (record) => record.protocol === 'text_json', ).length, nativeDualDelegatePlanCount: initialMultiCall.length, + nativeMixedCollaborationPlanCount: + isSupervisorSwarmStaticIsolatedAutonomousChatSuite() + ? initialMultiCall.length + : 0, }; } @@ -8113,8 +9096,12 @@ function validateSupervisorSwarmActionPersistence( agentDb, deliveries, contextBundles, + isolatedInstances = [], ) { - const relevantRuns = supervisorSwarmRelevantRunKeys(deliveries); + const relevantRuns = supervisorSwarmRelevantRunKeys( + deliveries, + isolatedInstances, + ); const actionRecords = agentDb.filter( (record) => isNonEmptyString(record.actionId) && @@ -8151,6 +9138,16 @@ function validateSupervisorSwarmActionPersistence( const duplicateExecutingActionIdCount = duplicateCount( executing.map((record) => record.actionId), ); + const isolatedRunKeys = new Set( + isolatedInstances.map( + (instance) => `${instance.instanceId}\0${instance.runId}`, + ), + ); + const isolatedMutationActions = actionRecords.filter( + (record) => + isolatedRunKeys.has(`${record.agentId}\0${record.runId}`) && + supervisorSwarmProjectMutationTools.has(record.tool), + ); const repairAttempts = validateSupervisorSwarmClaimedContractRecovery( agentDb, deliveries, @@ -8162,6 +9159,10 @@ function validateSupervisorSwarmActionPersistence( duplicateExecutingActionIdCount === 0, 'supervisor-swarm-duplicate-action-or-receipt', ); + assert( + isolatedMutationActions.length === 0, + 'supervisor-swarm-mixed-isolated-mutation-action', + ); assert( repairAttempts.failedRepairActions.length <= 1, 'supervisor-swarm-repair-contract-rejected-repeatedly', @@ -8177,30 +9178,224 @@ function validateSupervisorSwarmActionPersistence( 'supervisor-swarm-executing-action-without-unique-receipt', ); } - const initialActionIds = - state.supervisorSwarm.initialProviderBatch?.actionIds ?? []; + const initialBatchActions = + state.supervisorSwarm.initialProviderBatch?.actions ?? []; + const expectedInitialActionCount = + isSupervisorSwarmStaticIsolatedAutonomousChatSuite() ? 3 : 2; assert( - initialActionIds.length === 2 && - initialActionIds.every( - (actionId) => - executing.filter( - (record) => - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.actionId === actionId && - record.tool === 'agent.delegate', + initialBatchActions.length === expectedInitialActionCount, + 'supervisor-swarm-initial-batch-action-count-invalid', + ); + const matchesInitialBatchAction = (record, action) => + record.agentId === projectSupervisorAgentId && + record.taskId === state.supervisorSwarm.initialProviderBatch.taskId && + record.runId === state.initialRunId && + record.actionId === action.actionId && + record.actionFingerprint === action.actionFingerprint && + record.tool === action.tool; + let mixedSpawnConfirmationRequiredCount = 0; + let mixedSpawnApprovalCount = 0; + let mixedSpawnConfirmationOrderValid = false; + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + const delegateActions = initialBatchActions.filter( + (action) => action.tool === 'agent.delegate', + ); + const spawnActions = initialBatchActions.filter( + (action) => + action.tool === 'agent.spawn_isolated' && + action.actionId === state.supervisorSwarm.mixedSpawnActionId, + ); + const spawnAction = spawnActions[0]; + const indexedAgentDb = agentDb.map((record, index) => ({ record, index })); + const initialActionTimeline = (action) => { + const auto = action.tool === 'agent.delegate'; + const executions = indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_action.executing' && + record.executionMode === 'auto' && + matchesInitialBatchAction(record, action), + ); + const autoObserved = indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_action.observed' && + record.executionMode === 'auto' && + record.observationStatus === 'ok' && + matchesInitialBatchAction(record, action), + ); + const receipts = indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.action_receipt' && + record.sessionId === supervisorSwarmSessionId && + record.executionMode === (auto ? 'auto' : 'confirmation') && + record.status === 'ok' && + matchesInitialBatchAction(record, action), + ); + const terminalObservations = indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_observation' && + record.status === 'ok' && + record.decision === (auto ? 'auto' : 'approved') && + matchesInitialBatchAction(record, action), + ); + let sideEffects = []; + if (auto) { + const actionDeliveries = deliveries.filter( + (delivery) => + delivery.parentAgentId === projectSupervisorAgentId && + delivery.parentSessionId === supervisorSwarmSessionId && + delivery.parentRunId === state.initialRunId && + delivery.parentActionId === action.actionId && + delivery.repairOfDelegationId == null, + ); + sideEffects = + actionDeliveries.length === 1 + ? indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.agent.delegate' && + record.parentRunId === state.initialRunId && + record.delegationId === actionDeliveries[0].delegationId && + record.targetAgentId === actionDeliveries[0].targetAgentId && + record.runId === actionDeliveries[0].targetRunId, + ) + : []; + } else { + sideEffects = indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.agent.spawn_isolated' && + record.agentId === projectSupervisorAgentId && + record.sessionId === supervisorSwarmSessionId && + record.runId === state.initialRunId && + record.actionId === action.actionId, + ); + } + return { + action, + auto, + executions, + autoObserved, + receipts, + terminalObservations, + sideEffects, + }; + }; + const actionTimelines = initialBatchActions + .map(initialActionTimeline) + .sort( + (left, right) => left.action.actionIndex - right.action.actionIndex, + ); + const delegateTimelines = actionTimelines.filter(({ auto }) => auto); + const spawnTimeline = actionTimelines.find(({ auto }) => !auto); + const confirmationRequired = spawnAction + ? indexedAgentDb.filter( + ({ record }) => + record.recordType === + 'agent.runtime.provider_action_batch.confirmation_required' && + record.agentId === projectSupervisorAgentId && + record.sessionId === supervisorSwarmSessionId && + record.runId === state.initialRunId && + record.batchId === + state.supervisorSwarm.initialProviderBatch.batchId && + record.actionCount === expectedInitialActionCount && + record.actionIndex === spawnAction.actionIndex && + matchesInitialBatchAction(record, spawnAction), + ) + : []; + const approvals = spawnAction + ? indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_confirmation.approved' && + record.sessionId === supervisorSwarmSessionId && + record.confirmedRunId === state.initialRunId && + record.commandId === 'agent.spawn_isolated' && + matchesInitialBatchAction(record, spawnAction), + ) + : []; + const actionLifecycleOrderValid = actionTimelines.every((timeline) => { + if ( + timeline.sideEffects.length !== 1 || + timeline.receipts.length !== 1 || + timeline.terminalObservations.length !== 1 + ) { + return false; + } + if (timeline.auto) { + return ( + timeline.executions.length === 1 && + timeline.autoObserved.length === 1 && + timeline.executions[0].index < timeline.sideEffects[0].index && + timeline.sideEffects[0].index < timeline.autoObserved[0].index && + timeline.autoObserved[0].index < timeline.receipts[0].index && + timeline.receipts[0].index < timeline.terminalObservations[0].index + ); + } + return ( + timeline.executions.length === 0 && + timeline.autoObserved.length === 0 && + approvals.length === 1 && + approvals[0].index < timeline.sideEffects[0].index && + timeline.sideEffects[0].index < timeline.receipts[0].index && + timeline.receipts[0].index < timeline.terminalObservations[0].index + ); + }); + const batchActionOrderValid = actionTimelines.every((timeline, index) => { + if (timeline.action.actionIndex !== index) return false; + if (index === 0) return true; + const previousEnd = + actionTimelines[index - 1].terminalObservations[0]?.index; + const currentStart = timeline.auto + ? timeline.executions[0]?.index + : timeline.sideEffects[0]?.index; + return ( + Number.isSafeInteger(previousEnd) && + Number.isSafeInteger(currentStart) && + previousEnd < currentStart + ); + }); + mixedSpawnConfirmationRequiredCount = confirmationRequired.length; + mixedSpawnApprovalCount = approvals.length; + mixedSpawnConfirmationOrderValid = + confirmationRequired.length === 1 && + approvals.length === 1 && + confirmationRequired[0].index < approvals[0].index && + actionTimelines.every( + (timeline) => + approvals[0].index < + (timeline.auto + ? timeline.executions[0]?.index + : timeline.sideEffects[0]?.index), + ) && + actionLifecycleOrderValid && + batchActionOrderValid; + assert( + delegateActions.length === 2 && + new Set(delegateActions.map((action) => action.actionId)).size === 2 && + spawnActions.length === 1 && + spawnAction.executionMode === 'confirmation' && + delegateActions.every((action) => action.executionMode === 'auto') && + delegateTimelines.length === 2 && + spawnTimeline != null && + mixedSpawnConfirmationRequiredCount === 1 && + mixedSpawnApprovalCount === 1 && + mixedSpawnConfirmationOrderValid, + 'supervisor-swarm-mixed-initial-batch-action-persistence-invalid', + ); + } else { + assert( + initialBatchActions.every( + (action) => + executing.filter((record) => + matchesInitialBatchAction(record, action), ).length === 1 && receipts.filter( (record) => - record.agentId === projectSupervisorAgentId && - record.runId === state.initialRunId && - record.actionId === actionId && - record.tool === 'agent.delegate' && - record.status === 'ok', + record.sessionId === supervisorSwarmSessionId && + record.status === 'ok' && + matchesInitialBatchAction(record, action), ).length === 1, ), - 'supervisor-swarm-initial-batch-action-persistence-invalid', - ); + 'supervisor-swarm-initial-batch-action-persistence-invalid', + ); + } const matchesRecoveredRepairPending = (record) => record.agentId === state.supervisorSwarm.repairTargetAgentId && record.runId === state.supervisorSwarm.repairTargetRunId && @@ -8236,6 +9431,10 @@ function validateSupervisorSwarmActionPersistence( duplicateActionLifecycleCount, duplicateReceiptCount, duplicateExecutingActionIdCount, + isolatedMutationActionCount: isolatedMutationActions.length, + mixedSpawnConfirmationRequiredCount, + mixedSpawnApprovalCount, + mixedSpawnConfirmationOrderValid, recoveredRepairPendingActionCount: recoveredRepairReceipts.length, failedRepairActionCount: repairAttempts.failedRepairActions.length, targetedContractReadCount: repairAttempts.targetedContractReads.length, @@ -8310,7 +9509,11 @@ function validateSupervisorSwarmFinalization(agentDb, identity, assistant) { assistantAuditIndex >= 0 && assistantAuditIndex < persistedStageIndex, 'supervisor-swarm-finalization-assistant-order-invalid', ); - return { stageCount: records.length }; + return { + stageCount: records.length, + preparedIndex: agentDb.indexOf(records[0]), + finalizationId, + }; } function countSupervisorSwarmForbiddenPayloadFields(value) { @@ -8542,29 +9745,46 @@ async function validateSupervisorSwarmEvidence() { repairClaims[0].receipts.length === 1, 'supervisor-swarm-claim-shape-invalid', ); + const mixedState = isSupervisorSwarmStaticIsolatedAutonomousChatSuite() + ? validateSupervisorSwarmMixedIsolatedPersistence(persistence) + : null; - const [designContent, qualityContent, changedFiles, hostVerification] = - await Promise.all([ - fs.readFile( - path.join(state.projectRoot, supervisorSwarmDesignPath), - 'utf8', + const [ + designContent, + qualityContent, + changedFiles, + hostVerification, + isolatedEvidenceContents, + ] = await Promise.all([ + fs.readFile( + path.join(state.projectRoot, supervisorSwarmDesignPath), + 'utf8', + ), + fs.readFile( + path.join(state.projectRoot, supervisorSwarmQualityPath), + 'utf8', + ), + runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { + cwd: state.projectRoot, + timeoutMs: 30_000, + }), + runProcess(process.execPath, ['verify-e2e.mjs'], { + cwd: state.projectRoot, + timeoutMs: 120_000, + }), + Promise.all( + supervisorSwarmIsolatedReviews.map((review) => + fs.readFile(path.join(state.projectRoot, review.path), 'utf8'), ), - fs.readFile( - path.join(state.projectRoot, supervisorSwarmQualityPath), - 'utf8', - ), - runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { - cwd: state.projectRoot, - timeoutMs: 30_000, - }), - runProcess(process.execPath, ['verify-e2e.mjs'], { - cwd: state.projectRoot, - timeoutMs: 120_000, - }), - ]); + ), + ]); assert( designContent === supervisorSwarmDesignContent && qualityContent === supervisorSwarmQualityContent && + isolatedEvidenceContents.every( + (content, index) => + content === supervisorSwarmIsolatedReviews[index].content, + ) && hostVerification.stdout.includes(commandPassedMarker), 'supervisor-swarm-final-artifact-or-host-verification-invalid', ); @@ -8653,19 +9873,41 @@ async function validateSupervisorSwarmEvidence() { assert(steerRecordCount === 0, 'supervisor-swarm-unexpected-steer-record'); assert( - observeSupervisorSwarmInitialProviderOverlap(persistence.agentDb, initial), + observeSupervisorSwarmInitialProviderOverlap( + persistence.agentDb, + initial, + ) && + (!mixedState || + observeSupervisorSwarmStaticIsolatedProviderOverlap( + persistence.agentDb, + initial, + mixedState.instances, + )), 'supervisor-swarm-final-provider-overlap-missing', ); - const protocol = validateSupervisorSwarmNativeProtocol(persistence.agentDb); + if (mixedState) { + assert( + state.supervisorSwarm.staticIsolatedProviderRequestIds.length === 2 && + new Set(state.supervisorSwarm.staticIsolatedProviderRequestIds).size === + 2, + 'supervisor-swarm-mixed-provider-overlap-identity-invalid', + ); + } + const protocol = validateSupervisorSwarmNativeProtocol( + persistence.agentDb, + mixedState?.instances ?? [], + ); const provider = validateSupervisorSwarmProviderLifecycle( persistence.agentDb, deliveries, + mixedState?.instances ?? [], ); const transientRetry = supervisorSwarmTransientRetryEvidence(provider); const actions = validateSupervisorSwarmActionPersistence( persistence.agentDb, deliveries, persistence.contextBundles, + mixedState?.instances ?? [], ); const batchEvents = persistence.events.filter( (event) => @@ -8675,7 +9917,9 @@ async function validateSupervisorSwarmEvidence() { String(event.detail ?? '').includes( `batchId=${state.supervisorSwarm.initialProviderBatch.batchId}`, ) && - String(event.detail ?? '').includes('actionCount=2'), + String(event.detail ?? '').includes( + `actionCount=${isSupervisorSwarmStaticIsolatedAutonomousChatSuite() ? 3 : 2}`, + ), ); assert( batchEvents.length === 1, @@ -8715,9 +9959,19 @@ async function validateSupervisorSwarmEvidence() { const professionalAssistants = professionalMessages.filter( (message) => message.role === 'assistant', ); + const isolatedMessages = persistence.isolatedConversations.flatMap( + (entry) => entry.messages, + ); + const isolatedUsers = isolatedMessages.filter( + (message) => message.role === 'user', + ); + const isolatedAssistants = isolatedMessages.filter( + (message) => message.role === 'assistant', + ); const allConversationMessages = [ ...persistence.supervisorConversation, ...professionalMessages, + ...isolatedMessages, ...persistence.legacyConversation, ]; const duplicateMessageCount = duplicateCount( @@ -8738,12 +9992,17 @@ async function validateSupervisorSwarmEvidence() { supervisorSwarmDesignAgentId, supervisorSwarmQualityAgentId, ]); + const isolatedAgentIds = new Set( + (mixedState?.instances ?? []).map((instance) => instance.instanceId), + ); const professionalUserFacingAssistantCount = [ ...persistence.supervisorConversation, ...persistence.legacyConversation, ].filter( (message) => - message.role === 'assistant' && professionalAgentIds.has(message.agentId), + message.role === 'assistant' && + (professionalAgentIds.has(message.agentId) || + isolatedAgentIds.has(message.agentId)), ).length; let professionalFinalizationStageCount = 0; for (const delivery of deliveries) { @@ -8806,6 +10065,68 @@ async function validateSupervisorSwarmEvidence() { assistants[0], ).stageCount; } + let isolatedFinalizationStageCount = 0; + for (const instance of mixedState?.instances ?? []) { + const conversation = persistence.isolatedConversations.find( + (entry) => + entry.agentId === instance.instanceId && + entry.sessionId === instance.sessionId && + entry.runId === instance.runId, + ); + const expectedMessageId = finalMessageId( + instance.instanceId, + instance.sessionId, + instance.runId, + ); + const assistants = (conversation?.messages ?? []).filter( + (message) => + message.role === 'assistant' && + message.messageId === expectedMessageId && + message.agentId === instance.instanceId, + ); + const childTask = mixedState.isolatedTasks.find( + (task) => + task.agentId === instance.instanceId && + task.sessionId === instance.sessionId && + task.runId === instance.runId && + task.delegationId === instance.delegationId, + ); + assert( + childTask && + assistants.length === 1 && + assistantAudits.filter( + (record) => + record.agentId === instance.instanceId && + record.sessionId === instance.sessionId && + record.messageId === expectedMessageId, + ).length === 1 && + completedAudits.filter( + (record) => + record.agentId === instance.instanceId && + record.sessionId === instance.sessionId && + record.runId === instance.runId && + record.messageId === expectedMessageId, + ).length === 1 && + backgroundCompletedAudits.filter( + (record) => + record.agentId === instance.instanceId && + record.sessionId === instance.sessionId && + record.runId === instance.runId && + record.messageId === expectedMessageId, + ).length === 1, + 'supervisor-swarm-mixed-isolated-finalization-invalid', + ); + isolatedFinalizationStageCount += validateSupervisorSwarmFinalization( + persistence.agentDb, + { + agentId: instance.instanceId, + taskId: childTask.taskId, + sessionId: instance.sessionId, + runId: instance.runId, + }, + assistants[0], + ).stageCount; + } const professionalFinalMessageIds = new Set( deliveries.map((delivery) => finalMessageId( @@ -8815,15 +10136,28 @@ async function validateSupervisorSwarmEvidence() { ), ), ); + const isolatedFinalMessageIds = new Set( + (mixedState?.instances ?? []).map((instance) => + finalMessageId(instance.instanceId, instance.sessionId, instance.runId), + ), + ); assert( supervisorUsers.length === 1 && supervisorAssistants.length === 1 && supervisorAssistants[0].agentId === projectSupervisorAgentId && professionalUsers.length === deliveries.length && professionalAssistants.length === deliveries.length && + isolatedUsers.length === (mixedState?.instances.length ?? 0) && + isolatedAssistants.length === (mixedState?.instances.length ?? 0) && professionalUserFacingAssistantCount === 0 && persistence.legacyConversation.length === 0 && duplicateMessageCount === 0 && + assistantAudits.length === + 1 + deliveries.length + (mixedState?.instances.length ?? 0) && + completedAudits.length === + 1 + deliveries.length + (mixedState?.instances.length ?? 0) && + backgroundCompletedAudits.length === + 1 + deliveries.length + (mixedState?.instances.length ?? 0) && assistantAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -8835,6 +10169,11 @@ async function validateSupervisorSwarmEvidence() { professionalAgentIds.has(record.agentId) && professionalFinalMessageIds.has(record.messageId), ).length === deliveries.length && + assistantAudits.filter( + (record) => + isolatedAgentIds.has(record.agentId) && + isolatedFinalMessageIds.has(record.messageId), + ).length === (mixedState?.instances.length ?? 0) && completedAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -8843,6 +10182,8 @@ async function validateSupervisorSwarmEvidence() { completedAudits.filter((record) => professionalAgentIds.has(record.agentId), ).length === deliveries.length && + completedAudits.filter((record) => isolatedAgentIds.has(record.agentId)) + .length === (mixedState?.instances.length ?? 0) && backgroundCompletedAudits.filter( (record) => record.agentId === projectSupervisorAgentId && @@ -8850,7 +10191,10 @@ async function validateSupervisorSwarmEvidence() { ).length === 1 && backgroundCompletedAudits.filter((record) => professionalAgentIds.has(record.agentId), - ).length === deliveries.length, + ).length === deliveries.length && + backgroundCompletedAudits.filter((record) => + isolatedAgentIds.has(record.agentId), + ).length === (mixedState?.instances.length ?? 0), 'supervisor-swarm-user-reply-ownership-invalid', ); const finalization = validateSupervisorSwarmFinalization( @@ -8863,6 +10207,29 @@ async function validateSupervisorSwarmEvidence() { }, supervisorAssistants[0], ); + const staticClaimObservationIndexes = [ + initialClaims[0].actionId, + repairClaims[0].actionId, + ].map((actionId) => + persistence.agentDb.findIndex( + (record) => + record.recordType === 'agent.runtime.tool_observation' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId && + record.actionId === actionId && + record.tool === 'agent.run_status' && + record.status === 'ok', + ), + ); + assert( + staticClaimObservationIndexes.every( + (index) => index >= 0 && index < finalization.preparedIndex, + ) && + (!mixedState || + (mixedState.claimAuditIndex < mixedState.claimObservationIndex && + mixedState.claimObservationIndex < finalization.preparedIndex)), + 'supervisor-swarm-claim-observation-finalization-order-invalid', + ); const owner = await readSupervisorSwarmExecutionOwner( state.supervisorSwarm.newRunnerBootId, @@ -8917,6 +10284,11 @@ async function validateSupervisorSwarmEvidence() { delivery: deliveries, claim: persistence.claims, professionalConversation: professionalMessages, + isolatedConversation: isolatedMessages, + isolatedGroup: persistence.isolatedGroups, + isolatedInstance: persistence.isolatedInstances, + isolatedResult: persistence.isolatedResults, + isolatedJoinDelivery: persistence.isolatedJoinDeliveries, runtimeState: persistence.runtimeStates, contextBundle: persistence.contextBundles, }; @@ -8984,7 +10356,7 @@ async function validateSupervisorSwarmEvidence() { const secretLeakCount = (state.transcriptScanner?.count ?? 0) + projectSecretLeakCount; assert(secretLeakCount === 0, 'loaded-key-leak-detected'); - const autonomousModeEnabled = isSupervisorSwarmAutonomousChatSuite(); + const autonomousModeEnabled = isSupervisorSwarmInteractiveChatSuite(); const turnReport = state.supervisorSwarm.turnReport; const turnReportPrivateLeakCount = autonomousModeEnabled ? countExactSecrets(Buffer.from(JSON.stringify(turnReport ?? {})), [ @@ -9005,7 +10377,8 @@ async function validateSupervisorSwarmEvidence() { turnReport.sessionId === supervisorSwarmSessionId && turnReport.parentRunId === state.initialRunId && Number.isSafeInteger(turnReport.runtimeCount) && - turnReport.runtimeCount >= 3 && + turnReport.runtimeCount >= + (isSupervisorSwarmStaticIsolatedAutonomousChatSuite() ? 6 : 3) && turnReport.busyRuntimeCount === 0 && turnReport.pendingTaskCount === 0 && turnReport.runningTaskCount === 0 && @@ -9022,9 +10395,11 @@ async function validateSupervisorSwarmEvidence() { return buildSupervisorSwarmEvidence( { - scenario: autonomousModeEnabled - ? 'project-supervisor-autonomous-chat-dual-delegate-single-repair-runner-recovery' - : 'project-supervisor-dual-delegate-single-repair-runner-recovery', + scenario: isSupervisorSwarmStaticIsolatedAutonomousChatSuite() + ? 'project-supervisor-autonomous-chat-static-isolated-single-repair-runner-recovery' + : autonomousModeEnabled + ? 'project-supervisor-autonomous-chat-dual-delegate-single-repair-runner-recovery' + : 'project-supervisor-dual-delegate-single-repair-runner-recovery', targetAgentId: projectSupervisorAgentId, autonomousModeEnabled, autonomousTaskRecipeFree: state.supervisorSwarm.autonomousTaskRecipeFree, @@ -9058,7 +10433,11 @@ async function validateSupervisorSwarmEvidence() { providerRequestTimeoutMs: state.supervisorSwarm.effectiveRequestTimeoutMs, providerMaxRetries: state.supervisorSwarm.effectiveMaxRetries, providerRetryBackoffMs: state.supervisorSwarm.effectiveRetryBackoffMs, - providerAgentPolicies: state.supervisorSwarm.effectiveAgentPolicies, + providerDefaultPolicy: state.supervisorSwarm.effectiveDefaultPolicy, + providerConfiguredAgentPolicyCount: Object.keys( + state.supervisorSwarm.effectiveAgentPolicies, + ).length, + providerAgentPolicies: supervisorSwarmReportAgentPolicies(), isolatedAppDataUsed: true, formalConfigCliCallCount: state.isolatedRunner.sourceConfigCliCallCount, sourceRunnerEndpointUnchanged: false, @@ -9085,6 +10464,26 @@ async function validateSupervisorSwarmEvidence() { state.supervisorSwarm.initialProviderBatch.actionIds.length, initialProviderBatchCompletedEventCount: batchEvents.length, nativeDualDelegatePlanCount: protocol.nativeDualDelegatePlanCount, + mixedModeEnabled: Boolean(mixedState), + mixedSpawnActionCaptured: mixedState + ? isNonEmptyString(state.supervisorSwarm.mixedSpawnActionId) + : false, + mixedSpawnRequestHashStable: mixedState + ? hashJsonValue(mixedState.group.request) === + state.supervisorSwarm.mixedSpawnRequestHash + : false, + mixedSpawnConfirmationRequiredCount: + actions.mixedSpawnConfirmationRequiredCount, + mixedSpawnApprovalCount: actions.mixedSpawnApprovalCount, + mixedSpawnConfirmationOrderValid: + actions.mixedSpawnConfirmationOrderValid, + staticIsolatedProviderOverlapObserved: mixedState + ? state.supervisorSwarm.staticIsolatedProviderOverlapObserved + : false, + staticIsolatedProviderRequestIdentityCount: mixedState + ? new Set(state.supervisorSwarm.staticIsolatedProviderRequestIds).size + : 0, + mixedParentIdentityStable: Boolean(mixedState), initialDeliveryCount: initial.length, repairDeliveryCount: repairs.length, totalDeliveryCount: deliveries.length, @@ -9102,6 +10501,39 @@ async function validateSupervisorSwarmEvidence() { finalQualityArtifactMatched: true, hostVerificationPassed: state.supervisorSwarm.hostVerificationPassed, changedProjectFileCount: changedPaths.length, + isolatedGroupCount: mixedState ? 1 : 0, + isolatedInstanceCount: mixedState?.instances.length ?? 0, + isolatedTaskCount: mixedState?.isolatedTasks.length ?? 0, + isolatedResultCount: mixedState?.results.length ?? 0, + isolatedCompletedResultCount: + mixedState?.results.filter( + (record) => record.result?.status === 'completed', + ).length ?? 0, + isolatedJoinDeliveryCount: mixedState ? 1 : 0, + isolatedClaimedJoinCount: + mixedState?.joinDelivery.status === 'claimed-by-parent' ? 1 : 0, + isolatedParentWakeJoinCount: + mixedState && + isolatedJoinDeliveryTarget(mixedState.joinDelivery) === 'parent-wake' + ? 1 + : 0, + isolatedClaimAuditCount: mixedState ? 1 : 0, + isolatedClaimObservationCount: mixedState ? 1 : 0, + isolatedContinuationTaskCount: mixedState?.continuationTasks.length ?? 0, + isolatedContinuationAuditCount: + mixedState?.continuationAudits.length ?? 0, + isolatedProjectMutationCount: changedPaths.filter((changedPath) => + supervisorSwarmIsolatedReviews.some( + (review) => review.path === changedPath, + ), + ).length, + isolatedMutationActionCount: actions.isolatedMutationActionCount, + isolatedEvidenceFilesUnchanged: mixedState + ? isolatedEvidenceContents.every( + (content, index) => + content === supervisorSwarmIsolatedReviews[index].content, + ) + : false, runnerKillBoundaryObserved: state.supervisorSwarm.runnerKillBoundaryObserved, runnerBootChanged: true, @@ -9111,6 +10543,26 @@ async function validateSupervisorSwarmEvidence() { claimIdentitiesStableAcrossRecovery: true, pendingActionIdentityStableAcrossRecovery: true, providerStartedCountStableAcrossRecovery: true, + isolatedGroupIdentityStableAcrossRecovery: mixedState + ? state.identityStable + : false, + isolatedInstanceIdentitiesStableAcrossRecovery: mixedState + ? state.identityStable + : false, + isolatedResultIdentitiesStableAcrossRecovery: mixedState + ? state.identityStable + : false, + isolatedJoinIdentityStableAcrossRecovery: mixedState + ? state.identityStable + : false, + parentContextStableAcrossRecovery: mixedState + ? state.identityStable + : false, + providerIdentitySetStableAcrossRecovery: mixedState + ? state.identityStable + : false, + staticClaimObservationBeforeFinalization: true, + isolatedClaimObservationBeforeFinalization: mixedState ? true : false, confirmedActionCount: state.supervisorSwarm.confirmedActionCount, ...protocol, providerRequestIdentityCount: provider.requestIdentityCount, @@ -9127,6 +10579,7 @@ async function validateSupervisorSwarmEvidence() { parentFinalReplyProviderRequestCount: provider.parentFinalReplyCount, finalAssistantCount: supervisorAssistants.length, professionalAssistantCount: professionalAssistants.length, + isolatedAssistantCount: isolatedAssistants.length, professionalUserFacingAssistantCount, legacyConversationMessageCount: persistence.legacyConversation.length, completedAuditCount: completedAudits.filter( @@ -9140,12 +10593,56 @@ async function validateSupervisorSwarmEvidence() { backgroundCompletedAuditCount: backgroundCompletedAudits.length, finalizationStageCount: finalization.stageCount, professionalFinalizationStageCount, + isolatedFinalizationStageCount, totalFinalizationStageCount: - finalization.stageCount + professionalFinalizationStageCount, + finalization.stageCount + + professionalFinalizationStageCount + + isolatedFinalizationStageCount, structuredPlanCompletedStepCount: parentRuntime.planSteps.length, duplicateDeliveryCount: duplicateCount( deliveries.map((delivery) => delivery.delegationId), ), + duplicateIsolatedGroupCount: mixedState + ? duplicateCount( + persistence.isolatedGroups.map((group) => group.delegationGroupId), + ) + : 0, + duplicateIsolatedInstanceCount: mixedState + ? duplicateCount( + persistence.isolatedInstances.map( + (instance) => instance.instanceId, + ), + ) + : 0, + duplicateIsolatedResultCount: mixedState + ? duplicateCount( + persistence.isolatedResults.map( + (record) => record.result?.instanceId, + ), + ) + : 0, + duplicateIsolatedJoinDeliveryCount: mixedState + ? duplicateCount( + persistence.isolatedJoinDeliveries.map( + (delivery) => delivery.delegationGroupId, + ), + ) + : 0, + duplicateIsolatedClaimAuditCount: mixedState + ? duplicateCount( + persistence.agentDb + .filter( + (record) => + record.recordType === + 'agent.runtime.agent.isolated_join.claimed_by_parent' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId, + ) + .map( + (record) => `${record.delegationGroupId}:${record.actionId}`, + ), + ) + : 0, duplicateMessageCount, duplicateActionLifecycleCount: actions.duplicateActionLifecycleCount, duplicateExecutingActionIdCount: actions.duplicateExecutingActionIdCount, @@ -9189,6 +10686,10 @@ async function validateSupervisorSwarmEvidence() { paths: [ '.agent/runtime/delegation-deliveries', '.agent/runtime/delegation-claims', + '.agent/runtime/isolated-agents/groups', + '.agent/runtime/isolated-agents/instances', + '.agent/runtime/isolated-agents/results', + '.agent/runtime/isolated-agents/join-deliveries', '.agent/runtime/provider-action-batches', '.agent/runtime/tasks', '.agent/runtime/events', @@ -9215,32 +10716,113 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { const repairDeliveryIds = new Set( repairs.map((delivery) => delivery.delegationId), ); - const relevantRuns = supervisorSwarmRelevantRunKeys(deliveries); + const isolatedRecords = supervisorSwarmParentIsolatedRecords(persistence); + const relevantRuns = supervisorSwarmRelevantRunKeys( + deliveries, + isolatedRecords.instances, + ); const lifecycle = persistence.agentDb.filter( (record) => record.recordType === 'agent.runtime.provider_request.lifecycle' && relevantRuns.has(`${record.agentId}\0${record.runId}`), ); - const [designContent, qualityContent, pending, residualSidecars, owner] = - await Promise.all([ - fs - .readFile( - path.join(state.projectRoot, supervisorSwarmDesignPath), - 'utf8', + const indexedAgentDb = persistence.agentDb.map((record, index) => ({ + record, + index, + })); + const initialBatchActions = + state.supervisorSwarm.initialProviderBatch?.actions ?? []; + const mixedSpawnAction = initialBatchActions.find( + (action) => action.actionId === state.supervisorSwarm.mixedSpawnActionId, + ); + const matchesMixedInitialAction = (record, action) => + action != null && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId && + record.actionId === action.actionId && + record.actionFingerprint === action.actionFingerprint && + record.tool === action.tool; + const mixedSpawnConfirmationRequirements = indexedAgentDb.filter( + ({ record }) => + record.recordType === + 'agent.runtime.provider_action_batch.confirmation_required' && + record.sessionId === supervisorSwarmSessionId && + record.batchId === state.supervisorSwarm.initialProviderBatch?.batchId && + matchesMixedInitialAction(record, mixedSpawnAction), + ); + const mixedSpawnApprovals = indexedAgentDb.filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_confirmation.approved' && + record.sessionId === supervisorSwarmSessionId && + record.confirmedRunId === state.initialRunId && + matchesMixedInitialAction(record, mixedSpawnAction), + ); + const mixedDelegateExecutionIndexes = initialBatchActions + .filter((action) => action.tool === 'agent.delegate') + .flatMap((action) => + indexedAgentDb + .filter( + ({ record }) => + record.recordType === 'agent.runtime.tool_action.executing' && + matchesMixedInitialAction(record, action), ) - .catch(() => ''), - fs - .readFile( - path.join(state.projectRoot, supervisorSwarmQualityPath), - 'utf8', - ) - .catch(() => ''), - findPendingActions(), - readSupervisorSwarmResidualSidecarCounts(), - readJson( - path.join(state.projectRoot, '.agent/runtime/execution-owner.json'), - ).catch(() => null), - ]); + .map(({ index }) => index), + ); + const mixedSpawnReceiptIndexes = indexedAgentDb + .filter( + ({ record }) => + record.recordType === 'agent.runtime.action_receipt' && + record.sessionId === supervisorSwarmSessionId && + record.status === 'ok' && + matchesMixedInitialAction(record, mixedSpawnAction), + ) + .map(({ index }) => index); + const mixedSpawnConfirmationOrderValid = + mixedSpawnConfirmationRequirements.length === 1 && + mixedSpawnApprovals.length === 1 && + mixedDelegateExecutionIndexes.length === 2 && + mixedSpawnReceiptIndexes.length === 1 && + mixedSpawnConfirmationRequirements[0].index < + mixedSpawnApprovals[0].index && + mixedDelegateExecutionIndexes.every( + (index) => mixedSpawnApprovals[0].index < index, + ) && + mixedSpawnApprovals[0].index < mixedSpawnReceiptIndexes[0]; + const [ + designContent, + qualityContent, + pending, + residualSidecars, + owner, + isolatedEvidenceContents, + changedFiles, + ] = await Promise.all([ + fs + .readFile(path.join(state.projectRoot, supervisorSwarmDesignPath), 'utf8') + .catch(() => ''), + fs + .readFile( + path.join(state.projectRoot, supervisorSwarmQualityPath), + 'utf8', + ) + .catch(() => ''), + findPendingActions(), + readSupervisorSwarmResidualSidecarCounts(), + readJson( + path.join(state.projectRoot, '.agent/runtime/execution-owner.json'), + ).catch(() => null), + Promise.all( + supervisorSwarmIsolatedReviews.map((review) => + fs + .readFile(path.join(state.projectRoot, review.path), 'utf8') + .catch(() => ''), + ), + ), + runProcess('git', ['status', '--porcelain=v1', '--untracked-files=all'], { + cwd: state.projectRoot, + timeoutMs: 30_000, + }).catch(() => ({ stdout: '' })), + ]); const parentTasks = persistence.taskSnapshot.all.filter( (task) => task.agentId === projectSupervisorAgentId, ); @@ -9253,6 +10835,53 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { task.delegationId === delivery.delegationId, ), ); + const isolatedTasks = persistence.taskSnapshot.latest.filter((task) => + isolatedRecords.instances.some( + (instance) => + task.agentId === instance.instanceId && task.runId === instance.runId, + ), + ); + const continuationTasks = persistence.taskSnapshot.all.filter( + (task) => + task.source === 'agent-isolated-join' && + isolatedRecords.groups.some((group) => group.joinRunId === task.runId), + ); + const isolatedClaimAudits = persistence.agentDb.filter( + (record) => + record.recordType === + 'agent.runtime.agent.isolated_join.claimed_by_parent' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId, + ); + const isolatedClaimObservations = persistence.agentDb.filter( + (record) => + record.recordType === 'agent.runtime.tool_observation' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId && + record.tool === 'agent.run_status' && + record.status === 'ok' && + String(record.summary ?? '').includes('ready all-join'), + ); + const continuationAudits = persistence.agentDb.filter( + (record) => + record.recordType === 'agent.runtime.agent.isolated_join.dispatched' && + record.parentRunId === state.initialRunId, + ); + const changedPaths = changedFiles.stdout + .split(/\r?\n/u) + .map((line) => line.slice(3).trim()) + .filter(Boolean); + const publicChangedPaths = changedPaths.filter( + (changedPath) => + !changedPath.startsWith('.agent/') && + ![ + sentinelFileName, + '.env', + configFileName, + localConfigFileName, + gitSensitivePath, + ].includes(changedPath), + ); return buildSupervisorSwarmEvidence({ ...baseEvidence, providerModel: state.supervisorSwarm.effectiveModel, @@ -9261,7 +10890,11 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { providerRequestTimeoutMs: state.supervisorSwarm.effectiveRequestTimeoutMs, providerMaxRetries: state.supervisorSwarm.effectiveMaxRetries, providerRetryBackoffMs: state.supervisorSwarm.effectiveRetryBackoffMs, - providerAgentPolicies: state.supervisorSwarm.effectiveAgentPolicies, + providerDefaultPolicy: state.supervisorSwarm.effectiveDefaultPolicy, + providerConfiguredAgentPolicyCount: Object.keys( + state.supervisorSwarm.effectiveAgentPolicies, + ).length, + providerAgentPolicies: supervisorSwarmReportAgentPolicies(), taskCount: persistence.taskSnapshot.all.length, eventCount: persistence.events.length, agentDbRecordCount: persistence.agentDb.length, @@ -9271,6 +10904,10 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { (total, entry) => total + entry.messages.length, 0, ) + + persistence.isolatedConversations.reduce( + (total, entry) => total + entry.messages.length, + 0, + ) + persistence.legacyConversation.length, parentRunCount: new Set(parentTasks.map((task) => task.runId)).size, childRunCount: childTasks.length, @@ -9287,6 +10924,41 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { event.runId === state.initialRunId && event.eventType === 'provider_action_batch.completed', ).length, + mixedModeEnabled: isSupervisorSwarmStaticIsolatedAutonomousChatSuite(), + mixedSpawnActionCaptured: isNonEmptyString( + state.supervisorSwarm.mixedSpawnActionId, + ), + mixedSpawnRequestHashStable: + isolatedRecords.groups.length === 1 && + hashJsonValue(isolatedRecords.groups[0].request) === + state.supervisorSwarm.mixedSpawnRequestHash, + mixedSpawnConfirmationRequiredCount: + mixedSpawnConfirmationRequirements.length, + mixedSpawnApprovalCount: mixedSpawnApprovals.length, + mixedSpawnConfirmationOrderValid, + nativeMixedCollaborationPlanCount: persistence.agentDb.filter( + (record) => + record.recordType === 'agent.runtime.tool_plan.protocol' && + record.agentId === projectSupervisorAgentId && + record.runId === state.initialRunId && + Array.isArray(record.functionNames) && + record.functionNames.filter( + (name) => name === 'runtime_tool_agent_delegate', + ).length === 2 && + record.functionNames.filter( + (name) => name === 'runtime_tool_agent_spawn_isolated', + ).length === 1, + ).length, + staticIsolatedProviderOverlapObserved: + state.supervisorSwarm.staticIsolatedProviderOverlapObserved, + staticIsolatedProviderRequestIdentityCount: new Set( + state.supervisorSwarm.staticIsolatedProviderRequestIds, + ).size, + mixedParentIdentityStable: + isolatedRecords.groups.length === 1 && + isolatedRecords.groups[0].parentAgentId === projectSupervisorAgentId && + isolatedRecords.groups[0].parentSessionId === supervisorSwarmSessionId && + isolatedRecords.groups[0].parentRunId === state.initialRunId, initialDeliveryCount: initial.length, repairDeliveryCount: repairs.length, totalDeliveryCount: deliveries.length, @@ -9323,6 +10995,58 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { finalQualityArtifactMatched: qualityContent === supervisorSwarmQualityContent, hostVerificationPassed: state.supervisorSwarm.hostVerificationPassed, + changedProjectFileCount: publicChangedPaths.length, + isolatedGroupCount: isolatedRecords.groups.length, + isolatedInstanceCount: isolatedRecords.instances.length, + isolatedTaskCount: isolatedTasks.length, + isolatedResultCount: isolatedRecords.results.length, + isolatedCompletedResultCount: isolatedRecords.results.filter( + (record) => record.result?.status === 'completed', + ).length, + isolatedJoinDeliveryCount: isolatedRecords.joinDeliveries.length, + isolatedClaimedJoinCount: isolatedRecords.joinDeliveries.filter( + (delivery) => delivery.status === 'claimed-by-parent', + ).length, + isolatedParentWakeJoinCount: isolatedRecords.joinDeliveries.filter( + (delivery) => isolatedJoinDeliveryTarget(delivery) === 'parent-wake', + ).length, + isolatedClaimAuditCount: isolatedClaimAudits.length, + isolatedClaimObservationCount: isolatedClaimObservations.length, + isolatedContinuationTaskCount: continuationTasks.length, + isolatedContinuationAuditCount: continuationAudits.length, + isolatedProjectMutationCount: publicChangedPaths.filter((changedPath) => + supervisorSwarmIsolatedReviews.some( + (review) => review.path === changedPath, + ), + ).length, + isolatedMutationActionCount: persistence.agentDb.filter( + (record) => + isNonEmptyString(record.actionId) && + isolatedRecords.instances.some( + (instance) => + instance.instanceId === record.agentId && + instance.runId === record.runId, + ) && + supervisorSwarmProjectMutationTools.has(record.tool) && + [ + 'agent.runtime.tool_action.executing', + 'agent.runtime.tool_action.observed', + 'agent.runtime.tool_observation', + 'agent.runtime.action_receipt', + ].includes(record.recordType), + ).length, + isolatedEvidenceFilesUnchanged: isolatedEvidenceContents.every( + (content, index) => + content === supervisorSwarmIsolatedReviews[index].content, + ), + isolatedGroupIdentityStableAcrossRecovery: state.identityStable, + isolatedInstanceIdentitiesStableAcrossRecovery: state.identityStable, + isolatedResultIdentitiesStableAcrossRecovery: state.identityStable, + isolatedJoinIdentityStableAcrossRecovery: state.identityStable, + parentContextStableAcrossRecovery: state.identityStable, + providerIdentitySetStableAcrossRecovery: state.identityStable, + staticClaimObservationBeforeFinalization: false, + isolatedClaimObservationBeforeFinalization: false, providerRequestIdentityCount: new Set( lifecycle.map((record) => record.requestId).filter(Boolean), ).size, @@ -9377,6 +11101,40 @@ async function collectPartialSupervisorSwarmEvidence(baseEvidence) { entry.messages.filter((message) => message.role === 'assistant').length, 0, ), + isolatedAssistantCount: persistence.isolatedConversations.reduce( + (total, entry) => + total + + entry.messages.filter((message) => message.role === 'assistant').length, + 0, + ), + isolatedFinalizationStageCount: persistence.agentDb.filter( + (record) => + record.recordType === 'agent.runtime.finalization.lifecycle' && + isolatedRecords.instances.some( + (instance) => + instance.instanceId === record.agentId && + instance.runId === record.runId, + ), + ).length, + duplicateIsolatedGroupCount: duplicateCount( + isolatedRecords.groups.map((group) => group.delegationGroupId), + ), + duplicateIsolatedInstanceCount: duplicateCount( + isolatedRecords.instances.map((instance) => instance.instanceId), + ), + duplicateIsolatedResultCount: duplicateCount( + isolatedRecords.results.map((record) => record.result?.instanceId), + ), + duplicateIsolatedJoinDeliveryCount: duplicateCount( + isolatedRecords.joinDeliveries.map( + (delivery) => delivery.delegationGroupId, + ), + ), + duplicateIsolatedClaimAuditCount: duplicateCount( + isolatedClaimAudits.map( + (record) => `${record.delegationGroupId}:${record.actionId}`, + ), + ), legacyConversationMessageCount: persistence.legacyConversation.length, pendingActionCount: pending.length, providerActionBatchSidecarCount: residualSidecars.providerActionBatches, @@ -9462,6 +11220,7 @@ function parseArguments(args) { suite === supervisorSwarmSuite || suite === supervisorSwarmTransientRetrySuite || suite === supervisorSwarmAutonomousChatSuite || + suite === supervisorSwarmStaticIsolatedAutonomousChatSuite || suite === steerRunnerKillSuite || processSessionSuites.has(suite), 'unsupported-suite', @@ -9556,6 +11315,19 @@ function effectiveAgentLlmConfig(config, agentId) { }; } +function safeEffectiveAgentLlmPolicy(effective) { + return { + model: effective.model, + apiKind: effective.apiKind, + reasoningEffort: effective.reasoningEffort, + requestTimeoutMs: effective.requestTimeoutMs, + maxRetries: effective.maxRetries, + retryBackoffMs: effective.retryBackoffMs, + stream: effective.stream, + webSearchEnabled: effective.webSearchEnabled, + }; +} + function sameEffectiveAgentLlmWithoutStream(left, right) { return [ 'apiKey', @@ -9594,7 +11366,7 @@ function sameEffectiveAgentLlm(left, right) { function isolatedSuiteProtectsSourceAppData() { return ( isSupervisorSwarmTransientRetrySuite() || - isSupervisorSwarmAutonomousChatSuite() + isSupervisorSwarmInteractiveChatSuite() ); } @@ -9691,6 +11463,17 @@ function isolatedSuiteAppDataProfile() { codePrefix: 'supervisor-swarm-autonomous-chat-appdata', }; } + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + return { + prefix: + '.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-', + sentinelName: + supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName, + sentinelSchema: + supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema, + codePrefix: 'supervisor-swarm-static-isolated-autonomous-chat-appdata', + }; + } if (isSupervisorSwarmSuite()) { return { prefix: '.agent-runtime-real-e2e-supervisor-swarm-', @@ -10260,16 +12043,13 @@ async function prepareIsolatedSuiteAppData({ } if (isSupervisorSwarmSuite()) { const isolatedConfig = await loadConfig(appDataDir); - const effectiveConfigs = [ - projectSupervisorAgentId, - supervisorSwarmDesignAgentId, - supervisorSwarmQualityAgentId, - ].map((agentId) => [ + const requiredAgentIds = supervisorSwarmRequiredAgentIds; + const requiredEffectiveConfigs = requiredAgentIds.map((agentId) => [ agentId, effectiveAgentLlmConfig(isolatedConfig.config, agentId), ]); assert( - effectiveConfigs.every( + requiredEffectiveConfigs.every( ([agentId, effective]) => effective.model === 'gpt-5.5' && effective.apiKind === 'openai_chat' && @@ -10278,7 +12058,7 @@ async function prepareIsolatedSuiteAppData({ effective.requestTimeoutMs > 0 && Number.isSafeInteger(effective.maxRetries) && effective.maxRetries >= - (isSupervisorSwarmAutonomousChatSuite() || + (isSupervisorSwarmInteractiveChatSuite() || (isSupervisorSwarmTransientRetrySuite() && agentId !== supervisorSwarmDesignAgentId) ? 0 @@ -10294,7 +12074,41 @@ async function prepareIsolatedSuiteAppData({ ), 'supervisor-swarm-effective-openai-chat-gpt-5-5-config-invalid', ); - const supervisorEffective = effectiveConfigs.find( + const globalEffective = effectiveAgentLlmConfig( + { llm: isolatedConfig.config.llm }, + '', + ); + if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) { + assert( + globalEffective.model === 'gpt-5.5' && + globalEffective.apiKind === 'openai_chat' && + isNonEmptyString(globalEffective.reasoningEffort) && + Number.isSafeInteger(globalEffective.requestTimeoutMs) && + globalEffective.requestTimeoutMs > 0 && + Number.isSafeInteger(globalEffective.maxRetries) && + globalEffective.maxRetries >= 0 && + globalEffective.maxRetries <= 3 && + Number.isSafeInteger(globalEffective.retryBackoffMs) && + globalEffective.retryBackoffMs > 0 && + ['apiKey', 'baseUrl', 'model'].every((key) => + isNonEmptyString(globalEffective[key]), + ), + 'supervisor-swarm-mixed-default-provider-policy-invalid', + ); + } + const configuredAgentIds = Object.keys( + isPlainObject(isolatedConfig.config.agentLlm) + ? isolatedConfig.config.agentLlm + : {}, + ).filter(isNonEmptyString); + const policyAgentIds = [ + ...new Set([...requiredAgentIds, ...configuredAgentIds]), + ].sort(); + const effectivePolicies = policyAgentIds.map((agentId) => [ + agentId, + effectiveAgentLlmConfig(isolatedConfig.config, agentId), + ]); + const supervisorEffective = requiredEffectiveConfigs.find( ([agentId]) => agentId === projectSupervisorAgentId, )[1]; state.supervisorSwarm.effectiveModel = supervisorEffective.model; @@ -10306,19 +12120,12 @@ async function prepareIsolatedSuiteAppData({ state.supervisorSwarm.effectiveMaxRetries = supervisorEffective.maxRetries; state.supervisorSwarm.effectiveRetryBackoffMs = supervisorEffective.retryBackoffMs; + state.supervisorSwarm.effectiveDefaultPolicy = + safeEffectiveAgentLlmPolicy(globalEffective); state.supervisorSwarm.effectiveAgentPolicies = Object.fromEntries( - effectiveConfigs.map(([agentId, effective]) => [ + effectivePolicies.map(([agentId, effective]) => [ agentId, - { - model: effective.model, - apiKind: effective.apiKind, - reasoningEffort: effective.reasoningEffort, - requestTimeoutMs: effective.requestTimeoutMs, - maxRetries: effective.maxRetries, - retryBackoffMs: effective.retryBackoffMs, - stream: effective.stream, - webSearchEnabled: effective.webSearchEnabled, - }, + safeEffectiveAgentLlmPolicy(effective), ]), ); } @@ -15688,7 +17495,7 @@ async function confirmPendingActions( allowedTools = null, shouldConfirm = () => true, ) { - if (isSupervisorSwarmAutonomousChatSuite()) { + if (isSupervisorSwarmInteractiveChatSuite()) { await confirmSupervisorSwarmPendingActionsInChat( allowedTools, shouldConfirm, @@ -21437,6 +23244,8 @@ function supervisorSwarmEvidenceFieldTemplate() { providerRequestTimeoutMs: null, providerMaxRetries: null, providerRetryBackoffMs: null, + providerDefaultPolicy: {}, + providerConfiguredAgentPolicyCount: 0, providerAgentPolicies: {}, isolatedAppDataUsed: false, formalConfigCliCallCount: 0, @@ -21455,6 +23264,16 @@ function supervisorSwarmEvidenceFieldTemplate() { initialProviderBatchActionCount: 0, initialProviderBatchCompletedEventCount: 0, nativeDualDelegatePlanCount: 0, + mixedModeEnabled: false, + mixedSpawnActionCaptured: false, + mixedSpawnRequestHashStable: false, + mixedSpawnConfirmationRequiredCount: 0, + mixedSpawnApprovalCount: 0, + mixedSpawnConfirmationOrderValid: false, + nativeMixedCollaborationPlanCount: 0, + staticIsolatedProviderOverlapObserved: false, + staticIsolatedProviderRequestIdentityCount: 0, + mixedParentIdentityStable: false, initialDeliveryCount: 0, repairDeliveryCount: 0, totalDeliveryCount: 0, @@ -21469,6 +23288,21 @@ function supervisorSwarmEvidenceFieldTemplate() { finalQualityArtifactMatched: false, hostVerificationPassed: false, changedProjectFileCount: 0, + isolatedGroupCount: 0, + isolatedInstanceCount: 0, + isolatedTaskCount: 0, + isolatedResultCount: 0, + isolatedCompletedResultCount: 0, + isolatedJoinDeliveryCount: 0, + isolatedClaimedJoinCount: 0, + isolatedParentWakeJoinCount: 0, + isolatedClaimAuditCount: 0, + isolatedClaimObservationCount: 0, + isolatedContinuationTaskCount: 0, + isolatedContinuationAuditCount: 0, + isolatedProjectMutationCount: 0, + isolatedMutationActionCount: 0, + isolatedEvidenceFilesUnchanged: false, runnerKillBoundaryObserved: false, runnerBootChanged: false, runnerRecoveredFromPreviousBoot: false, @@ -21477,6 +23311,14 @@ function supervisorSwarmEvidenceFieldTemplate() { claimIdentitiesStableAcrossRecovery: false, pendingActionIdentityStableAcrossRecovery: false, providerStartedCountStableAcrossRecovery: false, + isolatedGroupIdentityStableAcrossRecovery: false, + isolatedInstanceIdentitiesStableAcrossRecovery: false, + isolatedResultIdentitiesStableAcrossRecovery: false, + isolatedJoinIdentityStableAcrossRecovery: false, + parentContextStableAcrossRecovery: false, + providerIdentitySetStableAcrossRecovery: false, + staticClaimObservationBeforeFinalization: false, + isolatedClaimObservationBeforeFinalization: false, confirmedActionCount: 0, toolPlanProtocolCount: 0, nativeRuntimeToolPlanCount: 0, @@ -21518,6 +23360,7 @@ function supervisorSwarmEvidenceFieldTemplate() { parentFinalReplyProviderRequestCount: 0, finalAssistantCount: 0, professionalAssistantCount: 0, + isolatedAssistantCount: 0, professionalUserFacingAssistantCount: 0, legacyConversationMessageCount: 0, completedAuditCount: 0, @@ -21525,9 +23368,15 @@ function supervisorSwarmEvidenceFieldTemplate() { backgroundCompletedAuditCount: 0, finalizationStageCount: 0, professionalFinalizationStageCount: 0, + isolatedFinalizationStageCount: 0, totalFinalizationStageCount: 0, structuredPlanCompletedStepCount: 0, duplicateDeliveryCount: 0, + duplicateIsolatedGroupCount: 0, + duplicateIsolatedInstanceCount: 0, + duplicateIsolatedResultCount: 0, + duplicateIsolatedJoinDeliveryCount: 0, + duplicateIsolatedClaimAuditCount: 0, duplicateMessageCount: 0, duplicateActionLifecycleCount: 0, duplicateExecutingActionIdCount: 0, @@ -22570,7 +24419,7 @@ function isSupervisorSwarmSuite() { return ( state.suite === supervisorSwarmSuite || isSupervisorSwarmTransientRetrySuite() || - isSupervisorSwarmAutonomousChatSuite() + isSupervisorSwarmInteractiveChatSuite() ); } @@ -22582,6 +24431,17 @@ function isSupervisorSwarmAutonomousChatSuite() { return state.suite === supervisorSwarmAutonomousChatSuite; } +function isSupervisorSwarmStaticIsolatedAutonomousChatSuite() { + return state.suite === supervisorSwarmStaticIsolatedAutonomousChatSuite; +} + +function isSupervisorSwarmInteractiveChatSuite() { + return ( + isSupervisorSwarmAutonomousChatSuite() || + isSupervisorSwarmStaticIsolatedAutonomousChatSuite() + ); +} + function isSteerRunnerKillSuite() { return state.suite === steerRunnerKillSuite; } @@ -25160,6 +27020,20 @@ function hashValue(value) { .digest('hex'); } +function canonicalJsonValue(value) { + if (Array.isArray(value)) return value.map(canonicalJsonValue); + if (!isPlainObject(value)) return value; + return Object.fromEntries( + Object.keys(value) + .sort() + .map((key) => [key, canonicalJsonValue(value[key])]), + ); +} + +function hashJsonValue(value) { + return hashValue(JSON.stringify(canonicalJsonValue(value))); +} + function codedError(code, cause) { const error = new Error(code, cause ? { cause } : undefined); error.code = code; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 8c0891a34..f9f9b0bb8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -33704,7 +33704,7 @@ pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt_for_agent( return prompt; } let prompt = format!( - "{prompt}\n\n你当前是项目唯一面向用户的 Project Supervisor,并拥有最终回复权。每一轮都必须把用户原始目标视为最高层业务目标,专业 Agent 回执只能补充证据,不能把回执内容改写成新目标。总控不能替代已有专业角色完成其领域交付:只要仓库目标同时包含两个以上互不依赖的专业方向,就必须自行查看静态角色目录,选择最匹配的不同专业 Agent,并在同一个 native planning 批次用带 acceptanceCriteria 和 expectedArtifacts 的 agent.delegate 发起委派,让这些方向并行;用户不需要点名 Agent、指定数量或提醒并行。只有没有匹配专业角色、纯协调工作或一两步轻量读取时才由总控直接处理。互不重叠的临时并行检查通过 agent.spawn_isolated 分派;已有委派未收束时不要重复委派。需要等待专业 Agent 时返回空 response,让 Runtime 的 delegate/all-join 完成屏障保持同一父 run;取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。只在所有必要回执已认领、所有必要返工也已认领、项目副作用已验证且没有待确认动作或待回答请求时给用户最终回复。不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。" + "{prompt}\n\n你当前是项目唯一面向用户的 Project Supervisor,并拥有最终回复权。每一轮都必须把用户原始目标视为最高层业务目标,专业 Agent 回执只能补充证据,不能把回执内容改写成新目标。总控不能替代已有专业角色完成其领域交付:只要仓库目标同时包含两个以上互不依赖的专业方向,就必须自行查看静态角色目录,选择最匹配的不同专业 Agent,并在同一个 native planning 批次用带 acceptanceCriteria 和 expectedArtifacts 的 agent.delegate 发起委派,让这些方向并行;用户不需要点名 Agent、指定数量或提醒并行。只有没有匹配专业角色、纯协调工作或一两步轻量读取时才由总控直接处理。互不重叠的临时并行检查通过 agent.spawn_isolated 分派;当同一目标同时需要边界清晰的专业委派和互不重叠的临时检查时,必须把两类协作放进同一个 native planning 批次一次性提交,不能拆成先后轮次。提交首个协作批次前,先分别完整枚举当前目标中的长期专业交付和临时隔离检查;两类都非空时,遗漏任一类的批次都不得提交。已有委派未收束时不要重复委派。需要等待专业 Agent 时返回空 response,让 Runtime 的 delegate/all-join 完成屏障保持同一父 run;取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。只在所有必要回执已认领、所有必要返工也已认领、项目副作用已验证且没有待确认动作或待回答请求时给用户最终回复。不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。" ); format!( "{prompt}\n\n普通 agent.run_status 的 claimedDelegateContracts 只提供已认领合同目录。语义复核或返工前必须用原 delegationId 再调用 agent.run_status,读取 claimedDelegateContract 中未截断的 acceptanceCriteria 和 expectedArtifacts,并在 repair agent.delegate 中逐项原样提交。若返工因合同未完整继承而失败,失败 observation 中的 claimedDelegateContract 是同一 durable delivery 的权威快照,必须逐项据此修正;只有该字段缺失或身份不确定时才按同一 delegationId 重读,不得无目标地重复 run_status 或从 action_history 摘要猜测。" diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index ecd9994a9..455014608 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -14768,6 +14768,197 @@ async fn project_supervisor_mixed_waiting_recovery_does_not_plan_until_all_join_ fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn project_supervisor_mixed_waiting_recovery_does_not_plan_until_static_delivery_ready() { + use platform_agent::game_creation::{ + GameCreationIsolatedAgentArtifact, GameCreationIsolatedAgentChildResult, + GameCreationIsolatedAgentChildSpec, GameCreationIsolatedAgentJoinMode, + GameCreationIsolatedAgentResultStatus, GameCreationIsolatedAgentSpawnRequest, + }; + + 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![final_tool_plan_response( + "不应在 static delivery ready 前请求 Provider", + )], + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "agentLlm": {{ + "project-supervisor": {{ + "apiKey": "mixed-reverse-waiting-key", + "baseUrl": {base_url:?}, + "model": "mixed-reverse-waiting-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + let task = "等待已认领的动态隔离 all-join 与尚未完成的静态专业回执"; + let parent_run_id = "project-supervisor-mixed-reverse-waiting-run"; + let mut parent_state = start_game_creator_agent_runtime_task_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + task, + parent_run_id, + "agent-chat", + "等待反向混合委派", + vec!["取得两类交付后统一收束".to_string()], + ) + .expect("start reverse mixed waiting parent"); + + let static_delivery = new_static_delegate_delivery( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &parent_state.session_id, + parent_run_id, + "project-supervisor-mixed-reverse-static-action", + "project-supervisor-mixed-reverse-static-delivery", + "design-director", + "project-supervisor-mixed-reverse-static-session", + "project-supervisor-mixed-reverse-static-run", + ); + create_or_read_static_delegate_delivery_at(&root, &static_delivery) + .expect("create waiting static delivery"); + + fs::create_dir_all(root.join("game/mixed-reverse")).expect("create mixed evidence directory"); + fs::write( + root.join("game/mixed-reverse/evidence.txt"), + "mixed reverse evidence\n", + ) + .expect("write mixed evidence"); + let isolated_request = GameCreationIsolatedAgentSpawnRequest { + children: vec![GameCreationIsolatedAgentChildSpec { + template_agent_id: "code-prototype".to_string(), + task: "完成独立 mixed 只读检查".to_string(), + acceptance_criteria: vec!["mixed 只读检查已完成".to_string()], + expected_artifacts: vec!["game/mixed-reverse/evidence.txt".to_string()], + write_scopes: vec!["game/mixed-reverse/**".to_string()], + }], + join_mode: GameCreationIsolatedAgentJoinMode::All, + }; + let group = create_or_read_isolated_group_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + &parent_state.session_id, + "project-supervisor-mixed-reverse-isolated-action", + &isolated_request, + ) + .expect("create reverse mixed isolated group"); + let instance = resolve_isolated_agent_instance_at(&root, &group.instance_ids[0]) + .expect("resolve reverse mixed isolated instance"); + record_isolated_child_result_at( + &root, + &GameCreationIsolatedAgentChildResult { + delegation_id: instance.delegation_id.clone(), + instance_id: instance.instance_id.clone(), + template_agent_id: instance.template_agent_id.clone(), + run_id: instance.run_id.clone(), + status: GameCreationIsolatedAgentResultStatus::Completed, + summary: "mixed 只读检查已完成".to_string(), + artifacts: vec![GameCreationIsolatedAgentArtifact { + path: "game/mixed-reverse/evidence.txt".to_string(), + sha256: "c".repeat(64), + }], + evidence: Vec::new(), + verified_revision: None, + error: None, + }, + ) + .expect("record reverse mixed isolated result") + .expect("reverse mixed isolated join ready"); + let claim_action_id = "project-supervisor-mixed-reverse-run-status-action"; + let claimed_observation = observe_agent_runtime_run_status( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + Some(claim_action_id), + &serde_json::json!({ "scope": "self" }), + ); + assert_eq!(claimed_observation.status, "ok"); + assert!(claimed_observation + .detail + .as_deref() + .is_some_and(|detail| detail.contains("readyIsolatedJoins"))); + assert!(isolated_join_completion_barrier_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + ) + .expect("read claimed isolated barrier") + .is_none()); + let waiting_static = static_delegate_completion_barrier_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + ) + .expect("read waiting static barrier"); + assert_eq!(waiting_static.waiting_count, 1); + + parent_state.status = "running".to_string(); + parent_state.phase = "waiting-for-isolated-join".to_string(); + parent_state.current_action = "等待动态隔离 Agent 的 all-join".to_string(); + parent_state.waiting_on = "静态与隔离 Agent 完成".to_string(); + parent_state.next_step = "保持等待,不请求 Provider".to_string(); + parent_state.loop_iteration = 5; + parent_state.max_loop_iterations = 18; + append_game_creator_agent_runtime_task(&root, &parent_state) + .expect("append reverse mixed waiting parent task"); + write_game_creator_agent_runtime_state(&root, &parent_state) + .expect("persist reverse mixed waiting parent state"); + let observations = vec![claimed_observation]; + let context = build_game_creator_agent_runtime_context_bundle( + &root, + &parent_state, + task, + &AgentRuntimeToolPlan::default(), + &observations, + 5, + &AgentRuntimeContextWindowTracker::default(), + ) + .expect("build reverse mixed waiting context"); + write_game_creator_agent_runtime_context_bundle(&root, &context) + .expect("persist reverse mixed waiting context"); + + resume_game_creator_agent_background_tasks_at(&root) + .expect("resume reverse mixed waiting parent"); + let deadline = std::time::Instant::now() + Duration::from_secs(2); + loop { + let runtime = + read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) + .expect("read reverse mixed waiting parent") + .state; + if runtime.phase == "waiting-for-delegate-receipts" { + assert_eq!(runtime.run_id, parent_run_id); + assert_eq!(runtime.loop_iteration, 5); + assert_eq!(runtime.max_loop_iterations, 18); + break; + } + assert!( + std::time::Instant::now() < deadline, + "mixed parent did not switch to static waiting" + ); + tokio::time::sleep(Duration::from_millis(10)).await; + } + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + resume_game_creator_agent_background_tasks_at(&root) + .expect("repeat reverse mixed waiting parent resume"); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + let stable = + read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) + .expect("read stable reverse mixed waiting parent") + .state; + assert_eq!(stable.run_id, parent_run_id); + assert_eq!(stable.phase, "waiting-for-delegate-receipts"); + assert_eq!(stable.loop_iteration, 5); + + fs::remove_dir_all(root).ok(); +} + #[test] fn repository_context_v1_pending_fingerprint_blocks_project_mutation() { let root = unique_project_path(); @@ -45182,6 +45373,8 @@ async fn project_supervisor_prompts_are_total_control_and_reject_isolated_templa "专业 Agent 回执只能补充证据", "总控不能替代已有专业角色", "同一个 native planning 批次", + "必须把两类协作放进同一个 native planning 批次一次性提交", + "两类都非空时,遗漏任一类的批次都不得提交", "用户不需要点名 Agent", "agent.delegate", "readyDelegateReceipts", diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0b8fc4752..a990797c8 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-07-17 AI 游戏创作 V1.31 使用同一父 run 收束静态与隔离协作 + +- 背景:V1.30 已证明 Supervisor 能在无编排配方的真实终端任务中自主选择多个 static 专业 Agent,但尚未证明同一父 run 同时存在 static delivery/claim 与 isolated all-join 时,等待、唤醒、恢复和唯一 finalization 可以组合。两类协议分别通过不能替代组合证据。 +- 决策:继续复用 `project-supervisor`、`--swarm-chat`、External Runner 和既有 durable 事实源,不新建第二套调度器或结果协议。用户任务只描述业务范围;仓库规则可声明验证要求和安全禁用边界,但不写 Agent 编排工具、调用顺序或 Runner 配方。Supervisor 在同一目标同时包含长期专业交付与临时隔离检查时,首个协作批次不得遗漏任一类。 +- 完成边界:static delivery/claim 与 isolated group/result/join delivery 保持各自状态机,但全部绑定同一个 parent Agent/Session/run;waiting phase 只投影当前首个 blocker,Runner 恢复和 finalization 必须在项目锁内重新枚举两类事实源。两类结果都已认领且其它 blocker 清零后,原 Supervisor run 才能写唯一用户 assistant。 +- 验证:确定性基线统一运行 `project_supervisor_mixed_`,真实行为运行 `npm run agc:mixed-swarm-e2e -- --config-dir `。失败尝试不得和后续轮次拼接;详细拓扑、一次性计数与当前 PASS 报告只维护在 Runtime V1.31 技术方案,不复制进长期共享决策。 +- 关联文档:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。 + ## 2026-07-17 AI 游戏创作 V1.30 使用自主 Supervisor 终端门禁和语义消息收敛 - 背景:V1.28 已证明预置双专业方向下的合同委派、repair、Runner 恢复和唯一回复,V1.29 已证明受控瞬态重试;但二者都没有证明 Supervisor 在用户不提供 Agent ID、数量、并行或 repair 配方时会自主编排,也没有把重复 `agent.message` 的持久幂等与后台 loop 有界收敛串成完整证据。 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index d0ef17a9b..296554fcb 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -120,6 +120,17 @@ npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-rea 终局必须同时得到 `turn.report=settled`、新增 Supervisor assistant 恰好 1、专业 assistant 只在内部 Session、队列/确认/用户输入/reconciliation/sidecar 全 0,以及重复 action/delivery/message/receipt/Provider lifecycle 和正文/凭据/绝对路径泄漏全 0。`agent.message` 完整回归还要证明同语义消息只写一次、后续 no-op 不刷新进展、6 轮后保持未完成计划并诚实 `budget-exhausted`。隔离 AppData 必须位于正式 AppData 同级并自动清理;失败尝试与后续 PASS 不能拼接,`maxRetries=0` 下的真实外部 Provider 失败应单独保留为失败证据。 +### AI 游戏创作静态与隔离混合 Swarm 复验 + +修改 static delivery/claim、isolated group/result/all-join、父 waiting phase、`agent.run_status` 混合认领、Supervisor 混合协作策略、Runner 恢复或 finalization blocker 后,先运行三条确定性回归,再运行独立真实终端 suite: + +```bash +cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_mixed_ -- --nocapture --test-threads=1 +npm run agc:mixed-swarm-e2e -- --config-dir +``` + +业务任务只写正式交付、临时检查和验证等结果范围,不写 Agent ID、数量、并行方式、具体工具或 Runner 操作。真实 suite 必须以单个独立 run 证明两类 Provider 真重叠、两类 durable 记录绑定同一父 Session/run、认领 observation 早于唯一父 finalization、Runner 恢复身份稳定、isolated 实际零 mutation、唯一用户回复、零重复/残留/泄漏并完成 sentinel 清理;不能把 isolated 的 suite 零写入要求解释成生产权限层面的只读沙箱。命令未运行、退出非零或报告字段不完整时不得标记 PASS,也不得把失败轮和后续成功轮拼接。 + ### AI 游戏创作 Runtime V1.10 持久进程定向复验 V1.10 的 PTY 只通过四个 Runner-owned 工具开放;不要把 V1.2 `command.exec` 改成长驻入口。最小工具输入保持结构化: diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md index 9315078c7..56d8d4b48 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md @@ -1148,6 +1148,21 @@ V1.30 新增独立 `supervisor-swarm-autonomous-chat` 真实 Provider suite, V1.30 至此只证明自主 static 专业编排与真实终端聊天可组合;同一父 run 的 static delivery + isolated all-join 真实组合恢复,以及 Tauri/WebView 宿主级 Supervisor E2E 仍需各自独立门禁。 +## V1.31 Project Supervisor 静态与隔离子 Agent 混合协作门禁 + +V1.31 新增独立 `supervisor-swarm-static-isolated-autonomous-chat` 真实 Provider suite,用于验证同一个 `project-supervisor` Session/run 可以自主同时使用 static `agent.delegate` 与 dynamic `agent.spawn_isolated(joinMode=all)`,并由同一个完成屏障、恢复链和 finalization journal 唯一收束。该 suite 复用 V1.28-V1.30 的 static delivery/claim/repair、isolated group/result/join delivery、External Runner、`--swarm-chat`、隔离 AppData 和零泄漏事实源,不新增调度器、对话入口、结果 sidecar 或第二种用户回复。 + +- 用户任务只明确业务范围包括仓库既有的正式交付、临时检查和实际验证,不得出现 Agent ID、数量、并行、static/isolated、delegate/spawn/join、repair 次数、run/action 身份或 Runner 操作。一次性仓库规则可声明既有验证要求和安全禁用边界,但不得指定 Agent 编排工具、调用顺序或 Runner 配方。Supervisor 系统策略要求提交首个协作批次前分别枚举长期专业交付与临时隔离检查;两类都非空时不得遗漏任一类。 +- 首个形成协作的 native Provider 批次必须包含两个不同 static `agent.delegate` 与一个 `agent.spawn_isolated`;spawn 请求固定 `joinMode=all`,三个 child 的 expectedArtifacts 指向三个既有证据文件,writeScopes 互不重叠。批次必须先停在 `waiting-confirmation / nextActionIndex=0`,唯一 `provider_action_batch.confirmation_required` 与唯一 approval 都绑定 spawn 和原批次,approval 必须早于三个 action 的任何真实副作用;随后每个 action 的 side effect、observed、receipt 和终态 observation 严格按 `actionIndex` 推进。两个 static child 的 Provider 区间必须真实重叠,且至少一个 static child 与一个 isolated child 的 Provider 区间也必须真实重叠,不能用 action 时间、同 Agent retry 或格式修复冒充并行。 +- static 与 isolated 继续使用各自 durable 事实源。static 的 2 份初始 delivery 与 1 份 repair 分别由两个 Observed claim 认领 2/1 份 receipt;isolated 形成 1 个 group、3 个唯一 instance/result、1 个 all-join delivery,并由同一父 run 的一个 `agent.run_status` action 认领。两类记录的 parent Agent/Session/run 必须一致;该 suite 要求 isolated child 的项目 mutation action 和实际文件修改均为 0,但这不是把生产 isolated 权限模型改成只读沙箱。 +- completion blocker 在项目锁内先检查 plan/Goal,再按 `provider-action-batch -> process -> isolated join -> static receipts` fail closed,随后检查 response revision 和 verification。单一 waiting phase 只是当前首个 blocker 的 UI 投影,不是事实源;Runner 恢复和每次 finalization 都必须重新枚举两类 barrier。`project_supervisor_mixed_waiting_recovery_does_not_plan_until_all_join_ready`、`project_supervisor_mixed_waiting_recovery_does_not_plan_until_static_delivery_ready` 与 `project_supervisor_mixed_run_status_recovery_reuses_partial_isolated_claim_after_revision_drift` 三条确定性回归分别覆盖双向等待切换和同 action 部分认领恢复,不能代替真实 Provider suite。 +- repair 待确认动作持久化后执行 pidfd Runner 强杀。强杀前后必须逐项比较 static delivery/claim、isolated group/instance/result/join delivery、父 task/context、pending action 和完整 Provider started identity set;boot 必须变化,任何 child/action/receipt/join 不得重放。static claim 与 isolated join claim 的 observation 都必须早于父 finalization prepared,父计划、验证、确认、用户输入、process、两类 barrier 全部清零后,原 Supervisor run 才能写唯一 assistant 和 `turn.report=settled`。 +- 最终报告必须单独给出两类 Provider 重叠、group/instance/result/join/claim、两类 parent identity、跨恢复身份稳定、isolated 项目修改、重复 continuation/group/join/action/receipt、残留 sidecar 和公共泄漏计数。任何一项缺失、从不同尝试拼接、用户/仓库规则含编排配方、isolated 修改项目、未认领即 final 或额外用户回复都必须 FAIL;确定性回归或 V1.30 static PASS 不能替代该门禁。 + +2026-07-17 最终正式 `openai_chat / gpt-5.5` 独立轮 **PASS**。该轮形成 149 条 task、261 条 event、451 条 Agent DB 和 14 条会话消息;67 个 Provider request identity 全部唯一闭合为 `67 started / 67 terminal / 67 completed / 0 failed`,37/37 个成功工具计划与 24/24 个格式修复均为 `native_runtime_tools`,wrapper/text fallback 为 0。首批 3 个 action 的 confirmation-required/approval 各 1 且时序有效,static-static 与 static-isolated Provider 区间均真实重叠;static 形成 2 份初始 delivery、1 份 repair 和 2 个 Observed claim,isolated 形成 1 个 group、3 个 completed result、1 个 parent-wake claimed join,全部绑定同一父 Session/run。 + +Runner pidfd 强杀后的 boot、父 context、pending action、两类 durable identity 和完整 Provider identity set 均稳定恢复;isolated mutation action、isolated 文件修改、continuation、重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle、残留 sidecar和公共正文/凭据/绝对路径/报告泄漏均为 0。`turn.report=settled`,父计划 4/4 completed,正式 Supervisor assistant 恰好 1,内部专业 assistant 3、isolated assistant 3,最终 disposable 项目与隔离 AppData 均自动清理。此前不完整编排、child 合同不满足、外部 Provider 终态失败和调试验收器误判均各自作为独立失败轮停止,未与本轮 PASS 拼接。 + ## 验收命令 - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture` @@ -1157,6 +1172,8 @@ V1.30 至此只证明自主 static 专业编排与真实终端聊天可组合; - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml mcp_ -- --nocapture` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml provider_action_batch_ -- --nocapture` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml parallel_read_batch_ -- --nocapture` +- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_mixed_ -- --nocapture --test-threads=1` +- `npm run agc:mixed-swarm-e2e -- --config-dir ` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml swarm_cli::tests -- --nocapture` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run -- --nocapture` - `npm run ai-game-creator-shell:typecheck` diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 725d57207..919be019e 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -589,5 +589,6 @@ game-project/ - V1.28 已于 2026-07-17 完成正式 `openai_chat / gpt-5.5` `supervisor-swarm` PASS:同一 native 批次双专业委派、真实 Provider 重叠、2 份初始 delivery、1 次 targeted contract read、1 份唯一 repair、pidfd Runner 强杀/boot 恢复、同一父 Session/run、唯一 Supervisor assistant 和 3 条内部专业 assistant 全部成立。报告包含 46/46 闭合且 completed 的 Provider lifecycle,成功计划 24/24、格式修复 20/20 全为原生工具协议;重复、残留 sidecar、Provider payload、私有正文、API Key、项目/正式配置绝对路径、报告、secret 与 lure 泄漏均为 0。正式 AppData 零 CLI 调用且源 Runner endpoint 未变化;规范复验命令为 `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite supervisor-swarm`。 - 2026-07-17 追加 `supervisor-swarm-transient-retry` 受控故障门禁:一次性本地回环代理只让 `design-director` 首个请求在正文转发前断线,并暂停后继请求,直到验收器确认唯一 failed lifecycle、唯一 retry audit、新 `-transient-1` identity,以及 action/receipt/子委派/claim/assistant/pending/revision/upstream forwarding 全为 0。E2E 启动 CLI/Runner 时会把 loopback 合并进 `NO_PROXY / no_proxy`,避免继承的系统 HTTP 代理接触故障门禁请求中的凭据和正文。最终加强版正式 `gpt-5.5` 报告为 46/46 lifecycle 闭合、45 completed/1 failed/1 retry;代理观察到的 10 个目标 Agent 请求与该 Agent lifecycle 数量一致,放行后完整双 Agent、唯一 repair、Runner 强杀恢复、唯一 Supervisor assistant、零重复/残留/泄漏继续 PASS。隔离 AppData 创建在正式目录同级,source-dir guard 与 `sourceAppDataDirectoryUntouched` 证明正式 AppData 未被写入,源配置与 endpoint 身份保持只读,失败 partial report 保留已取得的 retry checkpoint,代理与隔离现场全部清理。该 suite 只证明显式重试和既有协作链可组合,不把预置双 Agent fixture 扩大解释为自主编排;无 Agent ID/并行/repair 配方的自主 suite、真实 `agc:chat`、static+isolated 组合和 Tauri 宿主 E2E 仍待单独验收。 - 2026-07-17 V1.30 `supervisor-swarm-autonomous-chat` 最终真实 PASS:唯一业务任务和仓库规则均不包含编排配方;Supervisor 在 1 个 native 批次自主选择两个不同专业 Agent,真实 Provider 重叠后形成 2 个初始 delivery,并基于 acceptance criteria 自主创建 1 个继承原合同的 repair。最终报告包含 110 条 task、197 条 event、330 条 Agent DB、9 条会话消息和 `51 started / 51 terminal / 51 completed / 0 failed` Provider lifecycle;28/28 成功计划与 19/19 格式修复均为原生工具协议,父计划 4/4 completed,Runner pidfd 强杀恢复后身份稳定,`turn.report` settled、正式 assistant 1、内部专业 assistant 3,重复、sidecar、正文、密钥、诱饵、项目/配置路径和报告泄漏均为 0。两次较早的独立尝试在 `maxRetries=0` 下各遇到 1 次外部 Provider 终态失败并中止,未与最终 PASS 拼接。 +- 2026-07-17 Runtime V1.31 的同父 run 混合协作门禁已完成独立真实 PASS;详细业务任务边界、首批 confirmation gate、static/isolated durable 合同、三条确定性回归、真实报告数字和失败轮隔离记录统一以同一 Runtime 文档的“V1.31 Project Supervisor 静态与隔离子 Agent 混合协作门禁”为事实源。App 侧复验入口为 `npm run agc:mixed-swarm-e2e -- --config-dir `,不在实施计划重复维护一次性拓扑和计数。 - `agent.message` 使用来源 Agent/run、目标 Agent/Session 和清洗后正文 SHA-256 形成稳定语义身份。同一语义消息只允许写 1 条目标 tool conversation、1 条 `conversation.message` 和 1 条 `agent.runtime.agent.message`;后续 Runtime action 仍完整落账,但返回 `messageAppended=false` 且不算新的 loop 进展。专业 Agent 不得用重复消息替代最终回执;持续重复时最多经过当前 6 轮停滞窗口即以 `loop-budget-exhausted` 失败,保留 `in_progress` 计划且不写 completed。完整后台回归同时断言 6 个 actionId、同一 action fingerprint、6 组 action/observation/receipt、消息持久化唯一、receipt 零正文、第 7 次 Provider 请求为 0。 - 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。 diff --git a/package.json b/package.json index 366520bbc..bbab1985e 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "ai-game-creator-shell:agent-task": "npm --prefix apps/ai-game-creator-shell run agent-task --", "agc:chat": "npm --prefix apps/ai-game-creator-shell run chat --", "agc:swarm": "npm --prefix apps/ai-game-creator-shell run swarm --", + "agc:mixed-swarm-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:mixed-swarm-real-e2e --", "ai-game-creator-shell:agent-run": "npm --prefix apps/ai-game-creator-shell run agent-run --", "ai-game-creator-shell:agent-run:smoke": "npm --prefix apps/ai-game-creator-shell run agent-run:smoke", "ai-game-creator-shell:agent-runtime:real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:real-e2e --",