dsh-decision-layer 0.7.0 → 0.7.2
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.en.md +33 -19
- package/README.md +33 -21
- package/docs/en/DETAIL.md +20 -0
- package/docs/zh/DETAIL.md +20 -0
- package/lib/client.js +185 -35
- package/lib/host/backend.d.ts +15 -0
- package/lib/host/backend.js +149 -0
- package/lib/host/config-store.d.ts +12 -0
- package/lib/host/config-store.js +254 -0
- package/lib/host/danger-gate.d.ts +45 -0
- package/lib/host/danger-gate.js +196 -0
- package/lib/host/excerpt.d.ts +25 -0
- package/lib/host/excerpt.js +62 -0
- package/lib/host/index.d.ts +6 -0
- package/lib/host/index.js +290 -0
- package/lib/host/log-analyze.d.ts +121 -0
- package/lib/host/log-analyze.js +254 -0
- package/lib/host/log-read.d.ts +4 -0
- package/lib/host/log-read.js +34 -0
- package/lib/host/log-store.d.ts +15 -0
- package/lib/host/log-store.js +98 -0
- package/lib/host/loop-guard.d.ts +10 -0
- package/lib/host/loop-guard.js +52 -0
- package/lib/host/manager-api.d.ts +20 -0
- package/lib/host/manager-api.js +174 -0
- package/lib/host/narrowing.d.ts +48 -0
- package/lib/host/narrowing.js +213 -0
- package/lib/host/quality-trend.d.ts +14 -0
- package/lib/host/quality-trend.js +22 -0
- package/lib/host/self-check.d.ts +27 -0
- package/lib/host/self-check.js +104 -0
- package/lib/host/session-state.d.ts +19 -0
- package/lib/host/session-state.js +288 -0
- package/lib/host/state-store.d.ts +14 -0
- package/lib/host/state-store.js +109 -0
- package/lib/host/steer-once.d.ts +13 -0
- package/lib/host/steer-once.js +25 -0
- package/lib/host/task-completion.d.ts +29 -0
- package/lib/host/task-completion.js +148 -0
- package/lib/host/types.d.ts +343 -0
- package/lib/host/types.js +11 -0
- package/locale/en.json +2 -1
- package/locale/zh.json +2 -1
- package/package.json +19 -10
- package/service/.gitkeep +0 -0
- package/service/backend.mjs +0 -127
- package/service/config-store.mjs +0 -311
- package/service/danger-gate.mjs +0 -182
- package/service/dsh.mjs +0 -282
- package/service/log-analyze.mjs +0 -256
- package/service/log-read.mjs +0 -27
- package/service/log-store.mjs +0 -82
- package/service/loop-guard.mjs +0 -51
- package/service/manager-api.mjs +0 -137
- package/service/narrowing.mjs +0 -210
- package/service/quality-trend.mjs +0 -29
- package/service/self-check.mjs +0 -131
- package/service/session-state.mjs +0 -257
- package/service/state-store.mjs +0 -100
- package/service/steer-once.mjs +0 -25
- package/service/task-completion.mjs +0 -163
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createSteerOnce } from "./steer-once.js";
|
|
2
|
+
import { errorReason, isObject, isProbability } from "./types.js";
|
|
3
|
+
const MAX_STATE = 8 * 1024;
|
|
4
|
+
const MAX_CONDITION = 512;
|
|
5
|
+
const MAX_CONDITIONS = 20;
|
|
6
|
+
const MAX_EVIDENCE = 40;
|
|
7
|
+
const STEER_TEXT = "\u672C\u56DE\u5408\u4EFB\u52A1\u5B8C\u6210\u6838\u5BF9\u53D1\u73B0\u6709\u660E\u786E\u4EA4\u4ED8\u6761\u4EF6\u5C1A\u672A\u6EE1\u8DB3\uFF1A\u8BF7\u5BF9\u7167\u7528\u6237\u5217\u51FA\u7684\u6761\u4EF6\u9010\u9879\u590D\u6838\uFF0C\u8865\u5B8C\u672A\u5B8C\u6210\u7684\u90E8\u5206\u3002";
|
|
8
|
+
function extractConditions(userText) {
|
|
9
|
+
const text = typeof userText === "string" ? userText : "";
|
|
10
|
+
if (!text.trim()) return [];
|
|
11
|
+
const conditions = [];
|
|
12
|
+
const push = (value) => {
|
|
13
|
+
const item = value.trim().replace(/\s+/g, " ").slice(0, MAX_CONDITION);
|
|
14
|
+
if (item && !conditions.includes(item)) conditions.push(item);
|
|
15
|
+
};
|
|
16
|
+
const lines = text.split(/\r?\n/);
|
|
17
|
+
let matchedStructured = false;
|
|
18
|
+
for (const line of lines) {
|
|
19
|
+
const numbered = line.match(/^\s*(?:[((]?\d+[)).、]|\d+\s*[.)、])\s+(.*)$/)?.[1];
|
|
20
|
+
const bullet = line.match(/^\s*[-*•]\s+(.*)$/)?.[1];
|
|
21
|
+
if (numbered?.trim()) {
|
|
22
|
+
push(numbered);
|
|
23
|
+
matchedStructured = true;
|
|
24
|
+
} else if (bullet?.trim()) {
|
|
25
|
+
push(bullet);
|
|
26
|
+
matchedStructured = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!matchedStructured) {
|
|
30
|
+
const compact = text.trim();
|
|
31
|
+
if (!/[\r\n]/.test(compact)) {
|
|
32
|
+
const parts = compact.split(/[、,,]/).map((part) => part.trim()).filter(Boolean);
|
|
33
|
+
if (parts.length >= 2) for (const part of parts) push(part);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return conditions.slice(0, MAX_CONDITIONS);
|
|
37
|
+
}
|
|
38
|
+
function completionQuestions(conditions, evidence, answer) {
|
|
39
|
+
const items = Array.isArray(conditions) ? conditions.slice(0, MAX_CONDITIONS) : [];
|
|
40
|
+
const tools = Array.isArray(evidence) ? evidence.filter((item) => isObject(item) && typeof item.tool === "string").slice(0, MAX_EVIDENCE).map((item) => ({ tool: item.tool.slice(0, 128), outcome: item.outcome === "error" ? "error" : "ok" })) : [];
|
|
41
|
+
const state = {
|
|
42
|
+
conditions: items.map((text, index) => ({ id: `c${index}`, text })),
|
|
43
|
+
toolEvidence: tools,
|
|
44
|
+
finalReply: String(answer ?? "").slice(0, MAX_STATE),
|
|
45
|
+
note: "\u5DE5\u5177\u8BC1\u636E\uFF08toolEvidence\uFF09\u662F\u5224\u5B9A\u4F9D\u636E\uFF1BfinalReply \u81EA\u79F0\u5B8C\u6210\u4E0D\u4F5C\u4E3A\u8BC1\u636E\u3002\u7F3A\u5C11\u8BC1\u636E\u65F6\u9009 insufficient_evidence\u3002"
|
|
46
|
+
};
|
|
47
|
+
const questions = Object.fromEntries(items.map((text, index) => [`c${index}`, {
|
|
48
|
+
type: "choice",
|
|
49
|
+
instructions: {
|
|
50
|
+
condition: text,
|
|
51
|
+
question: "\u6839\u636E\u5F53\u524D\u5DE5\u5177\u8BC1\u636E\u4E0E\u6700\u7EC8\u56DE\u590D\uFF0C\u8FD9\u4E2A\u4EA4\u4ED8\u6761\u4EF6\u7684\u5B8C\u6210\u60C5\u51B5\u5C5E\u4E8E\u54EA\u4E00\u79CD\uFF1F"
|
|
52
|
+
},
|
|
53
|
+
criteria: {
|
|
54
|
+
satisfied: "\u6709\u5DE5\u5177\u8BC1\u636E\u6216\u7528\u6237\u660E\u786E\u786E\u8BA4\u8868\u660E\u8BE5\u6761\u4EF6\u5DF2\u5B8C\u6210",
|
|
55
|
+
unsatisfied: "\u6709\u8BC1\u636E\u8868\u660E\u8BE5\u6761\u4EF6\u672A\u5B8C\u6210\u6216\u505A\u9519",
|
|
56
|
+
insufficient_evidence: "\u73B0\u6709\u8BC1\u636E\u4E0D\u8DB3\u4EE5\u5224\u65AD\u8BE5\u6761\u4EF6\u662F\u5426\u5B8C\u6210"
|
|
57
|
+
}
|
|
58
|
+
}]));
|
|
59
|
+
return { state, questions };
|
|
60
|
+
}
|
|
61
|
+
const DEFAULT_MIN_CONFIDENCE = 0.4;
|
|
62
|
+
function createTaskCompletion({ evaluate, sessions, steer, settings, minConfidence = DEFAULT_MIN_CONFIDENCE } = {}) {
|
|
63
|
+
if (typeof evaluate !== "function") throw new Error("Task completion requires an evaluator");
|
|
64
|
+
const steerOnce = createSteerOnce();
|
|
65
|
+
const resolveSettings = async () => {
|
|
66
|
+
let raw = {};
|
|
67
|
+
try {
|
|
68
|
+
raw = (typeof settings === "function" ? await settings() : settings) ?? {};
|
|
69
|
+
} catch {
|
|
70
|
+
raw = {};
|
|
71
|
+
}
|
|
72
|
+
const mode = raw?.mode === "steer" ? "steer" : "observe";
|
|
73
|
+
const minConditions = Number.isSafeInteger(raw?.minConditions) && raw.minConditions >= 2 ? raw.minConditions : 2;
|
|
74
|
+
const confidenceFloor = isProbability(raw?.minConfidence) ? raw.minConfidence : minConfidence;
|
|
75
|
+
return { mode, minConditions, minConfidence: confidenceFloor };
|
|
76
|
+
};
|
|
77
|
+
const record = (turn, outcome, detail = {}) => {
|
|
78
|
+
const id = turn?.agent?.session?.id;
|
|
79
|
+
if (!sessions || typeof id !== "string" || typeof sessions.recordComplete !== "function") return;
|
|
80
|
+
try {
|
|
81
|
+
sessions.recordComplete(id, outcome, detail);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
if (typeof sessions.log !== "function") return;
|
|
85
|
+
try {
|
|
86
|
+
sessions.log(id, { kind: "complete", outcome, ...detail });
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const skip = { evaluated: false, steered: false };
|
|
91
|
+
return {
|
|
92
|
+
async review(turn) {
|
|
93
|
+
const output = typeof turn?.output === "string" ? turn.output.trim() : "";
|
|
94
|
+
const { mode, minConditions, minConfidence: confidenceFloor } = await resolveSettings();
|
|
95
|
+
const conditions = extractConditions(turn?.userText);
|
|
96
|
+
if (conditions.length < minConditions) return { ...skip };
|
|
97
|
+
const evidence = Array.isArray(turn?.evidence) ? turn.evidence : [];
|
|
98
|
+
if (evidence.length === 0) return { ...skip };
|
|
99
|
+
const id = turn?.agent?.session?.id;
|
|
100
|
+
if (sessions && typeof id === "string") {
|
|
101
|
+
try {
|
|
102
|
+
if (sessions.snapshot(id).enabled === false) return { ...skip };
|
|
103
|
+
} catch {
|
|
104
|
+
return { ...skip };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let answers;
|
|
108
|
+
try {
|
|
109
|
+
const result = await evaluate(completionQuestions(conditions, evidence, output), { signal: turn?.signal });
|
|
110
|
+
answers = isObject(result?.answers) ? result.answers : void 0;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
record(turn, "error", { reason: errorReason(error) });
|
|
113
|
+
return { ...skip };
|
|
114
|
+
}
|
|
115
|
+
if (!answers) {
|
|
116
|
+
record(turn, "error", { reason: "invalid-response" });
|
|
117
|
+
return { ...skip };
|
|
118
|
+
}
|
|
119
|
+
let satisfied = 0, unsatisfied = 0, insufficient = 0;
|
|
120
|
+
let unsatisfiedMinConf = 1;
|
|
121
|
+
for (let index = 0; index < conditions.length; index++) {
|
|
122
|
+
const answer = answers[`c${index}`];
|
|
123
|
+
if (!isObject(answer) || answer.type !== "choice" || typeof answer.choice !== "string") {
|
|
124
|
+
insufficient++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (answer.choice === "satisfied") satisfied++;
|
|
128
|
+
else if (answer.choice === "unsatisfied") {
|
|
129
|
+
unsatisfied++;
|
|
130
|
+
if (isProbability(answer.confidence)) unsatisfiedMinConf = Math.min(unsatisfiedMinConf, answer.confidence);
|
|
131
|
+
} else insufficient++;
|
|
132
|
+
}
|
|
133
|
+
const outcome = unsatisfied > 0 ? "unsatisfied" : "ok";
|
|
134
|
+
const detail = { conditions: conditions.length, satisfied, unsatisfied, insufficient };
|
|
135
|
+
const confident = unsatisfied > 0 && unsatisfiedMinConf >= confidenceFloor;
|
|
136
|
+
const steered = mode === "steer" && confident && typeof steer === "function" && turn?.agent ? steerOnce.attempt(turn, () => steer(turn.agent, STEER_TEXT)) : false;
|
|
137
|
+
if (steered) detail.steered = true;
|
|
138
|
+
record(turn, outcome, detail);
|
|
139
|
+
return { evaluated: true, steered, satisfied, unsatisfied, insufficient };
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export {
|
|
144
|
+
DEFAULT_MIN_CONFIDENCE,
|
|
145
|
+
completionQuestions,
|
|
146
|
+
createTaskCompletion,
|
|
147
|
+
extractConditions
|
|
148
|
+
};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types and guards for the host half.
|
|
3
|
+
*
|
|
4
|
+
* Three kinds of shape appear here, and they are deliberately kept apart:
|
|
5
|
+
* - wire contracts (EvaluateInput / EvaluateResult) — the one protocol with the
|
|
6
|
+
* adjudication backend, validated in backend.ts;
|
|
7
|
+
* - configuration (ResolvedConfig / *Settings) — validated in config-store.ts;
|
|
8
|
+
* - host runtime surfaces (HostContext, ToolExec, AgentLike) — our own
|
|
9
|
+
* structural view of what the Cordis host hands us. They are declared here
|
|
10
|
+
* instead of imported from @deepseek-ai/* so a host minor upgrade cannot
|
|
11
|
+
* break compilation: every field is optional and every boundary re-validates.
|
|
12
|
+
*/
|
|
13
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
14
|
+
export type JsonObject = Record<string, unknown>;
|
|
15
|
+
export declare const isObject: (value: unknown) => value is JsonObject;
|
|
16
|
+
export declare const isProbability: (value: unknown) => value is number;
|
|
17
|
+
/** The stable reason code a backend failure carries, or a generic fallback. */
|
|
18
|
+
export declare const errorReason: (error: unknown, fallback?: string) => string;
|
|
19
|
+
export type ChoiceQuestion = {
|
|
20
|
+
type: 'choice';
|
|
21
|
+
instructions: unknown;
|
|
22
|
+
criteria: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
export type ScoreQuestion = {
|
|
25
|
+
type: 'score';
|
|
26
|
+
instructions: unknown;
|
|
27
|
+
criteria: readonly unknown[];
|
|
28
|
+
};
|
|
29
|
+
export type NoulQuestion = {
|
|
30
|
+
type: 'noul';
|
|
31
|
+
instructions: unknown;
|
|
32
|
+
criteria?: unknown;
|
|
33
|
+
};
|
|
34
|
+
export type Question = ChoiceQuestion | ScoreQuestion | NoulQuestion;
|
|
35
|
+
export type QuestionMap = Record<string, Question>;
|
|
36
|
+
export type EvaluateInput = {
|
|
37
|
+
state: unknown;
|
|
38
|
+
questions: QuestionMap;
|
|
39
|
+
};
|
|
40
|
+
export type NoulAnswer = {
|
|
41
|
+
type: 'noul';
|
|
42
|
+
noul: number;
|
|
43
|
+
};
|
|
44
|
+
export type ScoredAnswer = {
|
|
45
|
+
confidence: number;
|
|
46
|
+
probabilities: Record<string, number>;
|
|
47
|
+
};
|
|
48
|
+
export type ScoreAnswer = ScoredAnswer & {
|
|
49
|
+
type: 'score';
|
|
50
|
+
score: number;
|
|
51
|
+
};
|
|
52
|
+
export type ChoiceAnswer = ScoredAnswer & {
|
|
53
|
+
type: 'choice';
|
|
54
|
+
choice: string;
|
|
55
|
+
};
|
|
56
|
+
export type Answer = NoulAnswer | ScoreAnswer | ChoiceAnswer;
|
|
57
|
+
export type AnswerMap = Record<string, Answer>;
|
|
58
|
+
export type EvaluateResult = {
|
|
59
|
+
model: string;
|
|
60
|
+
answers: AnswerMap;
|
|
61
|
+
usage?: {
|
|
62
|
+
input_tokens: number;
|
|
63
|
+
output_tokens: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export type Evaluator = (input: EvaluateInput, options?: {
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
}) => Promise<EvaluateResult>;
|
|
69
|
+
export type CheckMode = 'observe' | 'steer';
|
|
70
|
+
export type NarrowMode = 'observe' | 'enforce';
|
|
71
|
+
export type BatchMode = 'auto' | 'single' | 'split';
|
|
72
|
+
export type FeatureKey = 'gate' | 'check' | 'narrow' | 'complete';
|
|
73
|
+
export type Severity = 'normal' | 'warn' | 'severe';
|
|
74
|
+
export type DangerRuleAdditions = {
|
|
75
|
+
addToolNames?: string[];
|
|
76
|
+
removeToolNames?: string[];
|
|
77
|
+
addCommandPatterns?: string[];
|
|
78
|
+
addPathPatterns?: string[];
|
|
79
|
+
};
|
|
80
|
+
/** Compiled rules: the built-in patterns plus the configured additions. */
|
|
81
|
+
export type DangerRuleSet = {
|
|
82
|
+
toolNames: readonly string[];
|
|
83
|
+
commandPatterns: readonly RegExp[];
|
|
84
|
+
pathPatterns: readonly RegExp[];
|
|
85
|
+
};
|
|
86
|
+
export type GateSettings = {
|
|
87
|
+
minConfidence?: number;
|
|
88
|
+
};
|
|
89
|
+
export type CheckSettings = {
|
|
90
|
+
mode?: CheckMode;
|
|
91
|
+
rubric?: string[];
|
|
92
|
+
lowScoreThreshold?: number;
|
|
93
|
+
minConfidence?: number;
|
|
94
|
+
trendMinConfidence?: number;
|
|
95
|
+
trendRun?: number;
|
|
96
|
+
trendSevereRun?: number;
|
|
97
|
+
};
|
|
98
|
+
export type NarrowSettings = {
|
|
99
|
+
mode?: NarrowMode;
|
|
100
|
+
threshold?: number;
|
|
101
|
+
keepPrefixes?: string[];
|
|
102
|
+
maxCandidates?: number;
|
|
103
|
+
batchMode?: BatchMode;
|
|
104
|
+
};
|
|
105
|
+
export type CompleteSettings = {
|
|
106
|
+
mode?: CheckMode;
|
|
107
|
+
minConditions?: number;
|
|
108
|
+
minConfidence?: number;
|
|
109
|
+
};
|
|
110
|
+
export type Features = Partial<Record<FeatureKey, boolean>>;
|
|
111
|
+
/** What the backend needs from the config to make one request. */
|
|
112
|
+
export type BackendConfig = {
|
|
113
|
+
url: string;
|
|
114
|
+
apiKey: string;
|
|
115
|
+
model: string;
|
|
116
|
+
httpApprovedUrl: string;
|
|
117
|
+
};
|
|
118
|
+
/** Fully resolved configuration; every field the decision points read. */
|
|
119
|
+
export type ResolvedConfig = {
|
|
120
|
+
url: string;
|
|
121
|
+
apiKey: string;
|
|
122
|
+
model: string;
|
|
123
|
+
httpApprovedUrl: string;
|
|
124
|
+
dangerRules?: DangerRuleAdditions;
|
|
125
|
+
gateSettings?: GateSettings;
|
|
126
|
+
checkSettings?: CheckSettings;
|
|
127
|
+
narrowSettings?: NarrowSettings;
|
|
128
|
+
completeSettings?: CompleteSettings;
|
|
129
|
+
features?: Features;
|
|
130
|
+
};
|
|
131
|
+
/** What the config page may read back: the API key is reduced to a flag. */
|
|
132
|
+
export type ConfigView = {
|
|
133
|
+
url: string;
|
|
134
|
+
model: string;
|
|
135
|
+
apiKeySet: boolean;
|
|
136
|
+
httpApprovedUrl: string;
|
|
137
|
+
dangerRules?: DangerRuleAdditions;
|
|
138
|
+
gateSettings?: GateSettings;
|
|
139
|
+
checkSettings?: CheckSettings;
|
|
140
|
+
narrowSettings?: NarrowSettings;
|
|
141
|
+
completeSettings?: CompleteSettings;
|
|
142
|
+
features?: Features;
|
|
143
|
+
};
|
|
144
|
+
/** The quality-trend knobs, shared by the live path and the log backtest. */
|
|
145
|
+
export type TrendConfig = {
|
|
146
|
+
trendMinConfidence?: number;
|
|
147
|
+
trendRun?: number;
|
|
148
|
+
trendSevereRun?: number;
|
|
149
|
+
lowScoreThreshold?: number;
|
|
150
|
+
};
|
|
151
|
+
export type LogKind = 'gate' | 'check' | 'narrow' | 'complete';
|
|
152
|
+
/**
|
|
153
|
+
* One decision-log row. Fields are optional because rows are also read back
|
|
154
|
+
* from disk: persisted snapshots are untrusted input, and only `kind` is
|
|
155
|
+
* enforced before a row is accepted.
|
|
156
|
+
*/
|
|
157
|
+
export type LogRow = {
|
|
158
|
+
id?: string;
|
|
159
|
+
at?: number;
|
|
160
|
+
kind: LogKind;
|
|
161
|
+
outcome?: string;
|
|
162
|
+
tool?: string;
|
|
163
|
+
suggestion?: string;
|
|
164
|
+
action?: string;
|
|
165
|
+
reason?: string;
|
|
166
|
+
subject?: string;
|
|
167
|
+
task?: string;
|
|
168
|
+
turn?: number;
|
|
169
|
+
command?: string;
|
|
170
|
+
path?: string;
|
|
171
|
+
mode?: string;
|
|
172
|
+
score?: number;
|
|
173
|
+
confidence?: number;
|
|
174
|
+
dropped?: number;
|
|
175
|
+
kept?: number;
|
|
176
|
+
applied?: boolean;
|
|
177
|
+
candidates?: number;
|
|
178
|
+
conditions?: number;
|
|
179
|
+
satisfied?: number;
|
|
180
|
+
unsatisfied?: number;
|
|
181
|
+
insufficient?: number;
|
|
182
|
+
steered?: boolean;
|
|
183
|
+
lowConfidence?: boolean;
|
|
184
|
+
tools?: string[];
|
|
185
|
+
};
|
|
186
|
+
/** The research-log sink record: the panel row plus its session tag. */
|
|
187
|
+
export type SessionSinkRow = LogRow & {
|
|
188
|
+
sessionId: string;
|
|
189
|
+
};
|
|
190
|
+
export type SessionCounters = {
|
|
191
|
+
attempts: number;
|
|
192
|
+
failures: number;
|
|
193
|
+
};
|
|
194
|
+
export type GateCounters = SessionCounters & {
|
|
195
|
+
ask: number;
|
|
196
|
+
deny: number;
|
|
197
|
+
allow: number;
|
|
198
|
+
actual: {
|
|
199
|
+
allow: number;
|
|
200
|
+
deny: number;
|
|
201
|
+
error: number;
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
export type CheckCounters = SessionCounters & {
|
|
205
|
+
low: number;
|
|
206
|
+
run: number;
|
|
207
|
+
severity: Severity;
|
|
208
|
+
};
|
|
209
|
+
export type NarrowCounters = SessionCounters & {
|
|
210
|
+
applied: number;
|
|
211
|
+
dropped: number;
|
|
212
|
+
};
|
|
213
|
+
export type CompleteCounters = SessionCounters & {
|
|
214
|
+
unsatisfied: number;
|
|
215
|
+
satisfied: number;
|
|
216
|
+
insufficient: number;
|
|
217
|
+
steered: number;
|
|
218
|
+
};
|
|
219
|
+
/** Live per-session state (the durable snapshot's payload). */
|
|
220
|
+
export type SessionState = {
|
|
221
|
+
enabled: boolean;
|
|
222
|
+
attempts: number;
|
|
223
|
+
failures: number;
|
|
224
|
+
gate: GateCounters;
|
|
225
|
+
check: CheckCounters;
|
|
226
|
+
narrow: NarrowCounters;
|
|
227
|
+
complete: CompleteCounters;
|
|
228
|
+
log: LogRow[];
|
|
229
|
+
};
|
|
230
|
+
/** The panel-facing projection; sections without attempts are omitted. */
|
|
231
|
+
export type SessionSnapshot = {
|
|
232
|
+
enabled: boolean;
|
|
233
|
+
hasAutomaticDecisions: boolean;
|
|
234
|
+
attempts: number;
|
|
235
|
+
failures: number;
|
|
236
|
+
gate?: GateCounters;
|
|
237
|
+
check?: {
|
|
238
|
+
attempts: number;
|
|
239
|
+
failures: number;
|
|
240
|
+
low: number;
|
|
241
|
+
trend: {
|
|
242
|
+
run: number;
|
|
243
|
+
severity: Severity;
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
narrow?: NarrowCounters;
|
|
247
|
+
complete?: CompleteCounters;
|
|
248
|
+
log?: LogRow[];
|
|
249
|
+
};
|
|
250
|
+
/** The counter shape rebuilt from persisted JSONL, without the master switch. */
|
|
251
|
+
export type SessionSeed = Omit<SessionState, 'enabled'>;
|
|
252
|
+
/**
|
|
253
|
+
* The slice of the session store the decision points use. Every method is
|
|
254
|
+
* optional: a test or an older host may inject only the parts it cares about.
|
|
255
|
+
*/
|
|
256
|
+
export type SessionsFace = {
|
|
257
|
+
snapshot: (id: string) => SessionSnapshot;
|
|
258
|
+
log?: (id: string, entry: Record<string, unknown>) => void;
|
|
259
|
+
record?: (id: string, kind: 'gate', outcome: string) => void;
|
|
260
|
+
recordActual?: (id: string, outcome: 'allow' | 'deny' | 'error') => void;
|
|
261
|
+
recordCheck?: (id: string, outcome: string, trend?: Record<string, unknown>) => void;
|
|
262
|
+
recordNarrow?: (id: string, outcome: string, dropped: number, applied: boolean) => void;
|
|
263
|
+
recordComplete?: (id: string, outcome: string, detail?: Record<string, unknown>) => void;
|
|
264
|
+
ensureLoaded?: (id: string) => Promise<unknown>;
|
|
265
|
+
setEnabled?: (id: string, enabled: boolean) => void;
|
|
266
|
+
};
|
|
267
|
+
export type ToolCandidate = {
|
|
268
|
+
name: string;
|
|
269
|
+
description?: string;
|
|
270
|
+
};
|
|
271
|
+
export type AgentLike = {
|
|
272
|
+
session?: {
|
|
273
|
+
id?: string;
|
|
274
|
+
deriveMessages?: () => unknown;
|
|
275
|
+
};
|
|
276
|
+
steer?: (message: unknown) => unknown;
|
|
277
|
+
ctx?: {
|
|
278
|
+
tools?: {
|
|
279
|
+
restrict?: (options: {
|
|
280
|
+
allow: string[];
|
|
281
|
+
}) => (() => void) | undefined;
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
export type ToolExec = {
|
|
286
|
+
name?: string;
|
|
287
|
+
arguments?: unknown;
|
|
288
|
+
agent?: AgentLike;
|
|
289
|
+
signal?: AbortSignal;
|
|
290
|
+
[key: string]: unknown;
|
|
291
|
+
};
|
|
292
|
+
export type NextStep = () => unknown;
|
|
293
|
+
export type ToolSchema = {
|
|
294
|
+
name: string;
|
|
295
|
+
description?: string;
|
|
296
|
+
};
|
|
297
|
+
export type ToolRegistration = {
|
|
298
|
+
name: string;
|
|
299
|
+
description: string;
|
|
300
|
+
parameters: Record<string, unknown>;
|
|
301
|
+
output: Record<string, unknown>;
|
|
302
|
+
timeoutMs?: number;
|
|
303
|
+
execute: (args: unknown, exec: ToolExec) => unknown;
|
|
304
|
+
};
|
|
305
|
+
export type SkillRegistration = {
|
|
306
|
+
name: string;
|
|
307
|
+
description: string;
|
|
308
|
+
content: string;
|
|
309
|
+
source: string;
|
|
310
|
+
};
|
|
311
|
+
export type ManagerRoute = {
|
|
312
|
+
kind: 'exact';
|
|
313
|
+
path: string;
|
|
314
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
315
|
+
};
|
|
316
|
+
export type WebServerFace = {
|
|
317
|
+
port: number;
|
|
318
|
+
host?: string;
|
|
319
|
+
register: (route: ManagerRoute) => () => void;
|
|
320
|
+
};
|
|
321
|
+
export type WebFace = {
|
|
322
|
+
webServer: WebServerFace;
|
|
323
|
+
};
|
|
324
|
+
export type ToolsFace = {
|
|
325
|
+
guard?: (guard: (exec: ToolExec) => string | undefined) => unknown;
|
|
326
|
+
register: (tool: ToolRegistration) => unknown;
|
|
327
|
+
schemas?: (agent?: AgentLike) => unknown;
|
|
328
|
+
};
|
|
329
|
+
export type SkillsFace = {
|
|
330
|
+
register: (skill: SkillRegistration) => unknown;
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* The Cordis context our apply() needs. Handlers are typed per call site
|
|
334
|
+
* because each event carries a different payload; `never[]` here keeps them
|
|
335
|
+
* assignable without leaking `any` into the implementation.
|
|
336
|
+
*/
|
|
337
|
+
export type HostContext = {
|
|
338
|
+
on: (event: string, handler: (...args: never[]) => unknown) => (() => void) | undefined;
|
|
339
|
+
effect?: (install: () => (() => void) | void) => unknown;
|
|
340
|
+
tools: ToolsFace;
|
|
341
|
+
skills: SkillsFace;
|
|
342
|
+
inject: (keys: string[], install: (web: WebFace) => (() => void) | void) => unknown;
|
|
343
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2
|
+
const isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
3
|
+
const errorReason = (error, fallback = "unreachable") => {
|
|
4
|
+
const reason = error?.reason;
|
|
5
|
+
return typeof reason === "string" ? reason : fallback;
|
|
6
|
+
};
|
|
7
|
+
export {
|
|
8
|
+
errorReason,
|
|
9
|
+
isObject,
|
|
10
|
+
isProbability
|
|
11
|
+
};
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Decision Layer",
|
|
4
|
-
"description": "A structured decision layer for the DeepSeek Harness agent loop: a danger-call gate, output self-check, and loop guard backed by a pluggable adjudication model."
|
|
4
|
+
"description": "A structured decision layer for the DeepSeek Harness agent loop: a danger-call gate, output self-check, tool narrowing, task-completion review, and loop guard backed by a pluggable adjudication model.",
|
|
5
|
+
"detail": "docs/en/DETAIL.md"
|
|
5
6
|
}
|
|
6
7
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-decision-layer",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "A structured decision layer for the DeepSeek Harness agent loop: a danger-call gate, output self-check, tool narrowing, and loop guard backed by a pluggable adjudication model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"icon": "./icon.svg",
|
|
17
|
-
"main": "./
|
|
17
|
+
"main": "./lib/host/index.js",
|
|
18
18
|
"files": [
|
|
19
|
-
"
|
|
20
|
-
"skills",
|
|
19
|
+
"lib/host",
|
|
21
20
|
"lib/client.js",
|
|
21
|
+
"skills",
|
|
22
22
|
"locale/*.json",
|
|
23
|
+
"docs/zh/DETAIL.md",
|
|
24
|
+
"docs/en/DETAIL.md",
|
|
23
25
|
"icon.svg",
|
|
24
26
|
"cordis.patch.yml",
|
|
25
27
|
"README.md",
|
|
@@ -34,20 +36,25 @@
|
|
|
34
36
|
"url": "https://github.com/xbzbing/dsh-decision-layer.git"
|
|
35
37
|
},
|
|
36
38
|
"scripts": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
39
|
+
"build": "node build.mjs",
|
|
40
|
+
"typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json",
|
|
41
|
+
"test": "node build.mjs && node --test tests/*.test.mjs",
|
|
42
|
+
"check": "npm test && npm run pack:assert",
|
|
40
43
|
"pack:assert": "node scripts/assert-pack.mjs"
|
|
41
44
|
},
|
|
42
45
|
"exports": {
|
|
43
|
-
".":
|
|
46
|
+
".": {
|
|
47
|
+
"types": "./lib/host/index.d.ts",
|
|
48
|
+
"default": "./lib/host/index.js"
|
|
49
|
+
},
|
|
44
50
|
"./client": "./lib/client.js",
|
|
45
51
|
"./locale/*.json": "./locale/*.json",
|
|
46
52
|
"./package.json": "./package.json"
|
|
47
53
|
},
|
|
48
54
|
"peerDependencies": {
|
|
49
55
|
"@deepseek-ai/cordis": "~4.0.4",
|
|
50
|
-
"@deepseek-ai/dsh": ">=0.1.7-rc.2"
|
|
56
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.2",
|
|
57
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.2"
|
|
51
58
|
},
|
|
52
59
|
"devDependencies": {
|
|
53
60
|
"@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
|
|
@@ -56,6 +63,7 @@
|
|
|
56
63
|
"@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
|
|
57
64
|
"@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
|
|
58
65
|
"@testing-library/react": "^16.3.3",
|
|
66
|
+
"@types/node": "^20.19.0",
|
|
59
67
|
"@types/react": "~18.3.1",
|
|
60
68
|
"@types/react-dom": "~18.3.0",
|
|
61
69
|
"esbuild": "^0.25.12",
|
|
@@ -78,5 +86,6 @@
|
|
|
78
86
|
],
|
|
79
87
|
"platform": "web"
|
|
80
88
|
}
|
|
81
|
-
}
|
|
89
|
+
},
|
|
90
|
+
"types": "./lib/host/index.d.ts"
|
|
82
91
|
}
|
package/service/.gitkeep
DELETED
|
File without changes
|