@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/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision state machine (`docs/design/决策升级插件设计.md` §3/§4/§6) —
|
|
3
|
+
* no host API access, no I/O, no clocks (time is always a caller-supplied
|
|
4
|
+
* `now`). Every function takes JSON-safe state and returns the next state
|
|
5
|
+
* plus the audit transitions to persist; index.ts owns persistence (entries +
|
|
6
|
+
* events), timers and injection; http-server.ts owns transport;
|
|
7
|
+
* adjudication.ts owns the subagent chain.
|
|
8
|
+
*/
|
|
9
|
+
import { type AdjudicationVerdict, type DecisionLevel, type DecisionRecord, type DecisionReversibility, type DecisionRiskClass, type DecisionState, type DecisionTransitionEntry } from "./types.ts";
|
|
10
|
+
export interface EngineRun {
|
|
11
|
+
state: DecisionState;
|
|
12
|
+
transitions: DecisionTransitionEntry[];
|
|
13
|
+
}
|
|
14
|
+
export declare function validDecisionId(id: string): boolean;
|
|
15
|
+
/** Model-facing option input; reversibility may be absent (missing = redline, §3). */
|
|
16
|
+
export interface DecisionOptionInput {
|
|
17
|
+
label: string;
|
|
18
|
+
description: string;
|
|
19
|
+
consequences: string;
|
|
20
|
+
reversibility?: DecisionReversibility;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Escalation classification (§3): redline when ANY option is irreversible or
|
|
24
|
+
* carries no reversibility field (conservative default); otherwise the
|
|
25
|
+
* model's risk_class picks routine (L1) vs significant (L2). The redline is
|
|
26
|
+
* never the model's risk self-assessment.
|
|
27
|
+
*/
|
|
28
|
+
export declare function classifyDecision(options: readonly DecisionOptionInput[], riskClass: DecisionRiskClass): DecisionLevel;
|
|
29
|
+
export interface DecisionAskInput {
|
|
30
|
+
id: string;
|
|
31
|
+
question: string;
|
|
32
|
+
options: DecisionOptionInput[];
|
|
33
|
+
contextDigest: string;
|
|
34
|
+
riskClass: DecisionRiskClass;
|
|
35
|
+
}
|
|
36
|
+
/** Validates the ask input and returns the normalized record (status asked, redLine derived). */
|
|
37
|
+
export declare function buildDecisionRecord(input: DecisionAskInput, now: number): DecisionRecord;
|
|
38
|
+
export declare function findDecision(state: DecisionState, id: string): DecisionRecord | undefined;
|
|
39
|
+
/** "Active" per §4.2/§4.3: waiting for the user, or timed out into adjudication. */
|
|
40
|
+
export declare function isDecisionActive(record: DecisionRecord): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* decision_ask: create the record and land the `asked` transition.
|
|
43
|
+
* Single-active discipline: at most one decision may sit in
|
|
44
|
+
* `asked` — a second ask is a structured rejection pointing at
|
|
45
|
+
* resolve/cancel. The rolling retention (§ 有界性) never evicts an active
|
|
46
|
+
* record: the oldest terminal record goes first, and only when none exists
|
|
47
|
+
* is the new ask rejected.
|
|
48
|
+
*/
|
|
49
|
+
export declare function applyDecisionAsk(state: DecisionState, input: DecisionAskInput, now: number): EngineRun;
|
|
50
|
+
export interface DecisionAnswerInput {
|
|
51
|
+
decisionId: string;
|
|
52
|
+
choice: number;
|
|
53
|
+
source: "user" | "adjudication";
|
|
54
|
+
note?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* decision_resolve (§4.2): close the decision with the chosen option index.
|
|
58
|
+
* `source: "adjudication"` requires the injected verdict to already exist in
|
|
59
|
+
* the record AND the choice to match it — the model must not fabricate or
|
|
60
|
+
* override an adjudication.
|
|
61
|
+
*/
|
|
62
|
+
export declare function applyDecisionAnswer(state: DecisionState, input: DecisionAnswerInput, now: number): EngineRun;
|
|
63
|
+
export type TimeoutMode = "halt" | "adjudicate";
|
|
64
|
+
/**
|
|
65
|
+
* Timeout fire (§5.1/§5.2): redline lands `timed-out.halted` (never
|
|
66
|
+
* adjudicated); reversible levels land `timed-out.adjudicating` before the
|
|
67
|
+
* chain runs.
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyDecisionTimeout(state: DecisionState, input: {
|
|
70
|
+
decisionId: string;
|
|
71
|
+
mode: TimeoutMode;
|
|
72
|
+
}, now: number): EngineRun;
|
|
73
|
+
export type AdjudicationFailureCode = "unavailable" | "unparseable";
|
|
74
|
+
/**
|
|
75
|
+
* Adjudication failed (§5.4): capability missing, submit/drain error, or an
|
|
76
|
+
* output that violates the JSON protocol. Terminal halted-like state — no
|
|
77
|
+
* retry, no silent downgrade.
|
|
78
|
+
*/
|
|
79
|
+
export declare function applyAdjudicationFailure(state: DecisionState, input: {
|
|
80
|
+
decisionId: string;
|
|
81
|
+
code: AdjudicationFailureCode;
|
|
82
|
+
detail: string;
|
|
83
|
+
}, now: number): EngineRun;
|
|
84
|
+
/**
|
|
85
|
+
* Stores a successful adjudication verdict (§5 留痕: 裁决书全文同步写入
|
|
86
|
+
* decision.state) without a transition event — the vocabulary has none for
|
|
87
|
+
* success; the audit trail closes when the model resolves
|
|
88
|
+
* (`answered.adjudication`).
|
|
89
|
+
*/
|
|
90
|
+
export declare function recordVerdict(state: DecisionState, input: {
|
|
91
|
+
decisionId: string;
|
|
92
|
+
verdict: AdjudicationVerdict;
|
|
93
|
+
}, now: number): EngineRun;
|
|
94
|
+
/** /decision cancel (§4.3): the user decides "do nothing". Clears active status. */
|
|
95
|
+
export declare function applyDecisionCancel(state: DecisionState, input: {
|
|
96
|
+
decisionId: string;
|
|
97
|
+
}, now: number): EngineRun;
|
|
98
|
+
export interface StateEntryView {
|
|
99
|
+
customType?: string;
|
|
100
|
+
data?: unknown;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Rebuilds the decision state from the session's append-only entries: the
|
|
104
|
+
* most recent `decision.state` snapshot is authoritative; a malformed one
|
|
105
|
+
* reads as an empty state instead of crashing re-entry (goal/alignment
|
|
106
|
+
* precedent). Both the versioned shape (`{ version: 1, decisions }`, §6)
|
|
107
|
+
* and a legacy bare container are accepted — a missing version reads as 1,
|
|
108
|
+
* a foreign version reads as empty (conservative default).
|
|
109
|
+
*/
|
|
110
|
+
export declare function rebuildDecisionState(entries: readonly StateEntryView[]): DecisionState;
|
|
111
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure decision state machine (`docs/design/决策升级插件设计.md` §3/§4/§6) —\n * no host API access, no I/O, no clocks (time is always a caller-supplied\n * `now`). Every function takes JSON-safe state and returns the next state\n * plus the audit transitions to persist; index.ts owns persistence (entries +\n * events), timers and injection; http-server.ts owns transport;\n * adjudication.ts owns the subagent chain.\n */\n\nimport {\n\ttype AdjudicationVerdict,\n\tDECISION_LIMITS,\n\tDecisionError,\n\ttype DecisionLevel,\n\ttype DecisionOption,\n\ttype DecisionRecord,\n\ttype DecisionReversibility,\n\ttype DecisionRiskClass,\n\ttype DecisionState,\n\ttype DecisionTransitionEntry,\n} from \"./types.ts\";\n\nexport interface EngineRun {\n\tstate: DecisionState;\n\ttransitions: DecisionTransitionEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Validation & classification (§3)\n// ---------------------------------------------------------------------------\n\nconst ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nexport function validDecisionId(id: string): boolean {\n\t// 入参可能来自未校验的工具输入(空对象 → undefined):test() 会把实参\n\t// 字符串化为 \"undefined\" 而\"通过\"模式匹配,必须先做类型守卫。\n\treturn typeof id === \"string\" && ID_PATTERN.test(id) && id.length <= 128;\n}\n\nfunction nonEmptyString(value: unknown, name: string, max: number): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") {\n\t\tthrow new DecisionError(\"invalid-input\", `${name} must be a non-empty string`);\n\t}\n\tif (value.length > max) {\n\t\tthrow new DecisionError(\"invalid-input\", `${name} exceeds ${max} characters`);\n\t}\n\treturn value;\n}\n\n/** Model-facing option input; reversibility may be absent (missing = redline, §3). */\nexport interface DecisionOptionInput {\n\tlabel: string;\n\tdescription: string;\n\tconsequences: string;\n\treversibility?: DecisionReversibility;\n}\n\n/**\n * Escalation classification (§3): redline when ANY option is irreversible or\n * carries no reversibility field (conservative default); otherwise the\n * model's risk_class picks routine (L1) vs significant (L2). The redline is\n * never the model's risk self-assessment.\n */\nexport function classifyDecision(options: readonly DecisionOptionInput[], riskClass: DecisionRiskClass): DecisionLevel {\n\tfor (const option of options) {\n\t\tif (option.reversibility === undefined) return \"redline\";\n\t\tif (option.reversibility === \"irreversible\") return \"redline\";\n\t}\n\treturn riskClass;\n}\n\n/**\n * Validates one option shape. An explicit `reversibility` outside the\n * two-value vocabulary is invalid input (the schema enforces literals; this\n * guards unvalidated callers). A MISSING field is accepted here — the\n * classifier turns it into a redline.\n */\nfunction validateOption(input: DecisionOptionInput, index: number): DecisionOption {\n\tif (input === null || typeof input !== \"object\") {\n\t\tthrow new DecisionError(\"invalid-option\", `option ${index} must be an object`);\n\t}\n\tif (\n\t\tinput.reversibility !== undefined &&\n\t\tinput.reversibility !== \"reversible\" &&\n\t\tinput.reversibility !== \"irreversible\"\n\t) {\n\t\tthrow new DecisionError(\n\t\t\t\"invalid-option\",\n\t\t\t`option ${index} reversibility must be \"reversible\" or \"irreversible\": ${String(input.reversibility)}`,\n\t\t);\n\t}\n\treturn {\n\t\tlabel: nonEmptyString(input.label, `option ${index} label`, DECISION_LIMITS.labelMaxLength),\n\t\tdescription: nonEmptyString(\n\t\t\tinput.description,\n\t\t\t`option ${index} description`,\n\t\t\tDECISION_LIMITS.descriptionMaxLength,\n\t\t),\n\t\tconsequences: nonEmptyString(\n\t\t\tinput.consequences,\n\t\t\t`option ${index} consequences`,\n\t\t\tDECISION_LIMITS.consequencesMaxLength,\n\t\t),\n\t\t...(input.reversibility === undefined ? {} : { reversibility: input.reversibility }),\n\t};\n}\n\nexport interface DecisionAskInput {\n\tid: string;\n\tquestion: string;\n\toptions: DecisionOptionInput[];\n\tcontextDigest: string;\n\triskClass: DecisionRiskClass;\n}\n\n/** Validates the ask input and returns the normalized record (status asked, redLine derived). */\nexport function buildDecisionRecord(input: DecisionAskInput, now: number): DecisionRecord {\n\tif (!validDecisionId(input.id)) {\n\t\tthrow new DecisionError(\n\t\t\t\"invalid-id\",\n\t\t\t`decision id must match [A-Za-z0-9][A-Za-z0-9._-]* (no leading dot): ${input.id}`,\n\t\t);\n\t}\n\tif (!Array.isArray(input.options)) {\n\t\tthrow new DecisionError(\"invalid-input\", \"options must be an array\");\n\t}\n\tif (input.options.length < DECISION_LIMITS.optionsMin || input.options.length > DECISION_LIMITS.optionsMax) {\n\t\tthrow new DecisionError(\n\t\t\t\"invalid-input\",\n\t\t\t`options must carry ${DECISION_LIMITS.optionsMin}..${DECISION_LIMITS.optionsMax} items, got ${input.options.length}`,\n\t\t);\n\t}\n\tconst riskClass = input.riskClass;\n\tif (riskClass !== \"routine\" && riskClass !== \"significant\") {\n\t\tthrow new DecisionError(\"invalid-input\", `risk_class must be \"routine\" or \"significant\": ${String(riskClass)}`);\n\t}\n\tconst options = input.options.map((option, index) => validateOption(option, index));\n\tconst question = nonEmptyString(input.question, \"question\", DECISION_LIMITS.questionMaxLength);\n\tconst contextDigest = nonEmptyString(input.contextDigest, \"context_digest\", DECISION_LIMITS.contextDigestMaxLength);\n\treturn {\n\t\tid: input.id,\n\t\tquestion,\n\t\toptions,\n\t\tcontextDigest,\n\t\triskClass,\n\t\tredLine: classifyDecision(options, riskClass) === \"redline\",\n\t\tstatus: \"asked\",\n\t\trevision: 0,\n\t\tcreatedAt: now,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// State helpers\n// ---------------------------------------------------------------------------\n\nexport function findDecision(state: DecisionState, id: string): DecisionRecord | undefined {\n\treturn state.decisions.find((record) => record.id === id);\n}\n\n/** \"Active\" per §4.2/§4.3: waiting for the user, or timed out into adjudication. */\nexport function isDecisionActive(record: DecisionRecord): boolean {\n\treturn record.status === \"asked\" || record.status === \"adjudicating\";\n}\n\nfunction requireActive(state: DecisionState, id: string): DecisionRecord {\n\tconst record = findDecision(state, id);\n\tif (!record) throw new DecisionError(\"not-found\", `unknown decision: ${id}`);\n\tif (!isDecisionActive(record)) {\n\t\tthrow new DecisionError(\"not-active\", `decision is not active (status ${record.status}): ${id}`);\n\t}\n\treturn record;\n}\n\nfunction replaceDecision(state: DecisionState, updated: DecisionRecord): DecisionState {\n\treturn { version: 1, decisions: state.decisions.map((record) => (record.id === updated.id ? updated : record)) };\n}\n\n// ---------------------------------------------------------------------------\n// Transitions (§4, §5, §6)\n// ---------------------------------------------------------------------------\n\n/**\n * decision_ask: create the record and land the `asked` transition.\n * Single-active discipline: at most one decision may sit in\n * `asked` — a second ask is a structured rejection pointing at\n * resolve/cancel. The rolling retention (§ 有界性) never evicts an active\n * record: the oldest terminal record goes first, and only when none exists\n * is the new ask rejected.\n */\nexport function applyDecisionAsk(state: DecisionState, input: DecisionAskInput, now: number): EngineRun {\n\tif (findDecision(state, input.id)) {\n\t\tthrow new DecisionError(\"invalid-input\", `decision id already exists: ${input.id}`);\n\t}\n\tconst active = state.decisions.find((record) => record.status === \"asked\");\n\tif (active !== undefined) {\n\t\tthrow new DecisionError(\n\t\t\t\"decision-active-exists\",\n\t\t\t`decision ${active.id} is still awaiting an answer; resolve it or cancel it first (/decision cancel ${active.id})`,\n\t\t);\n\t}\n\tconst record = buildDecisionRecord(input, now);\n\tconst decisions = [...state.decisions, record];\n\tif (decisions.length > DECISION_LIMITS.decisionsMax) {\n\t\tconst oldestTerminal = decisions.findIndex((candidate) => !isDecisionActive(candidate));\n\t\tif (oldestTerminal < 0) {\n\t\t\tthrow new DecisionError(\n\t\t\t\t\"state-limit-reached\",\n\t\t\t\t`decision state reached its ${DECISION_LIMITS.decisionsMax}-record limit with no terminal record to evict`,\n\t\t\t);\n\t\t}\n\t\tdecisions.splice(oldestTerminal, 1);\n\t}\n\treturn {\n\t\tstate: { version: 1, decisions },\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: \"asked\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: \"decision-asked\",\n\t\t\t\t\tdetail: `level=${record.redLine ? \"redline\" : record.riskClass} options=${record.options.length}`,\n\t\t\t\t},\n\t\t\t\trevision: record.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport interface DecisionAnswerInput {\n\tdecisionId: string;\n\tchoice: number;\n\tsource: \"user\" | \"adjudication\";\n\tnote?: string;\n}\n\n/**\n * decision_resolve (§4.2): close the decision with the chosen option index.\n * `source: \"adjudication\"` requires the injected verdict to already exist in\n * the record AND the choice to match it — the model must not fabricate or\n * override an adjudication.\n */\nexport function applyDecisionAnswer(state: DecisionState, input: DecisionAnswerInput, now: number): EngineRun {\n\tvoid now; // the answer itself carries no timestamp field; kept for call-shape symmetry\n\tconst record = requireActive(state, input.decisionId);\n\tif (\n\t\t!Number.isInteger(input.choice) ||\n\t\t(input.choice as number) < 0 ||\n\t\t(input.choice as number) >= record.options.length\n\t) {\n\t\tthrow new DecisionError(\n\t\t\t\"choice-out-of-range\",\n\t\t\t`choice must be an option index within 0..${record.options.length - 1}: ${String(input.choice)}`,\n\t\t);\n\t}\n\tif (input.source === \"adjudication\") {\n\t\tif (record.status !== \"adjudicating\" || record.verdict === undefined) {\n\t\t\tthrow new DecisionError(\n\t\t\t\t\"adjudication-not-recorded\",\n\t\t\t\t`no injected adjudication exists for decision ${record.id}; the model must not claim an adjudicated answer`,\n\t\t\t);\n\t\t}\n\t\tif (record.verdict.choice !== input.choice) {\n\t\t\tthrow new DecisionError(\n\t\t\t\t\"adjudication-choice-mismatch\",\n\t\t\t\t`choice ${input.choice} does not match the injected adjudication (choice ${record.verdict.choice})`,\n\t\t\t);\n\t\t}\n\t} else if (record.status !== \"asked\") {\n\t\tthrow new DecisionError(\n\t\t\t\"not-active\",\n\t\t\t`decision ${record.id} already timed out (status ${record.status}); a user answer can no longer land through decision_resolve`,\n\t\t);\n\t}\n\tconst note =\n\t\tinput.note === undefined ? undefined : nonEmptyString(input.note, \"note\", DECISION_LIMITS.noteMaxLength);\n\tconst updated: DecisionRecord = {\n\t\t...record,\n\t\tstatus: \"answered\",\n\t\trevision: record.revision + 1,\n\t\tresolvedBy: input.source,\n\t\tchoice: input.choice,\n\t\t...(note === undefined ? {} : { note }),\n\t};\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: input.source === \"user\" ? \"answered.user\" : \"answered.adjudication\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: input.source === \"user\" ? \"user-answered\" : \"adjudication-resolved\",\n\t\t\t\t\tdetail: `choice=${input.choice}${note === undefined ? \"\" : ` note=${note}`}`,\n\t\t\t\t},\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport type TimeoutMode = \"halt\" | \"adjudicate\";\n\n/**\n * Timeout fire (§5.1/§5.2): redline lands `timed-out.halted` (never\n * adjudicated); reversible levels land `timed-out.adjudicating` before the\n * chain runs.\n */\nexport function applyDecisionTimeout(\n\tstate: DecisionState,\n\tinput: { decisionId: string; mode: TimeoutMode },\n\tnow: number,\n): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tif (record.status !== \"asked\") {\n\t\tthrow new DecisionError(\"not-active\", `timeout fires on a non-asked decision: ${record.id}`);\n\t}\n\tif (input.mode === \"halt\" && !record.redLine) {\n\t\tthrow new DecisionError(\"invalid-input\", `decision ${record.id} is not a redline; use adjudicate`);\n\t}\n\tif (input.mode === \"adjudicate\" && record.redLine) {\n\t\tthrow new DecisionError(\"invalid-input\", `decision ${record.id} is a redline; it must halt, not adjudicate`);\n\t}\n\tconst updated: DecisionRecord = {\n\t\t...record,\n\t\tstatus: input.mode === \"halt\" ? \"halted\" : \"adjudicating\",\n\t\trevision: record.revision + 1,\n\t};\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: input.mode === \"halt\" ? \"timed-out.halted\" : \"timed-out.adjudicating\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: input.mode === \"halt\" ? \"redline-kept-stopped\" : \"adjudication-dispatched\",\n\t\t\t\t\tdetail:\n\t\t\t\t\t\tinput.mode === \"halt\"\n\t\t\t\t\t\t\t? \"irreversible fork stays stopped for the user\"\n\t\t\t\t\t\t\t: \"reversible fork handed to the adjudication chain\",\n\t\t\t\t},\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport type AdjudicationFailureCode = \"unavailable\" | \"unparseable\";\n\n/**\n * Adjudication failed (§5.4): capability missing, submit/drain error, or an\n * output that violates the JSON protocol. Terminal halted-like state — no\n * retry, no silent downgrade.\n */\nexport function applyAdjudicationFailure(\n\tstate: DecisionState,\n\tinput: { decisionId: string; code: AdjudicationFailureCode; detail: string },\n\tnow: number,\n): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tif (record.status !== \"adjudicating\") {\n\t\tthrow new DecisionError(\"not-active\", `adjudication failure lands on an adjudicating decision: ${record.id}`);\n\t}\n\tconst updated: DecisionRecord = {\n\t\t...record,\n\t\tstatus: input.code === \"unavailable\" ? \"unavailable\" : \"unparseable\",\n\t\trevision: record.revision + 1,\n\t};\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: input.code === \"unavailable\" ? \"adjudication.unavailable\" : \"adjudication.unparseable\",\n\t\t\t\treason: { code: `adjudication-${input.code}`, detail: input.detail },\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\n/**\n * Stores a successful adjudication verdict (§5 留痕: 裁决书全文同步写入\n * decision.state) without a transition event — the vocabulary has none for\n * success; the audit trail closes when the model resolves\n * (`answered.adjudication`).\n */\nexport function recordVerdict(\n\tstate: DecisionState,\n\tinput: { decisionId: string; verdict: AdjudicationVerdict },\n\tnow: number,\n): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tif (record.status !== \"adjudicating\") {\n\t\tthrow new DecisionError(\"not-active\", `a verdict lands on an adjudicating decision: ${record.id}`);\n\t}\n\tconst updated: DecisionRecord = { ...record, verdict: input.verdict };\n\treturn { state: replaceDecision(state, updated), transitions: [] };\n}\n\n/** /decision cancel (§4.3): the user decides \"do nothing\". Clears active status. */\nexport function applyDecisionCancel(state: DecisionState, input: { decisionId: string }, now: number): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tconst updated: DecisionRecord = { ...record, status: \"cancelled\", revision: record.revision + 1 };\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: \"cancelled\",\n\t\t\t\treason: { code: \"decision-cancelled\", detail: \"user cancelled the fork (do nothing)\" },\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot rebuild (§6): reverse scan, malformed snapshot reads as empty\n// ---------------------------------------------------------------------------\n\nexport interface StateEntryView {\n\tcustomType?: string;\n\tdata?: unknown;\n}\n\n/**\n * Rebuilds the decision state from the session's append-only entries: the\n * most recent `decision.state` snapshot is authoritative; a malformed one\n * reads as an empty state instead of crashing re-entry (goal/alignment\n * precedent). Both the versioned shape (`{ version: 1, decisions }`, §6)\n * and a legacy bare container are accepted — a missing version reads as 1,\n * a foreign version reads as empty (conservative default).\n */\nexport function rebuildDecisionState(entries: readonly StateEntryView[]): DecisionState {\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry?.customType !== \"decision.state\") continue;\n\t\tconst data = entry.data;\n\t\tif (data === null || typeof data !== \"object\" || Array.isArray(data)) return { version: 1, decisions: [] };\n\t\tconst record = data as { version?: unknown; decisions?: unknown };\n\t\tif (record.version !== undefined && record.version !== 1) return { version: 1, decisions: [] };\n\t\tif (!Array.isArray(record.decisions)) return { version: 1, decisions: [] };\n\t\treturn { version: 1, decisions: [...record.decisions] };\n\t}\n\treturn { version: 1, decisions: [] };\n}\n"]}
|
package/dist/engine.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision state machine (`docs/design/决策升级插件设计.md` §3/§4/§6) —
|
|
3
|
+
* no host API access, no I/O, no clocks (time is always a caller-supplied
|
|
4
|
+
* `now`). Every function takes JSON-safe state and returns the next state
|
|
5
|
+
* plus the audit transitions to persist; index.ts owns persistence (entries +
|
|
6
|
+
* events), timers and injection; http-server.ts owns transport;
|
|
7
|
+
* adjudication.ts owns the subagent chain.
|
|
8
|
+
*/
|
|
9
|
+
import { DECISION_LIMITS, DecisionError, } from "./types.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Validation & classification (§3)
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
14
|
+
export function validDecisionId(id) {
|
|
15
|
+
// 入参可能来自未校验的工具输入(空对象 → undefined):test() 会把实参
|
|
16
|
+
// 字符串化为 "undefined" 而"通过"模式匹配,必须先做类型守卫。
|
|
17
|
+
return typeof id === "string" && ID_PATTERN.test(id) && id.length <= 128;
|
|
18
|
+
}
|
|
19
|
+
function nonEmptyString(value, name, max) {
|
|
20
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
21
|
+
throw new DecisionError("invalid-input", `${name} must be a non-empty string`);
|
|
22
|
+
}
|
|
23
|
+
if (value.length > max) {
|
|
24
|
+
throw new DecisionError("invalid-input", `${name} exceeds ${max} characters`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Escalation classification (§3): redline when ANY option is irreversible or
|
|
30
|
+
* carries no reversibility field (conservative default); otherwise the
|
|
31
|
+
* model's risk_class picks routine (L1) vs significant (L2). The redline is
|
|
32
|
+
* never the model's risk self-assessment.
|
|
33
|
+
*/
|
|
34
|
+
export function classifyDecision(options, riskClass) {
|
|
35
|
+
for (const option of options) {
|
|
36
|
+
if (option.reversibility === undefined)
|
|
37
|
+
return "redline";
|
|
38
|
+
if (option.reversibility === "irreversible")
|
|
39
|
+
return "redline";
|
|
40
|
+
}
|
|
41
|
+
return riskClass;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates one option shape. An explicit `reversibility` outside the
|
|
45
|
+
* two-value vocabulary is invalid input (the schema enforces literals; this
|
|
46
|
+
* guards unvalidated callers). A MISSING field is accepted here — the
|
|
47
|
+
* classifier turns it into a redline.
|
|
48
|
+
*/
|
|
49
|
+
function validateOption(input, index) {
|
|
50
|
+
if (input === null || typeof input !== "object") {
|
|
51
|
+
throw new DecisionError("invalid-option", `option ${index} must be an object`);
|
|
52
|
+
}
|
|
53
|
+
if (input.reversibility !== undefined &&
|
|
54
|
+
input.reversibility !== "reversible" &&
|
|
55
|
+
input.reversibility !== "irreversible") {
|
|
56
|
+
throw new DecisionError("invalid-option", `option ${index} reversibility must be "reversible" or "irreversible": ${String(input.reversibility)}`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
label: nonEmptyString(input.label, `option ${index} label`, DECISION_LIMITS.labelMaxLength),
|
|
60
|
+
description: nonEmptyString(input.description, `option ${index} description`, DECISION_LIMITS.descriptionMaxLength),
|
|
61
|
+
consequences: nonEmptyString(input.consequences, `option ${index} consequences`, DECISION_LIMITS.consequencesMaxLength),
|
|
62
|
+
...(input.reversibility === undefined ? {} : { reversibility: input.reversibility }),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Validates the ask input and returns the normalized record (status asked, redLine derived). */
|
|
66
|
+
export function buildDecisionRecord(input, now) {
|
|
67
|
+
if (!validDecisionId(input.id)) {
|
|
68
|
+
throw new DecisionError("invalid-id", `decision id must match [A-Za-z0-9][A-Za-z0-9._-]* (no leading dot): ${input.id}`);
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(input.options)) {
|
|
71
|
+
throw new DecisionError("invalid-input", "options must be an array");
|
|
72
|
+
}
|
|
73
|
+
if (input.options.length < DECISION_LIMITS.optionsMin || input.options.length > DECISION_LIMITS.optionsMax) {
|
|
74
|
+
throw new DecisionError("invalid-input", `options must carry ${DECISION_LIMITS.optionsMin}..${DECISION_LIMITS.optionsMax} items, got ${input.options.length}`);
|
|
75
|
+
}
|
|
76
|
+
const riskClass = input.riskClass;
|
|
77
|
+
if (riskClass !== "routine" && riskClass !== "significant") {
|
|
78
|
+
throw new DecisionError("invalid-input", `risk_class must be "routine" or "significant": ${String(riskClass)}`);
|
|
79
|
+
}
|
|
80
|
+
const options = input.options.map((option, index) => validateOption(option, index));
|
|
81
|
+
const question = nonEmptyString(input.question, "question", DECISION_LIMITS.questionMaxLength);
|
|
82
|
+
const contextDigest = nonEmptyString(input.contextDigest, "context_digest", DECISION_LIMITS.contextDigestMaxLength);
|
|
83
|
+
return {
|
|
84
|
+
id: input.id,
|
|
85
|
+
question,
|
|
86
|
+
options,
|
|
87
|
+
contextDigest,
|
|
88
|
+
riskClass,
|
|
89
|
+
redLine: classifyDecision(options, riskClass) === "redline",
|
|
90
|
+
status: "asked",
|
|
91
|
+
revision: 0,
|
|
92
|
+
createdAt: now,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// State helpers
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
export function findDecision(state, id) {
|
|
99
|
+
return state.decisions.find((record) => record.id === id);
|
|
100
|
+
}
|
|
101
|
+
/** "Active" per §4.2/§4.3: waiting for the user, or timed out into adjudication. */
|
|
102
|
+
export function isDecisionActive(record) {
|
|
103
|
+
return record.status === "asked" || record.status === "adjudicating";
|
|
104
|
+
}
|
|
105
|
+
function requireActive(state, id) {
|
|
106
|
+
const record = findDecision(state, id);
|
|
107
|
+
if (!record)
|
|
108
|
+
throw new DecisionError("not-found", `unknown decision: ${id}`);
|
|
109
|
+
if (!isDecisionActive(record)) {
|
|
110
|
+
throw new DecisionError("not-active", `decision is not active (status ${record.status}): ${id}`);
|
|
111
|
+
}
|
|
112
|
+
return record;
|
|
113
|
+
}
|
|
114
|
+
function replaceDecision(state, updated) {
|
|
115
|
+
return { version: 1, decisions: state.decisions.map((record) => (record.id === updated.id ? updated : record)) };
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Transitions (§4, §5, §6)
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
/**
|
|
121
|
+
* decision_ask: create the record and land the `asked` transition.
|
|
122
|
+
* Single-active discipline: at most one decision may sit in
|
|
123
|
+
* `asked` — a second ask is a structured rejection pointing at
|
|
124
|
+
* resolve/cancel. The rolling retention (§ 有界性) never evicts an active
|
|
125
|
+
* record: the oldest terminal record goes first, and only when none exists
|
|
126
|
+
* is the new ask rejected.
|
|
127
|
+
*/
|
|
128
|
+
export function applyDecisionAsk(state, input, now) {
|
|
129
|
+
if (findDecision(state, input.id)) {
|
|
130
|
+
throw new DecisionError("invalid-input", `decision id already exists: ${input.id}`);
|
|
131
|
+
}
|
|
132
|
+
const active = state.decisions.find((record) => record.status === "asked");
|
|
133
|
+
if (active !== undefined) {
|
|
134
|
+
throw new DecisionError("decision-active-exists", `decision ${active.id} is still awaiting an answer; resolve it or cancel it first (/decision cancel ${active.id})`);
|
|
135
|
+
}
|
|
136
|
+
const record = buildDecisionRecord(input, now);
|
|
137
|
+
const decisions = [...state.decisions, record];
|
|
138
|
+
if (decisions.length > DECISION_LIMITS.decisionsMax) {
|
|
139
|
+
const oldestTerminal = decisions.findIndex((candidate) => !isDecisionActive(candidate));
|
|
140
|
+
if (oldestTerminal < 0) {
|
|
141
|
+
throw new DecisionError("state-limit-reached", `decision state reached its ${DECISION_LIMITS.decisionsMax}-record limit with no terminal record to evict`);
|
|
142
|
+
}
|
|
143
|
+
decisions.splice(oldestTerminal, 1);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
state: { version: 1, decisions },
|
|
147
|
+
transitions: [
|
|
148
|
+
{
|
|
149
|
+
decisionId: record.id,
|
|
150
|
+
event: "asked",
|
|
151
|
+
reason: {
|
|
152
|
+
code: "decision-asked",
|
|
153
|
+
detail: `level=${record.redLine ? "redline" : record.riskClass} options=${record.options.length}`,
|
|
154
|
+
},
|
|
155
|
+
revision: record.revision,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* decision_resolve (§4.2): close the decision with the chosen option index.
|
|
162
|
+
* `source: "adjudication"` requires the injected verdict to already exist in
|
|
163
|
+
* the record AND the choice to match it — the model must not fabricate or
|
|
164
|
+
* override an adjudication.
|
|
165
|
+
*/
|
|
166
|
+
export function applyDecisionAnswer(state, input, now) {
|
|
167
|
+
void now; // the answer itself carries no timestamp field; kept for call-shape symmetry
|
|
168
|
+
const record = requireActive(state, input.decisionId);
|
|
169
|
+
if (!Number.isInteger(input.choice) ||
|
|
170
|
+
input.choice < 0 ||
|
|
171
|
+
input.choice >= record.options.length) {
|
|
172
|
+
throw new DecisionError("choice-out-of-range", `choice must be an option index within 0..${record.options.length - 1}: ${String(input.choice)}`);
|
|
173
|
+
}
|
|
174
|
+
if (input.source === "adjudication") {
|
|
175
|
+
if (record.status !== "adjudicating" || record.verdict === undefined) {
|
|
176
|
+
throw new DecisionError("adjudication-not-recorded", `no injected adjudication exists for decision ${record.id}; the model must not claim an adjudicated answer`);
|
|
177
|
+
}
|
|
178
|
+
if (record.verdict.choice !== input.choice) {
|
|
179
|
+
throw new DecisionError("adjudication-choice-mismatch", `choice ${input.choice} does not match the injected adjudication (choice ${record.verdict.choice})`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (record.status !== "asked") {
|
|
183
|
+
throw new DecisionError("not-active", `decision ${record.id} already timed out (status ${record.status}); a user answer can no longer land through decision_resolve`);
|
|
184
|
+
}
|
|
185
|
+
const note = input.note === undefined ? undefined : nonEmptyString(input.note, "note", DECISION_LIMITS.noteMaxLength);
|
|
186
|
+
const updated = {
|
|
187
|
+
...record,
|
|
188
|
+
status: "answered",
|
|
189
|
+
revision: record.revision + 1,
|
|
190
|
+
resolvedBy: input.source,
|
|
191
|
+
choice: input.choice,
|
|
192
|
+
...(note === undefined ? {} : { note }),
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
state: replaceDecision(state, updated),
|
|
196
|
+
transitions: [
|
|
197
|
+
{
|
|
198
|
+
decisionId: record.id,
|
|
199
|
+
event: input.source === "user" ? "answered.user" : "answered.adjudication",
|
|
200
|
+
reason: {
|
|
201
|
+
code: input.source === "user" ? "user-answered" : "adjudication-resolved",
|
|
202
|
+
detail: `choice=${input.choice}${note === undefined ? "" : ` note=${note}`}`,
|
|
203
|
+
},
|
|
204
|
+
revision: updated.revision,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Timeout fire (§5.1/§5.2): redline lands `timed-out.halted` (never
|
|
211
|
+
* adjudicated); reversible levels land `timed-out.adjudicating` before the
|
|
212
|
+
* chain runs.
|
|
213
|
+
*/
|
|
214
|
+
export function applyDecisionTimeout(state, input, now) {
|
|
215
|
+
void now;
|
|
216
|
+
const record = requireActive(state, input.decisionId);
|
|
217
|
+
if (record.status !== "asked") {
|
|
218
|
+
throw new DecisionError("not-active", `timeout fires on a non-asked decision: ${record.id}`);
|
|
219
|
+
}
|
|
220
|
+
if (input.mode === "halt" && !record.redLine) {
|
|
221
|
+
throw new DecisionError("invalid-input", `decision ${record.id} is not a redline; use adjudicate`);
|
|
222
|
+
}
|
|
223
|
+
if (input.mode === "adjudicate" && record.redLine) {
|
|
224
|
+
throw new DecisionError("invalid-input", `decision ${record.id} is a redline; it must halt, not adjudicate`);
|
|
225
|
+
}
|
|
226
|
+
const updated = {
|
|
227
|
+
...record,
|
|
228
|
+
status: input.mode === "halt" ? "halted" : "adjudicating",
|
|
229
|
+
revision: record.revision + 1,
|
|
230
|
+
};
|
|
231
|
+
return {
|
|
232
|
+
state: replaceDecision(state, updated),
|
|
233
|
+
transitions: [
|
|
234
|
+
{
|
|
235
|
+
decisionId: record.id,
|
|
236
|
+
event: input.mode === "halt" ? "timed-out.halted" : "timed-out.adjudicating",
|
|
237
|
+
reason: {
|
|
238
|
+
code: input.mode === "halt" ? "redline-kept-stopped" : "adjudication-dispatched",
|
|
239
|
+
detail: input.mode === "halt"
|
|
240
|
+
? "irreversible fork stays stopped for the user"
|
|
241
|
+
: "reversible fork handed to the adjudication chain",
|
|
242
|
+
},
|
|
243
|
+
revision: updated.revision,
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Adjudication failed (§5.4): capability missing, submit/drain error, or an
|
|
250
|
+
* output that violates the JSON protocol. Terminal halted-like state — no
|
|
251
|
+
* retry, no silent downgrade.
|
|
252
|
+
*/
|
|
253
|
+
export function applyAdjudicationFailure(state, input, now) {
|
|
254
|
+
void now;
|
|
255
|
+
const record = requireActive(state, input.decisionId);
|
|
256
|
+
if (record.status !== "adjudicating") {
|
|
257
|
+
throw new DecisionError("not-active", `adjudication failure lands on an adjudicating decision: ${record.id}`);
|
|
258
|
+
}
|
|
259
|
+
const updated = {
|
|
260
|
+
...record,
|
|
261
|
+
status: input.code === "unavailable" ? "unavailable" : "unparseable",
|
|
262
|
+
revision: record.revision + 1,
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
state: replaceDecision(state, updated),
|
|
266
|
+
transitions: [
|
|
267
|
+
{
|
|
268
|
+
decisionId: record.id,
|
|
269
|
+
event: input.code === "unavailable" ? "adjudication.unavailable" : "adjudication.unparseable",
|
|
270
|
+
reason: { code: `adjudication-${input.code}`, detail: input.detail },
|
|
271
|
+
revision: updated.revision,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Stores a successful adjudication verdict (§5 留痕: 裁决书全文同步写入
|
|
278
|
+
* decision.state) without a transition event — the vocabulary has none for
|
|
279
|
+
* success; the audit trail closes when the model resolves
|
|
280
|
+
* (`answered.adjudication`).
|
|
281
|
+
*/
|
|
282
|
+
export function recordVerdict(state, input, now) {
|
|
283
|
+
void now;
|
|
284
|
+
const record = requireActive(state, input.decisionId);
|
|
285
|
+
if (record.status !== "adjudicating") {
|
|
286
|
+
throw new DecisionError("not-active", `a verdict lands on an adjudicating decision: ${record.id}`);
|
|
287
|
+
}
|
|
288
|
+
const updated = { ...record, verdict: input.verdict };
|
|
289
|
+
return { state: replaceDecision(state, updated), transitions: [] };
|
|
290
|
+
}
|
|
291
|
+
/** /decision cancel (§4.3): the user decides "do nothing". Clears active status. */
|
|
292
|
+
export function applyDecisionCancel(state, input, now) {
|
|
293
|
+
void now;
|
|
294
|
+
const record = requireActive(state, input.decisionId);
|
|
295
|
+
const updated = { ...record, status: "cancelled", revision: record.revision + 1 };
|
|
296
|
+
return {
|
|
297
|
+
state: replaceDecision(state, updated),
|
|
298
|
+
transitions: [
|
|
299
|
+
{
|
|
300
|
+
decisionId: record.id,
|
|
301
|
+
event: "cancelled",
|
|
302
|
+
reason: { code: "decision-cancelled", detail: "user cancelled the fork (do nothing)" },
|
|
303
|
+
revision: updated.revision,
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Rebuilds the decision state from the session's append-only entries: the
|
|
310
|
+
* most recent `decision.state` snapshot is authoritative; a malformed one
|
|
311
|
+
* reads as an empty state instead of crashing re-entry (goal/alignment
|
|
312
|
+
* precedent). Both the versioned shape (`{ version: 1, decisions }`, §6)
|
|
313
|
+
* and a legacy bare container are accepted — a missing version reads as 1,
|
|
314
|
+
* a foreign version reads as empty (conservative default).
|
|
315
|
+
*/
|
|
316
|
+
export function rebuildDecisionState(entries) {
|
|
317
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
318
|
+
const entry = entries[index];
|
|
319
|
+
if (entry?.customType !== "decision.state")
|
|
320
|
+
continue;
|
|
321
|
+
const data = entry.data;
|
|
322
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
323
|
+
return { version: 1, decisions: [] };
|
|
324
|
+
const record = data;
|
|
325
|
+
if (record.version !== undefined && record.version !== 1)
|
|
326
|
+
return { version: 1, decisions: [] };
|
|
327
|
+
if (!Array.isArray(record.decisions))
|
|
328
|
+
return { version: 1, decisions: [] };
|
|
329
|
+
return { version: 1, decisions: [...record.decisions] };
|
|
330
|
+
}
|
|
331
|
+
return { version: 1, decisions: [] };
|
|
332
|
+
}
|
|
333
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure decision state machine (`docs/design/决策升级插件设计.md` §3/§4/§6) —\n * no host API access, no I/O, no clocks (time is always a caller-supplied\n * `now`). Every function takes JSON-safe state and returns the next state\n * plus the audit transitions to persist; index.ts owns persistence (entries +\n * events), timers and injection; http-server.ts owns transport;\n * adjudication.ts owns the subagent chain.\n */\n\nimport {\n\ttype AdjudicationVerdict,\n\tDECISION_LIMITS,\n\tDecisionError,\n\ttype DecisionLevel,\n\ttype DecisionOption,\n\ttype DecisionRecord,\n\ttype DecisionReversibility,\n\ttype DecisionRiskClass,\n\ttype DecisionState,\n\ttype DecisionTransitionEntry,\n} from \"./types.ts\";\n\nexport interface EngineRun {\n\tstate: DecisionState;\n\ttransitions: DecisionTransitionEntry[];\n}\n\n// ---------------------------------------------------------------------------\n// Validation & classification (§3)\n// ---------------------------------------------------------------------------\n\nconst ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nexport function validDecisionId(id: string): boolean {\n\t// 入参可能来自未校验的工具输入(空对象 → undefined):test() 会把实参\n\t// 字符串化为 \"undefined\" 而\"通过\"模式匹配,必须先做类型守卫。\n\treturn typeof id === \"string\" && ID_PATTERN.test(id) && id.length <= 128;\n}\n\nfunction nonEmptyString(value: unknown, name: string, max: number): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") {\n\t\tthrow new DecisionError(\"invalid-input\", `${name} must be a non-empty string`);\n\t}\n\tif (value.length > max) {\n\t\tthrow new DecisionError(\"invalid-input\", `${name} exceeds ${max} characters`);\n\t}\n\treturn value;\n}\n\n/** Model-facing option input; reversibility may be absent (missing = redline, §3). */\nexport interface DecisionOptionInput {\n\tlabel: string;\n\tdescription: string;\n\tconsequences: string;\n\treversibility?: DecisionReversibility;\n}\n\n/**\n * Escalation classification (§3): redline when ANY option is irreversible or\n * carries no reversibility field (conservative default); otherwise the\n * model's risk_class picks routine (L1) vs significant (L2). The redline is\n * never the model's risk self-assessment.\n */\nexport function classifyDecision(options: readonly DecisionOptionInput[], riskClass: DecisionRiskClass): DecisionLevel {\n\tfor (const option of options) {\n\t\tif (option.reversibility === undefined) return \"redline\";\n\t\tif (option.reversibility === \"irreversible\") return \"redline\";\n\t}\n\treturn riskClass;\n}\n\n/**\n * Validates one option shape. An explicit `reversibility` outside the\n * two-value vocabulary is invalid input (the schema enforces literals; this\n * guards unvalidated callers). A MISSING field is accepted here — the\n * classifier turns it into a redline.\n */\nfunction validateOption(input: DecisionOptionInput, index: number): DecisionOption {\n\tif (input === null || typeof input !== \"object\") {\n\t\tthrow new DecisionError(\"invalid-option\", `option ${index} must be an object`);\n\t}\n\tif (\n\t\tinput.reversibility !== undefined &&\n\t\tinput.reversibility !== \"reversible\" &&\n\t\tinput.reversibility !== \"irreversible\"\n\t) {\n\t\tthrow new DecisionError(\n\t\t\t\"invalid-option\",\n\t\t\t`option ${index} reversibility must be \"reversible\" or \"irreversible\": ${String(input.reversibility)}`,\n\t\t);\n\t}\n\treturn {\n\t\tlabel: nonEmptyString(input.label, `option ${index} label`, DECISION_LIMITS.labelMaxLength),\n\t\tdescription: nonEmptyString(\n\t\t\tinput.description,\n\t\t\t`option ${index} description`,\n\t\t\tDECISION_LIMITS.descriptionMaxLength,\n\t\t),\n\t\tconsequences: nonEmptyString(\n\t\t\tinput.consequences,\n\t\t\t`option ${index} consequences`,\n\t\t\tDECISION_LIMITS.consequencesMaxLength,\n\t\t),\n\t\t...(input.reversibility === undefined ? {} : { reversibility: input.reversibility }),\n\t};\n}\n\nexport interface DecisionAskInput {\n\tid: string;\n\tquestion: string;\n\toptions: DecisionOptionInput[];\n\tcontextDigest: string;\n\triskClass: DecisionRiskClass;\n}\n\n/** Validates the ask input and returns the normalized record (status asked, redLine derived). */\nexport function buildDecisionRecord(input: DecisionAskInput, now: number): DecisionRecord {\n\tif (!validDecisionId(input.id)) {\n\t\tthrow new DecisionError(\n\t\t\t\"invalid-id\",\n\t\t\t`decision id must match [A-Za-z0-9][A-Za-z0-9._-]* (no leading dot): ${input.id}`,\n\t\t);\n\t}\n\tif (!Array.isArray(input.options)) {\n\t\tthrow new DecisionError(\"invalid-input\", \"options must be an array\");\n\t}\n\tif (input.options.length < DECISION_LIMITS.optionsMin || input.options.length > DECISION_LIMITS.optionsMax) {\n\t\tthrow new DecisionError(\n\t\t\t\"invalid-input\",\n\t\t\t`options must carry ${DECISION_LIMITS.optionsMin}..${DECISION_LIMITS.optionsMax} items, got ${input.options.length}`,\n\t\t);\n\t}\n\tconst riskClass = input.riskClass;\n\tif (riskClass !== \"routine\" && riskClass !== \"significant\") {\n\t\tthrow new DecisionError(\"invalid-input\", `risk_class must be \"routine\" or \"significant\": ${String(riskClass)}`);\n\t}\n\tconst options = input.options.map((option, index) => validateOption(option, index));\n\tconst question = nonEmptyString(input.question, \"question\", DECISION_LIMITS.questionMaxLength);\n\tconst contextDigest = nonEmptyString(input.contextDigest, \"context_digest\", DECISION_LIMITS.contextDigestMaxLength);\n\treturn {\n\t\tid: input.id,\n\t\tquestion,\n\t\toptions,\n\t\tcontextDigest,\n\t\triskClass,\n\t\tredLine: classifyDecision(options, riskClass) === \"redline\",\n\t\tstatus: \"asked\",\n\t\trevision: 0,\n\t\tcreatedAt: now,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// State helpers\n// ---------------------------------------------------------------------------\n\nexport function findDecision(state: DecisionState, id: string): DecisionRecord | undefined {\n\treturn state.decisions.find((record) => record.id === id);\n}\n\n/** \"Active\" per §4.2/§4.3: waiting for the user, or timed out into adjudication. */\nexport function isDecisionActive(record: DecisionRecord): boolean {\n\treturn record.status === \"asked\" || record.status === \"adjudicating\";\n}\n\nfunction requireActive(state: DecisionState, id: string): DecisionRecord {\n\tconst record = findDecision(state, id);\n\tif (!record) throw new DecisionError(\"not-found\", `unknown decision: ${id}`);\n\tif (!isDecisionActive(record)) {\n\t\tthrow new DecisionError(\"not-active\", `decision is not active (status ${record.status}): ${id}`);\n\t}\n\treturn record;\n}\n\nfunction replaceDecision(state: DecisionState, updated: DecisionRecord): DecisionState {\n\treturn { version: 1, decisions: state.decisions.map((record) => (record.id === updated.id ? updated : record)) };\n}\n\n// ---------------------------------------------------------------------------\n// Transitions (§4, §5, §6)\n// ---------------------------------------------------------------------------\n\n/**\n * decision_ask: create the record and land the `asked` transition.\n * Single-active discipline: at most one decision may sit in\n * `asked` — a second ask is a structured rejection pointing at\n * resolve/cancel. The rolling retention (§ 有界性) never evicts an active\n * record: the oldest terminal record goes first, and only when none exists\n * is the new ask rejected.\n */\nexport function applyDecisionAsk(state: DecisionState, input: DecisionAskInput, now: number): EngineRun {\n\tif (findDecision(state, input.id)) {\n\t\tthrow new DecisionError(\"invalid-input\", `decision id already exists: ${input.id}`);\n\t}\n\tconst active = state.decisions.find((record) => record.status === \"asked\");\n\tif (active !== undefined) {\n\t\tthrow new DecisionError(\n\t\t\t\"decision-active-exists\",\n\t\t\t`decision ${active.id} is still awaiting an answer; resolve it or cancel it first (/decision cancel ${active.id})`,\n\t\t);\n\t}\n\tconst record = buildDecisionRecord(input, now);\n\tconst decisions = [...state.decisions, record];\n\tif (decisions.length > DECISION_LIMITS.decisionsMax) {\n\t\tconst oldestTerminal = decisions.findIndex((candidate) => !isDecisionActive(candidate));\n\t\tif (oldestTerminal < 0) {\n\t\t\tthrow new DecisionError(\n\t\t\t\t\"state-limit-reached\",\n\t\t\t\t`decision state reached its ${DECISION_LIMITS.decisionsMax}-record limit with no terminal record to evict`,\n\t\t\t);\n\t\t}\n\t\tdecisions.splice(oldestTerminal, 1);\n\t}\n\treturn {\n\t\tstate: { version: 1, decisions },\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: \"asked\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: \"decision-asked\",\n\t\t\t\t\tdetail: `level=${record.redLine ? \"redline\" : record.riskClass} options=${record.options.length}`,\n\t\t\t\t},\n\t\t\t\trevision: record.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport interface DecisionAnswerInput {\n\tdecisionId: string;\n\tchoice: number;\n\tsource: \"user\" | \"adjudication\";\n\tnote?: string;\n}\n\n/**\n * decision_resolve (§4.2): close the decision with the chosen option index.\n * `source: \"adjudication\"` requires the injected verdict to already exist in\n * the record AND the choice to match it — the model must not fabricate or\n * override an adjudication.\n */\nexport function applyDecisionAnswer(state: DecisionState, input: DecisionAnswerInput, now: number): EngineRun {\n\tvoid now; // the answer itself carries no timestamp field; kept for call-shape symmetry\n\tconst record = requireActive(state, input.decisionId);\n\tif (\n\t\t!Number.isInteger(input.choice) ||\n\t\t(input.choice as number) < 0 ||\n\t\t(input.choice as number) >= record.options.length\n\t) {\n\t\tthrow new DecisionError(\n\t\t\t\"choice-out-of-range\",\n\t\t\t`choice must be an option index within 0..${record.options.length - 1}: ${String(input.choice)}`,\n\t\t);\n\t}\n\tif (input.source === \"adjudication\") {\n\t\tif (record.status !== \"adjudicating\" || record.verdict === undefined) {\n\t\t\tthrow new DecisionError(\n\t\t\t\t\"adjudication-not-recorded\",\n\t\t\t\t`no injected adjudication exists for decision ${record.id}; the model must not claim an adjudicated answer`,\n\t\t\t);\n\t\t}\n\t\tif (record.verdict.choice !== input.choice) {\n\t\t\tthrow new DecisionError(\n\t\t\t\t\"adjudication-choice-mismatch\",\n\t\t\t\t`choice ${input.choice} does not match the injected adjudication (choice ${record.verdict.choice})`,\n\t\t\t);\n\t\t}\n\t} else if (record.status !== \"asked\") {\n\t\tthrow new DecisionError(\n\t\t\t\"not-active\",\n\t\t\t`decision ${record.id} already timed out (status ${record.status}); a user answer can no longer land through decision_resolve`,\n\t\t);\n\t}\n\tconst note =\n\t\tinput.note === undefined ? undefined : nonEmptyString(input.note, \"note\", DECISION_LIMITS.noteMaxLength);\n\tconst updated: DecisionRecord = {\n\t\t...record,\n\t\tstatus: \"answered\",\n\t\trevision: record.revision + 1,\n\t\tresolvedBy: input.source,\n\t\tchoice: input.choice,\n\t\t...(note === undefined ? {} : { note }),\n\t};\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: input.source === \"user\" ? \"answered.user\" : \"answered.adjudication\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: input.source === \"user\" ? \"user-answered\" : \"adjudication-resolved\",\n\t\t\t\t\tdetail: `choice=${input.choice}${note === undefined ? \"\" : ` note=${note}`}`,\n\t\t\t\t},\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport type TimeoutMode = \"halt\" | \"adjudicate\";\n\n/**\n * Timeout fire (§5.1/§5.2): redline lands `timed-out.halted` (never\n * adjudicated); reversible levels land `timed-out.adjudicating` before the\n * chain runs.\n */\nexport function applyDecisionTimeout(\n\tstate: DecisionState,\n\tinput: { decisionId: string; mode: TimeoutMode },\n\tnow: number,\n): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tif (record.status !== \"asked\") {\n\t\tthrow new DecisionError(\"not-active\", `timeout fires on a non-asked decision: ${record.id}`);\n\t}\n\tif (input.mode === \"halt\" && !record.redLine) {\n\t\tthrow new DecisionError(\"invalid-input\", `decision ${record.id} is not a redline; use adjudicate`);\n\t}\n\tif (input.mode === \"adjudicate\" && record.redLine) {\n\t\tthrow new DecisionError(\"invalid-input\", `decision ${record.id} is a redline; it must halt, not adjudicate`);\n\t}\n\tconst updated: DecisionRecord = {\n\t\t...record,\n\t\tstatus: input.mode === \"halt\" ? \"halted\" : \"adjudicating\",\n\t\trevision: record.revision + 1,\n\t};\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: input.mode === \"halt\" ? \"timed-out.halted\" : \"timed-out.adjudicating\",\n\t\t\t\treason: {\n\t\t\t\t\tcode: input.mode === \"halt\" ? \"redline-kept-stopped\" : \"adjudication-dispatched\",\n\t\t\t\t\tdetail:\n\t\t\t\t\t\tinput.mode === \"halt\"\n\t\t\t\t\t\t\t? \"irreversible fork stays stopped for the user\"\n\t\t\t\t\t\t\t: \"reversible fork handed to the adjudication chain\",\n\t\t\t\t},\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport type AdjudicationFailureCode = \"unavailable\" | \"unparseable\";\n\n/**\n * Adjudication failed (§5.4): capability missing, submit/drain error, or an\n * output that violates the JSON protocol. Terminal halted-like state — no\n * retry, no silent downgrade.\n */\nexport function applyAdjudicationFailure(\n\tstate: DecisionState,\n\tinput: { decisionId: string; code: AdjudicationFailureCode; detail: string },\n\tnow: number,\n): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tif (record.status !== \"adjudicating\") {\n\t\tthrow new DecisionError(\"not-active\", `adjudication failure lands on an adjudicating decision: ${record.id}`);\n\t}\n\tconst updated: DecisionRecord = {\n\t\t...record,\n\t\tstatus: input.code === \"unavailable\" ? \"unavailable\" : \"unparseable\",\n\t\trevision: record.revision + 1,\n\t};\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: input.code === \"unavailable\" ? \"adjudication.unavailable\" : \"adjudication.unparseable\",\n\t\t\t\treason: { code: `adjudication-${input.code}`, detail: input.detail },\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\n/**\n * Stores a successful adjudication verdict (§5 留痕: 裁决书全文同步写入\n * decision.state) without a transition event — the vocabulary has none for\n * success; the audit trail closes when the model resolves\n * (`answered.adjudication`).\n */\nexport function recordVerdict(\n\tstate: DecisionState,\n\tinput: { decisionId: string; verdict: AdjudicationVerdict },\n\tnow: number,\n): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tif (record.status !== \"adjudicating\") {\n\t\tthrow new DecisionError(\"not-active\", `a verdict lands on an adjudicating decision: ${record.id}`);\n\t}\n\tconst updated: DecisionRecord = { ...record, verdict: input.verdict };\n\treturn { state: replaceDecision(state, updated), transitions: [] };\n}\n\n/** /decision cancel (§4.3): the user decides \"do nothing\". Clears active status. */\nexport function applyDecisionCancel(state: DecisionState, input: { decisionId: string }, now: number): EngineRun {\n\tvoid now;\n\tconst record = requireActive(state, input.decisionId);\n\tconst updated: DecisionRecord = { ...record, status: \"cancelled\", revision: record.revision + 1 };\n\treturn {\n\t\tstate: replaceDecision(state, updated),\n\t\ttransitions: [\n\t\t\t{\n\t\t\t\tdecisionId: record.id,\n\t\t\t\tevent: \"cancelled\",\n\t\t\t\treason: { code: \"decision-cancelled\", detail: \"user cancelled the fork (do nothing)\" },\n\t\t\t\trevision: updated.revision,\n\t\t\t},\n\t\t],\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Snapshot rebuild (§6): reverse scan, malformed snapshot reads as empty\n// ---------------------------------------------------------------------------\n\nexport interface StateEntryView {\n\tcustomType?: string;\n\tdata?: unknown;\n}\n\n/**\n * Rebuilds the decision state from the session's append-only entries: the\n * most recent `decision.state` snapshot is authoritative; a malformed one\n * reads as an empty state instead of crashing re-entry (goal/alignment\n * precedent). Both the versioned shape (`{ version: 1, decisions }`, §6)\n * and a legacy bare container are accepted — a missing version reads as 1,\n * a foreign version reads as empty (conservative default).\n */\nexport function rebuildDecisionState(entries: readonly StateEntryView[]): DecisionState {\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry?.customType !== \"decision.state\") continue;\n\t\tconst data = entry.data;\n\t\tif (data === null || typeof data !== \"object\" || Array.isArray(data)) return { version: 1, decisions: [] };\n\t\tconst record = data as { version?: unknown; decisions?: unknown };\n\t\tif (record.version !== undefined && record.version !== 1) return { version: 1, decisions: [] };\n\t\tif (!Array.isArray(record.decisions)) return { version: 1, decisions: [] };\n\t\treturn { version: 1, decisions: [...record.decisions] };\n\t}\n\treturn { version: 1, decisions: [] };\n}\n"]}
|
package/dist/entry.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CapabilityAPI } from "@agent-forge/plugin-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.
|
|
4
|
+
*
|
|
5
|
+
* The capability factory returns its registrations as an array, while the
|
|
6
|
+
* manifest loader contract expects `void` or a single `{ dispose() }` instance
|
|
7
|
+
* (`normalizePluginInstance` does not accept arrays). The wrapper collects the
|
|
8
|
+
* registrations and disposes them in reverse order on plugin teardown — the
|
|
9
|
+
* same disposal semantics as the host's embedded `syncRegistrationFactory`.
|
|
10
|
+
*/
|
|
11
|
+
export default function createDecisionEscalationPluginEntry(api: CapabilityAPI): Promise<{
|
|
12
|
+
dispose: () => void;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D;;;;;;;;GAQG;AACH,wBAA8B,mCAAmC,CAChE,GAAG,EAAE,aAAa,GAChB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC,CAWlC","sourcesContent":["import type { CapabilityAPI } from \"@agent-forge/plugin-sdk\";\nimport { createCapability } from \"./index.ts\";\n\n/**\n * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.\n *\n * The capability factory returns its registrations as an array, while the\n * manifest loader contract expects `void` or a single `{ dispose() }` instance\n * (`normalizePluginInstance` does not accept arrays). The wrapper collects the\n * registrations and disposes them in reverse order on plugin teardown — the\n * same disposal semantics as the host's embedded `syncRegistrationFactory`.\n */\nexport default async function createDecisionEscalationPluginEntry(\n\tapi: CapabilityAPI,\n): Promise<{ dispose: () => void }> {\n\tconst registrations = await createCapability(api);\n\treturn {\n\t\tdispose: () => {\n\t\t\tfor (const registration of [...registrations].reverse()) {\n\t\t\t\t// Registration disposal in the capability runtime is synchronous;\n\t\t\t\t// void consumes the declared Promise possibility of the contract.\n\t\t\t\tvoid registration.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|
package/dist/entry.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createCapability } from "./index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.
|
|
4
|
+
*
|
|
5
|
+
* The capability factory returns its registrations as an array, while the
|
|
6
|
+
* manifest loader contract expects `void` or a single `{ dispose() }` instance
|
|
7
|
+
* (`normalizePluginInstance` does not accept arrays). The wrapper collects the
|
|
8
|
+
* registrations and disposes them in reverse order on plugin teardown — the
|
|
9
|
+
* same disposal semantics as the host's embedded `syncRegistrationFactory`.
|
|
10
|
+
*/
|
|
11
|
+
export default async function createDecisionEscalationPluginEntry(api) {
|
|
12
|
+
const registrations = await createCapability(api);
|
|
13
|
+
return {
|
|
14
|
+
dispose: () => {
|
|
15
|
+
for (const registration of [...registrations].reverse()) {
|
|
16
|
+
// Registration disposal in the capability runtime is synchronous;
|
|
17
|
+
// void consumes the declared Promise possibility of the contract.
|
|
18
|
+
void registration.dispose();
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../src/entry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;GAQG;AACH,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,mCAAmC,CAChE,GAAkB,EACiB;IACnC,MAAM,aAAa,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAClD,OAAO;QACN,OAAO,EAAE,GAAG,EAAE,CAAC;YACd,KAAK,MAAM,YAAY,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;gBACzD,kEAAkE;gBAClE,kEAAkE;gBAClE,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC;YAC7B,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF","sourcesContent":["import type { CapabilityAPI } from \"@agent-forge/plugin-sdk\";\nimport { createCapability } from \"./index.ts\";\n\n/**\n * Default-export factory consumed by the host's `loadCapabilityPluginFromManifest`.\n *\n * The capability factory returns its registrations as an array, while the\n * manifest loader contract expects `void` or a single `{ dispose() }` instance\n * (`normalizePluginInstance` does not accept arrays). The wrapper collects the\n * registrations and disposes them in reverse order on plugin teardown — the\n * same disposal semantics as the host's embedded `syncRegistrationFactory`.\n */\nexport default async function createDecisionEscalationPluginEntry(\n\tapi: CapabilityAPI,\n): Promise<{ dispose: () => void }> {\n\tconst registrations = await createCapability(api);\n\treturn {\n\t\tdispose: () => {\n\t\t\tfor (const registration of [...registrations].reverse()) {\n\t\t\t\t// Registration disposal in the capability runtime is synchronous;\n\t\t\t\t// void consumes the declared Promise possibility of the contract.\n\t\t\t\tvoid registration.dispose();\n\t\t\t}\n\t\t},\n\t};\n}\n"]}
|