Reject explicit failures in task graph acceptance

Treat explicit inability or permission failures as failed nodes even when the CLI reports a completed process outcome.

Checks: task graph tests (14 passed); desktop pack.
This commit is contained in:
Ryder Sun committed 2026-11-06 23:54:25 +08:00
1 parent 9c8945fb76
commit ba22c5d2c2
2 files changed
+27

No files matched your search

@@ -6,6 +6,7 @@ const {
publicCollaborationText,
} = require('../collaboration/collaboration-records.cjs')
const { unlimitedReviewContract } = require('./local-workspace-context.cjs')
const { explicitTaskFailure } = require('./local-workspace-auto-runner-natural-decisions.cjs')
const {
parseTaskGraphCursor,
readyTaskGraphNodes,
@@ -121,6 +122,7 @@ const taskGraphMethods = {
const conclusion = publicCollaborationText(result?.message?.content || '')
const refs = verifiedRefs || this.taskGraphVerifiedOutcomeRefs(node, result, controller)
return result?.message?.trace?.status === 'completed'
&& !explicitTaskFailure(result?.message?.content)
&& (!node.acceptance.requireConclusion
|| (Boolean(conclusion) && result.message.trace.truncated !== true))
&& refs.artifactIds.length >= node.acceptance.minArtifactRefs
@@ -69,6 +69,12 @@ test('task graph does not accept a truncated required conclusion', () => {
assert.equal(taskGraphMethods.taskGraphResultAccepted(node, result), true)
})
test('task graph rejects explicit failure even when no conclusion is required', () => {
const node = graphNode({ acceptance: { requireConclusion: false, minArtifactRefs: 0, minEvidenceRefs: 0 } })
const result = { message: { content: '权限不足,无法读取所需文件。', trace: { status: 'completed' } } }
assert.equal(taskGraphMethods.taskGraphResultAccepted(node, result), false)
})
function challengeBindingInput(workspace, targetKinds, round, bodyHash = 'a'.repeat(64), runId = 'run-binding') {
const controller = workspace.createRunController('auto', targetKinds, 'root-binding', 8, false)
controller.runId = runId
@@ -328,6 +334,25 @@ test('task graph does not accept a partial Agent result as a completed node', as
assert.deepEqual(calls, ['codex'])
})
test('task graph does not complete when a terminal reply explicitly reports failure', async t => {
const { directory, options } = fixture()
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))
const ledger = new RunLedger({ storagePath: path.join(directory, 'run-ledger.json') })
options.runLedger = ledger
options.runAgent = async () => ({ text: 'I checked the workspace.\nI cannot complete the task.', outcome: 'completed' })
const workspace = new LocalWorkspace(options)
await workspace.refreshAgents()
const group = workspace.createGroup({ name: 'Failed graph response', agentKinds: ['codex', 'hermes'], workdir: directory, allowWrite: false })
await workspace.sendMessage({ groupId: group.id, text: 'Complete this task.', mode: 'auto', maxRounds: 2,
targetKinds: ['codex', 'hermes'], workflow: { template: 'task-graph', nodes: [graphNode({ terminal: true,
acceptance: { requireConclusion: true, minArtifactRefs: 0, minEvidenceRefs: 0 },
})] } })
await workspace.activeRuns.get(group.id).promise
const durable = ledger.list(group.id)[0]
assert.notEqual(durable.status, 'completed')
assert.equal(durable.orchestration.taskGraph.nodeStates[0].status, 'failed')
})
test('task graph does not count fabricated outcome references as evidence', async t => {
const { directory, options } = fixture()
t.after(() => fs.rmSync(directory, { recursive: true, force: true }))