@chenpengfei/daily-brief 0.1.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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +28 -0
- package/config/sources.example.yaml +20 -0
- package/dist/src/adapters/fixture.js +70 -0
- package/dist/src/adapters/github-trending.js +183 -0
- package/dist/src/adapters/index.js +5 -0
- package/dist/src/adapters/rss.js +156 -0
- package/dist/src/adapters/types.js +1 -0
- package/dist/src/adapters/x.js +115 -0
- package/dist/src/agent/daily-brief-agent.js +350 -0
- package/dist/src/agent/index.js +10 -0
- package/dist/src/agent/model-runtime-config.js +221 -0
- package/dist/src/agent/model-stage-runtime.js +63 -0
- package/dist/src/agent/signal-narrative.js +247 -0
- package/dist/src/agent/signal-selection-ranking.js +276 -0
- package/dist/src/agent/source-grounding-audit.js +148 -0
- package/dist/src/agent/source-grounding-repair.js +159 -0
- package/dist/src/agent/source-item-understanding.js +206 -0
- package/dist/src/agent/stage-contracts.js +205 -0
- package/dist/src/agent/stage-runner.js +66 -0
- package/dist/src/brief/daily-brief.js +234 -0
- package/dist/src/brief/index.js +1 -0
- package/dist/src/cli.js +531 -0
- package/dist/src/collection/collect.js +67 -0
- package/dist/src/collection/index.js +1 -0
- package/dist/src/config/credential-store.js +169 -0
- package/dist/src/config/date-key.js +25 -0
- package/dist/src/config/index.js +5 -0
- package/dist/src/config/model-config.js +123 -0
- package/dist/src/config/paths.js +20 -0
- package/dist/src/config/source-registry.js +48 -0
- package/dist/src/discord/delivery.js +84 -0
- package/dist/src/discord/index.js +1 -0
- package/dist/src/domain/index.js +2 -0
- package/dist/src/domain/source-item.js +21 -0
- package/dist/src/domain/source.js +93 -0
- package/dist/src/storage/agent-run-artifact.js +44 -0
- package/dist/src/storage/brief-archive.js +17 -0
- package/dist/src/storage/index.js +3 -0
- package/dist/src/storage/source-item-store.js +63 -0
- package/dist/src/workflow/index.js +1 -0
- package/dist/src/workflow/status.js +95 -0
- package/docs/operations.md +74 -0
- package/docs/release-workflow.md +220 -0
- package/docs/user-manual.md +146 -0
- package/package.json +65 -0
- package/templates/daily-brief.md +9 -0
- package/templates/discord-notification.md +7 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { createStageModelRuntime } from "./model-stage-runtime.js";
|
|
3
|
+
import { runAgentStage } from "./stage-runner.js";
|
|
4
|
+
export async function runSourceGroundingRepairStage(input) {
|
|
5
|
+
const request = buildRepairRequest(input.brief, input.sourceItems, input.auditFindings);
|
|
6
|
+
const runtime = createStageModelRuntime({
|
|
7
|
+
config: input.modelRuntimeConfig,
|
|
8
|
+
env: input.modelRuntimeEnv ?? process.env,
|
|
9
|
+
fauxResponse: JSON.stringify(buildFauxRepairOutput(input.brief))
|
|
10
|
+
});
|
|
11
|
+
try {
|
|
12
|
+
const response = await runRepairAgent(request, runtime);
|
|
13
|
+
const sourceItemIds = input.sourceItems.map((item) => item.id);
|
|
14
|
+
const signalIds = input.brief.signals.map((signal) => signal.id);
|
|
15
|
+
const result = await runAgentStage({
|
|
16
|
+
stage: "repair",
|
|
17
|
+
artifact: input.artifact,
|
|
18
|
+
inputRefs: {
|
|
19
|
+
...(input.inputRefs ?? {}),
|
|
20
|
+
sourceItemIds,
|
|
21
|
+
signalIds,
|
|
22
|
+
repair: { stage: "audit", findings: input.auditFindings }
|
|
23
|
+
},
|
|
24
|
+
validationContext: { sourceItemIds, signalIds },
|
|
25
|
+
execute: async () => response.text
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
brief: applyRepair(input.brief, result.output),
|
|
29
|
+
repair: result.output,
|
|
30
|
+
events: response.events
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
runtime.unregister?.();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildRepairRequest(brief, sourceItems, auditFindings) {
|
|
38
|
+
return {
|
|
39
|
+
brief,
|
|
40
|
+
auditFindings,
|
|
41
|
+
citedSourceItems: sourceItems.map((item) => ({
|
|
42
|
+
id: item.id,
|
|
43
|
+
sourceId: item.sourceId,
|
|
44
|
+
platform: item.platform,
|
|
45
|
+
url: item.url,
|
|
46
|
+
title: item.title,
|
|
47
|
+
analyzableText: item.analyzableText,
|
|
48
|
+
...(item.metadata ? { metadata: item.metadata } : {})
|
|
49
|
+
}))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function runRepairAgent(request, runtime) {
|
|
53
|
+
const events = [];
|
|
54
|
+
const agent = new Agent({
|
|
55
|
+
initialState: {
|
|
56
|
+
systemPrompt: [
|
|
57
|
+
"你是 Daily Brief Agent 的 Source-grounding Repair Stage。",
|
|
58
|
+
"你只根据 Audit findings 对 brief 做最小必要修改。",
|
|
59
|
+
"不得引入新事实,不得删除 citation 来逃避问题,不得扩大叙述范围。",
|
|
60
|
+
"如果某个断言无法被 cited Source Items 支撑,必须改弱、改成集合性表述,或明确写成 Source Item 只表明的范围。",
|
|
61
|
+
"输出必须是 JSON object,不要 Markdown,不要解释。"
|
|
62
|
+
].join("\n"),
|
|
63
|
+
model: runtime.model,
|
|
64
|
+
thinkingLevel: runtime.thinkingLevel
|
|
65
|
+
},
|
|
66
|
+
sessionId: "daily-brief-source-grounding-repair",
|
|
67
|
+
...(runtime.getApiKey ? { getApiKey: runtime.getApiKey } : {})
|
|
68
|
+
});
|
|
69
|
+
agent.subscribe((event) => {
|
|
70
|
+
events.push(`repair:${event.type}`);
|
|
71
|
+
});
|
|
72
|
+
await agent.prompt([
|
|
73
|
+
"请根据 Audit findings 修复 Daily Brief narrative。",
|
|
74
|
+
"",
|
|
75
|
+
"JSON schema:",
|
|
76
|
+
"{",
|
|
77
|
+
" \"stage\": \"repair\",",
|
|
78
|
+
" \"executiveSummary\": \"可选:修复后的 Executive Summary\",",
|
|
79
|
+
" \"signalNarratives\": [",
|
|
80
|
+
" {",
|
|
81
|
+
" \"signalId\": \"signal id from input\",",
|
|
82
|
+
" \"focusAreas\": [\"Agent 架构|AI Coding\"],",
|
|
83
|
+
" \"directions\": [\"先进工具|长程任务|持续学习|自我改进|人与 Agent 的边界\"],",
|
|
84
|
+
" \"whatItIs\": \"修复后的是什么\",",
|
|
85
|
+
" \"whatItIsNot\": \"修复后的不是什么\",",
|
|
86
|
+
" \"minimalExample\": \"修复后的最小例子\",",
|
|
87
|
+
" \"whyItMatters\": \"修复后的为什么重要\"",
|
|
88
|
+
" }",
|
|
89
|
+
" ]",
|
|
90
|
+
"}",
|
|
91
|
+
"",
|
|
92
|
+
"修复要求:",
|
|
93
|
+
"- 只修改 Audit findings 指出的相关 Signal。",
|
|
94
|
+
"- 保留读者可读性和具体性。",
|
|
95
|
+
"- 一一映射是允许的,但每个映射都必须被对应 Source Item 直接支撑;不确定时改成集合性或更弱的表述。",
|
|
96
|
+
"- 修复后仍必须能通过 Source-grounding Audit。",
|
|
97
|
+
"",
|
|
98
|
+
"Input:",
|
|
99
|
+
JSON.stringify(request, null, 2)
|
|
100
|
+
].join("\n"));
|
|
101
|
+
const text = latestAssistantText(agent);
|
|
102
|
+
if (!text) {
|
|
103
|
+
throw new Error("Repair Stage did not return text");
|
|
104
|
+
}
|
|
105
|
+
return { text, events };
|
|
106
|
+
}
|
|
107
|
+
function buildFauxRepairOutput(brief) {
|
|
108
|
+
return {
|
|
109
|
+
stage: "repair",
|
|
110
|
+
executiveSummary: brief.executiveSummary,
|
|
111
|
+
signalNarratives: brief.signals.map((signal) => ({
|
|
112
|
+
signalId: signal.id,
|
|
113
|
+
focusAreas: signal.focusAreas ?? [],
|
|
114
|
+
directions: signal.directions ?? [],
|
|
115
|
+
whatItIs: weakenRiskyMapping(signal.summary.whatItIs),
|
|
116
|
+
whatItIsNot: signal.summary.whatItIsNot,
|
|
117
|
+
minimalExample: signal.summary.minimalExample,
|
|
118
|
+
whyItMatters: signal.whyItMatters
|
|
119
|
+
}))
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function weakenRiskyMapping(value) {
|
|
123
|
+
return value
|
|
124
|
+
.replace(/分别(?:指向|对应|代表|说明|表明|描述)/g, "共同表明")
|
|
125
|
+
.replace(/一一对应/g, "共同对应")
|
|
126
|
+
.replace(/respectively/gi, "collectively");
|
|
127
|
+
}
|
|
128
|
+
function applyRepair(brief, repair) {
|
|
129
|
+
const bySignalId = new Map(repair.signalNarratives.map((narrative) => [narrative.signalId, narrative]));
|
|
130
|
+
return {
|
|
131
|
+
...brief,
|
|
132
|
+
...(repair.executiveSummary ? { executiveSummary: repair.executiveSummary } : {}),
|
|
133
|
+
signals: brief.signals.map((signal) => {
|
|
134
|
+
const narrative = bySignalId.get(signal.id);
|
|
135
|
+
if (!narrative) {
|
|
136
|
+
return signal;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...signal,
|
|
140
|
+
focusAreas: narrative.focusAreas,
|
|
141
|
+
directions: narrative.directions,
|
|
142
|
+
summary: {
|
|
143
|
+
whatItIs: narrative.whatItIs,
|
|
144
|
+
whatItIsNot: narrative.whatItIsNot,
|
|
145
|
+
minimalExample: narrative.minimalExample
|
|
146
|
+
},
|
|
147
|
+
whyItMatters: narrative.whyItMatters
|
|
148
|
+
};
|
|
149
|
+
})
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function latestAssistantText(agent) {
|
|
153
|
+
const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
|
|
154
|
+
const text = assistantMessage?.content
|
|
155
|
+
.filter((block) => block.type === "text")
|
|
156
|
+
.map((block) => block.text)
|
|
157
|
+
.join("");
|
|
158
|
+
return text && text.trim().length > 0 ? text.trim() : undefined;
|
|
159
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { createStageModelRuntime } from "./model-stage-runtime.js";
|
|
3
|
+
import { runAgentStage } from "./stage-runner.js";
|
|
4
|
+
import { AgentStageValidationError } from "./stage-contracts.js";
|
|
5
|
+
export async function runSourceItemUnderstandingStage(input) {
|
|
6
|
+
if (input.sourceItems.length === 0) {
|
|
7
|
+
return { annotations: [], batchCount: 0, events: [] };
|
|
8
|
+
}
|
|
9
|
+
const batches = batchSourceItems(input.sourceItems, input.maxBatchCharacters ?? 60_000);
|
|
10
|
+
const annotations = [];
|
|
11
|
+
const events = [];
|
|
12
|
+
for (const [index, batch] of batches.entries()) {
|
|
13
|
+
const request = buildUnderstandingRequest(batch);
|
|
14
|
+
const runtime = createStageModelRuntime({
|
|
15
|
+
config: input.modelRuntimeConfig,
|
|
16
|
+
env: input.modelRuntimeEnv ?? process.env,
|
|
17
|
+
fauxResponse: JSON.stringify(buildFauxUnderstandingOutput(request))
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
const response = await runUnderstandingAgent(request, runtime);
|
|
21
|
+
events.push(...response.events.map((event) => `understanding:${index + 1}/${batches.length}:${event}`));
|
|
22
|
+
const stage = await runAgentStage({
|
|
23
|
+
stage: "understanding",
|
|
24
|
+
artifact: input.artifact ?? createInMemoryArtifact(input.modelRuntimeConfig),
|
|
25
|
+
inputRefs: {
|
|
26
|
+
...(input.inputRefs ?? {}),
|
|
27
|
+
sourceItemIds: batch.map((item) => item.id),
|
|
28
|
+
batch: { index: index + 1, total: batches.length }
|
|
29
|
+
},
|
|
30
|
+
validationContext: {
|
|
31
|
+
sourceItemIds: batch.map((item) => item.id)
|
|
32
|
+
},
|
|
33
|
+
execute: async () => response.text
|
|
34
|
+
});
|
|
35
|
+
annotations.push(...stage.output.sourceItemAnnotations);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
runtime.unregister?.();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
validateMergedAnnotations(input.sourceItems, annotations);
|
|
42
|
+
return {
|
|
43
|
+
annotations,
|
|
44
|
+
batchCount: batches.length,
|
|
45
|
+
events
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export function batchSourceItems(sourceItems, maxBatchCharacters) {
|
|
49
|
+
if (!Number.isFinite(maxBatchCharacters) || maxBatchCharacters <= 0) {
|
|
50
|
+
throw new Error("maxBatchCharacters must be a positive number");
|
|
51
|
+
}
|
|
52
|
+
const batches = [];
|
|
53
|
+
let current = [];
|
|
54
|
+
let currentSize = 0;
|
|
55
|
+
for (const item of sourceItems) {
|
|
56
|
+
const size = estimateSourceItemSize(item);
|
|
57
|
+
if (current.length > 0 && currentSize + size > maxBatchCharacters) {
|
|
58
|
+
batches.push(current);
|
|
59
|
+
current = [];
|
|
60
|
+
currentSize = 0;
|
|
61
|
+
}
|
|
62
|
+
current.push(item);
|
|
63
|
+
currentSize += size;
|
|
64
|
+
}
|
|
65
|
+
if (current.length > 0) {
|
|
66
|
+
batches.push(current);
|
|
67
|
+
}
|
|
68
|
+
return batches;
|
|
69
|
+
}
|
|
70
|
+
function buildUnderstandingRequest(sourceItems) {
|
|
71
|
+
return {
|
|
72
|
+
sourceItems: sourceItems.map((item) => ({
|
|
73
|
+
id: item.id,
|
|
74
|
+
sourceId: item.sourceId,
|
|
75
|
+
platform: item.platform,
|
|
76
|
+
url: item.url,
|
|
77
|
+
title: item.title,
|
|
78
|
+
analyzableText: item.analyzableText,
|
|
79
|
+
...(item.metadata ? { metadata: item.metadata } : {})
|
|
80
|
+
}))
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function runUnderstandingAgent(request, runtime) {
|
|
84
|
+
const events = [];
|
|
85
|
+
const agent = new Agent({
|
|
86
|
+
initialState: {
|
|
87
|
+
systemPrompt: [
|
|
88
|
+
"你是 Daily Brief Agent 的 Source Item Understanding Stage。",
|
|
89
|
+
"你只能基于输入 Source Items 生成结构化理解标注。",
|
|
90
|
+
"不要联网,不要使用工具,不要补充未引用事实。输出必须是 JSON object。"
|
|
91
|
+
].join("\n"),
|
|
92
|
+
model: runtime.model,
|
|
93
|
+
thinkingLevel: runtime.thinkingLevel
|
|
94
|
+
},
|
|
95
|
+
sessionId: "daily-brief-understanding",
|
|
96
|
+
...(runtime.getApiKey ? { getApiKey: runtime.getApiKey } : {})
|
|
97
|
+
});
|
|
98
|
+
agent.subscribe((event) => {
|
|
99
|
+
events.push(event.type);
|
|
100
|
+
});
|
|
101
|
+
await agent.prompt([
|
|
102
|
+
"请为每个 Source Item 生成一条 annotation。",
|
|
103
|
+
"",
|
|
104
|
+
"JSON schema:",
|
|
105
|
+
"{",
|
|
106
|
+
" \"stage\": \"understanding\",",
|
|
107
|
+
" \"sourceItemAnnotations\": [",
|
|
108
|
+
" {",
|
|
109
|
+
" \"sourceItemId\": \"input id\",",
|
|
110
|
+
" \"claims\": [\"只基于 Source Item 的简短 claims\"],",
|
|
111
|
+
" \"summary\": \"一句简短中文理解\",",
|
|
112
|
+
" \"focusAreaRelevance\": \"strong|partial|weak|none\",",
|
|
113
|
+
" \"evidenceBoundary\": \"说明证据边界和不能推出什么\",",
|
|
114
|
+
" \"relevance\": \"relevant|not_relevant|uncertain\",",
|
|
115
|
+
" \"evidence\": [\"支撑判断的短证据片段\"],",
|
|
116
|
+
" \"weakItemHints\": [\"如果较弱,为什么弱;否则空数组\"]",
|
|
117
|
+
" }",
|
|
118
|
+
" ]",
|
|
119
|
+
"}",
|
|
120
|
+
"",
|
|
121
|
+
"关注领域:Agent 架构、AI Coding。关注方向:先进工具、长程任务、持续学习、自我改进、人与 Agent 的边界。",
|
|
122
|
+
"Input:",
|
|
123
|
+
JSON.stringify(request, null, 2)
|
|
124
|
+
].join("\n"));
|
|
125
|
+
const text = latestAssistantText(agent);
|
|
126
|
+
if (!text) {
|
|
127
|
+
throw new Error("Understanding Stage did not return text");
|
|
128
|
+
}
|
|
129
|
+
return { text, events };
|
|
130
|
+
}
|
|
131
|
+
function buildFauxUnderstandingOutput(request) {
|
|
132
|
+
return {
|
|
133
|
+
stage: "understanding",
|
|
134
|
+
sourceItemAnnotations: request.sourceItems.map((item) => {
|
|
135
|
+
const relevance = classifyRelevance(item);
|
|
136
|
+
const evidence = firstEvidence(item.analyzableText);
|
|
137
|
+
return {
|
|
138
|
+
sourceItemId: item.id,
|
|
139
|
+
claims: [evidence],
|
|
140
|
+
summary: `当前 Source Item 表明:${evidence}`,
|
|
141
|
+
focusAreaRelevance: relevance === "relevant" ? "strong" : "none",
|
|
142
|
+
evidenceBoundary: "仅能说明该 Source Item 自身提供的内容,不能外推项目成熟度或外部采用情况。",
|
|
143
|
+
relevance,
|
|
144
|
+
evidence: [evidence],
|
|
145
|
+
weakItemHints: relevance === "relevant" ? [] : ["未明显命中 Agent 架构或 AI Coding 关注领域。"]
|
|
146
|
+
};
|
|
147
|
+
})
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function validateMergedAnnotations(sourceItems, annotations) {
|
|
151
|
+
const expected = new Set(sourceItems.map((item) => item.id));
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const issues = [];
|
|
154
|
+
for (const annotation of annotations) {
|
|
155
|
+
if (!expected.has(annotation.sourceItemId)) {
|
|
156
|
+
issues.push(`sourceItemId references unknown id: ${annotation.sourceItemId}`);
|
|
157
|
+
}
|
|
158
|
+
if (seen.has(annotation.sourceItemId)) {
|
|
159
|
+
issues.push(`duplicate annotation for sourceItemId: ${annotation.sourceItemId}`);
|
|
160
|
+
}
|
|
161
|
+
seen.add(annotation.sourceItemId);
|
|
162
|
+
}
|
|
163
|
+
for (const sourceItem of sourceItems) {
|
|
164
|
+
if (!seen.has(sourceItem.id)) {
|
|
165
|
+
issues.push(`missing annotation for sourceItemId: ${sourceItem.id}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (issues.length > 0) {
|
|
169
|
+
throw new AgentStageValidationError("understanding", issues);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function createInMemoryArtifact(modelRuntimeConfig) {
|
|
173
|
+
return {
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
runId: "in-memory-understanding",
|
|
176
|
+
date: new Date().toISOString().slice(0, 10),
|
|
177
|
+
startedAt: new Date().toISOString(),
|
|
178
|
+
model: {
|
|
179
|
+
provider: modelRuntimeConfig.provider,
|
|
180
|
+
model: modelRuntimeConfig.model,
|
|
181
|
+
...(modelRuntimeConfig.credentialRef ? { credentialRef: modelRuntimeConfig.credentialRef } : {})
|
|
182
|
+
},
|
|
183
|
+
inputRefs: {},
|
|
184
|
+
stages: []
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function latestAssistantText(agent) {
|
|
188
|
+
const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
|
|
189
|
+
const text = assistantMessage?.content
|
|
190
|
+
.filter((block) => block.type === "text")
|
|
191
|
+
.map((block) => block.text)
|
|
192
|
+
.join("");
|
|
193
|
+
return text && text.trim().length > 0 ? text.trim() : undefined;
|
|
194
|
+
}
|
|
195
|
+
function estimateSourceItemSize(item) {
|
|
196
|
+
return item.id.length + item.title.length + item.url.length + item.analyzableText.length;
|
|
197
|
+
}
|
|
198
|
+
function classifyRelevance(item) {
|
|
199
|
+
const text = `${item.title} ${item.analyzableText}`.toLowerCase();
|
|
200
|
+
const terms = ["agent architecture", "agent runtime", "coding agent", "ai coding", "tool execution", "memory", "workflow"];
|
|
201
|
+
return terms.some((term) => text.includes(term)) ? "relevant" : "not_relevant";
|
|
202
|
+
}
|
|
203
|
+
function firstEvidence(text) {
|
|
204
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
205
|
+
return normalized.length > 140 ? `${normalized.slice(0, 137)}...` : normalized || "Source Item 没有可分析文本。";
|
|
206
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export const AGENT_STAGE_NAMES = ["understanding", "selection", "ranking", "narrative", "audit", "repair"];
|
|
2
|
+
export class AgentStageValidationError extends Error {
|
|
3
|
+
stage;
|
|
4
|
+
issues;
|
|
5
|
+
constructor(stage, issues) {
|
|
6
|
+
super(`Invalid ${stage} stage output:\n${issues.map((issue) => `- ${issue}`).join("\n")}`);
|
|
7
|
+
this.name = "AgentStageValidationError";
|
|
8
|
+
this.stage = stage;
|
|
9
|
+
this.issues = issues;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function parseAgentStageOutput(stage, value, context = {}) {
|
|
13
|
+
let parsed;
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(value);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
throw new AgentStageValidationError(stage, [`output must be valid JSON: ${message}`]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
parsed = value;
|
|
25
|
+
}
|
|
26
|
+
return validateAgentStageOutput(stage, parsed, context);
|
|
27
|
+
}
|
|
28
|
+
export function validateAgentStageOutput(stage, value, context = {}) {
|
|
29
|
+
const issues = [];
|
|
30
|
+
if (!isRecord(value)) {
|
|
31
|
+
throw new AgentStageValidationError(stage, ["output must be an object"]);
|
|
32
|
+
}
|
|
33
|
+
if (value.stage !== stage) {
|
|
34
|
+
issues.push(`stage must be ${stage}`);
|
|
35
|
+
}
|
|
36
|
+
if (stage === "understanding") {
|
|
37
|
+
validateUnderstanding(value, context, issues);
|
|
38
|
+
}
|
|
39
|
+
else if (stage === "selection") {
|
|
40
|
+
validateSelection(value, context, issues);
|
|
41
|
+
}
|
|
42
|
+
else if (stage === "ranking") {
|
|
43
|
+
validateRanking(value, context, issues);
|
|
44
|
+
}
|
|
45
|
+
else if (stage === "narrative" || stage === "repair") {
|
|
46
|
+
validateNarrative(value, context, issues);
|
|
47
|
+
}
|
|
48
|
+
else if (stage === "audit") {
|
|
49
|
+
validateAudit(value, context, issues);
|
|
50
|
+
}
|
|
51
|
+
if (issues.length > 0) {
|
|
52
|
+
throw new AgentStageValidationError(stage, issues);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function validateUnderstanding(value, context, issues) {
|
|
57
|
+
const annotations = readArray(value.sourceItemAnnotations, "sourceItemAnnotations", issues);
|
|
58
|
+
for (const [index, annotation] of annotations.entries()) {
|
|
59
|
+
if (!isRecord(annotation)) {
|
|
60
|
+
issues.push(`sourceItemAnnotations[${index}] must be an object`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const sourceItemId = readString(annotation.sourceItemId, `sourceItemAnnotations[${index}].sourceItemId`, issues);
|
|
64
|
+
validateStringArray(annotation.claims, `sourceItemAnnotations[${index}].claims`, issues);
|
|
65
|
+
readString(annotation.summary, `sourceItemAnnotations[${index}].summary`, issues);
|
|
66
|
+
readString(annotation.evidenceBoundary, `sourceItemAnnotations[${index}].evidenceBoundary`, issues);
|
|
67
|
+
if (!["relevant", "not_relevant", "uncertain"].includes(String(annotation.relevance))) {
|
|
68
|
+
issues.push(`sourceItemAnnotations[${index}].relevance must be relevant, not_relevant, or uncertain`);
|
|
69
|
+
}
|
|
70
|
+
if (!["strong", "partial", "weak", "none"].includes(String(annotation.focusAreaRelevance))) {
|
|
71
|
+
issues.push(`sourceItemAnnotations[${index}].focusAreaRelevance must be strong, partial, weak, or none`);
|
|
72
|
+
}
|
|
73
|
+
validateStringArray(annotation.evidence, `sourceItemAnnotations[${index}].evidence`, issues);
|
|
74
|
+
validateStringArray(annotation.weakItemHints, `sourceItemAnnotations[${index}].weakItemHints`, issues);
|
|
75
|
+
validateKnownRef("sourceItemId", sourceItemId, context.sourceItemIds, issues);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function validateSelection(value, context, issues) {
|
|
79
|
+
const signals = readArray(value.candidateSignals, "candidateSignals", issues);
|
|
80
|
+
for (const [index, signal] of signals.entries()) {
|
|
81
|
+
if (!isRecord(signal)) {
|
|
82
|
+
issues.push(`candidateSignals[${index}] must be an object`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
readString(signal.signalId, `candidateSignals[${index}].signalId`, issues);
|
|
86
|
+
readString(signal.title, `candidateSignals[${index}].title`, issues);
|
|
87
|
+
readString(signal.reason, `candidateSignals[${index}].reason`, issues);
|
|
88
|
+
if (signal.strength !== "strong" && signal.strength !== "weak") {
|
|
89
|
+
issues.push(`candidateSignals[${index}].strength must be strong or weak`);
|
|
90
|
+
}
|
|
91
|
+
if (signal.signalType !== undefined &&
|
|
92
|
+
!["architecture", "ai-coding", "tool-repo", "risk"].includes(String(signal.signalType))) {
|
|
93
|
+
issues.push(`candidateSignals[${index}].signalType must be architecture, ai-coding, tool-repo, or risk`);
|
|
94
|
+
}
|
|
95
|
+
for (const sourceItemId of validateStringArray(signal.sourceItemIds, `candidateSignals[${index}].sourceItemIds`, issues)) {
|
|
96
|
+
validateKnownRef("sourceItemId", sourceItemId, context.sourceItemIds, issues);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (value.excludedSourceItems !== undefined) {
|
|
100
|
+
const excluded = readArray(value.excludedSourceItems, "excludedSourceItems", issues);
|
|
101
|
+
for (const [index, exclusion] of excluded.entries()) {
|
|
102
|
+
if (!isRecord(exclusion)) {
|
|
103
|
+
issues.push(`excludedSourceItems[${index}] must be an object`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const sourceItemId = readString(exclusion.sourceItemId, `excludedSourceItems[${index}].sourceItemId`, issues);
|
|
107
|
+
readString(exclusion.reason, `excludedSourceItems[${index}].reason`, issues);
|
|
108
|
+
validateKnownRef("sourceItemId", sourceItemId, context.sourceItemIds, issues);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function validateRanking(value, context, issues) {
|
|
113
|
+
const signals = readArray(value.rankedSignals, "rankedSignals", issues);
|
|
114
|
+
for (const [index, signal] of signals.entries()) {
|
|
115
|
+
if (!isRecord(signal)) {
|
|
116
|
+
issues.push(`rankedSignals[${index}] must be an object`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const signalId = readString(signal.signalId, `rankedSignals[${index}].signalId`, issues);
|
|
120
|
+
readString(signal.reason, `rankedSignals[${index}].reason`, issues);
|
|
121
|
+
if (!Number.isInteger(signal.rank) || Number(signal.rank) < 1) {
|
|
122
|
+
issues.push(`rankedSignals[${index}].rank must be a positive integer`);
|
|
123
|
+
}
|
|
124
|
+
validateKnownRef("signalId", signalId, context.signalIds, issues);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function validateNarrative(value, context, issues) {
|
|
128
|
+
if (value.executiveSummary !== undefined) {
|
|
129
|
+
readString(value.executiveSummary, "executiveSummary", issues);
|
|
130
|
+
}
|
|
131
|
+
const narratives = readArray(value.signalNarratives, "signalNarratives", issues);
|
|
132
|
+
for (const [index, narrative] of narratives.entries()) {
|
|
133
|
+
if (!isRecord(narrative)) {
|
|
134
|
+
issues.push(`signalNarratives[${index}] must be an object`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const signalId = readString(narrative.signalId, `signalNarratives[${index}].signalId`, issues);
|
|
138
|
+
validateStringArray(narrative.focusAreas, `signalNarratives[${index}].focusAreas`, issues);
|
|
139
|
+
validateStringArray(narrative.directions, `signalNarratives[${index}].directions`, issues);
|
|
140
|
+
readString(narrative.whatItIs, `signalNarratives[${index}].whatItIs`, issues);
|
|
141
|
+
readString(narrative.whatItIsNot, `signalNarratives[${index}].whatItIsNot`, issues);
|
|
142
|
+
readString(narrative.minimalExample, `signalNarratives[${index}].minimalExample`, issues);
|
|
143
|
+
readString(narrative.whyItMatters, `signalNarratives[${index}].whyItMatters`, issues);
|
|
144
|
+
validateKnownRef("signalId", signalId, context.signalIds, issues);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function validateAudit(value, context, issues) {
|
|
148
|
+
if (value.status !== "passed" && value.status !== "failed") {
|
|
149
|
+
issues.push("status must be passed or failed");
|
|
150
|
+
}
|
|
151
|
+
const findings = readArray(value.findings, "findings", issues);
|
|
152
|
+
for (const [index, finding] of findings.entries()) {
|
|
153
|
+
if (!isRecord(finding)) {
|
|
154
|
+
issues.push(`findings[${index}] must be an object`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (finding.signalId !== undefined) {
|
|
158
|
+
const signalId = readString(finding.signalId, `findings[${index}].signalId`, issues);
|
|
159
|
+
validateKnownRef("signalId", signalId, context.signalIds, issues);
|
|
160
|
+
}
|
|
161
|
+
if (finding.sourceItemId !== undefined) {
|
|
162
|
+
const sourceItemId = readString(finding.sourceItemId, `findings[${index}].sourceItemId`, issues);
|
|
163
|
+
validateKnownRef("sourceItemId", sourceItemId, context.sourceItemIds, issues);
|
|
164
|
+
}
|
|
165
|
+
readString(finding.issue, `findings[${index}].issue`, issues);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function readArray(value, field, issues) {
|
|
169
|
+
if (!Array.isArray(value)) {
|
|
170
|
+
issues.push(`${field} must be an array`);
|
|
171
|
+
return [];
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function validateStringArray(value, field, issues) {
|
|
176
|
+
const entries = readArray(value, field, issues);
|
|
177
|
+
const strings = [];
|
|
178
|
+
for (const [index, entry] of entries.entries()) {
|
|
179
|
+
if (typeof entry !== "string" || entry.trim().length === 0) {
|
|
180
|
+
issues.push(`${field}[${index}] must be a non-empty string`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
strings.push(entry.trim());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return strings;
|
|
187
|
+
}
|
|
188
|
+
function readString(value, field, issues) {
|
|
189
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
190
|
+
issues.push(`${field} must be a non-empty string`);
|
|
191
|
+
return "";
|
|
192
|
+
}
|
|
193
|
+
return value.trim();
|
|
194
|
+
}
|
|
195
|
+
function validateKnownRef(kind, value, known, issues) {
|
|
196
|
+
if (!value || !known) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (!known.includes(value)) {
|
|
200
|
+
issues.push(`${kind} references unknown id: ${value}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function isRecord(value) {
|
|
204
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
205
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AgentStageValidationError, parseAgentStageOutput } from "./stage-contracts.js";
|
|
2
|
+
import { writeAgentRunArtifact } from "../storage/agent-run-artifact.js";
|
|
3
|
+
export async function runAgentStage(input) {
|
|
4
|
+
const startedAt = new Date();
|
|
5
|
+
try {
|
|
6
|
+
const rawOutput = await input.execute();
|
|
7
|
+
const output = parseAgentStageOutput(input.stage, rawOutput, input.validationContext);
|
|
8
|
+
const record = {
|
|
9
|
+
stage: input.stage,
|
|
10
|
+
status: "succeeded",
|
|
11
|
+
startedAt: startedAt.toISOString(),
|
|
12
|
+
completedAt: new Date().toISOString(),
|
|
13
|
+
inputRefs: input.inputRefs ?? {},
|
|
14
|
+
output,
|
|
15
|
+
validation: {
|
|
16
|
+
status: "passed",
|
|
17
|
+
issues: []
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
input.artifact.stages.push(record);
|
|
21
|
+
return {
|
|
22
|
+
output,
|
|
23
|
+
record,
|
|
24
|
+
...(await maybeWriteArtifact(input))
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const failure = toAgentRunFailure(error);
|
|
29
|
+
const record = {
|
|
30
|
+
stage: input.stage,
|
|
31
|
+
status: "failed",
|
|
32
|
+
startedAt: startedAt.toISOString(),
|
|
33
|
+
completedAt: new Date().toISOString(),
|
|
34
|
+
inputRefs: input.inputRefs ?? {},
|
|
35
|
+
validation: {
|
|
36
|
+
status: failure.kind === "validation" ? "failed" : "passed",
|
|
37
|
+
issues: failure.issues ?? []
|
|
38
|
+
},
|
|
39
|
+
failure
|
|
40
|
+
};
|
|
41
|
+
input.artifact.stages.push(record);
|
|
42
|
+
input.artifact.failure = failure;
|
|
43
|
+
await maybeWriteArtifact(input);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function maybeWriteArtifact(input) {
|
|
48
|
+
if (!input.date || !input.artifactRoot) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
const written = await writeAgentRunArtifact(input.artifact, input.date, input.artifactRoot);
|
|
52
|
+
return { artifactPath: written.path };
|
|
53
|
+
}
|
|
54
|
+
function toAgentRunFailure(error) {
|
|
55
|
+
if (error instanceof AgentStageValidationError) {
|
|
56
|
+
return {
|
|
57
|
+
kind: "validation",
|
|
58
|
+
message: error.message,
|
|
59
|
+
issues: error.issues
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
kind: "execution",
|
|
64
|
+
message: error instanceof Error ? error.message : String(error)
|
|
65
|
+
};
|
|
66
|
+
}
|