@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.md +28 -0
  4. package/config/sources.example.yaml +20 -0
  5. package/dist/src/adapters/fixture.js +70 -0
  6. package/dist/src/adapters/github-trending.js +183 -0
  7. package/dist/src/adapters/index.js +5 -0
  8. package/dist/src/adapters/rss.js +156 -0
  9. package/dist/src/adapters/types.js +1 -0
  10. package/dist/src/adapters/x.js +115 -0
  11. package/dist/src/agent/daily-brief-agent.js +350 -0
  12. package/dist/src/agent/index.js +10 -0
  13. package/dist/src/agent/model-runtime-config.js +221 -0
  14. package/dist/src/agent/model-stage-runtime.js +63 -0
  15. package/dist/src/agent/signal-narrative.js +247 -0
  16. package/dist/src/agent/signal-selection-ranking.js +276 -0
  17. package/dist/src/agent/source-grounding-audit.js +148 -0
  18. package/dist/src/agent/source-grounding-repair.js +159 -0
  19. package/dist/src/agent/source-item-understanding.js +206 -0
  20. package/dist/src/agent/stage-contracts.js +205 -0
  21. package/dist/src/agent/stage-runner.js +66 -0
  22. package/dist/src/brief/daily-brief.js +234 -0
  23. package/dist/src/brief/index.js +1 -0
  24. package/dist/src/cli.js +531 -0
  25. package/dist/src/collection/collect.js +67 -0
  26. package/dist/src/collection/index.js +1 -0
  27. package/dist/src/config/credential-store.js +169 -0
  28. package/dist/src/config/date-key.js +25 -0
  29. package/dist/src/config/index.js +5 -0
  30. package/dist/src/config/model-config.js +123 -0
  31. package/dist/src/config/paths.js +20 -0
  32. package/dist/src/config/source-registry.js +48 -0
  33. package/dist/src/discord/delivery.js +84 -0
  34. package/dist/src/discord/index.js +1 -0
  35. package/dist/src/domain/index.js +2 -0
  36. package/dist/src/domain/source-item.js +21 -0
  37. package/dist/src/domain/source.js +93 -0
  38. package/dist/src/storage/agent-run-artifact.js +44 -0
  39. package/dist/src/storage/brief-archive.js +17 -0
  40. package/dist/src/storage/index.js +3 -0
  41. package/dist/src/storage/source-item-store.js +63 -0
  42. package/dist/src/workflow/index.js +1 -0
  43. package/dist/src/workflow/status.js +95 -0
  44. package/docs/operations.md +74 -0
  45. package/docs/release-workflow.md +220 -0
  46. package/docs/user-manual.md +146 -0
  47. package/package.json +65 -0
  48. package/templates/daily-brief.md +9 -0
  49. package/templates/discord-notification.md +7 -0
