@vauban-org/agent-sdk 1.7.0 → 1.9.0
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/CONTRACT.md +67 -0
- package/dist/alerts/digest.d.ts +113 -0
- package/dist/alerts/digest.d.ts.map +1 -0
- package/dist/alerts/digest.js +160 -0
- package/dist/alerts/digest.js.map +1 -0
- package/dist/alerts/index.d.ts +12 -0
- package/dist/alerts/index.d.ts.map +1 -0
- package/dist/alerts/index.js +11 -0
- package/dist/alerts/index.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestration/ooda/types.d.ts +3 -2
- package/dist/orchestration/ooda/types.d.ts.map +1 -1
- package/dist/orchestration/ooda/types.js.map +1 -1
- package/dist/quality/index.d.ts +114 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +168 -0
- package/dist/quality/index.js.map +1 -0
- package/package.json +1 -1
- package/src/alerts/digest.ts +260 -0
- package/src/alerts/index.ts +12 -0
- package/src/index.ts +15 -0
- package/src/orchestration/ooda/types.ts +3 -2
- package/src/quality/index.ts +231 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quality/index.ts — Outcome quality scoring (ADR-ECO-039).
|
|
3
|
+
*
|
|
4
|
+
* Promoted from forge consumer in SDK 1.8.0 — see Brief 2026-05-17.
|
|
5
|
+
*
|
|
6
|
+
* Companion to {@link OutcomeRecord.quality}. Each agent's
|
|
7
|
+
* `outcomeMapping(feedback)` returns a normalized quality score (0..1)
|
|
8
|
+
* computed from feedback signals. The Command Center backend reads this
|
|
9
|
+
* into the `agent_run.outcome_quality` column (NUMERIC(5,4)), which
|
|
10
|
+
* powers the EconomicObserver ROI weighting and the Quality Foreman
|
|
11
|
+
* regression checks.
|
|
12
|
+
*
|
|
13
|
+
* Design notes:
|
|
14
|
+
* - Pure function (no I/O, no SDK imports beyond types) — safe to import
|
|
15
|
+
* from any agent and cheap to unit-test.
|
|
16
|
+
* - Signals are agent-agnostic — each agent maps its own feedback fields
|
|
17
|
+
* into a shared {@link QualityInputs} shape.
|
|
18
|
+
* - Default score is `0.5` ("neutral, no information"). Signals nudge it
|
|
19
|
+
* up (positive outcomes) or down (errors). Final value is clamped to
|
|
20
|
+
* `[0, 1]`.
|
|
21
|
+
* - `customScore` lets an agent provide its own pre-computed score (e.g.
|
|
22
|
+
* a model-based eval) and bypass the heuristic.
|
|
23
|
+
* - Prod-validated on 33 forge agents (ADR-ECO-039 rollout 2026-05).
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* Feedback signals consumed by {@link computeQuality}. All fields are
|
|
29
|
+
* optional — pass only the signals the calling agent emits.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
export interface QualityInputs {
|
|
34
|
+
/** Sentinel-style: number of threats blocked (e.g. paused contracts). */
|
|
35
|
+
readonly threatsBlocked?: number;
|
|
36
|
+
/** Sentinel/ops-style: alerts sent to operators. */
|
|
37
|
+
readonly alertsSent?: number;
|
|
38
|
+
/** Content-style: number of posts/articles successfully published. */
|
|
39
|
+
readonly postsPublished?: number;
|
|
40
|
+
/** Content-style: posts blocked by HITL or constitutional gate. */
|
|
41
|
+
readonly postsRejectedByHITL?: number;
|
|
42
|
+
/** Finance/treasury-style: invoices or proposals processed successfully. */
|
|
43
|
+
readonly invoicesProcessed?: number;
|
|
44
|
+
/** Generic: number of errors encountered during the cycle. */
|
|
45
|
+
readonly errorsEncountered?: number;
|
|
46
|
+
/** Lessons / retrospective bullets extracted (proxy for reflection depth). */
|
|
47
|
+
readonly lessons?: readonly unknown[];
|
|
48
|
+
/** Explicit override — bypasses the heuristic when defined. */
|
|
49
|
+
readonly customScore?: number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Per-signal contribution to the final score. Returned by
|
|
53
|
+
* {@link computeQualityWithBreakdown}.
|
|
54
|
+
*
|
|
55
|
+
* @public
|
|
56
|
+
*/
|
|
57
|
+
export interface QualityContribution {
|
|
58
|
+
/** Source signal name (matches a {@link QualityInputs} field). */
|
|
59
|
+
readonly signal: string;
|
|
60
|
+
/** Signed delta applied to the running score (positive or negative). */
|
|
61
|
+
readonly delta: number;
|
|
62
|
+
/** Human-readable explanation, e.g. `"1 post published"`. */
|
|
63
|
+
readonly reason: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Breakdown view of {@link computeQuality} — score plus the ordered list of
|
|
67
|
+
* non-zero contributions. Powers debug-grade observability surfaces
|
|
68
|
+
* (e.g. `/agents/[id]` quality trend tooltip).
|
|
69
|
+
*
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
export interface QualityBreakdown {
|
|
73
|
+
/** Final clamped score in `[0, 1]`. */
|
|
74
|
+
readonly score: number;
|
|
75
|
+
/** Contributions in evaluation order. Empty when no signal applies. */
|
|
76
|
+
readonly contributions: readonly QualityContribution[];
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Compute a 0..1 quality score from agent feedback signals.
|
|
80
|
+
*
|
|
81
|
+
* Defaults to `0.5` when no signal is provided. Each positive signal nudges
|
|
82
|
+
* the score upward; errors and HITL rejections nudge it downward. The
|
|
83
|
+
* result is clamped to `[0, 1]`.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* computeQuality({ postsPublished: 1 }) // → 0.7
|
|
87
|
+
* computeQuality({ errorsEncountered: 1 }) // → 0.3
|
|
88
|
+
* computeQuality({ customScore: 0.95 }) // → 0.95
|
|
89
|
+
* computeQuality({}) // → 0.5
|
|
90
|
+
*
|
|
91
|
+
* @public
|
|
92
|
+
*/
|
|
93
|
+
export declare function computeQuality(inputs: QualityInputs): number;
|
|
94
|
+
/**
|
|
95
|
+
* Same heuristic as {@link computeQuality}, but also returns the ordered
|
|
96
|
+
* list of non-zero contributions for debug-grade observability.
|
|
97
|
+
*
|
|
98
|
+
* When `customScore` is provided, the breakdown contains a single entry
|
|
99
|
+
* tagged `"customScore"` and the heuristic is bypassed.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* computeQualityWithBreakdown({ postsPublished: 1, lessons: ["a"] })
|
|
103
|
+
* // → {
|
|
104
|
+
* // score: 0.75,
|
|
105
|
+
* // contributions: [
|
|
106
|
+
* // { signal: "postsPublished", delta: 0.2, reason: "1 post published" },
|
|
107
|
+
* // { signal: "lessons", delta: 0.05, reason: "1 lesson extracted" },
|
|
108
|
+
* // ],
|
|
109
|
+
* // }
|
|
110
|
+
*
|
|
111
|
+
* @public
|
|
112
|
+
*/
|
|
113
|
+
export declare function computeQualityWithBreakdown(inputs: QualityInputs): QualityBreakdown;
|
|
114
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/quality/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,yEAAyE;IACzE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,mEAAmE;IACnE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,4EAA4E;IAC5E,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,8DAA8D;IAC9D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IACtC,+DAA+D;IAC/D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,QAAQ,CAAC,aAAa,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACxD;AAOD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAiB5D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,aAAa,GACpB,gBAAgB,CAyFlB"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quality/index.ts — Outcome quality scoring (ADR-ECO-039).
|
|
3
|
+
*
|
|
4
|
+
* Promoted from forge consumer in SDK 1.8.0 — see Brief 2026-05-17.
|
|
5
|
+
*
|
|
6
|
+
* Companion to {@link OutcomeRecord.quality}. Each agent's
|
|
7
|
+
* `outcomeMapping(feedback)` returns a normalized quality score (0..1)
|
|
8
|
+
* computed from feedback signals. The Command Center backend reads this
|
|
9
|
+
* into the `agent_run.outcome_quality` column (NUMERIC(5,4)), which
|
|
10
|
+
* powers the EconomicObserver ROI weighting and the Quality Foreman
|
|
11
|
+
* regression checks.
|
|
12
|
+
*
|
|
13
|
+
* Design notes:
|
|
14
|
+
* - Pure function (no I/O, no SDK imports beyond types) — safe to import
|
|
15
|
+
* from any agent and cheap to unit-test.
|
|
16
|
+
* - Signals are agent-agnostic — each agent maps its own feedback fields
|
|
17
|
+
* into a shared {@link QualityInputs} shape.
|
|
18
|
+
* - Default score is `0.5` ("neutral, no information"). Signals nudge it
|
|
19
|
+
* up (positive outcomes) or down (errors). Final value is clamped to
|
|
20
|
+
* `[0, 1]`.
|
|
21
|
+
* - `customScore` lets an agent provide its own pre-computed score (e.g.
|
|
22
|
+
* a model-based eval) and bypass the heuristic.
|
|
23
|
+
* - Prod-validated on 33 forge agents (ADR-ECO-039 rollout 2026-05).
|
|
24
|
+
*
|
|
25
|
+
* @public
|
|
26
|
+
*/
|
|
27
|
+
function clamp01(n) {
|
|
28
|
+
if (!Number.isFinite(n))
|
|
29
|
+
return 0.5;
|
|
30
|
+
return Math.max(0, Math.min(1, n));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compute a 0..1 quality score from agent feedback signals.
|
|
34
|
+
*
|
|
35
|
+
* Defaults to `0.5` when no signal is provided. Each positive signal nudges
|
|
36
|
+
* the score upward; errors and HITL rejections nudge it downward. The
|
|
37
|
+
* result is clamped to `[0, 1]`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* computeQuality({ postsPublished: 1 }) // → 0.7
|
|
41
|
+
* computeQuality({ errorsEncountered: 1 }) // → 0.3
|
|
42
|
+
* computeQuality({ customScore: 0.95 }) // → 0.95
|
|
43
|
+
* computeQuality({}) // → 0.5
|
|
44
|
+
*
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
export function computeQuality(inputs) {
|
|
48
|
+
if (typeof inputs.customScore === "number") {
|
|
49
|
+
return clamp01(inputs.customScore);
|
|
50
|
+
}
|
|
51
|
+
let q = 0.5;
|
|
52
|
+
if ((inputs.errorsEncountered ?? 0) > 0)
|
|
53
|
+
q -= 0.2;
|
|
54
|
+
if ((inputs.postsRejectedByHITL ?? 0) > 0)
|
|
55
|
+
q -= 0.1;
|
|
56
|
+
if ((inputs.postsPublished ?? 0) > 0)
|
|
57
|
+
q += 0.2;
|
|
58
|
+
if ((inputs.threatsBlocked ?? 0) > 0)
|
|
59
|
+
q += 0.3;
|
|
60
|
+
if ((inputs.alertsSent ?? 0) > 0)
|
|
61
|
+
q += 0.1;
|
|
62
|
+
if ((inputs.invoicesProcessed ?? 0) > 0)
|
|
63
|
+
q += 0.2;
|
|
64
|
+
if ((inputs.lessons?.length ?? 0) > 0)
|
|
65
|
+
q += 0.05;
|
|
66
|
+
return clamp01(q);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Same heuristic as {@link computeQuality}, but also returns the ordered
|
|
70
|
+
* list of non-zero contributions for debug-grade observability.
|
|
71
|
+
*
|
|
72
|
+
* When `customScore` is provided, the breakdown contains a single entry
|
|
73
|
+
* tagged `"customScore"` and the heuristic is bypassed.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* computeQualityWithBreakdown({ postsPublished: 1, lessons: ["a"] })
|
|
77
|
+
* // → {
|
|
78
|
+
* // score: 0.75,
|
|
79
|
+
* // contributions: [
|
|
80
|
+
* // { signal: "postsPublished", delta: 0.2, reason: "1 post published" },
|
|
81
|
+
* // { signal: "lessons", delta: 0.05, reason: "1 lesson extracted" },
|
|
82
|
+
* // ],
|
|
83
|
+
* // }
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
export function computeQualityWithBreakdown(inputs) {
|
|
88
|
+
if (typeof inputs.customScore === "number") {
|
|
89
|
+
const clamped = clamp01(inputs.customScore);
|
|
90
|
+
return {
|
|
91
|
+
score: clamped,
|
|
92
|
+
contributions: [
|
|
93
|
+
{
|
|
94
|
+
signal: "customScore",
|
|
95
|
+
delta: clamped - 0.5,
|
|
96
|
+
reason: `customScore override = ${inputs.customScore}`,
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
let q = 0.5;
|
|
102
|
+
const contributions = [];
|
|
103
|
+
const errors = inputs.errorsEncountered ?? 0;
|
|
104
|
+
if (errors > 0) {
|
|
105
|
+
q -= 0.2;
|
|
106
|
+
contributions.push({
|
|
107
|
+
signal: "errorsEncountered",
|
|
108
|
+
delta: -0.2,
|
|
109
|
+
reason: `${errors} error${errors === 1 ? "" : "s"} encountered`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
const rejects = inputs.postsRejectedByHITL ?? 0;
|
|
113
|
+
if (rejects > 0) {
|
|
114
|
+
q -= 0.1;
|
|
115
|
+
contributions.push({
|
|
116
|
+
signal: "postsRejectedByHITL",
|
|
117
|
+
delta: -0.1,
|
|
118
|
+
reason: `${rejects} post${rejects === 1 ? "" : "s"} rejected by HITL`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const posts = inputs.postsPublished ?? 0;
|
|
122
|
+
if (posts > 0) {
|
|
123
|
+
q += 0.2;
|
|
124
|
+
contributions.push({
|
|
125
|
+
signal: "postsPublished",
|
|
126
|
+
delta: 0.2,
|
|
127
|
+
reason: `${posts} post${posts === 1 ? "" : "s"} published`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const threats = inputs.threatsBlocked ?? 0;
|
|
131
|
+
if (threats > 0) {
|
|
132
|
+
q += 0.3;
|
|
133
|
+
contributions.push({
|
|
134
|
+
signal: "threatsBlocked",
|
|
135
|
+
delta: 0.3,
|
|
136
|
+
reason: `${threats} threat${threats === 1 ? "" : "s"} blocked`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const alerts = inputs.alertsSent ?? 0;
|
|
140
|
+
if (alerts > 0) {
|
|
141
|
+
q += 0.1;
|
|
142
|
+
contributions.push({
|
|
143
|
+
signal: "alertsSent",
|
|
144
|
+
delta: 0.1,
|
|
145
|
+
reason: `${alerts} alert${alerts === 1 ? "" : "s"} sent`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const invoices = inputs.invoicesProcessed ?? 0;
|
|
149
|
+
if (invoices > 0) {
|
|
150
|
+
q += 0.2;
|
|
151
|
+
contributions.push({
|
|
152
|
+
signal: "invoicesProcessed",
|
|
153
|
+
delta: 0.2,
|
|
154
|
+
reason: `${invoices} invoice${invoices === 1 ? "" : "s"} processed`,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const lessonCount = inputs.lessons?.length ?? 0;
|
|
158
|
+
if (lessonCount > 0) {
|
|
159
|
+
q += 0.05;
|
|
160
|
+
contributions.push({
|
|
161
|
+
signal: "lessons",
|
|
162
|
+
delta: 0.05,
|
|
163
|
+
reason: `${lessonCount} lesson${lessonCount === 1 ? "" : "s"} extracted`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { score: clamp01(q), contributions };
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/quality/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alerts/digest.ts — Adaptive notification batching (SDK 1.9.0).
|
|
3
|
+
*
|
|
4
|
+
* Promoted from forge's `src/agents/23-security-auditor/agent.ts`
|
|
5
|
+
* `buildEscalationMessages()` after a 47-alert Telegram flood during the
|
|
6
|
+
* first Dependabot-enabled cycle (2026-05-17). Generalized so any agent
|
|
7
|
+
* dispatching bulk operator alerts (devops, finance, devrel, inbox-monitor,
|
|
8
|
+
* security-auditor) can adopt it without re-implementing the
|
|
9
|
+
* threshold/sort/cap logic.
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
* - Pure function — no I/O, no transport dependency. Caller dispatches the
|
|
13
|
+
* returned strings to Telegram/Slack/email/whatever channel.
|
|
14
|
+
* - 0 items → no message at all (empty array).
|
|
15
|
+
* - 1..threshold items → one full-detail message per item.
|
|
16
|
+
* - More than threshold items → one digest message, sorted by severity
|
|
17
|
+
* (configurable order), capped at `maxChars`, with truncation footer +
|
|
18
|
+
* optional details pointer.
|
|
19
|
+
* - Severities outside the configured order are appended last with a
|
|
20
|
+
* default bullet emoji.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A single alert item fed to {@link buildAlertDigest}. All fields except
|
|
27
|
+
* `severity` and `title` are optional — render skips empty lines.
|
|
28
|
+
*
|
|
29
|
+
* @public
|
|
30
|
+
*/
|
|
31
|
+
export interface AlertItem {
|
|
32
|
+
/**
|
|
33
|
+
* Severity bucket. Built-in semantics: `"critical" | "high" | "medium" |
|
|
34
|
+
* "low"`. Any other string is accepted and ranked according to
|
|
35
|
+
* {@link AlertDigestOptions.severityOrder} (or appended last with a
|
|
36
|
+
* default emoji when not listed).
|
|
37
|
+
*/
|
|
38
|
+
readonly severity:
|
|
39
|
+
| "critical"
|
|
40
|
+
| "high"
|
|
41
|
+
| "medium"
|
|
42
|
+
| "low"
|
|
43
|
+
| (string & Record<never, never>);
|
|
44
|
+
/** Human-readable headline (rendered after the emoji). */
|
|
45
|
+
readonly title: string;
|
|
46
|
+
/** Affected component / repo / file (e.g. `"vauban-org/forge:axios"`). */
|
|
47
|
+
readonly component?: string;
|
|
48
|
+
/** External reference (e.g. `"CVE-2026-0001"`). */
|
|
49
|
+
readonly ref?: string;
|
|
50
|
+
/** Remediation hint shown in individual messages. */
|
|
51
|
+
readonly remediation?: string;
|
|
52
|
+
/** Direct URL for follow-up (shown in individual messages). */
|
|
53
|
+
readonly url?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Options for {@link buildAlertDigest}. All fields optional with sensible
|
|
58
|
+
* defaults matching forge's prod-validated security-auditor behaviour.
|
|
59
|
+
*
|
|
60
|
+
* @public
|
|
61
|
+
*/
|
|
62
|
+
export interface AlertDigestOptions {
|
|
63
|
+
/**
|
|
64
|
+
* Header prefix label (e.g. `"SECURITY"`, `"DEVOPS"`). Default
|
|
65
|
+
* `"ALERT"`. Appears in the digest header — ignored in per-item mode.
|
|
66
|
+
*/
|
|
67
|
+
readonly label?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Cutoff between per-item and digest modes. `<=` threshold yields N
|
|
70
|
+
* messages, `>` threshold yields a single digest. Default `3`.
|
|
71
|
+
*/
|
|
72
|
+
readonly threshold?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Hard cap on the rendered digest length (chars). Default `4000`
|
|
75
|
+
* (Telegram-safe — Telegram limit is 4096).
|
|
76
|
+
*/
|
|
77
|
+
readonly maxChars?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Pointer to detailed source (e.g. `"query Brain category=forge_alert"`).
|
|
80
|
+
* Appended on a final line of every digest message when provided.
|
|
81
|
+
*/
|
|
82
|
+
readonly detailsPointer?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Severity sort order, highest priority first. Default
|
|
85
|
+
* `["critical", "high", "medium", "low"]`. Severities not listed go
|
|
86
|
+
* after listed ones, in input order.
|
|
87
|
+
*/
|
|
88
|
+
readonly severityOrder?: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* Per-severity emoji map. Default
|
|
91
|
+
* `{ critical: "🚨", high: "⚠️", medium: "ℹ️", low: "🔵" }`. Missing
|
|
92
|
+
* severities fall back to `"•"`.
|
|
93
|
+
*/
|
|
94
|
+
readonly emojiMap?: Readonly<Record<string, string>>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const DEFAULT_THRESHOLD = 3;
|
|
98
|
+
const DEFAULT_MAX_CHARS = 4000;
|
|
99
|
+
const DEFAULT_LABEL = "ALERT";
|
|
100
|
+
const DEFAULT_SEVERITY_ORDER: readonly string[] = [
|
|
101
|
+
"critical",
|
|
102
|
+
"high",
|
|
103
|
+
"medium",
|
|
104
|
+
"low",
|
|
105
|
+
];
|
|
106
|
+
const DEFAULT_EMOJI_MAP: Readonly<Record<string, string>> = {
|
|
107
|
+
critical: "🚨",
|
|
108
|
+
high: "⚠️",
|
|
109
|
+
medium: "ℹ️",
|
|
110
|
+
low: "🔵",
|
|
111
|
+
};
|
|
112
|
+
const FALLBACK_EMOJI = "•";
|
|
113
|
+
|
|
114
|
+
function emojiFor(
|
|
115
|
+
severity: string,
|
|
116
|
+
map: Readonly<Record<string, string>>
|
|
117
|
+
): string {
|
|
118
|
+
return map[severity] ?? FALLBACK_EMOJI;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function severityRank(severity: string, order: readonly string[]): number {
|
|
122
|
+
const idx = order.indexOf(severity);
|
|
123
|
+
return idx === -1 ? order.length : idx;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderIndividual(
|
|
127
|
+
item: AlertItem,
|
|
128
|
+
emojiMap: Readonly<Record<string, string>>
|
|
129
|
+
): string {
|
|
130
|
+
const emoji = emojiFor(item.severity, emojiMap);
|
|
131
|
+
const ref = item.ref ? ` [${item.ref}]` : "";
|
|
132
|
+
const lines: string[] = [`${emoji} ${item.title}${ref}`];
|
|
133
|
+
if (item.component) lines.push(`Component: ${item.component}`);
|
|
134
|
+
if (item.remediation) lines.push(`Remediation: ${item.remediation}`);
|
|
135
|
+
if (item.url) lines.push(item.url);
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildHeader(
|
|
140
|
+
label: string,
|
|
141
|
+
total: number,
|
|
142
|
+
counts: ReadonlyMap<string, number>,
|
|
143
|
+
order: readonly string[],
|
|
144
|
+
emojiMap: Readonly<Record<string, string>>
|
|
145
|
+
): string {
|
|
146
|
+
const breakdown: string[] = [];
|
|
147
|
+
for (const sev of order) {
|
|
148
|
+
const c = counts.get(sev) ?? 0;
|
|
149
|
+
if (c > 0) breakdown.push(`${c} ${sev}`);
|
|
150
|
+
}
|
|
151
|
+
const topSev = order.find((s) => (counts.get(s) ?? 0) > 0);
|
|
152
|
+
const headEmoji = topSev ? emojiFor(topSev, emojiMap) : FALLBACK_EMOJI;
|
|
153
|
+
const breakdownStr = breakdown.length > 0 ? ` (${breakdown.join(", ")})` : "";
|
|
154
|
+
return `${headEmoji} ${label} — ${total} items${breakdownStr}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Build an adaptive alert digest from a list of items.
|
|
159
|
+
*
|
|
160
|
+
* - 0 items → `[]` (no message at all).
|
|
161
|
+
* - 1..threshold items → one message per item (full detail preserved).
|
|
162
|
+
* - More than threshold → one digest message, severity-sorted, capped at
|
|
163
|
+
* `maxChars`, with truncation footer `"… +K more"` when items overflow,
|
|
164
|
+
* followed by `"Details: <detailsPointer>"` when a pointer is provided.
|
|
165
|
+
*
|
|
166
|
+
* Header counts only non-zero severities, in the configured order.
|
|
167
|
+
* Severities not in `severityOrder` are appended last with a default
|
|
168
|
+
* bullet emoji `"•"` (or a user-provided emoji from `emojiMap`).
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* buildAlertDigest([]) // → []
|
|
172
|
+
* buildAlertDigest([critical, high, low]) // → 3 messages
|
|
173
|
+
* buildAlertDigest(arrayOf(50, "high"), {
|
|
174
|
+
* label: "DEPENDABOT",
|
|
175
|
+
* detailsPointer: "query Brain category=forge_alert tag=security",
|
|
176
|
+
* }) // → 1 digest message
|
|
177
|
+
*
|
|
178
|
+
* @public
|
|
179
|
+
*/
|
|
180
|
+
export function buildAlertDigest(
|
|
181
|
+
items: readonly AlertItem[],
|
|
182
|
+
opts: AlertDigestOptions = {}
|
|
183
|
+
): string[] {
|
|
184
|
+
if (items.length === 0) return [];
|
|
185
|
+
|
|
186
|
+
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
187
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
188
|
+
const label = opts.label ?? DEFAULT_LABEL;
|
|
189
|
+
const severityOrder = opts.severityOrder ?? DEFAULT_SEVERITY_ORDER;
|
|
190
|
+
const emojiMap = opts.emojiMap ?? DEFAULT_EMOJI_MAP;
|
|
191
|
+
const detailsPointer = opts.detailsPointer;
|
|
192
|
+
|
|
193
|
+
if (items.length <= threshold) {
|
|
194
|
+
return items.map((it) => renderIndividual(it, emojiMap));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Sort copy: by severityOrder rank, then preserve input order (stable).
|
|
198
|
+
const sorted = items
|
|
199
|
+
.map((it, idx) => ({ it, idx }))
|
|
200
|
+
.sort((a, b) => {
|
|
201
|
+
const ra = severityRank(a.it.severity, severityOrder);
|
|
202
|
+
const rb = severityRank(b.it.severity, severityOrder);
|
|
203
|
+
if (ra !== rb) return ra - rb;
|
|
204
|
+
return a.idx - b.idx;
|
|
205
|
+
})
|
|
206
|
+
.map((x) => x.it);
|
|
207
|
+
|
|
208
|
+
const counts = new Map<string, number>();
|
|
209
|
+
for (const it of sorted) {
|
|
210
|
+
counts.set(it.severity, (counts.get(it.severity) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const header = buildHeader(
|
|
214
|
+
label,
|
|
215
|
+
items.length,
|
|
216
|
+
counts,
|
|
217
|
+
severityOrder,
|
|
218
|
+
emojiMap
|
|
219
|
+
);
|
|
220
|
+
const footerLines: string[] = [];
|
|
221
|
+
if (detailsPointer) footerLines.push(`Details: ${detailsPointer}`);
|
|
222
|
+
|
|
223
|
+
// Compute fixed cost: header + body lines + footer.
|
|
224
|
+
// We need to leave room for an eventual "… +K more" line.
|
|
225
|
+
const truncationLineEstimate = 16; // "\n… +999 more"
|
|
226
|
+
const footerStr = footerLines.length > 0 ? "\n" + footerLines.join("\n") : "";
|
|
227
|
+
|
|
228
|
+
const bodyLines: string[] = [];
|
|
229
|
+
let renderedSoFar = header.length;
|
|
230
|
+
let truncated = 0;
|
|
231
|
+
|
|
232
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
233
|
+
const it = sorted[i]!;
|
|
234
|
+
const emoji = emojiFor(it.severity, emojiMap);
|
|
235
|
+
const ref = it.ref ? ` [${it.ref}]` : "";
|
|
236
|
+
const line = `${emoji} ${it.title}${ref} — ${it.component ?? "?"}`;
|
|
237
|
+
// +1 for the leading "\n" before this line
|
|
238
|
+
const lineCost = 1 + line.length;
|
|
239
|
+
const remainingItems = sorted.length - i;
|
|
240
|
+
// If adding this line would prevent us from fitting truncation footer
|
|
241
|
+
// for the remaining items, stop here.
|
|
242
|
+
const projected =
|
|
243
|
+
renderedSoFar +
|
|
244
|
+
lineCost +
|
|
245
|
+
footerStr.length +
|
|
246
|
+
(remainingItems > 1 ? truncationLineEstimate : 0);
|
|
247
|
+
if (projected > maxChars) {
|
|
248
|
+
truncated = sorted.length - i;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
bodyLines.push(line);
|
|
252
|
+
renderedSoFar += lineCost;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const out: string[] = [header, ...bodyLines];
|
|
256
|
+
if (truncated > 0) out.push(`… +${truncated} more`);
|
|
257
|
+
if (footerLines.length > 0) out.push(...footerLines);
|
|
258
|
+
|
|
259
|
+
return [out.join("\n")];
|
|
260
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alerts/ — Adaptive notification batching (SDK 1.9.0).
|
|
3
|
+
*
|
|
4
|
+
* Promoted from forge's security-auditor `buildEscalationMessages()`
|
|
5
|
+
* after the 47-alert Telegram flood (2026-05-17). Pure, transport-agnostic
|
|
6
|
+
* helper; caller dispatches the returned strings.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { buildAlertDigest } from "./digest.js";
|
|
12
|
+
export type { AlertItem, AlertDigestOptions } from "./digest.js";
|
package/src/index.ts
CHANGED
|
@@ -571,6 +571,21 @@ export type {
|
|
|
571
571
|
// 13. Outcomes module (SDK v0.5.1 — Sprint-522)
|
|
572
572
|
export * from "./outcomes/index.js";
|
|
573
573
|
|
|
574
|
+
// 13b. Quality scoring helper (SDK 1.8.0 — promoted from forge 2026-05-17)
|
|
575
|
+
export {
|
|
576
|
+
computeQuality,
|
|
577
|
+
computeQualityWithBreakdown,
|
|
578
|
+
} from "./quality/index.js";
|
|
579
|
+
export type {
|
|
580
|
+
QualityInputs,
|
|
581
|
+
QualityBreakdown,
|
|
582
|
+
QualityContribution,
|
|
583
|
+
} from "./quality/index.js";
|
|
584
|
+
|
|
585
|
+
// 13c. Alert digest helper (SDK 1.9.0 — promoted from forge 2026-05-17)
|
|
586
|
+
export { buildAlertDigest } from "./alerts/index.js";
|
|
587
|
+
export type { AlertItem, AlertDigestOptions } from "./alerts/index.js";
|
|
588
|
+
|
|
574
589
|
// 14. Proof module (SDK v0.5.2 — Sprint-521 Bloc 1)
|
|
575
590
|
// Note: proof/index.js re-exports from @vauban-org/proof-core (optional peerDep).
|
|
576
591
|
// Import via subpath @vauban-org/agent-sdk/proof — do NOT wildcard here.
|
|
@@ -232,8 +232,9 @@ export interface OutcomeRecord {
|
|
|
232
232
|
* the BUSINESS quality of the cycle output (publishedCount, threatsBlocked,
|
|
233
233
|
* invoicesProcessed, etc.). Persisted to `agent_run.outcome_quality` by
|
|
234
234
|
* the CC backend's telemetry-ingest finish handler (SDK 1.6 / ADR-ECO-039).
|
|
235
|
-
* Use
|
|
236
|
-
* scoring across agents, or
|
|
235
|
+
* Use the SDK's `computeQuality()` helper (or `computeQualityWithBreakdown()`
|
|
236
|
+
* for debug-grade observability) for consistent scoring across agents, or
|
|
237
|
+
* override with a custom score. See `@vauban-org/agent-sdk` quality module.
|
|
237
238
|
*/
|
|
238
239
|
readonly quality?: number;
|
|
239
240
|
readonly is_pending_backfill?: boolean;
|