@ai-agent-forge/plugin-decision-escalation 0.0.0-stage → 0.88.15
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.md +67 -2
- package/agent-forge.json +19 -0
- package/dist/adjudication.d.ts +89 -0
- package/dist/adjudication.d.ts.map +1 -0
- package/dist/adjudication.js +289 -0
- package/dist/adjudication.js.map +1 -0
- package/dist/engine.d.ts +111 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +333 -0
- package/dist/engine.js.map +1 -0
- package/dist/entry.d.ts +14 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +23 -0
- package/dist/entry.js.map +1 -0
- package/dist/http-server.d.ts +46 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +219 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +592 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +112 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -5
- package/plugin.json +12 -0
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agent-forge/plugin-decision-escalation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Decision escalation capability plugin for Agent Forge: a formal ask channel
|
|
4
|
+
for forks — the model asks with `decision_ask`, the user answers on a local
|
|
5
|
+
confirmation page or in TUI text, and an unattended fork never stalls
|
|
6
|
+
forever: reversible levels are adjudicated by an independent subagent chain
|
|
7
|
+
on timeout, irreversible ones stay stopped.
|
|
8
|
+
|
|
9
|
+
- Design contract (authoritative): `docs/design/决策升级插件设计.md`
|
|
10
|
+
- Position: a decision channel alongside the goal/plan/todo layers; the
|
|
11
|
+
main agent keeps L0 (trivial fork) autonomy — this plugin only takes over
|
|
12
|
+
when a fork is worth a formal ask.
|
|
13
|
+
|
|
14
|
+
## Levels (设计 §3)
|
|
15
|
+
|
|
16
|
+
| Level | Verdict | On timeout |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| L0 | reversible, local impact | not this plugin's business |
|
|
19
|
+
| L1 (`routine`) | reversible, single module | one independent adjudicator agent |
|
|
20
|
+
| L2 (`significant`) | reversible but directional | one advocate per reversible option + independent judge |
|
|
21
|
+
| L3 (redline) | any option `reversibility: "irreversible"` or field missing | never adjudicated — stays stopped, fork written back to the user |
|
|
22
|
+
|
|
23
|
+
The redline is engine-derived from per-option `reversibility`, never the
|
|
24
|
+
model's risk self-assessment; `risk_class` only picks L1 vs L2 handling.
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
| Tool | Purpose |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `decision_ask` | Ask a fork (2..4 closed options + symmetric `context_digest` brief + `risk_class`). Returns `decisionId`, page URL, `timeoutMs`, `riskClass`, `redLine` immediately; non-blocking. |
|
|
31
|
+
| `decision_resolve` | Book the answer: `source: "user"` (page or TUI text) or `source: "adjudication"` — the latter only when an injected verdict exists and matches; fabricating one is rejected. |
|
|
32
|
+
|
|
33
|
+
Command: `/decision log [N]` · `/decision show <id>` · `/decision cancel <id>`.
|
|
34
|
+
|
|
35
|
+
## Timeout de-escalation (设计 §5)
|
|
36
|
+
|
|
37
|
+
Factory default 180s (`config.timeoutMs` overrides). On timeout without a
|
|
38
|
+
user answer: redline forks inject a `decision.notice` halted notice; L1/L2
|
|
39
|
+
forks dispatch the adjudication chain through the public
|
|
40
|
+
`agent-forge.agent-task-scheduler@1` capability (subagent-delegate),
|
|
41
|
+
readonly profile with `read/grep/ls/find` only. Every chain failure —
|
|
42
|
+
capability missing, submit/drain error, subagent not succeeded, output
|
|
43
|
+
violating the `{"choice","confidence","rationale","keyEvidence"}` JSON
|
|
44
|
+
protocol — lands as `adjudication.unavailable` / `adjudication.unparseable`
|
|
45
|
+
and the same halted behavior: stop, no retry, no silent downgrade.
|
|
46
|
+
|
|
47
|
+
## Persistence protocol
|
|
48
|
+
|
|
49
|
+
Structurally identical to the goal/plan/alignment plugins: append-only
|
|
50
|
+
`decision.state` full snapshots plus one `decision.transition` audit entry
|
|
51
|
+
per conversion (`asked | answered.user | answered.adjudication |
|
|
52
|
+
timed-out.halted | timed-out.adjudicating | adjudication.unavailable |
|
|
53
|
+
adjudication.unparseable | cancelled`), announced as `decision.changed`.
|
|
54
|
+
The full adjudication verdict rides the `decision.state` snapshot for
|
|
55
|
+
`/decision show`. User answers inject as `decision.answered`
|
|
56
|
+
(`display: false`, model context); notices inject as `decision.notice`
|
|
57
|
+
(`display: true`, user visible).
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- Requires a host session that exposes `SessionAPI.sendMessage` (answer and
|
|
62
|
+
notice injection); without it `decision_ask` fails fast and the rest of
|
|
63
|
+
the surface stays usable.
|
|
64
|
+
- The confirmation page is one of two answer channels for a present user;
|
|
65
|
+
unattended sessions never depend on it.
|
|
66
|
+
- Deterministic tests only (fake CapabilityAPI + fake scheduler handle +
|
|
67
|
+
real `node:http` on an OS-assigned port); no real model calls. Adjudication
|
|
68
|
+
quality is a model quality attribute and deliberately untested (设计 §9).
|
package/agent-forge.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"packageFormatVersion": 1,
|
|
3
|
+
"id": "agent-forge.plugin.decision-escalation",
|
|
4
|
+
"pluginVersion": "0.88.15",
|
|
5
|
+
"apiVersion": "1",
|
|
6
|
+
"minHostVersion": "0.84.4",
|
|
7
|
+
"platform": {
|
|
8
|
+
"os": [
|
|
9
|
+
"windows",
|
|
10
|
+
"macos",
|
|
11
|
+
"linux"
|
|
12
|
+
],
|
|
13
|
+
"runtime": [
|
|
14
|
+
{
|
|
15
|
+
"name": "node"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adjudication chain (`docs/design/决策升级插件设计.md` §5): on timeout, a
|
|
3
|
+
* reversible fork is decided by independent subagents dispatched through the
|
|
4
|
+
* public `agent-forge.agent-task-scheduler@1` capability (provided by
|
|
5
|
+
* subagent-delegate).
|
|
6
|
+
*
|
|
7
|
+
* - L1 (routine): one adjudicator agent reads the code itself and picks.
|
|
8
|
+
* - L2 (significant): one advocate per reversible option (parallel submit),
|
|
9
|
+
* then a judge whose input is the dossiers + the symmetric brief + a
|
|
10
|
+
* spot-check instruction — never the main agent's leaning material.
|
|
11
|
+
* - Every failure (capability missing, submit/drain error, subagent not
|
|
12
|
+
* succeeded, missing text, protocol violation) surfaces as
|
|
13
|
+
* `{ ok: false }`; the caller maps it to a halted notice — no retry, no
|
|
14
|
+
* silent downgrade.
|
|
15
|
+
*
|
|
16
|
+
* Request/response vocabulary is verified against
|
|
17
|
+
* `packages/plugin-sdk/src/agent-task-scheduler.ts` (authoritative) and the
|
|
18
|
+
* provider in `plugins/subagent-delegate/src/scheduler-capability.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import { type CapabilityAPI } from "@agent-forge/plugin-sdk";
|
|
21
|
+
import { type DecisionOptionInput } from "./engine.ts";
|
|
22
|
+
import { type AdjudicationVerdict, type DecisionRiskClass } from "./types.ts";
|
|
23
|
+
/** Budget handed to each submit (deadlineAt) and drain (timeoutMs). */
|
|
24
|
+
export declare const ADJUDICATION_BUDGET_MS = 240000;
|
|
25
|
+
/** Structural face of a scheduler handle this module needs (fake-injectable in tests). */
|
|
26
|
+
export interface SchedulerCaller {
|
|
27
|
+
call(input: unknown, options?: {
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}): {
|
|
30
|
+
result: Promise<unknown>;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the scheduler capability through the public capability directory.
|
|
35
|
+
* Returns undefined when the capability is absent — per design §5.4 an absent
|
|
36
|
+
* scheduler is adjudication-unavailable, never a silent fallback; the caller
|
|
37
|
+
* turns undefined into `adjudication.unavailable` + halted.
|
|
38
|
+
*/
|
|
39
|
+
export declare function requireSchedulerCaller(api: CapabilityAPI): SchedulerCaller | undefined;
|
|
40
|
+
export type AdjudicationOutcome = {
|
|
41
|
+
readonly ok: true;
|
|
42
|
+
readonly verdict: AdjudicationVerdict;
|
|
43
|
+
} | {
|
|
44
|
+
readonly ok: false;
|
|
45
|
+
readonly code: "unavailable" | "unparseable";
|
|
46
|
+
readonly detail: string;
|
|
47
|
+
};
|
|
48
|
+
export interface AdjudicationInput {
|
|
49
|
+
decisionId: string;
|
|
50
|
+
question: string;
|
|
51
|
+
options: readonly DecisionOptionInput[];
|
|
52
|
+
contextDigest: string;
|
|
53
|
+
riskClass: DecisionRiskClass;
|
|
54
|
+
}
|
|
55
|
+
export interface AdjudicationRunOptions {
|
|
56
|
+
budgetMs?: number;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
/** Test seam for the clock that derives submit deadlines. */
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Advocate prompt (§5.3): one agent argues for ONE option, but must
|
|
63
|
+
* independently investigate the codebase — the brief is the starting point,
|
|
64
|
+
* not the conclusion.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildAdvocatePrompt(option: DecisionOptionInput, index: number, input: AdjudicationInput): string;
|
|
67
|
+
/** L1 adjudicator prompt (§5.2): investigate independently, then pick. */
|
|
68
|
+
export declare function buildAdjudicatorPrompt(input: AdjudicationInput): string;
|
|
69
|
+
/**
|
|
70
|
+
* Judge prompt (§5.3): dossiers + symmetric brief + spot-check instruction.
|
|
71
|
+
* It deliberately contains nothing about what the main agent leans toward —
|
|
72
|
+
* the dossiers are the only per-option arguments.
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildJudgePrompt(dossiers: readonly string[], input: AdjudicationInput): string;
|
|
75
|
+
/**
|
|
76
|
+
* Parses the verdict JSON protocol (§5): an object with an in-range integer
|
|
77
|
+
* `choice`, "high"/"low" confidence, a non-empty rationale and a string
|
|
78
|
+
* keyEvidence array. Structural violations reject; length overruns truncate
|
|
79
|
+
* against DECISION_LIMITS — a salvageable verdict must not be
|
|
80
|
+
* thrown away over size, the state snapshot carries the bounds.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseVerdict(text: string, optionCount: number): AdjudicationVerdict | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* Runs the whole chain (§5). Redline forks never reach this module — the
|
|
85
|
+
* caller checks `record.redLine` first; as a belt-and-suspenders guard a
|
|
86
|
+
* redline input short-circuits here too, without submitting anything.
|
|
87
|
+
*/
|
|
88
|
+
export declare function runAdjudication(caller: SchedulerCaller, input: AdjudicationInput, options?: AdjudicationRunOptions): Promise<AdjudicationOutcome>;
|
|
89
|
+
//# sourceMappingURL=adjudication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adjudication.d.ts","sourceRoot":"","sources":["../src/adjudication.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAKN,KAAK,aAAa,EAElB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAoB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,KAAK,mBAAmB,EAAmB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/F,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,SAAU,CAAC;AAE9C,0FAA0F;AAC1F,MAAM,WAAW,eAAe;IAC/B,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG;QAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;CACvF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,aAAa,GAAG,eAAe,GAAG,SAAS,CAOtF;AAED,MAAM,MAAM,mBAAmB,GAC5B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAA;CAAE,GAC5D;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,aAAa,GAAG,aAAa,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjG,MAAM,WAAW,iBAAiB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,iBAAiB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAwCD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAehH;AAED,2EAA0E;AAC1E,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAcvE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAkB9F;AAMD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS,CA0B/F;AA+FD;;;;GAIG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,eAAe,EACvB,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE,sBAA2B,GAClC,OAAO,CAAC,mBAAmB,CAAC,CAoD9B","sourcesContent":["/**\n * Adjudication chain (`docs/design/决策升级插件设计.md` §5): on timeout, a\n * reversible fork is decided by independent subagents dispatched through the\n * public `agent-forge.agent-task-scheduler@1` capability (provided by\n * subagent-delegate).\n *\n * - L1 (routine): one adjudicator agent reads the code itself and picks.\n * - L2 (significant): one advocate per reversible option (parallel submit),\n * then a judge whose input is the dossiers + the symmetric brief + a\n * spot-check instruction — never the main agent's leaning material.\n * - Every failure (capability missing, submit/drain error, subagent not\n * succeeded, missing text, protocol violation) surfaces as\n * `{ ok: false }`; the caller maps it to a halted notice — no retry, no\n * silent downgrade.\n *\n * Request/response vocabulary is verified against\n * `packages/plugin-sdk/src/agent-task-scheduler.ts` (authoritative) and the\n * provider in `plugins/subagent-delegate/src/scheduler-capability.ts`.\n */\n\nimport {\n\tAGENT_TASK_SCHEDULER_CAPABILITY_ID,\n\ttype AgentTaskChildProfileV1,\n\ttype AgentTaskSchedulerResponseV1,\n\ttype AgentTaskSchedulerResultV1,\n\ttype CapabilityAPI,\n\ttype CapabilityHandle,\n} from \"@agent-forge/plugin-sdk\";\nimport { classifyDecision, type DecisionOptionInput } from \"./engine.ts\";\nimport { type AdjudicationVerdict, DECISION_LIMITS, type DecisionRiskClass } from \"./types.ts\";\n\n/** Budget handed to each submit (deadlineAt) and drain (timeoutMs). */\nexport const ADJUDICATION_BUDGET_MS = 240_000;\n\n/** Structural face of a scheduler handle this module needs (fake-injectable in tests). */\nexport interface SchedulerCaller {\n\tcall(input: unknown, options?: { signal?: AbortSignal }): { result: Promise<unknown> };\n}\n\n/**\n * Resolves the scheduler capability through the public capability directory.\n * Returns undefined when the capability is absent — per design §5.4 an absent\n * scheduler is adjudication-unavailable, never a silent fallback; the caller\n * turns undefined into `adjudication.unavailable` + halted.\n */\nexport function requireSchedulerCaller(api: CapabilityAPI): SchedulerCaller | undefined {\n\ttry {\n\t\tconst handle = api.capabilities.require(AGENT_TASK_SCHEDULER_CAPABILITY_ID, { version: 1 });\n\t\treturn handle as CapabilityHandle<unknown> as unknown as SchedulerCaller;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport type AdjudicationOutcome =\n\t| { readonly ok: true; readonly verdict: AdjudicationVerdict }\n\t| { readonly ok: false; readonly code: \"unavailable\" | \"unparseable\"; readonly detail: string };\n\nexport interface AdjudicationInput {\n\tdecisionId: string;\n\tquestion: string;\n\toptions: readonly DecisionOptionInput[];\n\tcontextDigest: string;\n\triskClass: DecisionRiskClass;\n}\n\nexport interface AdjudicationRunOptions {\n\tbudgetMs?: number;\n\tsignal?: AbortSignal;\n\t/** Test seam for the clock that derives submit deadlines. */\n\tnow?: () => number;\n}\n\n/** Child profile shared by every adjudication subagent (§5.2): readonly + read-only tools. */\nconst CHILD_TOOLS = [\"read\", \"grep\", \"ls\", \"find\"] as const;\n\nfunction childProfile(roleId: string, systemPromptAppend: string): AgentTaskChildProfileV1 {\n\treturn {\n\t\tversion: 1,\n\t\troleId,\n\t\ttools: [...CHILD_TOOLS],\n\t\tmode: \"readonly\",\n\t\tsystemPromptAppend,\n\t};\n}\n\nconst OUTPUT_PROTOCOL = [\n\t\"输出协议(必须严格遵守):最终回复只输出一个 JSON 对象,不要输出任何其他文字:\",\n\t'{\"choice\": <选项下标数字>, \"confidence\": \"high\"|\"low\", \"rationale\": \"<一句话理由>\", \"keyEvidence\": [\"<path:line 证据>\", ...]}',\n].join(\"\\n\");\n\n/**\n * Symmetric brief block (§8: 简报必须对称) — both adjudicator and judge see\n * exactly this, plus the explicit instruction that the brief is one input,\n * not a verdict.\n */\nfunction briefBlock(input: AdjudicationInput): string {\n\tconst lines = [\n\t\t`== 决策简报(对称简报,来自主 agent;只当作输入,不得当作结论)==`,\n\t\tinput.contextDigest,\n\t\t\"== 选项 ==\".concat(\"\"),\n\t];\n\tinput.options.forEach((option, index) => {\n\t\tconst reversibility = option.reversibility === undefined ? \"未声明\" : option.reversibility;\n\t\tlines.push(\n\t\t\t`[${index}] ${option.label} — ${option.description} 后果: ${option.consequences} 可逆性: ${reversibility}`,\n\t\t);\n\t});\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Advocate prompt (§5.3): one agent argues for ONE option, but must\n * independently investigate the codebase — the brief is the starting point,\n * not the conclusion.\n */\nexport function buildAdvocatePrompt(option: DecisionOptionInput, index: number, input: AdjudicationInput): string {\n\treturn [\n\t\t`你是一名独立 advocate,任务是调查代码库后为下面的选项构建论证书。`,\n\t\t`分叉问题:${input.question}`,\n\t\t\"\",\n\t\tbriefBlock(input),\n\t\t\"\",\n\t\t`你只论证选项 [${index}] ${option.label}。纪律:`,\n\t\t`- 独立调查:用 read/grep/ls/find 自己抽查代码,验证简报里的支持与反对证据,不得只复述简报。`,\n\t\t`- 论证要诚实:如实列出该选项的代价、风险与不成立条件;发现致命缺陷必须写明,不得掩盖。`,\n\t\t`- 证据必须可核查:每条关键证据给出 path:line。`,\n\t\t\"\",\n\t\tOUTPUT_PROTOCOL,\n\t\t\"注意:你的 choice 字段固定填你被指派的选项下标,你的职责是用证据评估它,而不是重新站队。\",\n\t].join(\"\\n\");\n}\n\n/** L1 adjudicator prompt (§5.2): investigate independently, then pick. */\nexport function buildAdjudicatorPrompt(input: AdjudicationInput): string {\n\treturn [\n\t\t`你是一名独立裁决 agent。主 agent 在下面的分叉上等不到用户作答,请你调查代码库后独立裁决。`,\n\t\t`分叉问题:${input.question}`,\n\t\t\"\",\n\t\tbriefBlock(input),\n\t\t\"\",\n\t\t\"纪律:\",\n\t\t\"- 独立调查:用 read/grep/ls/find 自行抽查与该分叉相关的代码与文档,简报只是输入,不是结论。\",\n\t\t\"- 对称评估:对每个选项同等力度地找支持与反对证据;重点核对可逆性与后果声明是否属实。\",\n\t\t\"- 证据必须可核查:keyEvidence 每条给出 path:line。\",\n\t\t\"\",\n\t\tOUTPUT_PROTOCOL,\n\t].join(\"\\n\");\n}\n\n/**\n * Judge prompt (§5.3): dossiers + symmetric brief + spot-check instruction.\n * It deliberately contains nothing about what the main agent leans toward —\n * the dossiers are the only per-option arguments.\n */\nexport function buildJudgePrompt(dossiers: readonly string[], input: AdjudicationInput): string {\n\treturn [\n\t\t`你是一名独立法官。若干 advocate 为下面的分叉各提交了一份论证书,请你裁决。`,\n\t\t`分叉问题:${input.question}`,\n\t\t\"\",\n\t\tbriefBlock(input),\n\t\t\"\",\n\t\t\"== advocate 论证书 ==\".concat(\"\"),\n\t\tdossiers.map((dossier, index) => `--- 论证书 [${index}] ---\\n${dossier}`).join(\"\\n\"),\n\t\t\"\",\n\t\t\"纪律:\",\n\t\t\"- 论证书只是主张:抽查其关键证据(用 read/grep/ls/find 验证 path:line),不得偏信任何一方。\",\n\t\t\"- 简报是对称输入,不是倾向:若论证书与简报冲突,以你自己的抽查为准。\",\n\t\t\"- 独立判断:重点核对每个选项的可逆性与后果声明;选择整体代价最小的可执行选项。\",\n\t\t\"- 证据必须可核查:keyEvidence 每条给出 path:line。\",\n\t\t\"\",\n\t\tOUTPUT_PROTOCOL,\n\t].join(\"\\n\");\n}\n\nconst VERDICT_ROLE_ID = \"decision-adjudicator\";\nconst ADVOCATE_ROLE_ID = \"decision-advocate\";\nconst JUDGE_ROLE_ID = \"decision-judge\";\n\n/**\n * Parses the verdict JSON protocol (§5): an object with an in-range integer\n * `choice`, \"high\"/\"low\" confidence, a non-empty rationale and a string\n * keyEvidence array. Structural violations reject; length overruns truncate\n * against DECISION_LIMITS — a salvageable verdict must not be\n * thrown away over size, the state snapshot carries the bounds.\n */\nexport function parseVerdict(text: string, optionCount: number): AdjudicationVerdict | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n\tconst record = parsed as Record<string, unknown>;\n\tif (!Number.isInteger(record.choice) || (record.choice as number) < 0 || (record.choice as number) >= optionCount) {\n\t\treturn undefined;\n\t}\n\tif (record.confidence !== \"high\" && record.confidence !== \"low\") return undefined;\n\tif (typeof record.rationale !== \"string\" || record.rationale.trim() === \"\") return undefined;\n\tif (!Array.isArray(record.keyEvidence)) return undefined;\n\tconst evidence: string[] = [];\n\tfor (const item of record.keyEvidence) {\n\t\tif (typeof item !== \"string\" || item.trim() === \"\") return undefined;\n\t\tevidence.push(item.slice(0, DECISION_LIMITS.verdictEvidenceItemMaxLength));\n\t}\n\treturn {\n\t\tchoice: record.choice as number,\n\t\tconfidence: record.confidence,\n\t\trationale: record.rationale.slice(0, DECISION_LIMITS.verdictRationaleMaxLength),\n\t\tkeyEvidence: evidence.slice(0, DECISION_LIMITS.verdictEvidenceMax),\n\t};\n}\n\nasync function callScheduler(caller: SchedulerCaller, request: unknown, signal?: AbortSignal): Promise<unknown> {\n\tconst operation = caller.call(request, signal === undefined ? undefined : { signal });\n\treturn await operation.result;\n}\n\nfunction isSchedulerEnvelope(value: unknown): value is AgentTaskSchedulerResponseV1 {\n\treturn value !== null && typeof value === \"object\" && \"action\" in (value as Record<string, unknown>);\n}\n\n/** submit → taskId; any throw/shape violation is adjudication-unavailable. */\nasync function submitTask(\n\tcaller: SchedulerCaller,\n\tprompt: string,\n\troleId: string,\n\tdeadlineAt: number,\n\tsignal?: AbortSignal,\n): Promise<{ ok: true; taskId: string } | { ok: false; detail: string }> {\n\tlet response: unknown;\n\ttry {\n\t\tresponse = await callScheduler(\n\t\t\tcaller,\n\t\t\t{\n\t\t\t\tversion: 1,\n\t\t\t\taction: \"submit\",\n\t\t\t\trequest: {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tprompt,\n\t\t\t\t\tprofile: childProfile(roleId, \"你在为一次分叉裁决做独立调查;只读,不改任何文件。\"),\n\t\t\t\t\tdeadlineAt,\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t} catch (error) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tdetail: `scheduler submit failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t};\n\t}\n\tif (\n\t\t!isSchedulerEnvelope(response) ||\n\t\tresponse.action !== \"submit\" ||\n\t\ttypeof response.taskId !== \"string\" ||\n\t\tresponse.taskId === \"\"\n\t) {\n\t\treturn { ok: false, detail: \"scheduler submit returned a malformed envelope\" };\n\t}\n\treturn { ok: true, taskId: response.taskId };\n}\n\n/** drain → terminal result; a non-succeeded status is a subagent failure (unavailable). */\nasync function drainTask(\n\tcaller: SchedulerCaller,\n\ttaskId: string,\n\ttimeoutMs: number,\n\tsignal?: AbortSignal,\n): Promise<{ ok: true; result: AgentTaskSchedulerResultV1 } | { ok: false; detail: string }> {\n\tlet response: unknown;\n\ttry {\n\t\tresponse = await callScheduler(caller, { version: 1, action: \"drain\", taskId, timeoutMs }, signal);\n\t} catch (error) {\n\t\treturn { ok: false, detail: `scheduler drain failed: ${error instanceof Error ? error.message : String(error)}` };\n\t}\n\tif (!isSchedulerEnvelope(response) || response.action !== \"drain\" || response.result === undefined) {\n\t\treturn { ok: false, detail: \"scheduler drain returned a malformed envelope\" };\n\t}\n\tconst result: AgentTaskSchedulerResultV1 = response.result;\n\tif (result.status !== \"succeeded\") {\n\t\tconst reason = result.error?.message ?? result.status;\n\t\treturn { ok: false, detail: `adjudication subagent did not complete: ${reason}` };\n\t}\n\treturn { ok: true, result };\n}\n\n/** submit + drain + text in one step; missing text is a protocol violation (unparseable). */\nasync function runSubagent(\n\tcaller: SchedulerCaller,\n\tprompt: string,\n\troleId: string,\n\tdeadlineAt: number,\n\ttimeoutMs: number,\n\tsignal?: AbortSignal,\n): Promise<{ ok: true; text: string } | { ok: false; code: \"unavailable\" | \"unparseable\"; detail: string }> {\n\tconst submitted = await submitTask(caller, prompt, roleId, deadlineAt, signal);\n\tif (!submitted.ok) return { ok: false, code: \"unavailable\", detail: submitted.detail };\n\tconst drained = await drainTask(caller, submitted.taskId, timeoutMs, signal);\n\tif (!drained.ok) return { ok: false, code: \"unavailable\", detail: drained.detail };\n\tif (typeof drained.result.text !== \"string\" || drained.result.text.trim() === \"\") {\n\t\treturn { ok: false, code: \"unparseable\", detail: \"adjudication subagent produced no text\" };\n\t}\n\treturn { ok: true, text: drained.result.text };\n}\n\n/**\n * Runs the whole chain (§5). Redline forks never reach this module — the\n * caller checks `record.redLine` first; as a belt-and-suspenders guard a\n * redline input short-circuits here too, without submitting anything.\n */\nexport async function runAdjudication(\n\tcaller: SchedulerCaller,\n\tinput: AdjudicationInput,\n\toptions: AdjudicationRunOptions = {},\n): Promise<AdjudicationOutcome> {\n\tconst now = options.now ?? Date.now;\n\tconst budgetMs = options.budgetMs ?? ADJUDICATION_BUDGET_MS;\n\tconst signal = options.signal;\n\t// 红线兜底(§3/§5.1):不可逆分叉不派任何子代理。\n\tif (classifyDecision(input.options, input.riskClass) === \"redline\") {\n\t\treturn { ok: false, code: \"unavailable\", detail: \"redline decision must not be adjudicated\" };\n\t}\n\tconst deadlineAt = now() + budgetMs;\n\tif (input.riskClass === \"significant\") {\n\t\t// L2: one advocate per option (all reversible by classification), then the judge.\n\t\tconst advocateRuns = input.options.map((option, index) =>\n\t\t\trunSubagent(caller, buildAdvocatePrompt(option, index, input), ADVOCATE_ROLE_ID, deadlineAt, budgetMs, signal),\n\t\t);\n\t\tconst advocateOutcomes = await Promise.all(advocateRuns);\n\t\tconst dossiers: string[] = [];\n\t\tfor (const outcome of advocateOutcomes) {\n\t\t\tif (!outcome.ok) return outcome;\n\t\t\tdossiers.push(outcome.text);\n\t\t}\n\t\t// 法官拿到独立预算:deadlineAt 在 submit 前按当前时刻重算,\n\t\t// 不与并行 advocates 共用链头时刻——advocate 论证耗时不侵蚀法官窗口。\n\t\tconst judge = await runSubagent(\n\t\t\tcaller,\n\t\t\tbuildJudgePrompt(dossiers, input),\n\t\t\tJUDGE_ROLE_ID,\n\t\t\tnow() + budgetMs,\n\t\t\tbudgetMs,\n\t\t\tsignal,\n\t\t);\n\t\tif (!judge.ok) return judge;\n\t\tconst verdict = parseVerdict(judge.text, input.options.length);\n\t\tif (verdict === undefined) {\n\t\t\treturn { ok: false, code: \"unparseable\", detail: \"judge output violates the verdict JSON protocol\" };\n\t\t}\n\t\treturn { ok: true, verdict };\n\t}\n\t// L1: a single adjudicator.\n\tconst outcome = await runSubagent(\n\t\tcaller,\n\t\tbuildAdjudicatorPrompt(input),\n\t\tVERDICT_ROLE_ID,\n\t\tdeadlineAt,\n\t\tbudgetMs,\n\t\tsignal,\n\t);\n\tif (!outcome.ok) return outcome;\n\tconst verdict = parseVerdict(outcome.text, input.options.length);\n\tif (verdict === undefined) {\n\t\treturn { ok: false, code: \"unparseable\", detail: \"adjudicator output violates the verdict JSON protocol\" };\n\t}\n\treturn { ok: true, verdict };\n}\n"]}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adjudication chain (`docs/design/决策升级插件设计.md` §5): on timeout, a
|
|
3
|
+
* reversible fork is decided by independent subagents dispatched through the
|
|
4
|
+
* public `agent-forge.agent-task-scheduler@1` capability (provided by
|
|
5
|
+
* subagent-delegate).
|
|
6
|
+
*
|
|
7
|
+
* - L1 (routine): one adjudicator agent reads the code itself and picks.
|
|
8
|
+
* - L2 (significant): one advocate per reversible option (parallel submit),
|
|
9
|
+
* then a judge whose input is the dossiers + the symmetric brief + a
|
|
10
|
+
* spot-check instruction — never the main agent's leaning material.
|
|
11
|
+
* - Every failure (capability missing, submit/drain error, subagent not
|
|
12
|
+
* succeeded, missing text, protocol violation) surfaces as
|
|
13
|
+
* `{ ok: false }`; the caller maps it to a halted notice — no retry, no
|
|
14
|
+
* silent downgrade.
|
|
15
|
+
*
|
|
16
|
+
* Request/response vocabulary is verified against
|
|
17
|
+
* `packages/plugin-sdk/src/agent-task-scheduler.ts` (authoritative) and the
|
|
18
|
+
* provider in `plugins/subagent-delegate/src/scheduler-capability.ts`.
|
|
19
|
+
*/
|
|
20
|
+
import { AGENT_TASK_SCHEDULER_CAPABILITY_ID, } from "@agent-forge/plugin-sdk";
|
|
21
|
+
import { classifyDecision } from "./engine.js";
|
|
22
|
+
import { DECISION_LIMITS } from "./types.js";
|
|
23
|
+
/** Budget handed to each submit (deadlineAt) and drain (timeoutMs). */
|
|
24
|
+
export const ADJUDICATION_BUDGET_MS = 240_000;
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the scheduler capability through the public capability directory.
|
|
27
|
+
* Returns undefined when the capability is absent — per design §5.4 an absent
|
|
28
|
+
* scheduler is adjudication-unavailable, never a silent fallback; the caller
|
|
29
|
+
* turns undefined into `adjudication.unavailable` + halted.
|
|
30
|
+
*/
|
|
31
|
+
export function requireSchedulerCaller(api) {
|
|
32
|
+
try {
|
|
33
|
+
const handle = api.capabilities.require(AGENT_TASK_SCHEDULER_CAPABILITY_ID, { version: 1 });
|
|
34
|
+
return handle;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Child profile shared by every adjudication subagent (§5.2): readonly + read-only tools. */
|
|
41
|
+
const CHILD_TOOLS = ["read", "grep", "ls", "find"];
|
|
42
|
+
function childProfile(roleId, systemPromptAppend) {
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
roleId,
|
|
46
|
+
tools: [...CHILD_TOOLS],
|
|
47
|
+
mode: "readonly",
|
|
48
|
+
systemPromptAppend,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const OUTPUT_PROTOCOL = [
|
|
52
|
+
"输出协议(必须严格遵守):最终回复只输出一个 JSON 对象,不要输出任何其他文字:",
|
|
53
|
+
'{"choice": <选项下标数字>, "confidence": "high"|"low", "rationale": "<一句话理由>", "keyEvidence": ["<path:line 证据>", ...]}',
|
|
54
|
+
].join("\n");
|
|
55
|
+
/**
|
|
56
|
+
* Symmetric brief block (§8: 简报必须对称) — both adjudicator and judge see
|
|
57
|
+
* exactly this, plus the explicit instruction that the brief is one input,
|
|
58
|
+
* not a verdict.
|
|
59
|
+
*/
|
|
60
|
+
function briefBlock(input) {
|
|
61
|
+
const lines = [
|
|
62
|
+
`== 决策简报(对称简报,来自主 agent;只当作输入,不得当作结论)==`,
|
|
63
|
+
input.contextDigest,
|
|
64
|
+
"== 选项 ==".concat(""),
|
|
65
|
+
];
|
|
66
|
+
input.options.forEach((option, index) => {
|
|
67
|
+
const reversibility = option.reversibility === undefined ? "未声明" : option.reversibility;
|
|
68
|
+
lines.push(`[${index}] ${option.label} — ${option.description} 后果: ${option.consequences} 可逆性: ${reversibility}`);
|
|
69
|
+
});
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Advocate prompt (§5.3): one agent argues for ONE option, but must
|
|
74
|
+
* independently investigate the codebase — the brief is the starting point,
|
|
75
|
+
* not the conclusion.
|
|
76
|
+
*/
|
|
77
|
+
export function buildAdvocatePrompt(option, index, input) {
|
|
78
|
+
return [
|
|
79
|
+
`你是一名独立 advocate,任务是调查代码库后为下面的选项构建论证书。`,
|
|
80
|
+
`分叉问题:${input.question}`,
|
|
81
|
+
"",
|
|
82
|
+
briefBlock(input),
|
|
83
|
+
"",
|
|
84
|
+
`你只论证选项 [${index}] ${option.label}。纪律:`,
|
|
85
|
+
`- 独立调查:用 read/grep/ls/find 自己抽查代码,验证简报里的支持与反对证据,不得只复述简报。`,
|
|
86
|
+
`- 论证要诚实:如实列出该选项的代价、风险与不成立条件;发现致命缺陷必须写明,不得掩盖。`,
|
|
87
|
+
`- 证据必须可核查:每条关键证据给出 path:line。`,
|
|
88
|
+
"",
|
|
89
|
+
OUTPUT_PROTOCOL,
|
|
90
|
+
"注意:你的 choice 字段固定填你被指派的选项下标,你的职责是用证据评估它,而不是重新站队。",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
/** L1 adjudicator prompt (§5.2): investigate independently, then pick. */
|
|
94
|
+
export function buildAdjudicatorPrompt(input) {
|
|
95
|
+
return [
|
|
96
|
+
`你是一名独立裁决 agent。主 agent 在下面的分叉上等不到用户作答,请你调查代码库后独立裁决。`,
|
|
97
|
+
`分叉问题:${input.question}`,
|
|
98
|
+
"",
|
|
99
|
+
briefBlock(input),
|
|
100
|
+
"",
|
|
101
|
+
"纪律:",
|
|
102
|
+
"- 独立调查:用 read/grep/ls/find 自行抽查与该分叉相关的代码与文档,简报只是输入,不是结论。",
|
|
103
|
+
"- 对称评估:对每个选项同等力度地找支持与反对证据;重点核对可逆性与后果声明是否属实。",
|
|
104
|
+
"- 证据必须可核查:keyEvidence 每条给出 path:line。",
|
|
105
|
+
"",
|
|
106
|
+
OUTPUT_PROTOCOL,
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Judge prompt (§5.3): dossiers + symmetric brief + spot-check instruction.
|
|
111
|
+
* It deliberately contains nothing about what the main agent leans toward —
|
|
112
|
+
* the dossiers are the only per-option arguments.
|
|
113
|
+
*/
|
|
114
|
+
export function buildJudgePrompt(dossiers, input) {
|
|
115
|
+
return [
|
|
116
|
+
`你是一名独立法官。若干 advocate 为下面的分叉各提交了一份论证书,请你裁决。`,
|
|
117
|
+
`分叉问题:${input.question}`,
|
|
118
|
+
"",
|
|
119
|
+
briefBlock(input),
|
|
120
|
+
"",
|
|
121
|
+
"== advocate 论证书 ==".concat(""),
|
|
122
|
+
dossiers.map((dossier, index) => `--- 论证书 [${index}] ---\n${dossier}`).join("\n"),
|
|
123
|
+
"",
|
|
124
|
+
"纪律:",
|
|
125
|
+
"- 论证书只是主张:抽查其关键证据(用 read/grep/ls/find 验证 path:line),不得偏信任何一方。",
|
|
126
|
+
"- 简报是对称输入,不是倾向:若论证书与简报冲突,以你自己的抽查为准。",
|
|
127
|
+
"- 独立判断:重点核对每个选项的可逆性与后果声明;选择整体代价最小的可执行选项。",
|
|
128
|
+
"- 证据必须可核查:keyEvidence 每条给出 path:line。",
|
|
129
|
+
"",
|
|
130
|
+
OUTPUT_PROTOCOL,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
const VERDICT_ROLE_ID = "decision-adjudicator";
|
|
134
|
+
const ADVOCATE_ROLE_ID = "decision-advocate";
|
|
135
|
+
const JUDGE_ROLE_ID = "decision-judge";
|
|
136
|
+
/**
|
|
137
|
+
* Parses the verdict JSON protocol (§5): an object with an in-range integer
|
|
138
|
+
* `choice`, "high"/"low" confidence, a non-empty rationale and a string
|
|
139
|
+
* keyEvidence array. Structural violations reject; length overruns truncate
|
|
140
|
+
* against DECISION_LIMITS — a salvageable verdict must not be
|
|
141
|
+
* thrown away over size, the state snapshot carries the bounds.
|
|
142
|
+
*/
|
|
143
|
+
export function parseVerdict(text, optionCount) {
|
|
144
|
+
let parsed;
|
|
145
|
+
try {
|
|
146
|
+
parsed = JSON.parse(text);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
152
|
+
return undefined;
|
|
153
|
+
const record = parsed;
|
|
154
|
+
if (!Number.isInteger(record.choice) || record.choice < 0 || record.choice >= optionCount) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
if (record.confidence !== "high" && record.confidence !== "low")
|
|
158
|
+
return undefined;
|
|
159
|
+
if (typeof record.rationale !== "string" || record.rationale.trim() === "")
|
|
160
|
+
return undefined;
|
|
161
|
+
if (!Array.isArray(record.keyEvidence))
|
|
162
|
+
return undefined;
|
|
163
|
+
const evidence = [];
|
|
164
|
+
for (const item of record.keyEvidence) {
|
|
165
|
+
if (typeof item !== "string" || item.trim() === "")
|
|
166
|
+
return undefined;
|
|
167
|
+
evidence.push(item.slice(0, DECISION_LIMITS.verdictEvidenceItemMaxLength));
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
choice: record.choice,
|
|
171
|
+
confidence: record.confidence,
|
|
172
|
+
rationale: record.rationale.slice(0, DECISION_LIMITS.verdictRationaleMaxLength),
|
|
173
|
+
keyEvidence: evidence.slice(0, DECISION_LIMITS.verdictEvidenceMax),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function callScheduler(caller, request, signal) {
|
|
177
|
+
const operation = caller.call(request, signal === undefined ? undefined : { signal });
|
|
178
|
+
return await operation.result;
|
|
179
|
+
}
|
|
180
|
+
function isSchedulerEnvelope(value) {
|
|
181
|
+
return value !== null && typeof value === "object" && "action" in value;
|
|
182
|
+
}
|
|
183
|
+
/** submit → taskId; any throw/shape violation is adjudication-unavailable. */
|
|
184
|
+
async function submitTask(caller, prompt, roleId, deadlineAt, signal) {
|
|
185
|
+
let response;
|
|
186
|
+
try {
|
|
187
|
+
response = await callScheduler(caller, {
|
|
188
|
+
version: 1,
|
|
189
|
+
action: "submit",
|
|
190
|
+
request: {
|
|
191
|
+
version: 1,
|
|
192
|
+
prompt,
|
|
193
|
+
profile: childProfile(roleId, "你在为一次分叉裁决做独立调查;只读,不改任何文件。"),
|
|
194
|
+
deadlineAt,
|
|
195
|
+
},
|
|
196
|
+
}, signal);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
detail: `scheduler submit failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!isSchedulerEnvelope(response) ||
|
|
205
|
+
response.action !== "submit" ||
|
|
206
|
+
typeof response.taskId !== "string" ||
|
|
207
|
+
response.taskId === "") {
|
|
208
|
+
return { ok: false, detail: "scheduler submit returned a malformed envelope" };
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, taskId: response.taskId };
|
|
211
|
+
}
|
|
212
|
+
/** drain → terminal result; a non-succeeded status is a subagent failure (unavailable). */
|
|
213
|
+
async function drainTask(caller, taskId, timeoutMs, signal) {
|
|
214
|
+
let response;
|
|
215
|
+
try {
|
|
216
|
+
response = await callScheduler(caller, { version: 1, action: "drain", taskId, timeoutMs }, signal);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
return { ok: false, detail: `scheduler drain failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
220
|
+
}
|
|
221
|
+
if (!isSchedulerEnvelope(response) || response.action !== "drain" || response.result === undefined) {
|
|
222
|
+
return { ok: false, detail: "scheduler drain returned a malformed envelope" };
|
|
223
|
+
}
|
|
224
|
+
const result = response.result;
|
|
225
|
+
if (result.status !== "succeeded") {
|
|
226
|
+
const reason = result.error?.message ?? result.status;
|
|
227
|
+
return { ok: false, detail: `adjudication subagent did not complete: ${reason}` };
|
|
228
|
+
}
|
|
229
|
+
return { ok: true, result };
|
|
230
|
+
}
|
|
231
|
+
/** submit + drain + text in one step; missing text is a protocol violation (unparseable). */
|
|
232
|
+
async function runSubagent(caller, prompt, roleId, deadlineAt, timeoutMs, signal) {
|
|
233
|
+
const submitted = await submitTask(caller, prompt, roleId, deadlineAt, signal);
|
|
234
|
+
if (!submitted.ok)
|
|
235
|
+
return { ok: false, code: "unavailable", detail: submitted.detail };
|
|
236
|
+
const drained = await drainTask(caller, submitted.taskId, timeoutMs, signal);
|
|
237
|
+
if (!drained.ok)
|
|
238
|
+
return { ok: false, code: "unavailable", detail: drained.detail };
|
|
239
|
+
if (typeof drained.result.text !== "string" || drained.result.text.trim() === "") {
|
|
240
|
+
return { ok: false, code: "unparseable", detail: "adjudication subagent produced no text" };
|
|
241
|
+
}
|
|
242
|
+
return { ok: true, text: drained.result.text };
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Runs the whole chain (§5). Redline forks never reach this module — the
|
|
246
|
+
* caller checks `record.redLine` first; as a belt-and-suspenders guard a
|
|
247
|
+
* redline input short-circuits here too, without submitting anything.
|
|
248
|
+
*/
|
|
249
|
+
export async function runAdjudication(caller, input, options = {}) {
|
|
250
|
+
const now = options.now ?? Date.now;
|
|
251
|
+
const budgetMs = options.budgetMs ?? ADJUDICATION_BUDGET_MS;
|
|
252
|
+
const signal = options.signal;
|
|
253
|
+
// 红线兜底(§3/§5.1):不可逆分叉不派任何子代理。
|
|
254
|
+
if (classifyDecision(input.options, input.riskClass) === "redline") {
|
|
255
|
+
return { ok: false, code: "unavailable", detail: "redline decision must not be adjudicated" };
|
|
256
|
+
}
|
|
257
|
+
const deadlineAt = now() + budgetMs;
|
|
258
|
+
if (input.riskClass === "significant") {
|
|
259
|
+
// L2: one advocate per option (all reversible by classification), then the judge.
|
|
260
|
+
const advocateRuns = input.options.map((option, index) => runSubagent(caller, buildAdvocatePrompt(option, index, input), ADVOCATE_ROLE_ID, deadlineAt, budgetMs, signal));
|
|
261
|
+
const advocateOutcomes = await Promise.all(advocateRuns);
|
|
262
|
+
const dossiers = [];
|
|
263
|
+
for (const outcome of advocateOutcomes) {
|
|
264
|
+
if (!outcome.ok)
|
|
265
|
+
return outcome;
|
|
266
|
+
dossiers.push(outcome.text);
|
|
267
|
+
}
|
|
268
|
+
// 法官拿到独立预算:deadlineAt 在 submit 前按当前时刻重算,
|
|
269
|
+
// 不与并行 advocates 共用链头时刻——advocate 论证耗时不侵蚀法官窗口。
|
|
270
|
+
const judge = await runSubagent(caller, buildJudgePrompt(dossiers, input), JUDGE_ROLE_ID, now() + budgetMs, budgetMs, signal);
|
|
271
|
+
if (!judge.ok)
|
|
272
|
+
return judge;
|
|
273
|
+
const verdict = parseVerdict(judge.text, input.options.length);
|
|
274
|
+
if (verdict === undefined) {
|
|
275
|
+
return { ok: false, code: "unparseable", detail: "judge output violates the verdict JSON protocol" };
|
|
276
|
+
}
|
|
277
|
+
return { ok: true, verdict };
|
|
278
|
+
}
|
|
279
|
+
// L1: a single adjudicator.
|
|
280
|
+
const outcome = await runSubagent(caller, buildAdjudicatorPrompt(input), VERDICT_ROLE_ID, deadlineAt, budgetMs, signal);
|
|
281
|
+
if (!outcome.ok)
|
|
282
|
+
return outcome;
|
|
283
|
+
const verdict = parseVerdict(outcome.text, input.options.length);
|
|
284
|
+
if (verdict === undefined) {
|
|
285
|
+
return { ok: false, code: "unparseable", detail: "adjudicator output violates the verdict JSON protocol" };
|
|
286
|
+
}
|
|
287
|
+
return { ok: true, verdict };
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=adjudication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adjudication.js","sourceRoot":"","sources":["../src/adjudication.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Adjudication chain (`docs/design/决策升级插件设计.md` §5): on timeout, a\n * reversible fork is decided by independent subagents dispatched through the\n * public `agent-forge.agent-task-scheduler@1` capability (provided by\n * subagent-delegate).\n *\n * - L1 (routine): one adjudicator agent reads the code itself and picks.\n * - L2 (significant): one advocate per reversible option (parallel submit),\n * then a judge whose input is the dossiers + the symmetric brief + a\n * spot-check instruction — never the main agent's leaning material.\n * - Every failure (capability missing, submit/drain error, subagent not\n * succeeded, missing text, protocol violation) surfaces as\n * `{ ok: false }`; the caller maps it to a halted notice — no retry, no\n * silent downgrade.\n *\n * Request/response vocabulary is verified against\n * `packages/plugin-sdk/src/agent-task-scheduler.ts` (authoritative) and the\n * provider in `plugins/subagent-delegate/src/scheduler-capability.ts`.\n */\n\nimport {\n\tAGENT_TASK_SCHEDULER_CAPABILITY_ID,\n\ttype AgentTaskChildProfileV1,\n\ttype AgentTaskSchedulerResponseV1,\n\ttype AgentTaskSchedulerResultV1,\n\ttype CapabilityAPI,\n\ttype CapabilityHandle,\n} from \"@agent-forge/plugin-sdk\";\nimport { classifyDecision, type DecisionOptionInput } from \"./engine.ts\";\nimport { type AdjudicationVerdict, DECISION_LIMITS, type DecisionRiskClass } from \"./types.ts\";\n\n/** Budget handed to each submit (deadlineAt) and drain (timeoutMs). */\nexport const ADJUDICATION_BUDGET_MS = 240_000;\n\n/** Structural face of a scheduler handle this module needs (fake-injectable in tests). */\nexport interface SchedulerCaller {\n\tcall(input: unknown, options?: { signal?: AbortSignal }): { result: Promise<unknown> };\n}\n\n/**\n * Resolves the scheduler capability through the public capability directory.\n * Returns undefined when the capability is absent — per design §5.4 an absent\n * scheduler is adjudication-unavailable, never a silent fallback; the caller\n * turns undefined into `adjudication.unavailable` + halted.\n */\nexport function requireSchedulerCaller(api: CapabilityAPI): SchedulerCaller | undefined {\n\ttry {\n\t\tconst handle = api.capabilities.require(AGENT_TASK_SCHEDULER_CAPABILITY_ID, { version: 1 });\n\t\treturn handle as CapabilityHandle<unknown> as unknown as SchedulerCaller;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport type AdjudicationOutcome =\n\t| { readonly ok: true; readonly verdict: AdjudicationVerdict }\n\t| { readonly ok: false; readonly code: \"unavailable\" | \"unparseable\"; readonly detail: string };\n\nexport interface AdjudicationInput {\n\tdecisionId: string;\n\tquestion: string;\n\toptions: readonly DecisionOptionInput[];\n\tcontextDigest: string;\n\triskClass: DecisionRiskClass;\n}\n\nexport interface AdjudicationRunOptions {\n\tbudgetMs?: number;\n\tsignal?: AbortSignal;\n\t/** Test seam for the clock that derives submit deadlines. */\n\tnow?: () => number;\n}\n\n/** Child profile shared by every adjudication subagent (§5.2): readonly + read-only tools. */\nconst CHILD_TOOLS = [\"read\", \"grep\", \"ls\", \"find\"] as const;\n\nfunction childProfile(roleId: string, systemPromptAppend: string): AgentTaskChildProfileV1 {\n\treturn {\n\t\tversion: 1,\n\t\troleId,\n\t\ttools: [...CHILD_TOOLS],\n\t\tmode: \"readonly\",\n\t\tsystemPromptAppend,\n\t};\n}\n\nconst OUTPUT_PROTOCOL = [\n\t\"输出协议(必须严格遵守):最终回复只输出一个 JSON 对象,不要输出任何其他文字:\",\n\t'{\"choice\": <选项下标数字>, \"confidence\": \"high\"|\"low\", \"rationale\": \"<一句话理由>\", \"keyEvidence\": [\"<path:line 证据>\", ...]}',\n].join(\"\\n\");\n\n/**\n * Symmetric brief block (§8: 简报必须对称) — both adjudicator and judge see\n * exactly this, plus the explicit instruction that the brief is one input,\n * not a verdict.\n */\nfunction briefBlock(input: AdjudicationInput): string {\n\tconst lines = [\n\t\t`== 决策简报(对称简报,来自主 agent;只当作输入,不得当作结论)==`,\n\t\tinput.contextDigest,\n\t\t\"== 选项 ==\".concat(\"\"),\n\t];\n\tinput.options.forEach((option, index) => {\n\t\tconst reversibility = option.reversibility === undefined ? \"未声明\" : option.reversibility;\n\t\tlines.push(\n\t\t\t`[${index}] ${option.label} — ${option.description} 后果: ${option.consequences} 可逆性: ${reversibility}`,\n\t\t);\n\t});\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Advocate prompt (§5.3): one agent argues for ONE option, but must\n * independently investigate the codebase — the brief is the starting point,\n * not the conclusion.\n */\nexport function buildAdvocatePrompt(option: DecisionOptionInput, index: number, input: AdjudicationInput): string {\n\treturn [\n\t\t`你是一名独立 advocate,任务是调查代码库后为下面的选项构建论证书。`,\n\t\t`分叉问题:${input.question}`,\n\t\t\"\",\n\t\tbriefBlock(input),\n\t\t\"\",\n\t\t`你只论证选项 [${index}] ${option.label}。纪律:`,\n\t\t`- 独立调查:用 read/grep/ls/find 自己抽查代码,验证简报里的支持与反对证据,不得只复述简报。`,\n\t\t`- 论证要诚实:如实列出该选项的代价、风险与不成立条件;发现致命缺陷必须写明,不得掩盖。`,\n\t\t`- 证据必须可核查:每条关键证据给出 path:line。`,\n\t\t\"\",\n\t\tOUTPUT_PROTOCOL,\n\t\t\"注意:你的 choice 字段固定填你被指派的选项下标,你的职责是用证据评估它,而不是重新站队。\",\n\t].join(\"\\n\");\n}\n\n/** L1 adjudicator prompt (§5.2): investigate independently, then pick. */\nexport function buildAdjudicatorPrompt(input: AdjudicationInput): string {\n\treturn [\n\t\t`你是一名独立裁决 agent。主 agent 在下面的分叉上等不到用户作答,请你调查代码库后独立裁决。`,\n\t\t`分叉问题:${input.question}`,\n\t\t\"\",\n\t\tbriefBlock(input),\n\t\t\"\",\n\t\t\"纪律:\",\n\t\t\"- 独立调查:用 read/grep/ls/find 自行抽查与该分叉相关的代码与文档,简报只是输入,不是结论。\",\n\t\t\"- 对称评估:对每个选项同等力度地找支持与反对证据;重点核对可逆性与后果声明是否属实。\",\n\t\t\"- 证据必须可核查:keyEvidence 每条给出 path:line。\",\n\t\t\"\",\n\t\tOUTPUT_PROTOCOL,\n\t].join(\"\\n\");\n}\n\n/**\n * Judge prompt (§5.3): dossiers + symmetric brief + spot-check instruction.\n * It deliberately contains nothing about what the main agent leans toward —\n * the dossiers are the only per-option arguments.\n */\nexport function buildJudgePrompt(dossiers: readonly string[], input: AdjudicationInput): string {\n\treturn [\n\t\t`你是一名独立法官。若干 advocate 为下面的分叉各提交了一份论证书,请你裁决。`,\n\t\t`分叉问题:${input.question}`,\n\t\t\"\",\n\t\tbriefBlock(input),\n\t\t\"\",\n\t\t\"== advocate 论证书 ==\".concat(\"\"),\n\t\tdossiers.map((dossier, index) => `--- 论证书 [${index}] ---\\n${dossier}`).join(\"\\n\"),\n\t\t\"\",\n\t\t\"纪律:\",\n\t\t\"- 论证书只是主张:抽查其关键证据(用 read/grep/ls/find 验证 path:line),不得偏信任何一方。\",\n\t\t\"- 简报是对称输入,不是倾向:若论证书与简报冲突,以你自己的抽查为准。\",\n\t\t\"- 独立判断:重点核对每个选项的可逆性与后果声明;选择整体代价最小的可执行选项。\",\n\t\t\"- 证据必须可核查:keyEvidence 每条给出 path:line。\",\n\t\t\"\",\n\t\tOUTPUT_PROTOCOL,\n\t].join(\"\\n\");\n}\n\nconst VERDICT_ROLE_ID = \"decision-adjudicator\";\nconst ADVOCATE_ROLE_ID = \"decision-advocate\";\nconst JUDGE_ROLE_ID = \"decision-judge\";\n\n/**\n * Parses the verdict JSON protocol (§5): an object with an in-range integer\n * `choice`, \"high\"/\"low\" confidence, a non-empty rationale and a string\n * keyEvidence array. Structural violations reject; length overruns truncate\n * against DECISION_LIMITS — a salvageable verdict must not be\n * thrown away over size, the state snapshot carries the bounds.\n */\nexport function parseVerdict(text: string, optionCount: number): AdjudicationVerdict | undefined {\n\tlet parsed: unknown;\n\ttry {\n\t\tparsed = JSON.parse(text);\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return undefined;\n\tconst record = parsed as Record<string, unknown>;\n\tif (!Number.isInteger(record.choice) || (record.choice as number) < 0 || (record.choice as number) >= optionCount) {\n\t\treturn undefined;\n\t}\n\tif (record.confidence !== \"high\" && record.confidence !== \"low\") return undefined;\n\tif (typeof record.rationale !== \"string\" || record.rationale.trim() === \"\") return undefined;\n\tif (!Array.isArray(record.keyEvidence)) return undefined;\n\tconst evidence: string[] = [];\n\tfor (const item of record.keyEvidence) {\n\t\tif (typeof item !== \"string\" || item.trim() === \"\") return undefined;\n\t\tevidence.push(item.slice(0, DECISION_LIMITS.verdictEvidenceItemMaxLength));\n\t}\n\treturn {\n\t\tchoice: record.choice as number,\n\t\tconfidence: record.confidence,\n\t\trationale: record.rationale.slice(0, DECISION_LIMITS.verdictRationaleMaxLength),\n\t\tkeyEvidence: evidence.slice(0, DECISION_LIMITS.verdictEvidenceMax),\n\t};\n}\n\nasync function callScheduler(caller: SchedulerCaller, request: unknown, signal?: AbortSignal): Promise<unknown> {\n\tconst operation = caller.call(request, signal === undefined ? undefined : { signal });\n\treturn await operation.result;\n}\n\nfunction isSchedulerEnvelope(value: unknown): value is AgentTaskSchedulerResponseV1 {\n\treturn value !== null && typeof value === \"object\" && \"action\" in (value as Record<string, unknown>);\n}\n\n/** submit → taskId; any throw/shape violation is adjudication-unavailable. */\nasync function submitTask(\n\tcaller: SchedulerCaller,\n\tprompt: string,\n\troleId: string,\n\tdeadlineAt: number,\n\tsignal?: AbortSignal,\n): Promise<{ ok: true; taskId: string } | { ok: false; detail: string }> {\n\tlet response: unknown;\n\ttry {\n\t\tresponse = await callScheduler(\n\t\t\tcaller,\n\t\t\t{\n\t\t\t\tversion: 1,\n\t\t\t\taction: \"submit\",\n\t\t\t\trequest: {\n\t\t\t\t\tversion: 1,\n\t\t\t\t\tprompt,\n\t\t\t\t\tprofile: childProfile(roleId, \"你在为一次分叉裁决做独立调查;只读,不改任何文件。\"),\n\t\t\t\t\tdeadlineAt,\n\t\t\t\t},\n\t\t\t},\n\t\t\tsignal,\n\t\t);\n\t} catch (error) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tdetail: `scheduler submit failed: ${error instanceof Error ? error.message : String(error)}`,\n\t\t};\n\t}\n\tif (\n\t\t!isSchedulerEnvelope(response) ||\n\t\tresponse.action !== \"submit\" ||\n\t\ttypeof response.taskId !== \"string\" ||\n\t\tresponse.taskId === \"\"\n\t) {\n\t\treturn { ok: false, detail: \"scheduler submit returned a malformed envelope\" };\n\t}\n\treturn { ok: true, taskId: response.taskId };\n}\n\n/** drain → terminal result; a non-succeeded status is a subagent failure (unavailable). */\nasync function drainTask(\n\tcaller: SchedulerCaller,\n\ttaskId: string,\n\ttimeoutMs: number,\n\tsignal?: AbortSignal,\n): Promise<{ ok: true; result: AgentTaskSchedulerResultV1 } | { ok: false; detail: string }> {\n\tlet response: unknown;\n\ttry {\n\t\tresponse = await callScheduler(caller, { version: 1, action: \"drain\", taskId, timeoutMs }, signal);\n\t} catch (error) {\n\t\treturn { ok: false, detail: `scheduler drain failed: ${error instanceof Error ? error.message : String(error)}` };\n\t}\n\tif (!isSchedulerEnvelope(response) || response.action !== \"drain\" || response.result === undefined) {\n\t\treturn { ok: false, detail: \"scheduler drain returned a malformed envelope\" };\n\t}\n\tconst result: AgentTaskSchedulerResultV1 = response.result;\n\tif (result.status !== \"succeeded\") {\n\t\tconst reason = result.error?.message ?? result.status;\n\t\treturn { ok: false, detail: `adjudication subagent did not complete: ${reason}` };\n\t}\n\treturn { ok: true, result };\n}\n\n/** submit + drain + text in one step; missing text is a protocol violation (unparseable). */\nasync function runSubagent(\n\tcaller: SchedulerCaller,\n\tprompt: string,\n\troleId: string,\n\tdeadlineAt: number,\n\ttimeoutMs: number,\n\tsignal?: AbortSignal,\n): Promise<{ ok: true; text: string } | { ok: false; code: \"unavailable\" | \"unparseable\"; detail: string }> {\n\tconst submitted = await submitTask(caller, prompt, roleId, deadlineAt, signal);\n\tif (!submitted.ok) return { ok: false, code: \"unavailable\", detail: submitted.detail };\n\tconst drained = await drainTask(caller, submitted.taskId, timeoutMs, signal);\n\tif (!drained.ok) return { ok: false, code: \"unavailable\", detail: drained.detail };\n\tif (typeof drained.result.text !== \"string\" || drained.result.text.trim() === \"\") {\n\t\treturn { ok: false, code: \"unparseable\", detail: \"adjudication subagent produced no text\" };\n\t}\n\treturn { ok: true, text: drained.result.text };\n}\n\n/**\n * Runs the whole chain (§5). Redline forks never reach this module — the\n * caller checks `record.redLine` first; as a belt-and-suspenders guard a\n * redline input short-circuits here too, without submitting anything.\n */\nexport async function runAdjudication(\n\tcaller: SchedulerCaller,\n\tinput: AdjudicationInput,\n\toptions: AdjudicationRunOptions = {},\n): Promise<AdjudicationOutcome> {\n\tconst now = options.now ?? Date.now;\n\tconst budgetMs = options.budgetMs ?? ADJUDICATION_BUDGET_MS;\n\tconst signal = options.signal;\n\t// 红线兜底(§3/§5.1):不可逆分叉不派任何子代理。\n\tif (classifyDecision(input.options, input.riskClass) === \"redline\") {\n\t\treturn { ok: false, code: \"unavailable\", detail: \"redline decision must not be adjudicated\" };\n\t}\n\tconst deadlineAt = now() + budgetMs;\n\tif (input.riskClass === \"significant\") {\n\t\t// L2: one advocate per option (all reversible by classification), then the judge.\n\t\tconst advocateRuns = input.options.map((option, index) =>\n\t\t\trunSubagent(caller, buildAdvocatePrompt(option, index, input), ADVOCATE_ROLE_ID, deadlineAt, budgetMs, signal),\n\t\t);\n\t\tconst advocateOutcomes = await Promise.all(advocateRuns);\n\t\tconst dossiers: string[] = [];\n\t\tfor (const outcome of advocateOutcomes) {\n\t\t\tif (!outcome.ok) return outcome;\n\t\t\tdossiers.push(outcome.text);\n\t\t}\n\t\t// 法官拿到独立预算:deadlineAt 在 submit 前按当前时刻重算,\n\t\t// 不与并行 advocates 共用链头时刻——advocate 论证耗时不侵蚀法官窗口。\n\t\tconst judge = await runSubagent(\n\t\t\tcaller,\n\t\t\tbuildJudgePrompt(dossiers, input),\n\t\t\tJUDGE_ROLE_ID,\n\t\t\tnow() + budgetMs,\n\t\t\tbudgetMs,\n\t\t\tsignal,\n\t\t);\n\t\tif (!judge.ok) return judge;\n\t\tconst verdict = parseVerdict(judge.text, input.options.length);\n\t\tif (verdict === undefined) {\n\t\t\treturn { ok: false, code: \"unparseable\", detail: \"judge output violates the verdict JSON protocol\" };\n\t\t}\n\t\treturn { ok: true, verdict };\n\t}\n\t// L1: a single adjudicator.\n\tconst outcome = await runSubagent(\n\t\tcaller,\n\t\tbuildAdjudicatorPrompt(input),\n\t\tVERDICT_ROLE_ID,\n\t\tdeadlineAt,\n\t\tbudgetMs,\n\t\tsignal,\n\t);\n\tif (!outcome.ok) return outcome;\n\tconst verdict = parseVerdict(outcome.text, input.options.length);\n\tif (verdict === undefined) {\n\t\treturn { ok: false, code: \"unparseable\", detail: \"adjudicator output violates the verdict JSON protocol\" };\n\t}\n\treturn { ok: true, verdict };\n}\n"]}
|