@@ -0,0 +1,247 @@
1
+ import { Agent } from "@earendil-works/pi-agent-core";
2
+ import { createStageModelRuntime } from "./model-stage-runtime.js";
3
+ export async function enrichDailyBriefNarrativeWithAgent(input) {
4
+ const request = buildNarrativeRequest(input.brief, input.sourceItems);
5
+ const runtime = createStageModelRuntime({
6
+ config: input.modelRuntimeConfig,
7
+ env: input.modelRuntimeEnv ?? process.env,
8
+ fauxResponse: JSON.stringify(buildFauxNarrativeResponse(request))
9
+ });
10
+ try {
11
+ const response = await runNarrativeAgent(request, runtime);
12
+ const narrative = parseNarrativeResponse(response.text);
13
+ return {
14
+ brief: applyNarratives(input.brief, narrative),
15
+ events: response.events
16
+ };
17
+ }
18
+ finally {
19
+ runtime.unregister?.();
20
+ }
21
+ }
22
+ function buildNarrativeRequest(brief, sourceItems) {
23
+ const itemsById = new Map(sourceItems.map((item) => [item.id, item]));
24
+ return {
25
+ signals: brief.signals.map((signal) => ({
26
+ id: signal.id,
27
+ type: signal.type,
28
+ title: signal.title,
29
+ currentFallback: {
30
+ summary: signal.summary,
31
+ whyItMatters: signal.whyItMatters
32
+ },
33
+ citedSourceItems: signal.citations.flatMap((citation) => {
34
+ const item = itemsById.get(citation.sourceItemId);
35
+ if (!item) {
36
+ return [];
37
+ }
38
+ return [
39
+ {
40
+ id: item.id,
41
+ sourceId: item.sourceId,
42
+ platform: item.platform,
43
+ url: item.url,
44
+ title: item.title,
45
+ ...(item.author ? { author: item.author } : {}),
46
+ ...(item.publishedAt ? { publishedAt: item.publishedAt } : {}),
47
+ analyzableText: item.analyzableText,
48
+ ...(item.metadata ? { metadata: item.metadata } : {})
49
+ }
50
+ ];
51
+ })
52
+ }))
53
+ };
54
+ }
55
+ async function runNarrativeAgent(request, runtime) {
56
+ const events = [];
57
+ const agent = new Agent({
58
+ initialState: {
59
+ systemPrompt: [
60
+ "你是 Daily Brief Agent 的 Signal narrative 子任务。",
61
+ "你必须只基于用户提供的 cited Source Items 理解内容,生成读者可用的中文说明。",
62
+ "不要做开放研究,不要补充未引用事实,不要把 GitHub trending 当作质量背书。",
63
+ "保留项目名、库名、关键英文技术词。输出必须是 JSON object,不要 Markdown,不要解释。"
64
+ ].join("\n"),
65
+ model: runtime.model,
66
+ thinkingLevel: runtime.thinkingLevel
67
+ },
68
+ sessionId: "daily-brief-signal-narrative",
69
+ ...(runtime.getApiKey ? { getApiKey: runtime.getApiKey } : {})
70
+ });
71
+ agent.subscribe((event) => {
72
+ events.push(`signal_narrative:${event.type}`);
73
+ });
74
+ await agent.prompt([
75
+ "请为每个 Signal 生成 narrative 字段。",
76
+ "",
77
+ "JSON schema:",
78
+ "{",
79
+ " \"executiveSummary\": \"一句简洁中文 Executive Summary\",",
80
+ " \"signalNarratives\": [",
81
+ " {",
82
+ " \"signalId\": \"signal id from input\",",
83
+ " \"focusAreas\": [\"Agent 架构|AI Coding\"],",
84
+ " \"directions\": [\"先进工具|长程任务|持续学习|自我改进|人与 Agent 的边界\"],",
85
+ " \"whatItIs\": \"是什么:一句中文,说明它到底是什么\",",
86
+ " \"whatItIsNot\": \"不是什么:一句中文,澄清容易误解的边界\",",
87
+ " \"minimalExample\": \"最小例子:一句中文,用最小具体场景帮助理解\",",
88
+ " \"whyItMatters\": \"为什么重要:一句中文,具体说明它对 Agent Architecture 或 AI Coding 的意义\"",
89
+ " }",
90
+ " ]",
91
+ "}",
92
+ "",
93
+ "质量要求:",
94
+ "- 不要复述模板句。",
95
+ "- whyItMatters 必须具体到这个 Signal,不能只说“可能改变工具链”。",
96
+ "- whatItIsNot 不是否定项目价值,而是防止误读。",
97
+ "- minimalExample 要足够小,小到今天读者可以立刻想象怎么验证。",
98
+ "- 如果 Source Item 信息不足,明确写“当前 Source Item 只表明...”。",
99
+ "- 不要写“从 X 扩展到 Y”“正在走向...”这类演进判断,除非 cited Source Items 同时支撑 X 和 Y。",
100
+ "- 不要把 IDE 补全、长期维护、代码审查、harness 运行时调度等常见 AI Coding 背景知识写进 narrative,除非 cited Source Items 明确提到。",
101
+ "- minimalExample 只能使用 cited Source Items 明确描述的能力;不要发明 harness 何时调用 skill 之类的执行机制。",
102
+ "- whyItMatters 可以说明它对设计者提出了什么检查点,但不能声称它已经支撑未被 cited Source Items 直接提到的 workflow。",
103
+ "",
104
+ "Input:",
105
+ JSON.stringify(request, null, 2)
106
+ ].join("\n"));
107
+ const text = latestAssistantText(agent);
108
+ if (!text) {
109
+ throw new Error("Signal narrative Agent did not return text");
110
+ }
111
+ return { text, events };
112
+ }
113
+ function latestAssistantText(agent) {
114
+ const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
115
+ const text = assistantMessage?.content
116
+ .filter((block) => block.type === "text")
117
+ .map((block) => block.text)
118
+ .join("");
119
+ return text && text.trim().length > 0 ? text.trim() : undefined;
120
+ }
121
+ function parseNarrativeResponse(text) {
122
+ const jsonText = extractJsonObject(text);
123
+ const parsed = JSON.parse(jsonText);
124
+ if (!isRecord(parsed)) {
125
+ throw new Error("Signal narrative Agent returned non-object JSON");
126
+ }
127
+ return {
128
+ executiveSummary: readRequiredString(parsed, "executiveSummary"),
129
+ signalNarratives: readSignalNarrativeArray(parsed.signalNarratives)
130
+ };
131
+ }
132
+ function extractJsonObject(text) {
133
+ const withoutFence = text
134
+ .replace(/^```(?:json)?\s*/i, "")
135
+ .replace(/\s*```$/i, "")
136
+ .trim();
137
+ const start = withoutFence.indexOf("{");
138
+ const end = withoutFence.lastIndexOf("}");
139
+ if (start < 0 || end < start) {
140
+ throw new Error("Signal narrative Agent returned invalid JSON");
141
+ }
142
+ return withoutFence.slice(start, end + 1);
143
+ }
144
+ function readSignalNarrativeArray(value) {
145
+ if (!Array.isArray(value)) {
146
+ throw new Error("Signal narrative Agent requires signalNarratives array");
147
+ }
148
+ return value.map(parseSignalNarrative);
149
+ }
150
+ function parseSignalNarrative(value) {
151
+ if (!isRecord(value)) {
152
+ throw new Error("Signal narrative item must be an object");
153
+ }
154
+ return {
155
+ signalId: readRequiredString(value, "signalId"),
156
+ focusAreas: readRequiredStringArray(value, "focusAreas"),
157
+ directions: readRequiredStringArray(value, "directions"),
158
+ whatItIs: readRequiredString(value, "whatItIs"),
159
+ whatItIsNot: readRequiredString(value, "whatItIsNot"),
160
+ minimalExample: readRequiredString(value, "minimalExample"),
161
+ whyItMatters: readRequiredString(value, "whyItMatters")
162
+ };
163
+ }
164
+ function applyNarratives(brief, narrativeResponse) {
165
+ const bySignalId = new Map(narrativeResponse.signalNarratives.map((narrative) => [narrative.signalId, narrative]));
166
+ return {
167
+ ...brief,
168
+ executiveSummary: narrativeResponse.executiveSummary,
169
+ signals: brief.signals.map((signal) => {
170
+ const narrative = bySignalId.get(signal.id);
171
+ if (!narrative) {
172
+ throw new Error(`Signal narrative Agent omitted ${signal.id}`);
173
+ }
174
+ return {
175
+ ...signal,
176
+ focusAreas: narrative.focusAreas,
177
+ directions: narrative.directions,
178
+ summary: {
179
+ whatItIs: narrative.whatItIs,
180
+ whatItIsNot: narrative.whatItIsNot,
181
+ minimalExample: narrative.minimalExample
182
+ },
183
+ whyItMatters: narrative.whyItMatters
184
+ };
185
+ })
186
+ };
187
+ }
188
+ function buildFauxNarrativeResponse(request) {
189
+ return {
190
+ executiveSummary: request.signals.length === 0
191
+ ? "今天是 low-signal day:Agent Stages 没有选出足够强、可由 Source Items 支撑的 Signals。"
192
+ : `今天有 ${request.signals.length} 个 Agent-generated Signals,重点围绕 Agent 架构与 AI Coding 的可回溯变化。`,
193
+ signalNarratives: request.signals.map((signal) => {
194
+ const sourceText = signal.citedSourceItems[0]?.analyzableText ?? signal.title;
195
+ return {
196
+ signalId: signal.id,
197
+ focusAreas: inferFocusAreas(signal),
198
+ directions: inferDirections(signal),
199
+ whatItIs: `当前 Source Item 表明:${sourceText}`,
200
+ whatItIsNot: `它不是对 ${signal.title} 的成熟度背书;只是本次来源中可回溯的一条 ${signal.type} Signal。`,
201
+ minimalExample: `最小例子:围绕 ${signal.title} 选一个 README 或小任务,验证它描述的能力是否成立。`,
202
+ whyItMatters: `它值得关注,因为 ${signal.title} 暴露了一个可检查的 Agent Architecture 或 AI Coding 实践切面。`
203
+ };
204
+ })
205
+ };
206
+ }
207
+ function readRequiredString(source, key) {
208
+ const value = source[key];
209
+ if (typeof value !== "string" || value.trim().length === 0) {
210
+ throw new Error(`Signal narrative item requires ${key}`);
211
+ }
212
+ return value.trim();
213
+ }
214
+ function readRequiredStringArray(source, key) {
215
+ const value = source[key];
216
+ if (!Array.isArray(value) || value.length === 0 || value.some((entry) => typeof entry !== "string" || entry.trim().length === 0)) {
217
+ throw new Error(`Signal narrative item requires ${key}`);
218
+ }
219
+ return value.map((entry) => String(entry).trim());
220
+ }
221
+ function inferFocusAreas(signal) {
222
+ if (signal.type === "ai-coding") {
223
+ return ["AI Coding"];
224
+ }
225
+ if (signal.type === "architecture") {
226
+ return ["Agent 架构"];
227
+ }
228
+ return ["Agent 架构", "AI Coding"];
229
+ }
230
+ function inferDirections(signal) {
231
+ const text = `${signal.title} ${signal.citedSourceItems.map((item) => item.analyzableText).join(" ")}`.toLowerCase();
232
+ const directions = [];
233
+ if (signal.type === "tool-repo" || text.includes("tool"))
234
+ directions.push("先进工具");
235
+ if (text.includes("long") || text.includes("workflow"))
236
+ directions.push("长程任务");
237
+ if (text.includes("learning"))
238
+ directions.push("持续学习");
239
+ if (text.includes("improve") || text.includes("optimization"))
240
+ directions.push("自我改进");
241
+ if (text.includes("human") || text.includes("boundary"))
242
+ directions.push("人与 Agent 的边界");
243
+ return directions.length > 0 ? directions : ["先进工具"];
244
+ }
245
+ function isRecord(value) {
246
+ return typeof value === "object" && value !== null && !Array.isArray(value);
247
+ }
@@ -0,0 +1,276 @@
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 runSignalSelectionAndRankingStages(input) {
5
+ const modelRuntimeConfig = input.modelRuntimeConfig ?? {
6
+ provider: "faux",
7
+ model: "faux-daily-brief-renderer",
8
+ ready: true,
9
+ issues: []
10
+ };
11
+ const modelRuntimeEnv = input.modelRuntimeEnv ?? process.env;
12
+ const selectionRequest = buildSelectionRequest(input.sourceItems, input.annotations);
13
+ const selectionRuntime = createStageModelRuntime({
14
+ config: modelRuntimeConfig,
15
+ env: modelRuntimeEnv,
16
+ fauxResponse: JSON.stringify(buildFauxSelectionOutput(selectionRequest))
17
+ });
18
+ const selectionResponse = await runSelectionAgent(selectionRequest, selectionRuntime);
19
+ const selectionStage = await runAgentStage({
20
+ stage: "selection",
21
+ artifact: input.artifact,
22
+ inputRefs: { ...(input.inputRefs ?? {}), sourceItemIds: input.sourceItems.map((item) => item.id) },
23
+ validationContext: { sourceItemIds: input.sourceItems.map((item) => item.id) },
24
+ execute: async () => selectionResponse.text
25
+ });
26
+ const merged = mergeCandidateSignals(selectionStage.output.candidateSignals);
27
+ const rankingRequest = { maxSignals: input.maxSignals ?? 5, candidateSignals: merged };
28
+ const rankingRuntime = createStageModelRuntime({
29
+ config: modelRuntimeConfig,
30
+ env: modelRuntimeEnv,
31
+ fauxResponse: JSON.stringify(buildFauxRankingOutput(rankingRequest))
32
+ });
33
+ const rankingResponse = await runRankingAgent(rankingRequest, rankingRuntime);
34
+ const rankingStage = await runAgentStage({
35
+ stage: "ranking",
36
+ artifact: input.artifact,
37
+ inputRefs: {
38
+ ...(input.inputRefs ?? {}),
39
+ sourceItemIds: unique(merged.flatMap((signal) => signal.sourceItemIds)),
40
+ signalIds: merged.map((signal) => signal.signalId)
41
+ },
42
+ validationContext: { signalIds: merged.map((signal) => signal.signalId) },
43
+ execute: async () => rankingResponse.text
44
+ });
45
+ return {
46
+ signals: buildSignalsFromRanking(input.sourceItems, merged, rankingStage.output),
47
+ selection: selectionStage.output,
48
+ ranking: rankingStage.output,
49
+ events: [...selectionResponse.events, ...rankingResponse.events]
50
+ };
51
+ }
52
+ function buildSelectionRequest(sourceItems, annotations) {
53
+ return {
54
+ sourceItems: sourceItems.map((item) => ({
55
+ id: item.id,
56
+ sourceId: item.sourceId,
57
+ platform: item.platform,
58
+ url: item.url,
59
+ title: item.title,
60
+ analyzableText: item.analyzableText
61
+ })),
62
+ annotations
63
+ };
64
+ }
65
+ async function runSelectionAgent(request, runtime) {
66
+ const events = [];
67
+ const agent = new Agent({
68
+ initialState: {
69
+ systemPrompt: [
70
+ "你是 Daily Brief Agent 的 Signal Selection Stage。",
71
+ "你必须基于 Understanding annotations 和 Source Items 选择候选 Signal。",
72
+ "不要使用关键词兜底,不要按数组顺序照搬;候选必须能被 sourceItemIds 支撑。",
73
+ "输出必须是 JSON object。"
74
+ ].join("\n"),
75
+ model: runtime.model,
76
+ thinkingLevel: runtime.thinkingLevel
77
+ },
78
+ sessionId: "daily-brief-signal-selection",
79
+ ...(runtime.getApiKey ? { getApiKey: runtime.getApiKey } : {})
80
+ });
81
+ agent.subscribe((event) => {
82
+ events.push(`selection:${event.type}`);
83
+ });
84
+ await agent.prompt([
85
+ "请选择 Daily Brief 候选 Signals,并排除弱或不相关 Source Items。",
86
+ "",
87
+ "JSON schema:",
88
+ "{",
89
+ " \"stage\": \"selection\",",
90
+ " \"candidateSignals\": [",
91
+ " {",
92
+ " \"signalId\": \"stable unique signal id\",",
93
+ " \"title\": \"reader-facing title\",",
94
+ " \"signalType\": \"architecture|ai-coding|tool-repo|risk\",",
95
+ " \"strength\": \"strong|weak\",",
96
+ " \"sourceItemIds\": [\"cited source item ids\"],",
97
+ " \"reason\": \"why this is a candidate, grounded in annotations\"",
98
+ " }",
99
+ " ],",
100
+ " \"excludedSourceItems\": [{ \"sourceItemId\": \"id\", \"reason\": \"why excluded\" }]",
101
+ "}",
102
+ "",
103
+ "Selection lens: 领域包括 Agent 架构、AI Coding;方向包括先进工具、长程任务、持续学习、自我改进、人与 Agent 的边界。",
104
+ "Merge near-duplicate candidates yourself by using the same signalId and multiple sourceItemIds.",
105
+ "Title rules:",
106
+ "- title 只能概括 cited Source Items 直接共同说明的对象或能力。",
107
+ "- 不要在 title 中写“正在成为”“趋势”“继续升温”“生态演进”“走向”等 adoption/evolution 判断,除非输入 Source Items 直接证明这种变化。",
108
+ "- 如果只有若干 repository observations,title 应写成“X 类项目/能力出现于本次 Source Items”,而不是宣称整个生态变化。",
109
+ "",
110
+ "Input:",
111
+ JSON.stringify(request, null, 2)
112
+ ].join("\n"));
113
+ const text = latestAssistantText(agent);
114
+ if (!text) {
115
+ throw new Error("Selection Stage did not return text");
116
+ }
117
+ return { text, events };
118
+ }
119
+ async function runRankingAgent(request, runtime) {
120
+ const events = [];
121
+ const agent = new Agent({
122
+ initialState: {
123
+ systemPrompt: [
124
+ "你是 Daily Brief Agent 的 Signal Ranking Stage。",
125
+ "你必须从候选 Signals 中决定最终排序和 low-signal 结果。",
126
+ "不要补充候选外 Signal;如果候选不足或都弱,返回空 rankedSignals。",
127
+ "输出必须是 JSON object。"
128
+ ].join("\n"),
129
+ model: runtime.model,
130
+ thinkingLevel: runtime.thinkingLevel
131
+ },
132
+ sessionId: "daily-brief-signal-ranking",
133
+ ...(runtime.getApiKey ? { getApiKey: runtime.getApiKey } : {})
134
+ });
135
+ agent.subscribe((event) => {
136
+ events.push(`ranking:${event.type}`);
137
+ });
138
+ await agent.prompt([
139
+ "请对候选 Signals 排序,最多选择 maxSignals 个 strong signals。",
140
+ "",
141
+ "JSON schema:",
142
+ "{",
143
+ " \"stage\": \"ranking\",",
144
+ " \"rankedSignals\": [{ \"signalId\": \"candidate signal id\", \"rank\": 1, \"reason\": \"grounded ranking reason\" }]",
145
+ "}",
146
+ "",
147
+ "Input:",
148
+ JSON.stringify(request, null, 2)
149
+ ].join("\n"));
150
+ const text = latestAssistantText(agent);
151
+ if (!text) {
152
+ throw new Error("Ranking Stage did not return text");
153
+ }
154
+ return { text, events };
155
+ }
156
+ export function mergeCandidateSignals(candidates) {
157
+ const byId = new Map();
158
+ for (const candidate of candidates) {
159
+ const previous = byId.get(candidate.signalId);
160
+ if (!previous) {
161
+ byId.set(candidate.signalId, { ...candidate, sourceItemIds: unique(candidate.sourceItemIds) });
162
+ continue;
163
+ }
164
+ byId.set(candidate.signalId, {
165
+ ...previous,
166
+ strength: previous.strength === "strong" || candidate.strength === "strong" ? "strong" : "weak",
167
+ sourceItemIds: unique([...previous.sourceItemIds, ...candidate.sourceItemIds]),
168
+ reason: unique([previous.reason, candidate.reason]).join(" / ")
169
+ });
170
+ }
171
+ return [...byId.values()];
172
+ }
173
+ function buildFauxSelectionOutput(request) {
174
+ const itemById = new Map(request.sourceItems.map((item) => [item.id, item]));
175
+ const candidateSignals = [];
176
+ const excludedSourceItems = [];
177
+ for (const annotation of request.annotations) {
178
+ const item = itemById.get(annotation.sourceItemId);
179
+ if (!item) {
180
+ continue;
181
+ }
182
+ if (annotation.relevance === "relevant" && annotation.focusAreaRelevance !== "none") {
183
+ candidateSignals.push({
184
+ signalId: `signal:${item.url.trim().toLowerCase()}`,
185
+ title: item.title,
186
+ signalType: inferSignalType(item, annotation),
187
+ strength: annotation.focusAreaRelevance === "strong" || annotation.focusAreaRelevance === "partial" ? "strong" : "weak",
188
+ sourceItemIds: [item.id],
189
+ reason: annotation.summary
190
+ });
191
+ }
192
+ else {
193
+ excludedSourceItems.push({
194
+ sourceItemId: item.id,
195
+ reason: annotation.weakItemHints[0] ?? "Understanding Stage judged this Source Item too weak for a Signal."
196
+ });
197
+ }
198
+ }
199
+ return { stage: "selection", candidateSignals, excludedSourceItems };
200
+ }
201
+ function buildFauxRankingOutput(request) {
202
+ const strong = request.candidateSignals.filter((candidate) => candidate.strength === "strong");
203
+ return {
204
+ stage: "ranking",
205
+ rankedSignals: strong.slice(0, request.maxSignals).map((candidate, index) => ({
206
+ signalId: candidate.signalId,
207
+ rank: index + 1,
208
+ reason: candidate.reason
209
+ }))
210
+ };
211
+ }
212
+ function buildSignalsFromRanking(sourceItems, candidates, ranking) {
213
+ const itemById = new Map(sourceItems.map((item) => [item.id, item]));
214
+ const candidateById = new Map(candidates.map((candidate) => [candidate.signalId, candidate]));
215
+ return ranking.rankedSignals.flatMap((ranked) => {
216
+ const candidate = candidateById.get(ranked.signalId);
217
+ if (!candidate) {
218
+ return [];
219
+ }
220
+ const citedItems = candidate.sourceItemIds.flatMap((id) => {
221
+ const item = itemById.get(id);
222
+ return item ? [item] : [];
223
+ });
224
+ const citations = citedItems.map((item) => ({
225
+ sourceItemId: item.id,
226
+ sourceId: item.sourceId,
227
+ title: item.title,
228
+ url: item.url
229
+ }));
230
+ const first = citedItems[0];
231
+ if (!first) {
232
+ return [];
233
+ }
234
+ return [
235
+ {
236
+ id: candidate.signalId,
237
+ type: candidate.signalType ?? inferSignalType(first),
238
+ title: candidate.title,
239
+ summary: summarizeSelectedSignal(first, candidate.reason),
240
+ whyItMatters: ranked.reason,
241
+ citations
242
+ }
243
+ ];
244
+ });
245
+ }
246
+ function summarizeSelectedSignal(item, reason) {
247
+ return {
248
+ whatItIs: item.platform === "github" ? `它是一个 GitHub repository:${reason}` : `它是一个 Source-grounded Signal:${reason}`,
249
+ whatItIsNot: "不是未引用来源支撑的通用判断;当前只代表 Selection/Ranking Stages 基于 cited Source Items 选出的 Signal。",
250
+ minimalExample: "最小地看,先回到 citations 中的 Source Item,确认这个判断是否被原文直接支撑。"
251
+ };
252
+ }
253
+ function inferSignalType(item, annotation) {
254
+ const text = `${item.title} ${item.analyzableText} ${annotation?.claims.join(" ") ?? ""}`.toLowerCase();
255
+ if (text.includes("risk") || text.includes("failure") || text.includes("security")) {
256
+ return "risk";
257
+ }
258
+ if (text.includes("coding agent") || text.includes("ai coding")) {
259
+ return "ai-coding";
260
+ }
261
+ if (item.platform === "github" || text.includes("repo")) {
262
+ return "tool-repo";
263
+ }
264
+ return "architecture";
265
+ }
266
+ function unique(values) {
267
+ return [...new Set(values)];
268
+ }
269
+ function latestAssistantText(agent) {
270
+ const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
271
+ const text = assistantMessage?.content
272
+ .filter((block) => block.type === "text")
273
+ .map((block) => block.text)
274
+ .join("");
275
+ return text && text.trim().length > 0 ? text.trim() : undefined;
276
+ }
@@ -0,0 +1,148 @@
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 class AnalysisFailureError extends Error {
5
+ findings;
6
+ constructor(message, findings) {
7
+ super(message);
8
+ this.name = "AnalysisFailureError";
9
+ this.findings = findings;
10
+ }
11
+ }
12
+ export async function runSourceGroundingAuditStage(input) {
13
+ const modelRuntimeConfig = input.modelRuntimeConfig ?? {
14
+ provider: "faux",
15
+ model: "faux-daily-brief-renderer",
16
+ ready: true,
17
+ issues: []
18
+ };
19
+ const first = await recordAudit(input.brief, input.sourceItems, input.artifact, modelRuntimeConfig, input.modelRuntimeEnv ?? process.env, input.inputRefs);
20
+ if (first.status === "passed") {
21
+ return { brief: input.brief, audit: first, repairAttempts: 0 };
22
+ }
23
+ throw new AnalysisFailureError("Source-grounding audit failed", first.findings);
24
+ }
25
+ async function recordAudit(brief, sourceItems, artifact, modelRuntimeConfig, modelRuntimeEnv, inputRefs) {
26
+ const sourceItemIds = sourceItems.map((item) => item.id);
27
+ const signalIds = brief.signals.map((signal) => signal.id);
28
+ const request = buildAuditRequest(brief, sourceItems);
29
+ const runtime = createStageModelRuntime({
30
+ config: modelRuntimeConfig,
31
+ env: modelRuntimeEnv,
32
+ fauxResponse: JSON.stringify(buildFauxAuditOutput(brief, sourceItems))
33
+ });
34
+ const response = await runAuditAgent(request, runtime);
35
+ const result = await runAgentStage({
36
+ stage: "audit",
37
+ artifact,
38
+ inputRefs: { ...(inputRefs ?? {}), sourceItemIds, signalIds },
39
+ validationContext: { sourceItemIds, signalIds },
40
+ execute: async () => response.text
41
+ });
42
+ return result.output;
43
+ }
44
+ function buildAuditRequest(brief, sourceItems) {
45
+ return {
46
+ brief,
47
+ citedSourceItems: sourceItems.map((item) => ({
48
+ id: item.id,
49
+ sourceId: item.sourceId,
50
+ platform: item.platform,
51
+ url: item.url,
52
+ title: item.title,
53
+ analyzableText: item.analyzableText,
54
+ ...(item.metadata ? { metadata: item.metadata } : {})
55
+ }))
56
+ };
57
+ }
58
+ async function runAuditAgent(request, runtime) {
59
+ const events = [];
60
+ const agent = new Agent({
61
+ initialState: {
62
+ systemPrompt: [
63
+ "你是 Daily Brief Agent 的 Source-grounding Audit Stage。",
64
+ "你必须审查 brief 中每个 narrative 是否被 cited Source Items 支撑。",
65
+ "重点检查 unsupported claims、generic AI drift、open-ended research leakage、citation mismatch、coverage overstatement。",
66
+ "不要修复 brief,只输出 audit JSON。"
67
+ ].join("\n"),
68
+ model: runtime.model,
69
+ thinkingLevel: runtime.thinkingLevel
70
+ },
71
+ sessionId: "daily-brief-source-grounding-audit",
72
+ ...(runtime.getApiKey ? { getApiKey: runtime.getApiKey } : {})
73
+ });
74
+ agent.subscribe((event) => {
75
+ events.push(`audit:${event.type}`);
76
+ });
77
+ await agent.prompt([
78
+ "请审查 Daily Brief 是否 Source-grounded。",
79
+ "",
80
+ "JSON schema:",
81
+ "{",
82
+ " \"stage\": \"audit\",",
83
+ " \"status\": \"passed|failed\",",
84
+ " \"findings\": [{ \"signalId\": \"optional known signal id\", \"sourceItemId\": \"optional known source item id\", \"issue\": \"specific issue\" }]",
85
+ "}",
86
+ "",
87
+ "如果无法确认某个强断言被 cited Source Item 支撑,必须 failed。",
88
+ "如果 Source Coverage 有 partial failures,Executive Summary 不得声称完整覆盖。",
89
+ "Source Coverage 的 Processed N Source Items from M Sources 是运行覆盖统计,不表示 Top Signals 必须叙述每个 Source Item;不要因为未引用所有 Source Items 而 failed。",
90
+ "whyItMatters 和 minimalExample 可以包含基于 cited facts 的有限读者含义或设计检查点;只要它没有引入外部事实、质量背书、采用趋势或未支撑能力,不要仅因 Source Item 没有逐字说“设计者需要...”而 failed。",
91
+ "对规范性措辞的审查重点是是否越过 cited facts:例如“必须采用”“已经成熟”“广泛使用”需要直接支撑;“提醒设计者检查...”这类弱含义可由 cited capabilities 支撑。",
92
+ "",
93
+ "Input:",
94
+ JSON.stringify(request, null, 2)
95
+ ].join("\n"));
96
+ const text = latestAssistantText(agent);
97
+ if (!text) {
98
+ throw new Error("Audit Stage did not return text");
99
+ }
100
+ return { text, events };
101
+ }
102
+ function buildFauxAuditOutput(brief, sourceItems) {
103
+ const sourceItemIds = new Set(sourceItems.map((item) => item.id));
104
+ const findings = [];
105
+ if (brief.sourceCoverage.partialFailures.length > 0 && /全部|完整|all sources|complete/i.test(brief.executiveSummary)) {
106
+ findings.push({ issue: "Executive Summary overstates collection completeness despite partial failures." });
107
+ }
108
+ for (const signal of brief.signals) {
109
+ if (signal.citations.length === 0) {
110
+ findings.push({ signalId: signal.id, issue: "Signal has no citations." });
111
+ }
112
+ if (!signal.focusAreas || signal.focusAreas.length === 0) {
113
+ findings.push({ signalId: signal.id, issue: "Signal lens focusAreas are missing." });
114
+ }
115
+ if (!signal.directions || signal.directions.length === 0) {
116
+ findings.push({ signalId: signal.id, issue: "Signal lens directions are missing." });
117
+ }
118
+ for (const citation of signal.citations) {
119
+ if (!sourceItemIds.has(citation.sourceItemId)) {
120
+ findings.push({
121
+ signalId: signal.id,
122
+ issue: `Citation references unknown Source Item: ${citation.sourceItemId}`
123
+ });
124
+ }
125
+ }
126
+ const narrative = `${signal.summary.whatItIs} ${signal.summary.whatItIsNot} ${signal.summary.minimalExample} ${signal.whyItMatters}`;
127
+ if (/market leader|guaranteed|best in class|widely adopted/i.test(narrative)) {
128
+ findings.push({ signalId: signal.id, issue: "Narrative contains unsupported overconfident claim." });
129
+ }
130
+ if (signal.citations.length > 1 &&
131
+ /分别(?:指向|对应|代表|说明|表明|描述)|一一对应|respectively/i.test(narrative)) {
132
+ findings.push({ signalId: signal.id, issue: "Narrative contains risky per-source mapping across multiple citations." });
133
+ }
134
+ }
135
+ return {
136
+ stage: "audit",
137
+ status: findings.length === 0 ? "passed" : "failed",
138
+ findings
139
+ };
140
+ }
141
+ function latestAssistantText(agent) {
142
+ const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
143
+ const text = assistantMessage?.content
144
+ .filter((block) => block.type === "text")
145
+ .map((block) => block.text)
146
+ .join("");
147
+ return text && text.trim().length > 0 ? text.trim() : undefined;
148
+ }