@linxin666/dsh-client-ui-task-board 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +59 -20
- package/README.zh.md +59 -20
- package/lib/client.js +6208 -624
- package/lib/client.js.map +1 -1
- package/lib/index.js +4774 -493
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +31 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +4 -1
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/index.d.ts +23 -5
- package/lib/types/client/locales.d.ts +168 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +46 -11
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/tasks.d.ts +228 -17
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +753 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +95 -0
- package/lib/types/host/verification-runner.d.ts +139 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +133 -14
- package/lib/types/host-runner.d.ts +55 -10
- package/lib/types/host-service.d.ts +205 -11
- package/lib/types/index.d.ts +134 -26
- package/lib/types/protocol.d.ts +92 -10
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +28 -31
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +378 -66
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +12 -7
- package/src/client/board/NewTaskModal.tsx +168 -21
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +289 -43
- package/src/client/board/TaskForm.tsx +13 -3
- package/src/client/board/VerificationReport.tsx +322 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/status-key.ts +30 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +488 -63
- package/src/client/index.ts +82 -51
- package/src/client/locales.ts +343 -6
- package/src/client/native-panel.tsx +18 -2
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +105 -10
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/store.ts +80 -15
- package/src/core/tasks.ts +293 -45
- package/src/core/use-cases/task-create.ts +30 -9
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -7
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +1594 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +125 -38
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +487 -0
- package/src/host/verification-runner.ts +702 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +468 -71
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +106 -24
- package/src/host-service.ts +546 -40
- package/src/index.ts +305 -21
- package/src/protocol.ts +226 -28
- package/src/tool-surface.ts +95 -0
- package/lib/invariant.js +0 -6
- package/lib/types/invariant.d.ts +0 -3
- package/src/invariant.ts +0 -4
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goal acceptance gate: the board's evaluator-backed certification of a goal
|
|
3
|
+
* completion claim.
|
|
4
|
+
*
|
|
5
|
+
* The gate sits on the official tool pre-execute lifecycle and intercepts the
|
|
6
|
+
* one call that ends a goal run — `update_goal` with `action: 'complete'`
|
|
7
|
+
* — BEFORE it takes effect. This is why the board does not ask the agent to
|
|
8
|
+
* verify itself: the tool call that finishes the goal is refused until a
|
|
9
|
+
* matching acceptance pass record exists, so no prompt wording and no
|
|
10
|
+
* self-report can route around it.
|
|
11
|
+
*
|
|
12
|
+
* Semantics owned here:
|
|
13
|
+
* - one acceptance cycle per EXECUTION, spent at most twice: the first failure
|
|
14
|
+
* returns its findings to the fixing agent, the second ends the cycle;
|
|
15
|
+
* - a repeated completion call during a repair, a new automatic goal round, a
|
|
16
|
+
* plugin reload and a Host restart all reuse the same cycle rather than
|
|
17
|
+
* resetting it (the budget is persisted on the execution record);
|
|
18
|
+
* - an anomaly (timeout, authentication, unparseable answer, an unusable judge
|
|
19
|
+
* route) is recorded separately from a quality verdict and is never treated
|
|
20
|
+
* as one;
|
|
21
|
+
* - exhausting the anomaly budget does NOT judge the card: an environment that
|
|
22
|
+
* cannot answer is not the work failing, so the cycle is held open with no
|
|
23
|
+
* judge call until a user clears the recorded anomalies (issue #1828);
|
|
24
|
+
* - an attempt the time budget ends before any verdict is recorded as its own
|
|
25
|
+
* stage and charged to no budget at all (issue #1828);
|
|
26
|
+
* - an INVALID acceptance (a veto whose required evidence is missing,
|
|
27
|
+
* unlocatable, or aimed at the empty-work baseline) is recorded in its own
|
|
28
|
+
* stage, spends its OWN bounded budget, and never consumes a quality attempt
|
|
29
|
+
* or fails the card; when that budget is spent the cycle is held for a human
|
|
30
|
+
* exactly like a spent anomaly budget;
|
|
31
|
+
* - concurrent completion calls for one execution share one acceptance.
|
|
32
|
+
|
|
33
|
+
* The validity check itself is program-level (`assessAcceptanceValidity` in
|
|
34
|
+
* `core/verification.ts`), applied after the judge answers and BEFORE any
|
|
35
|
+
* quality verdict is written: a stronger prompt alone could not enforce it.
|
|
36
|
+
*/
|
|
37
|
+
import type { PreToolDecision } from '@deepseek-ai/dsh-tools'
|
|
38
|
+
import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
39
|
+
import {
|
|
40
|
+
MAX_EXCEPTION_ATTEMPTS,
|
|
41
|
+
MAX_INVALID_ATTEMPTS,
|
|
42
|
+
MAX_QUALITY_ATTEMPTS,
|
|
43
|
+
exceptionAttempts,
|
|
44
|
+
hasExceptionBudget,
|
|
45
|
+
finalFailureReason,
|
|
46
|
+
hasInvalidBudget,
|
|
47
|
+
hasQualityBudget,
|
|
48
|
+
invalidAttempts,
|
|
49
|
+
invalidAcceptanceFeedback,
|
|
50
|
+
invalidHoldReason,
|
|
51
|
+
passedAttempt,
|
|
52
|
+
qualityAttempts,
|
|
53
|
+
qualityFeedback,
|
|
54
|
+
type ExecutionVerification,
|
|
55
|
+
type VerificationAttempt,
|
|
56
|
+
} from '../core/verification.ts'
|
|
57
|
+
import { collectEvidence, evidenceEvents, runAcceptance } from './verification-runner.ts'
|
|
58
|
+
import { renderWorkspaceEvidence, type WorkspaceChangeSource } from './workspace-evidence.ts'
|
|
59
|
+
import { DEFAULT_VERIFICATION_BUDGET_MS, verificationBudgetMs, verificationCallTimeoutMs } from '../core/verification-budget.ts'
|
|
60
|
+
import type { HostTaskLedger } from '../host-ledger.ts'
|
|
61
|
+
import type { ExecutionRecord, TaskRecord } from '../core/tasks.ts'
|
|
62
|
+
|
|
63
|
+
/** The subset of the official goal service this gate drives. */
|
|
64
|
+
export interface GoalFace {
|
|
65
|
+
get(agent: unknown): { id: string; revision: number; objective: string } | undefined
|
|
66
|
+
block(agent: unknown, ref: { id: string; revision: number }, reason: { code: string; message: string }): unknown
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Everything the gate needs from its owner. */
|
|
70
|
+
export interface GoalVerificationGateDeps {
|
|
71
|
+
ledger: HostTaskLedger
|
|
72
|
+
/** Resolve the optional model runtime lazily (it may activate later). */
|
|
73
|
+
llm?: () => LlmRuntime | undefined
|
|
74
|
+
/** Resolve the optional goal service lazily. */
|
|
75
|
+
goals?: () => GoalFace | undefined
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the optional host change service lazily. Absent (or a deployment
|
|
78
|
+
* that serves none) simply means the judge sees the trajectory alone.
|
|
79
|
+
*/
|
|
80
|
+
workspaceChanges?: () => WorkspaceChangeSource | undefined
|
|
81
|
+
/**
|
|
82
|
+
* Ceiling of one judge request, read per acceptance (a row setting). Absent
|
|
83
|
+
* keeps the default, so a programmatic mount still judges.
|
|
84
|
+
*/
|
|
85
|
+
callTimeoutMs?: () => number
|
|
86
|
+
/**
|
|
87
|
+
* Total ceiling of one acceptance attempt, read per acceptance (a row
|
|
88
|
+
* setting). Absent keeps the default.
|
|
89
|
+
*/
|
|
90
|
+
budgetMs?: () => number
|
|
91
|
+
logger: { warn(message: string, ...rest: unknown[]): void }
|
|
92
|
+
now?: () => number
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The shape of one tool pre-execution the gate inspects. */
|
|
96
|
+
export interface GateExecution {
|
|
97
|
+
name: string
|
|
98
|
+
arguments: unknown
|
|
99
|
+
agent?: unknown
|
|
100
|
+
signal: AbortSignal
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Structured denial detail, mirroring the host's ToolErrorInfo. */
|
|
104
|
+
interface GateDenial {
|
|
105
|
+
name: string
|
|
106
|
+
code: string
|
|
107
|
+
reason: string
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Build the model-facing denial decision with its structured detail. */
|
|
111
|
+
function deny(reason: string, code: string, detail?: string): PreToolDecision {
|
|
112
|
+
const info: GateDenial = { name: 'TaskBoardGoalVerification', code, reason: detail ?? reason }
|
|
113
|
+
return { kind: 'deny', reason, info } as unknown as PreToolDecision
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Read `action` out of the tool arguments. */
|
|
117
|
+
function actionOf(args: unknown): string | undefined {
|
|
118
|
+
if (typeof args !== 'object' || args === null) return undefined
|
|
119
|
+
const action = (args as Record<string, unknown>).action
|
|
120
|
+
return typeof action === 'string' ? action : undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Bound one message so a judge error cannot flood the tool result. */
|
|
124
|
+
function bounded(text: string, max = 4_000): string {
|
|
125
|
+
return text.length <= max ? text : text.slice(0, max) + '…'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Create the goal acceptance gate.
|
|
130
|
+
* @param deps - the ledger, the model runtime, the goal service and the settings.
|
|
131
|
+
* @returns the `tools/pre-execute` listener body (without the waterfall next()).
|
|
132
|
+
*/
|
|
133
|
+
export function createGoalVerificationGate(deps: GoalVerificationGateDeps): (exec: GateExecution) => Promise<PreToolDecision | undefined> {
|
|
134
|
+
const now = deps.now ?? Date.now
|
|
135
|
+
/** One acceptance per execution, shared by concurrent completion calls. */
|
|
136
|
+
const inFlight = new Map<string, Promise<PreToolDecision>>()
|
|
137
|
+
|
|
138
|
+
/** Persist one execution's acceptance block. */
|
|
139
|
+
const write = (taskId: string, executionId: string, verification: ExecutionVerification): void => {
|
|
140
|
+
deps.ledger.setVerification(taskId, executionId, verification)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Best-effort: stop the goal so an exhausted cycle cannot burn more rounds. */
|
|
144
|
+
const blockGoal = (agent: unknown, message: string): void => {
|
|
145
|
+
const goals = deps.goals?.()
|
|
146
|
+
if (goals === undefined) return
|
|
147
|
+
try {
|
|
148
|
+
const view = goals.get(agent)
|
|
149
|
+
if (view === undefined) return
|
|
150
|
+
goals.block(agent, { id: view.id, revision: view.revision }, { code: 'task-board-verification', message })
|
|
151
|
+
} catch (error) {
|
|
152
|
+
deps.logger.warn('[dsh-task-board] could not block the goal after a failed acceptance', error)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Record the final failure of one cycle: the report stays, the card must fail. */
|
|
157
|
+
const finalize = (
|
|
158
|
+
taskId: string,
|
|
159
|
+
executionId: string,
|
|
160
|
+
verification: ExecutionVerification,
|
|
161
|
+
reason: string,
|
|
162
|
+
agent: unknown,
|
|
163
|
+
): PreToolDecision => {
|
|
164
|
+
write(taskId, executionId, { ...verification, inFlight: undefined, failedReason: reason, failedAt: now() })
|
|
165
|
+
blockGoal(agent, reason)
|
|
166
|
+
return deny(
|
|
167
|
+
bounded('[任务看板 · goal 验收判定失败] ' + reason + '\n本次执行不会完成;请如实向用户报告验收未通过、已记录的分数与问题,不要再尝试标记完成。'),
|
|
168
|
+
'TASK_BOARD_VERIFICATION_FAILED',
|
|
169
|
+
reason,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hold the cycle open after its non-quality budget ran out.
|
|
175
|
+
*
|
|
176
|
+
* A judge route that times out, cannot authenticate or cannot be resolved is
|
|
177
|
+
* the ENVIRONMENT failing, not the work, so this records the attempt and
|
|
178
|
+
* refuses the completion claim without writing a `failedReason` and without
|
|
179
|
+
* blocking the goal (issue #1828). The execution therefore survives for a
|
|
180
|
+
* user who fixes the environment and clears the recorded anomalies, instead
|
|
181
|
+
* of carrying a verdict that only the clock produced.
|
|
182
|
+
*/
|
|
183
|
+
const hold = (
|
|
184
|
+
taskId: string,
|
|
185
|
+
executionId: string,
|
|
186
|
+
verification: ExecutionVerification,
|
|
187
|
+
message: string,
|
|
188
|
+
detail: string,
|
|
189
|
+
code: string,
|
|
190
|
+
): PreToolDecision => {
|
|
191
|
+
write(taskId, executionId, { ...verification, inFlight: undefined })
|
|
192
|
+
return deny(bounded(message), code, detail)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The refusal one recorded anomaly returns to the fixing agent. */
|
|
196
|
+
const anomalyRefusal = (spend: ExecutionVerification, error: string | undefined): PreToolDecision =>
|
|
197
|
+
deny(
|
|
198
|
+
bounded('[任务看板 · 验收异常] ' + (error ?? 'unknown') + '\n这是验收异常而非质量判定,本次未消耗质量验收额度(已用 ' + exceptionAttempts(spend).length + '/' + MAX_EXCEPTION_ATTEMPTS + ')。请修复环境后再次调用 update_goal(action: complete)。'),
|
|
199
|
+
'TASK_BOARD_VERIFICATION_ANOMALY',
|
|
200
|
+
error,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The refusal one recorded INVALID acceptance returns.
|
|
205
|
+
*
|
|
206
|
+
* The claim is still refused (no pass record exists), but the agent is told
|
|
207
|
+
* the veto itself was unusable and is explicitly NOT asked to repair anything
|
|
208
|
+
* on that basis. The reason and scores stay recorded for review; the quality
|
|
209
|
+
* budget is untouched.
|
|
210
|
+
*/
|
|
211
|
+
const invalidRefusal = (spend: ExecutionVerification, attempt: VerificationAttempt): PreToolDecision =>
|
|
212
|
+
deny(
|
|
213
|
+
bounded(invalidAcceptanceFeedback(attempt, MAX_INVALID_ATTEMPTS - invalidAttempts(spend).length)),
|
|
214
|
+
'TASK_BOARD_VERIFICATION_INVALID',
|
|
215
|
+
attempt.error,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Hold a cycle whose invalid-acceptance budget is spent.
|
|
220
|
+
*
|
|
221
|
+
* No usable veto was ever produced, so the board neither passes nor fails the
|
|
222
|
+
* card: it stops calling the judge and leaves the execution for a human. Only
|
|
223
|
+
* an explicit user action (or a rerun) resumes it, which is what bounds the
|
|
224
|
+
* retries.
|
|
225
|
+
*/
|
|
226
|
+
const invalidHeld = (taskId: string, executionId: string, spend: ExecutionVerification): PreToolDecision =>
|
|
227
|
+
hold(
|
|
228
|
+
taskId,
|
|
229
|
+
executionId,
|
|
230
|
+
spend,
|
|
231
|
+
'[任务看板 · 验收无效] ' + invalidHoldReason(invalidAttempts(spend).length),
|
|
232
|
+
'acceptance produced no usable veto; a user action must clear the recorded invalid acceptances',
|
|
233
|
+
'TASK_BOARD_VERIFICATION_INVALID',
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
/** The refusal a spent anomaly budget returns: no verdict, and no card failure. */
|
|
237
|
+
const anomalyHeld = (taskId: string, executionId: string, spend: ExecutionVerification): PreToolDecision =>
|
|
238
|
+
hold(
|
|
239
|
+
taskId,
|
|
240
|
+
executionId,
|
|
241
|
+
spend,
|
|
242
|
+
'[任务看板 · 验收异常] 验收异常额度已用尽(' + MAX_EXCEPTION_ATTEMPTS + '/' + MAX_EXCEPTION_ATTEMPTS + '):本执行周期不再调用裁判模型,也不把这次执行判为失败。\n这是环境异常而非质量判定:请如实告诉用户验收环境当前不可用,由用户修复环境后用 task_board_manage(action=reset-verification) 清除已记录的异常,再重新完成目标。',
|
|
243
|
+
'acceptance anomaly budget exhausted; a user action must clear the recorded anomalies',
|
|
244
|
+
'TASK_BOARD_VERIFICATION_ANOMALY',
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
/** Record one attempt and decide whether the completion claim may proceed. */
|
|
248
|
+
const settle = async (input: {
|
|
249
|
+
task: TaskRecord
|
|
250
|
+
execution: ExecutionRecord
|
|
251
|
+
verification: ExecutionVerification
|
|
252
|
+
agent: unknown
|
|
253
|
+
objective: string
|
|
254
|
+
}): Promise<PreToolDecision> => {
|
|
255
|
+
const { task, execution, verification, agent, objective } = input
|
|
256
|
+
const route = verification.contract.route
|
|
257
|
+
const baselines: ExecutionVerification = { ...verification, inFlight: true }
|
|
258
|
+
if (route === undefined) {
|
|
259
|
+
const attempt: VerificationAttempt = {
|
|
260
|
+
index: exceptionAttempts(verification).length + 1,
|
|
261
|
+
at: now(),
|
|
262
|
+
stage: 'exception',
|
|
263
|
+
passed: false,
|
|
264
|
+
score: 0,
|
|
265
|
+
baseline: 0,
|
|
266
|
+
criteria: [],
|
|
267
|
+
findings: [],
|
|
268
|
+
usage: { calls: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0 },
|
|
269
|
+
evidence: { chars: 0, omittedCharacters: 0, entries: 0, hash: '' },
|
|
270
|
+
route: { provider: '', model: '' },
|
|
271
|
+
channel: 'explicit-tag',
|
|
272
|
+
rounds: 0,
|
|
273
|
+
error: '验收异常(裁判路由不可用): 宿主未提供模型目录,无法解析验收模型',
|
|
274
|
+
}
|
|
275
|
+
const attempts = [...verification.attempts, attempt]
|
|
276
|
+
const spend = { ...verification, attempts, inFlight: undefined }
|
|
277
|
+
if (exceptionAttempts(spend).length >= MAX_EXCEPTION_ATTEMPTS) {
|
|
278
|
+
return anomalyHeld(task.id, execution.id, spend)
|
|
279
|
+
}
|
|
280
|
+
write(task.id, execution.id, spend)
|
|
281
|
+
return anomalyRefusal(spend, attempt.error)
|
|
282
|
+
}
|
|
283
|
+
write(task.id, execution.id, baselines)
|
|
284
|
+
const llm = deps.llm?.()
|
|
285
|
+
if (llm === undefined) {
|
|
286
|
+
const attempt: VerificationAttempt = {
|
|
287
|
+
index: exceptionAttempts(verification).length + 1,
|
|
288
|
+
at: now(),
|
|
289
|
+
stage: 'exception',
|
|
290
|
+
passed: false,
|
|
291
|
+
score: 0,
|
|
292
|
+
baseline: 0,
|
|
293
|
+
criteria: [],
|
|
294
|
+
findings: [],
|
|
295
|
+
usage: { calls: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0 },
|
|
296
|
+
evidence: { chars: 0, omittedCharacters: 0, entries: 0, hash: '' },
|
|
297
|
+
route,
|
|
298
|
+
channel: 'explicit-tag',
|
|
299
|
+
rounds: 0,
|
|
300
|
+
error: '验收异常(裁判路由不可用): 本部署未提供 llm 服务',
|
|
301
|
+
}
|
|
302
|
+
const spend = { ...verification, attempts: [...verification.attempts, attempt], inFlight: undefined }
|
|
303
|
+
if (exceptionAttempts(spend).length >= MAX_EXCEPTION_ATTEMPTS) {
|
|
304
|
+
return anomalyHeld(task.id, execution.id, spend)
|
|
305
|
+
}
|
|
306
|
+
write(task.id, execution.id, spend)
|
|
307
|
+
return deny(bounded('[任务看板 · 验收异常] ' + attempt.error), 'TASK_BOARD_VERIFICATION_ANOMALY', attempt.error)
|
|
308
|
+
}
|
|
309
|
+
const session = (agent as { session?: unknown }).session
|
|
310
|
+
const evidence = collectEvidence(session, objective, execution.startedAt)
|
|
311
|
+
// Both ceilings are row settings read live: raising one unblocks a card
|
|
312
|
+
// whose acceptance is stuck on a slow route without a plugin reload.
|
|
313
|
+
const callTimeout = deps.callTimeoutMs?.() ?? verificationCallTimeoutMs(undefined)
|
|
314
|
+
const budget = deps.budgetMs?.() ?? DEFAULT_VERIFICATION_BUDGET_MS
|
|
315
|
+
const deadline = now() + budget
|
|
316
|
+
const acceptance = AbortSignal.timeout(budget)
|
|
317
|
+
// The host's own record of what changed on disk is offered next to the
|
|
318
|
+
// trajectory; an unavailable service or a failed read degrades to the
|
|
319
|
+
// trajectory alone instead of failing an acceptance already under way.
|
|
320
|
+
const source = deps.workspaceChanges?.()
|
|
321
|
+
const workspace = source === undefined
|
|
322
|
+
? { text: '', files: 0 }
|
|
323
|
+
: await renderWorkspaceEvidence(
|
|
324
|
+
evidenceEvents(session, execution.startedAt),
|
|
325
|
+
String((agent as { id?: unknown }).id ?? ''),
|
|
326
|
+
source,
|
|
327
|
+
{ signal: acceptance, warn: message => { deps.logger.warn(message) } },
|
|
328
|
+
)
|
|
329
|
+
const result = await runAcceptance({
|
|
330
|
+
llm,
|
|
331
|
+
route,
|
|
332
|
+
evidence,
|
|
333
|
+
threshold: verification.contract.threshold,
|
|
334
|
+
index: qualityAttempts(verification).length + 1,
|
|
335
|
+
context: workspace.text === '' ? undefined : workspace.text,
|
|
336
|
+
workspaceFiles: workspace.files,
|
|
337
|
+
signal: acceptance,
|
|
338
|
+
deadline,
|
|
339
|
+
callTimeoutMs: callTimeout,
|
|
340
|
+
now,
|
|
341
|
+
})
|
|
342
|
+
const attempt = result.attempt
|
|
343
|
+
const attempts = [...verification.attempts, attempt]
|
|
344
|
+
const spend: ExecutionVerification = { ...verification, attempts, inFlight: undefined }
|
|
345
|
+
// A budget stop is charged to no budget at all (issue #1828): the judge route
|
|
346
|
+
// may be healthy and the machine simply out of time, so it is neither an
|
|
347
|
+
// anomaly nor a verdict, and it must never close the card.
|
|
348
|
+
if (attempt.stage === 'budget') {
|
|
349
|
+
return hold(
|
|
350
|
+
task.id,
|
|
351
|
+
execution.id,
|
|
352
|
+
spend,
|
|
353
|
+
'[任务看板 · 验收未完成] ' + (attempt.error ?? '时间预算耗尽') + '\n本次没有产生任何质量判定,也不消耗质量或异常额度。请稍后重试;若裁判路由确实很慢,可调大验收的单次调用上限或总时间预算。',
|
|
354
|
+
attempt.error ?? 'the acceptance time budget ran out before a verdict',
|
|
355
|
+
'TASK_BOARD_VERIFICATION_BUDGET',
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
if (attempt.stage === 'exception') {
|
|
359
|
+
if (exceptionAttempts(spend).length >= MAX_EXCEPTION_ATTEMPTS) {
|
|
360
|
+
return anomalyHeld(task.id, execution.id, spend)
|
|
361
|
+
}
|
|
362
|
+
write(task.id, execution.id, spend)
|
|
363
|
+
return anomalyRefusal(spend, attempt.error)
|
|
364
|
+
}
|
|
365
|
+
// An invalid acceptance is recorded and returned to the agent, but it is
|
|
366
|
+
// NOT a quality verdict: it spends its own bounded budget, never the
|
|
367
|
+
// quality one, and it never fails the card.
|
|
368
|
+
if (attempt.stage === 'invalid') {
|
|
369
|
+
if (invalidAttempts(spend).length >= MAX_INVALID_ATTEMPTS) {
|
|
370
|
+
return invalidHeld(task.id, execution.id, spend)
|
|
371
|
+
}
|
|
372
|
+
write(task.id, execution.id, spend)
|
|
373
|
+
return invalidRefusal(spend, attempt)
|
|
374
|
+
}
|
|
375
|
+
if (attempt.passed) {
|
|
376
|
+
write(task.id, execution.id, spend)
|
|
377
|
+
return { kind: 'allow' }
|
|
378
|
+
}
|
|
379
|
+
const used = qualityAttempts(spend).length
|
|
380
|
+
if (used >= MAX_QUALITY_ATTEMPTS) {
|
|
381
|
+
return finalize(task.id, execution.id, spend, finalFailureReason(attempt, used, verification.contract.threshold), agent)
|
|
382
|
+
}
|
|
383
|
+
write(task.id, execution.id, spend)
|
|
384
|
+
return deny(
|
|
385
|
+
bounded(qualityFeedback(attempt, MAX_QUALITY_ATTEMPTS - used)),
|
|
386
|
+
'TASK_BOARD_VERIFICATION_FAILED',
|
|
387
|
+
locatedFindingDetail(attempt),
|
|
388
|
+
)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Evaluate one tool call; undefined means "not this gate's business". */
|
|
392
|
+
const evaluate = async (exec: GateExecution): Promise<PreToolDecision | undefined> => {
|
|
393
|
+
const binding = (() => {
|
|
394
|
+
const agent = exec.agent as { id?: unknown } | undefined
|
|
395
|
+
const sessionId = agent?.id === undefined ? '' : String(agent.id)
|
|
396
|
+
return sessionId === '' ? undefined : deps.ledger.findOpenExecutionBySession(sessionId)
|
|
397
|
+
})()
|
|
398
|
+
if (binding === undefined) return undefined
|
|
399
|
+
const verification = binding.execution.verification
|
|
400
|
+
if (verification === undefined || verification.contract.enabled === false) return undefined
|
|
401
|
+
// Only a goal run of the board's own execution is gated: a teammate
|
|
402
|
+
// execution is certified through its Lead's aggregated evidence.
|
|
403
|
+
if (verification.applicability !== 'enforced') return undefined
|
|
404
|
+
if (passedAttempt(verification) !== undefined) return { kind: 'allow' }
|
|
405
|
+
if (verification.failedReason !== undefined) {
|
|
406
|
+
return deny(
|
|
407
|
+
bounded('[任务看板 · goal 验收判定失败] ' + verification.failedReason + '\n本次执行已被判失败,不会再运行新的验收。请如实报告结果。'),
|
|
408
|
+
'TASK_BOARD_VERIFICATION_FAILED',
|
|
409
|
+
verification.failedReason,
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
// A spent INVALID budget comes first: the judge produced no usable veto, so
|
|
413
|
+
// calling it again would only burn quota against a budget that determines
|
|
414
|
+
// nothing, and the card must stay open for a human rather than be failed.
|
|
415
|
+
if (!hasInvalidBudget(verification)) {
|
|
416
|
+
return invalidHeld(binding.task.id, binding.execution.id, verification)
|
|
417
|
+
}
|
|
418
|
+
if (!hasQualityBudget(verification)) {
|
|
419
|
+
return finalize(binding.task.id, binding.execution.id, verification, 'goal 验收额度已耗尽,本次 execution 判失败。', exec.agent)
|
|
420
|
+
}
|
|
421
|
+
// A spent anomaly budget is an environment that could not answer, not a
|
|
422
|
+
// judged work: the claim is refused and NO judge call is spent until a user
|
|
423
|
+
// clears the recorded anomalies (issue #1828).
|
|
424
|
+
if (!hasExceptionBudget(verification)) {
|
|
425
|
+
return anomalyHeld(binding.task.id, binding.execution.id, verification)
|
|
426
|
+
}
|
|
427
|
+
const key = binding.execution.id
|
|
428
|
+
const running = inFlight.get(key)
|
|
429
|
+
if (running !== undefined) return await running
|
|
430
|
+
const job = (async (): Promise<PreToolDecision> => {
|
|
431
|
+
const view = (() => {
|
|
432
|
+
const goals = deps.goals?.()
|
|
433
|
+
try {
|
|
434
|
+
return goals?.get(exec.agent)
|
|
435
|
+
} catch {
|
|
436
|
+
return undefined
|
|
437
|
+
}
|
|
438
|
+
})()
|
|
439
|
+
const objective = view?.objective !== undefined && view.objective.trim() !== '' ? view.objective : taskPrompt(binding.task)
|
|
440
|
+
return await settle({ task: binding.task, execution: binding.execution, verification, agent: exec.agent, objective })
|
|
441
|
+
})()
|
|
442
|
+
inFlight.set(key, job)
|
|
443
|
+
try {
|
|
444
|
+
return await job
|
|
445
|
+
} finally {
|
|
446
|
+
inFlight.delete(key)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return async (exec: GateExecution): Promise<PreToolDecision | undefined> => {
|
|
451
|
+
if (exec.name !== 'update_goal') return undefined
|
|
452
|
+
if (actionOf(exec.arguments) !== 'complete') return undefined
|
|
453
|
+
if (exec.agent === undefined) return undefined
|
|
454
|
+
// A call the caller already cancelled is not a policy refusal: let the host
|
|
455
|
+
// produce its own cancellation result.
|
|
456
|
+
if (exec.signal.aborted) return undefined
|
|
457
|
+
try {
|
|
458
|
+
return await evaluate(exec)
|
|
459
|
+
} catch (error) {
|
|
460
|
+
deps.logger.warn('[dsh-task-board] goal acceptance gate failed; the completion claim is refused (fail closed)', error)
|
|
461
|
+
return deny(
|
|
462
|
+
bounded('[任务看板 · 验收异常] 验收流程自身出错,本次完成声明已被拒绝:' + (error instanceof Error ? error.message : String(error))),
|
|
463
|
+
'TASK_BOARD_VERIFICATION_ANOMALY',
|
|
464
|
+
error instanceof Error ? error.message : String(error),
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The structured detail of one quality refusal: the judge's own findings, in
|
|
472
|
+
* the order the acceptance recorded them, so the denial carries exactly the
|
|
473
|
+
* located evidence the report shows.
|
|
474
|
+
*/
|
|
475
|
+
function locatedFindingDetail(attempt: VerificationAttempt): string {
|
|
476
|
+
const lines: string[] = []
|
|
477
|
+
for (const finding of attempt.criterionFindings ?? []) {
|
|
478
|
+
lines.push('[' + finding.criterionId + ' ' + finding.location + '] "' + finding.quote + '"')
|
|
479
|
+
}
|
|
480
|
+
if (lines.length === 0) lines.push(...attempt.findings)
|
|
481
|
+
return lines.join('\n')
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** The bare execution prompt, used when the goal service carries no objective. */
|
|
485
|
+
function taskPrompt(task: TaskRecord): string {
|
|
486
|
+
return task.prompt.trim() !== '' ? task.prompt : task.title
|
|
487
|
+
}
|