be89296492
拆分 App 认证、壳层、运行配置与项目摘要模块 拆分 Tauri 项目能力与 Rust 测试领域模块 拆分界面测试与 Agent Runtime 真实 E2E 套件 补充源码扫描和客户端模块化文档约定
721 lines
25 KiB
JavaScript
721 lines
25 KiB
JavaScript
import {
|
|
answerRemainingInteractiveQuestions,
|
|
assert,
|
|
codedError,
|
|
hashValue,
|
|
isFailedTask,
|
|
sleep,
|
|
} from '../assertions/core.mjs';
|
|
import {
|
|
countExactSecrets,
|
|
disposableProjectPathVariants,
|
|
duplicateCount,
|
|
isNonEmptyString,
|
|
sumObjectValues,
|
|
} from '../assertions/runtime.mjs';
|
|
import { path } from '../dependencies.mjs';
|
|
import {
|
|
claimOwnedRunner,
|
|
ensureOwnedRunnerStableKillSupport,
|
|
prepareIsolatedSuiteAppData,
|
|
} from '../harness/app-data.mjs';
|
|
import { listFiles, readJson, readOptionalJsonl } from '../harness/io.mjs';
|
|
import {
|
|
prepareCliBinary,
|
|
runCli,
|
|
startInteractiveCli,
|
|
waitForInteractiveCliExit,
|
|
waitForInteractiveCliOutput,
|
|
writeInteractiveCliLine,
|
|
} from '../harness/process.mjs';
|
|
import { seedDisposableProject } from '../harness/project.mjs';
|
|
import {
|
|
agentConversationPath,
|
|
countSecretsInProject,
|
|
countSensitiveValuesBySurface,
|
|
killRunnerOnce,
|
|
readAllRuntimeEvents,
|
|
readRunnerStatus,
|
|
readTaskSnapshot,
|
|
runnerBootId,
|
|
waitForRunnerBootChange,
|
|
} from '../harness/runtime.mjs';
|
|
import {
|
|
projectSupervisorAgentId,
|
|
state,
|
|
userInputAnswerCanary,
|
|
userInputAnswerText,
|
|
userInputRuntimeSuite,
|
|
} from '../runtime-state.mjs';
|
|
|
|
export function buildUserInputTaskPrompt() {
|
|
return '为当前项目拟定一份首版角色规范方案。产品只有一项会实质改变方案的取舍尚未决定:美术表现应选轻量像素风还是手绘风。平台、受众、世界观和交付范围都已确定,不要顺带追问其它信息;不要猜测这项取舍,也不要修改文件、执行命令或调用外部工具。任务未完成时只需向用户澄清这一项取舍,获得明确答复后在同一轮给出三点简短方案。';
|
|
}
|
|
|
|
export function assertUserInputTaskPrompt(task) {
|
|
assert(
|
|
task.includes('只有一项') &&
|
|
task.includes('不要猜测') &&
|
|
task.includes('同一轮'),
|
|
'user-input-required-task-boundary-missing',
|
|
);
|
|
for (const forbidden of [
|
|
'user.input_request',
|
|
'waiting-for-user-input',
|
|
'requestId',
|
|
'responseId',
|
|
userInputAnswerCanary,
|
|
]) {
|
|
assert(!task.includes(forbidden), 'user-input-task-recipe-leak');
|
|
}
|
|
}
|
|
|
|
export async function runUserInputRuntimeE2e() {
|
|
await ensureOwnedRunnerStableKillSupport();
|
|
await seedDisposableProject();
|
|
state.cliBinary = await prepareCliBinary();
|
|
await prepareIsolatedSuiteAppData();
|
|
state.isolatedRunner.launchAttempted = true;
|
|
|
|
const task = buildUserInputTaskPrompt();
|
|
assertUserInputTaskPrompt(task);
|
|
state.initialTask = {
|
|
chars: [...task].length,
|
|
sha256: hashValue(task),
|
|
};
|
|
state.userInputCliSession = startInteractiveCli([
|
|
'--swarm-chat',
|
|
'--init',
|
|
state.projectRoot,
|
|
]);
|
|
await waitForInteractiveCliOutput(
|
|
state.userInputCliSession,
|
|
(output) => output.includes('Agent Swarm Chat'),
|
|
'user-input-cli-banner-timeout',
|
|
30_000,
|
|
);
|
|
writeInteractiveCliLine(state.userInputCliSession, task);
|
|
|
|
const pending = await waitForPendingUserInputRequest();
|
|
await waitForInteractiveCliOutput(
|
|
state.userInputCliSession,
|
|
(output) =>
|
|
output.includes(`[Needs input] agent=${projectSupervisorAgentId}`) &&
|
|
output.includes(`request=${pending.requestId}`),
|
|
'user-input-cli-question-timeout',
|
|
120_000,
|
|
);
|
|
await claimOwnedRunner();
|
|
const beforeKillRunner = await readRunnerStatus();
|
|
state.userInput.oldRunnerBootId = runnerBootId(beforeKillRunner);
|
|
assert(
|
|
isNonEmptyString(state.userInput.oldRunnerBootId),
|
|
'user-input-runner-boot-before-kill-missing',
|
|
);
|
|
await captureUserInputWaitingBoundary('before-kill');
|
|
await killRunnerOnce();
|
|
await runCli(['--agent-resume', state.projectRoot], { timeoutMs: 120_000 });
|
|
state.resumed = true;
|
|
const restarted = await waitForRunnerBootChange(
|
|
state.userInput.oldRunnerBootId,
|
|
);
|
|
state.userInput.newRunnerBootId = runnerBootId(restarted);
|
|
await claimOwnedRunner(restarted);
|
|
await captureUserInputWaitingBoundary('after-restart');
|
|
await sleep(750);
|
|
await captureUserInputWaitingBoundary('after-stable-window');
|
|
|
|
writeInteractiveCliLine(state.userInputCliSession, userInputAnswerText);
|
|
await answerRemainingInteractiveQuestions(state.userInputCliSession);
|
|
await waitForInteractiveCliOutput(
|
|
state.userInputCliSession,
|
|
(output) => output.includes(`[\u5df2\u56de\u7b54] ${pending.requestId}`),
|
|
'user-input-cli-answer-timeout',
|
|
60_000,
|
|
);
|
|
await waitForUserInputRuntimeCompletion();
|
|
await waitForInteractiveCliOutput(
|
|
state.userInputCliSession,
|
|
(output) =>
|
|
output.includes('\nAgent> ') ||
|
|
output.includes(
|
|
'[\u672c\u8f6e\u7ed3\u675f] 父 Agent 回复已完整流式输出。',
|
|
),
|
|
'user-input-cli-final-reply-timeout',
|
|
120_000,
|
|
);
|
|
writeInteractiveCliLine(state.userInputCliSession, '/quit');
|
|
await waitForInteractiveCliExit(state.userInputCliSession, 30_000);
|
|
state.userInputCliSession = null;
|
|
|
|
state.identityStable = true;
|
|
state.evidence = await validateUserInputRuntimeEvidence();
|
|
assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected');
|
|
}
|
|
|
|
export async function readUserInputSidecars() {
|
|
const files = (
|
|
await listFiles(path.join(state.projectRoot, '.agent/runtime/user-input'))
|
|
)
|
|
.filter((file) => file.endsWith('.json'))
|
|
.sort();
|
|
return Promise.all(
|
|
files.map(async (file) => ({ file, record: await readJson(file) })),
|
|
);
|
|
}
|
|
|
|
export async function readUserInputPersistence() {
|
|
const runtimeStatePath = path.join(
|
|
state.projectRoot,
|
|
'.agent/runtime/agents',
|
|
`${projectSupervisorAgentId}.json`,
|
|
);
|
|
const [
|
|
taskSnapshot,
|
|
events,
|
|
agentDb,
|
|
activity,
|
|
output,
|
|
runtimeState,
|
|
sidecars,
|
|
] = await Promise.all([
|
|
readTaskSnapshot(),
|
|
readAllRuntimeEvents(),
|
|
readOptionalJsonl(path.join(state.projectRoot, '.agent/agent.db')),
|
|
readOptionalJsonl(path.join(state.projectRoot, '.agent/activity.jsonl')),
|
|
readOptionalJsonl(path.join(state.projectRoot, '.agent/output.jsonl')),
|
|
readJson(runtimeStatePath).catch(() => null),
|
|
readUserInputSidecars(),
|
|
]);
|
|
const sessionId = runtimeState?.sessionId ?? state.initialSessionId;
|
|
const conversations = isNonEmptyString(sessionId)
|
|
? await readOptionalJsonl(
|
|
agentConversationPath(projectSupervisorAgentId, sessionId),
|
|
)
|
|
: [];
|
|
return {
|
|
taskSnapshot,
|
|
events,
|
|
agentDb,
|
|
activity,
|
|
output,
|
|
runtimeState,
|
|
sidecars,
|
|
conversations,
|
|
};
|
|
}
|
|
|
|
export function userInputProviderLifecycleStarted(agentDb) {
|
|
return agentDb.filter(
|
|
(record) =>
|
|
record.recordType === 'agent.runtime.provider_request.lifecycle' &&
|
|
record.agentId === projectSupervisorAgentId &&
|
|
record.runId === state.initialRunId &&
|
|
record.status === 'started',
|
|
);
|
|
}
|
|
|
|
export async function waitForPendingUserInputRequest() {
|
|
const deadline = Date.now() + 8 * 60 * 1000;
|
|
while (Date.now() < deadline) {
|
|
const persistence = await readUserInputPersistence();
|
|
const { runtimeState, sidecars, conversations, taskSnapshot, agentDb } =
|
|
persistence;
|
|
const latest = taskSnapshot.latest.find(
|
|
(task) =>
|
|
task.agentId === projectSupervisorAgentId &&
|
|
task.runId === runtimeState?.runId,
|
|
);
|
|
if (latest && isFailedTask(latest)) {
|
|
throw codedError('user-input-runtime-failed-before-question');
|
|
}
|
|
if (
|
|
runtimeState?.agentId === projectSupervisorAgentId &&
|
|
runtimeState.status === 'waiting-for-user-input' &&
|
|
runtimeState.phase === 'waiting-for-user-input' &&
|
|
sidecars.length === 1
|
|
) {
|
|
const record = sidecars[0].record;
|
|
assert(
|
|
record.schemaVersion === 'game-creator-runtime-user-input.v1' &&
|
|
record.agentId === projectSupervisorAgentId &&
|
|
record.runId === runtimeState.runId &&
|
|
record.sessionId === runtimeState.sessionId &&
|
|
record.status === 'pending' &&
|
|
Array.isArray(record.questions) &&
|
|
record.questions.length === 1 &&
|
|
record.responseId == null &&
|
|
Object.keys(record.answers ?? {}).length === 0,
|
|
'user-input-pending-sidecar-invalid',
|
|
);
|
|
const questionMessages = conversations.filter(
|
|
(message) =>
|
|
message.role === 'assistant' &&
|
|
message.messageId === record.questionMessageId,
|
|
);
|
|
assert(
|
|
questionMessages.length === 1 &&
|
|
questionMessages[0].content.includes(record.questions[0].question),
|
|
'user-input-question-conversation-invalid',
|
|
);
|
|
state.initialRunId = runtimeState.runId;
|
|
state.initialSessionId = runtimeState.sessionId;
|
|
state.userInput.requestId = record.requestId;
|
|
state.userInput.actionId = record.actionId;
|
|
state.userInput.questionMessageId = record.questionMessageId;
|
|
state.userInput.questionCount = record.questions.length;
|
|
state.userInput.optionCount = record.questions.reduce(
|
|
(count, question) => count + question.options.length,
|
|
0,
|
|
);
|
|
state.userInput.privateValues = [
|
|
...record.questions.map((question) => question.question),
|
|
...record.questions.flatMap((question) =>
|
|
question.options.map((option) => option.description),
|
|
),
|
|
].filter(isNonEmptyString);
|
|
state.userInput.providerStartedBeforeKill =
|
|
userInputProviderLifecycleStarted(agentDb).length;
|
|
state.userInput.conversationCountBeforeKill = conversations.length;
|
|
assert(
|
|
state.userInput.providerStartedBeforeKill > 0,
|
|
'user-input-provider-planning-lifecycle-missing',
|
|
);
|
|
return record;
|
|
}
|
|
if (runtimeState?.phase === 'needs-reconciliation') {
|
|
throw codedError('user-input-runtime-needs-reconciliation');
|
|
}
|
|
await sleep(250);
|
|
}
|
|
throw codedError('user-input-question-timeout');
|
|
}
|
|
|
|
export async function captureUserInputWaitingBoundary(stage) {
|
|
const persistence = await readUserInputPersistence();
|
|
const { runtimeState, sidecars, conversations, agentDb } = persistence;
|
|
assert(
|
|
runtimeState?.agentId === projectSupervisorAgentId &&
|
|
runtimeState.runId === state.initialRunId &&
|
|
runtimeState.sessionId === state.initialSessionId &&
|
|
runtimeState.status === 'waiting-for-user-input' &&
|
|
runtimeState.phase === 'waiting-for-user-input' &&
|
|
sidecars.length === 1 &&
|
|
sidecars[0].record.requestId === state.userInput.requestId &&
|
|
sidecars[0].record.actionId === state.userInput.actionId &&
|
|
sidecars[0].record.status === 'pending' &&
|
|
sidecars[0].record.responseId == null &&
|
|
conversations.length === state.userInput.conversationCountBeforeKill,
|
|
`user-input-${stage}-waiting-boundary-invalid`,
|
|
);
|
|
const providerStarted = userInputProviderLifecycleStarted(agentDb).length;
|
|
assert(
|
|
providerStarted === state.userInput.providerStartedBeforeKill,
|
|
`user-input-${stage}-provider-called-while-waiting`,
|
|
);
|
|
if (stage !== 'before-kill') {
|
|
state.userInput.providerStartedAfterRestart = providerStarted;
|
|
state.userInput.conversationCountAfterRestart = conversations.length;
|
|
}
|
|
}
|
|
|
|
export async function waitForUserInputRuntimeCompletion() {
|
|
const deadline = Date.now() + 8 * 60 * 1000;
|
|
while (Date.now() < deadline) {
|
|
const persistence = await readUserInputPersistence();
|
|
const { runtimeState, sidecars, taskSnapshot, conversations } = persistence;
|
|
if (sidecars.length > 1) {
|
|
throw codedError('user-input-unexpected-second-request');
|
|
}
|
|
const latest = taskSnapshot.latest.find(
|
|
(task) =>
|
|
task.agentId === projectSupervisorAgentId &&
|
|
task.runId === state.initialRunId,
|
|
);
|
|
if (latest && isFailedTask(latest)) {
|
|
throw codedError('user-input-runtime-failed-after-answer');
|
|
}
|
|
if (
|
|
runtimeState?.runId === state.initialRunId &&
|
|
runtimeState.sessionId === state.initialSessionId &&
|
|
runtimeState.status === 'idle' &&
|
|
runtimeState.phase === 'completed' &&
|
|
latest?.status === 'completed' &&
|
|
latest.phase === 'completed' &&
|
|
sidecars.length === 1 &&
|
|
sidecars[0].record.status === 'answered'
|
|
) {
|
|
const record = sidecars[0].record;
|
|
state.userInput.responseId = record.responseId;
|
|
state.userInput.answerMessageId = record.answerMessageId;
|
|
const finalAssistants = conversations.filter(
|
|
(message) =>
|
|
message.role === 'assistant' &&
|
|
message.messageId !== record.questionMessageId,
|
|
);
|
|
if (finalAssistants.length === 1) return persistence;
|
|
}
|
|
if (runtimeState?.phase === 'needs-reconciliation') {
|
|
throw codedError('user-input-runtime-needs-reconciliation-after-answer');
|
|
}
|
|
await sleep(250);
|
|
}
|
|
throw codedError('user-input-completion-timeout');
|
|
}
|
|
|
|
export function validateUserInputProviderLifecycle(agentDb) {
|
|
const lifecycle = agentDb.filter(
|
|
(record) =>
|
|
record.recordType === 'agent.runtime.provider_request.lifecycle' &&
|
|
record.agentId === projectSupervisorAgentId &&
|
|
record.runId === state.initialRunId,
|
|
);
|
|
const byRequest = new Map();
|
|
for (const record of lifecycle) {
|
|
assert(
|
|
isNonEmptyString(record.requestId) &&
|
|
isNonEmptyString(record.requestKind) &&
|
|
isNonEmptyString(record.requestSlot),
|
|
'user-input-provider-lifecycle-identity-invalid',
|
|
);
|
|
const records = byRequest.get(record.requestId) ?? [];
|
|
records.push(record);
|
|
byRequest.set(record.requestId, records);
|
|
}
|
|
for (const records of byRequest.values()) {
|
|
assert(
|
|
records.length === 2 &&
|
|
records[0].status === 'started' &&
|
|
['completed', 'failed', 'interrupted'].includes(records[1].status) &&
|
|
records[0].requestKind === records[1].requestKind &&
|
|
records[0].requestSlot === records[1].requestSlot &&
|
|
records[0].runId === records[1].runId,
|
|
'user-input-provider-lifecycle-sequence-invalid',
|
|
);
|
|
}
|
|
const started = lifecycle.filter((record) => record.status === 'started');
|
|
assert(
|
|
started.length >= 2 &&
|
|
started.length === byRequest.size &&
|
|
state.userInput.providerStartedBeforeKill ===
|
|
state.userInput.providerStartedAfterRestart,
|
|
'user-input-provider-lifecycle-count-invalid',
|
|
);
|
|
return {
|
|
requestIdentityCount: byRequest.size,
|
|
startedCount: started.length,
|
|
terminalCount: lifecycle.length - started.length,
|
|
};
|
|
}
|
|
|
|
export async function validateUserInputRuntimeEvidence() {
|
|
const persistence = await readUserInputPersistence();
|
|
const {
|
|
taskSnapshot,
|
|
events,
|
|
agentDb,
|
|
activity,
|
|
output,
|
|
runtimeState,
|
|
sidecars,
|
|
conversations,
|
|
} = persistence;
|
|
assert(sidecars.length === 1, 'user-input-sidecar-count-invalid');
|
|
const sidecar = sidecars[0].record;
|
|
const latest = taskSnapshot.latest.find(
|
|
(task) =>
|
|
task.agentId === projectSupervisorAgentId &&
|
|
task.runId === state.initialRunId,
|
|
);
|
|
assert(
|
|
runtimeState?.agentId === projectSupervisorAgentId &&
|
|
runtimeState.runId === state.initialRunId &&
|
|
runtimeState.sessionId === state.initialSessionId &&
|
|
runtimeState.status === 'idle' &&
|
|
runtimeState.phase === 'completed' &&
|
|
latest?.status === 'completed' &&
|
|
latest.phase === 'completed',
|
|
'user-input-final-runtime-identity-invalid',
|
|
);
|
|
assert(
|
|
sidecar.schemaVersion === 'game-creator-runtime-user-input.v1' &&
|
|
sidecar.agentId === projectSupervisorAgentId &&
|
|
sidecar.runId === state.initialRunId &&
|
|
sidecar.sessionId === state.initialSessionId &&
|
|
sidecar.requestId === state.userInput.requestId &&
|
|
sidecar.actionId === state.userInput.actionId &&
|
|
sidecar.status === 'answered' &&
|
|
sidecar.responseId === state.userInput.responseId &&
|
|
sidecar.questionMessageId === state.userInput.questionMessageId &&
|
|
sidecar.answerMessageId === state.userInput.answerMessageId &&
|
|
sidecar.questions.length === 1 &&
|
|
Object.keys(sidecar.answers).length === 1 &&
|
|
Object.values(sidecar.answers)[0] === userInputAnswerText,
|
|
'user-input-final-sidecar-invalid',
|
|
);
|
|
const questionMessages = conversations.filter(
|
|
(message) =>
|
|
message.role === 'assistant' &&
|
|
message.messageId === sidecar.questionMessageId,
|
|
);
|
|
const answerMessages = conversations.filter(
|
|
(message) =>
|
|
message.role === 'user' && message.messageId === sidecar.answerMessageId,
|
|
);
|
|
const finalAssistants = conversations.filter(
|
|
(message) =>
|
|
message.role === 'assistant' &&
|
|
message.messageId !== sidecar.questionMessageId,
|
|
);
|
|
assert(
|
|
questionMessages.length === 1 &&
|
|
answerMessages.length === 1 &&
|
|
answerMessages[0].content.includes(userInputAnswerCanary) &&
|
|
finalAssistants.length === 1,
|
|
'user-input-conversation-cardinality-invalid',
|
|
);
|
|
const duplicateMessageCount = duplicateCount(
|
|
conversations.map((message) => message.messageId).filter(Boolean),
|
|
);
|
|
assert(duplicateMessageCount === 0, 'user-input-duplicate-message-identity');
|
|
const observations = agentDb.filter(
|
|
(record) =>
|
|
record.recordType === 'agent.runtime.user_input.answered' &&
|
|
record.agentId === projectSupervisorAgentId &&
|
|
record.runId === state.initialRunId &&
|
|
record.actionId === sidecar.actionId &&
|
|
record.requestId === sidecar.requestId,
|
|
);
|
|
assert(
|
|
observations.length === 1 &&
|
|
JSON.stringify(observations[0]).includes('answerCount=1'),
|
|
'user-input-public-observation-count-invalid',
|
|
);
|
|
const lifecycle = validateUserInputProviderLifecycle(agentDb);
|
|
const completedAudits = agentDb.filter(
|
|
(record) =>
|
|
record.recordType === 'agent.runtime.completed' &&
|
|
record.agentId === projectSupervisorAgentId &&
|
|
record.runId === state.initialRunId,
|
|
);
|
|
assert(
|
|
completedAudits.length === 1,
|
|
'user-input-completed-audit-count-invalid',
|
|
);
|
|
const finalizationFiles = (
|
|
await listFiles(
|
|
path.join(state.projectRoot, '.agent/runtime/finalizations'),
|
|
)
|
|
).filter((file) => file.endsWith('.json'));
|
|
assert(
|
|
finalizationFiles.length === 0,
|
|
'user-input-finalization-journal-present',
|
|
);
|
|
assert(
|
|
countExactSecrets(
|
|
Buffer.from(finalAssistants.map((message) => message.content).join('\n')),
|
|
disposableProjectPathVariants(),
|
|
) === 0,
|
|
'user-input-final-assistant-project-path-leak',
|
|
);
|
|
|
|
const publicSurfaces = {
|
|
event: events,
|
|
agentDb,
|
|
activity,
|
|
output,
|
|
runtimeState,
|
|
};
|
|
const privateValues = [
|
|
userInputAnswerCanary,
|
|
userInputAnswerText,
|
|
...state.userInput.privateValues,
|
|
].filter(isNonEmptyString);
|
|
const taskPrivateValues = privateValues.filter(
|
|
(value) => !buildUserInputTaskPrompt().includes(value),
|
|
);
|
|
const privateBodyPublicCounts = countSensitiveValuesBySurface(
|
|
publicSurfaces,
|
|
privateValues,
|
|
'user-input-private-body-public',
|
|
);
|
|
const taskPrivateBodyPublicCounts = countSensitiveValuesBySurface(
|
|
{ task: taskSnapshot.all },
|
|
taskPrivateValues,
|
|
'user-input-private-body-public',
|
|
);
|
|
const apiKeyPublicCounts = countSensitiveValuesBySurface(
|
|
{ task: taskSnapshot.all, ...publicSurfaces },
|
|
state.secrets,
|
|
'user-input-api-key-public',
|
|
);
|
|
const projectPathPublicCounts = countSensitiveValuesBySurface(
|
|
{ task: taskSnapshot.all, ...publicSurfaces },
|
|
disposableProjectPathVariants(),
|
|
'user-input-project-path-public',
|
|
);
|
|
const sidecarSecretLeakCount = countExactSecrets(
|
|
Buffer.from(JSON.stringify(sidecar)),
|
|
state.secrets,
|
|
);
|
|
assert(
|
|
sidecarSecretLeakCount === 0,
|
|
'user-input-sidecar-secret-leak-detected',
|
|
);
|
|
const secretLeakCount = await countSecretsInProject(
|
|
state.projectRoot,
|
|
state.secrets,
|
|
);
|
|
assert(secretLeakCount === 0, 'user-input-project-secret-leak-detected');
|
|
|
|
return {
|
|
scenario: 'project-supervisor-needs-input-runner-restart',
|
|
targetAgentId: projectSupervisorAgentId,
|
|
providerModel: 'gpt-5.5',
|
|
isolatedAppDataUsed: true,
|
|
formalConfigCliCallCount: state.isolatedRunner.sourceConfigCliCallCount,
|
|
sourceRunnerEndpointUnchanged: false,
|
|
sourceConfigHardlinkCount: state.isolatedRunner.configLinks.length,
|
|
sourceConfigLinksVerified: false,
|
|
taskCount: taskSnapshot.all.length,
|
|
eventCount: events.length,
|
|
agentDbRecordCount: agentDb.length,
|
|
conversationMessageCount: conversations.length,
|
|
targetRunCount: new Set(
|
|
taskSnapshot.all
|
|
.filter((task) => task.agentId === projectSupervisorAgentId)
|
|
.map((task) => task.runId),
|
|
).size,
|
|
stableSessionCount: new Set(
|
|
taskSnapshot.all
|
|
.filter((task) => task.agentId === projectSupervisorAgentId)
|
|
.map((task) => task.sessionId),
|
|
).size,
|
|
userInputSidecarCount: sidecars.length,
|
|
userInputQuestionCount: sidecar.questions.length,
|
|
userInputOptionCount: state.userInput.optionCount,
|
|
userInputAnswerCount: Object.keys(sidecar.answers).length,
|
|
userInputQuestionMessageCount: questionMessages.length,
|
|
userInputAnswerMessageCount: answerMessages.length,
|
|
finalAssistantCount: finalAssistants.length,
|
|
completedAuditCount: completedAudits.length,
|
|
toolObservationCount: observations.length,
|
|
providerRequestIdentityCount: lifecycle.requestIdentityCount,
|
|
providerLifecycleStartedCount: lifecycle.startedCount,
|
|
providerLifecycleTerminalCount: lifecycle.terminalCount,
|
|
providerStartedBeforeRunnerKill: state.userInput.providerStartedBeforeKill,
|
|
providerStartedAfterRunnerRestart:
|
|
state.userInput.providerStartedAfterRestart,
|
|
providerCalledWhileWaiting: false,
|
|
conversationCountBeforeRunnerKill:
|
|
state.userInput.conversationCountBeforeKill,
|
|
conversationCountAfterRunnerRestart:
|
|
state.userInput.conversationCountAfterRestart,
|
|
runnerBootChanged:
|
|
state.userInput.oldRunnerBootId !== state.userInput.newRunnerBootId,
|
|
duplicateMessageCount,
|
|
finalizationJournalCount: finalizationFiles.length,
|
|
privateBodyPublicLeakCount:
|
|
sumObjectValues(privateBodyPublicCounts) +
|
|
sumObjectValues(taskPrivateBodyPublicCounts),
|
|
apiKeyPublicLeakCount: sumObjectValues(apiKeyPublicCounts),
|
|
projectPathPublicLeakCount: sumObjectValues(projectPathPublicCounts),
|
|
projectPathPublicSurfaceCount: Object.keys(projectPathPublicCounts).length,
|
|
userInputSidecarSecretLeakCount: sidecarSecretLeakCount,
|
|
userInputReportLeakCount: state.userInput.reportLeakCount,
|
|
userInputRunnerKillMethod: null,
|
|
userInputRunnerPidfdClaimCount: state.isolatedRunner.pidfdClaimCount,
|
|
userInputRunnerPidfdSignalCount: state.isolatedRunner.pidfdSignalCount,
|
|
userInputRunnerStopped: false,
|
|
userInputAppDataCleanupPerformed: false,
|
|
secretLeakCount,
|
|
lureLeakCount: state.lureLeakCount,
|
|
paths: [
|
|
'.agent/runtime/user-input',
|
|
'.agent/runtime/tasks',
|
|
'.agent/runtime/events',
|
|
'.agent/agent.db',
|
|
'.agent/conversations',
|
|
],
|
|
};
|
|
}
|
|
|
|
export async function collectPartialUserInputEvidence() {
|
|
const persistence = await readUserInputPersistence();
|
|
return {
|
|
taskCount: persistence.taskSnapshot.all.length,
|
|
eventCount: persistence.events.length,
|
|
agentDbRecordCount: persistence.agentDb.length,
|
|
conversationMessageCount: persistence.conversations.length,
|
|
userInputSidecarCount: persistence.sidecars.length,
|
|
userInputQuestionCount:
|
|
persistence.sidecars[0]?.record?.questions?.length ?? 0,
|
|
userInputAnswerCount: Object.keys(
|
|
persistence.sidecars[0]?.record?.answers ?? {},
|
|
).length,
|
|
finalAssistantCount: persistence.conversations.filter(
|
|
(message) =>
|
|
message.role === 'assistant' &&
|
|
message.messageId !==
|
|
persistence.sidecars[0]?.record?.questionMessageId,
|
|
).length,
|
|
};
|
|
}
|
|
|
|
export function emptyUserInputEvidence() {
|
|
return {
|
|
scenario: 'project-supervisor-needs-input-runner-restart',
|
|
targetAgentId: projectSupervisorAgentId,
|
|
providerModel: 'gpt-5.5',
|
|
isolatedAppDataUsed: false,
|
|
formalConfigCliCallCount: 0,
|
|
sourceRunnerEndpointUnchanged: false,
|
|
sourceConfigHardlinkCount: 0,
|
|
sourceConfigLinksVerified: false,
|
|
taskCount: 0,
|
|
eventCount: 0,
|
|
agentDbRecordCount: 0,
|
|
conversationMessageCount: 0,
|
|
targetRunCount: 0,
|
|
stableSessionCount: 0,
|
|
userInputSidecarCount: 0,
|
|
userInputQuestionCount: 0,
|
|
userInputOptionCount: 0,
|
|
userInputAnswerCount: 0,
|
|
userInputQuestionMessageCount: 0,
|
|
userInputAnswerMessageCount: 0,
|
|
finalAssistantCount: 0,
|
|
completedAuditCount: 0,
|
|
toolObservationCount: 0,
|
|
providerRequestIdentityCount: 0,
|
|
providerLifecycleStartedCount: 0,
|
|
providerLifecycleTerminalCount: 0,
|
|
providerStartedBeforeRunnerKill: 0,
|
|
providerStartedAfterRunnerRestart: 0,
|
|
providerCalledWhileWaiting: false,
|
|
conversationCountBeforeRunnerKill: 0,
|
|
conversationCountAfterRunnerRestart: 0,
|
|
runnerBootChanged: false,
|
|
duplicateMessageCount: 0,
|
|
finalizationJournalCount: 0,
|
|
privateBodyPublicLeakCount: 0,
|
|
apiKeyPublicLeakCount: 0,
|
|
projectPathPublicLeakCount: 0,
|
|
projectPathPublicSurfaceCount: 0,
|
|
userInputSidecarSecretLeakCount: 0,
|
|
userInputReportLeakCount: 0,
|
|
userInputRunnerKillMethod: null,
|
|
userInputRunnerPidfdClaimCount: 0,
|
|
userInputRunnerPidfdSignalCount: 0,
|
|
userInputRunnerStopped: false,
|
|
userInputAppDataCleanupPerformed: false,
|
|
secretLeakCount: 0,
|
|
lureLeakCount: 0,
|
|
paths: [],
|
|
};
|
|
}
|
|
|
|
export function isUserInputRuntimeSuite() {
|
|
return state.suite === userInputRuntimeSuite;
|
|
}
|