@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,350 @@
|
|
|
1
|
+
import { Agent } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { generateDailyBrief, renderDailyBriefMarkdown } from "../brief/index.js";
|
|
5
|
+
import { collectSources } from "../collection/index.js";
|
|
6
|
+
import { deliverCoreFailureNotification, deliverDiscordNotification } from "../discord/index.js";
|
|
7
|
+
import { briefArchivePath, createAgentRunArtifact, readSourceItems, writeAgentRunArtifact, writeBriefArchive } from "../storage/index.js";
|
|
8
|
+
import { readModelRuntimeConfig } from "./model-runtime-config.js";
|
|
9
|
+
import { enrichDailyBriefNarrativeWithAgent } from "./signal-narrative.js";
|
|
10
|
+
import { runSignalSelectionAndRankingStages } from "./signal-selection-ranking.js";
|
|
11
|
+
import { AnalysisFailureError, runSourceGroundingAuditStage } from "./source-grounding-audit.js";
|
|
12
|
+
import { runSourceGroundingRepairStage } from "./source-grounding-repair.js";
|
|
13
|
+
import { runSourceItemUnderstandingStage } from "./source-item-understanding.js";
|
|
14
|
+
import { runAgentStage } from "./stage-runner.js";
|
|
15
|
+
export async function runOnce(options = {}) {
|
|
16
|
+
const date = options.date ?? new Date();
|
|
17
|
+
const dateKey = options.dateKey;
|
|
18
|
+
let collection;
|
|
19
|
+
try {
|
|
20
|
+
collection = await collectSources({
|
|
21
|
+
date,
|
|
22
|
+
...(dateKey ? { dateKey } : {}),
|
|
23
|
+
fetchedAt: date,
|
|
24
|
+
...(options.sourceRegistryPath ? { sourceRegistryPath: options.sourceRegistryPath } : {}),
|
|
25
|
+
...(options.sourceItemRoot ? { sourceItemRoot: options.sourceItemRoot } : {})
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const coreFailure = {
|
|
30
|
+
kind: "unreadable-source-registry",
|
|
31
|
+
message: error instanceof Error ? error.message : String(error)
|
|
32
|
+
};
|
|
33
|
+
const delivery = await deliverCoreFailureNotification(coreFailure, {
|
|
34
|
+
...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
|
|
35
|
+
...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
archivePath: "",
|
|
39
|
+
markdown: "",
|
|
40
|
+
sourceCount: 0,
|
|
41
|
+
sourceItemCount: 0,
|
|
42
|
+
piEvents: [],
|
|
43
|
+
collection: { storePath: "", sources: [] },
|
|
44
|
+
delivery,
|
|
45
|
+
coreFailure
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const collectedItems = await readSourceItems(date, options.sourceItemRoot, dateKey);
|
|
49
|
+
const collectionFailure = classifyCollectionCoreFailure(collection, collectedItems.length);
|
|
50
|
+
if (collectionFailure) {
|
|
51
|
+
const delivery = await deliverCoreFailureNotification(collectionFailure, {
|
|
52
|
+
...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
|
|
53
|
+
...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
archivePath: "",
|
|
57
|
+
markdown: "",
|
|
58
|
+
sourceCount: collection.sources.length,
|
|
59
|
+
sourceItemCount: collectedItems.length,
|
|
60
|
+
piEvents: [],
|
|
61
|
+
collection,
|
|
62
|
+
delivery,
|
|
63
|
+
coreFailure: collectionFailure
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const collectionFailures = collectionFailureRefs(collection);
|
|
67
|
+
const generated = await generateOnce({
|
|
68
|
+
...options,
|
|
69
|
+
...(dateKey ? { dateKey } : {}),
|
|
70
|
+
sourceCount: collection.sources.length,
|
|
71
|
+
...(collectionFailures.length > 0
|
|
72
|
+
? {
|
|
73
|
+
partialFailures: collectionFailures.map((failure) => `${failure.sourceId}: ${failure.reason}`),
|
|
74
|
+
collectionFailures
|
|
75
|
+
}
|
|
76
|
+
: {})
|
|
77
|
+
});
|
|
78
|
+
const delivery = await deliverOnce({
|
|
79
|
+
...options,
|
|
80
|
+
date,
|
|
81
|
+
brief: generated.brief,
|
|
82
|
+
archivePath: generated.archivePath
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
archivePath: generated.archivePath,
|
|
86
|
+
markdown: generated.markdown,
|
|
87
|
+
sourceCount: collection.sources.length,
|
|
88
|
+
sourceItemCount: generated.sourceItemCount,
|
|
89
|
+
piEvents: generated.piEvents,
|
|
90
|
+
collection,
|
|
91
|
+
delivery,
|
|
92
|
+
...(generated.agentRunArtifactPath ? { agentRunArtifactPath: generated.agentRunArtifactPath } : {})
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function generateOnce(options = {}) {
|
|
96
|
+
const date = options.date ?? new Date();
|
|
97
|
+
const dateKey = options.dateKey;
|
|
98
|
+
const sourceItems = await readSourceItems(date, options.sourceItemRoot, dateKey);
|
|
99
|
+
const modelRuntimeConfig = readModelRuntimeConfig(options.modelRuntimeEnv);
|
|
100
|
+
if (sourceItems.length === 0) {
|
|
101
|
+
throw new Error("No usable Source Items found for generation; Daily Brief will not archive a false low-signal brief.");
|
|
102
|
+
}
|
|
103
|
+
if (!modelRuntimeConfig.ready) {
|
|
104
|
+
throw new Error(`Model runtime is not ready:\n${modelRuntimeConfig.issues.map((issue) => `- ${issue}`).join("\n")}`);
|
|
105
|
+
}
|
|
106
|
+
const baseBrief = generateDailyBrief({
|
|
107
|
+
date,
|
|
108
|
+
...(dateKey ? { dateKey } : {}),
|
|
109
|
+
sourceItems,
|
|
110
|
+
...(options.partialFailures ? { partialFailures: options.partialFailures } : {}),
|
|
111
|
+
...(options.sourceCount ? { sourceCount: options.sourceCount } : {})
|
|
112
|
+
});
|
|
113
|
+
const artifact = createAgentRunArtifact({
|
|
114
|
+
date,
|
|
115
|
+
...(dateKey ? { dateKey } : {}),
|
|
116
|
+
modelRuntimeConfig,
|
|
117
|
+
inputRefs: {
|
|
118
|
+
sourceItemIds: sourceItems.map((item) => item.id),
|
|
119
|
+
signalIds: baseBrief.signals.map((signal) => signal.id),
|
|
120
|
+
...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {})
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const collectionInputRefs = options.collectionFailures
|
|
125
|
+
? { collectionFailures: options.collectionFailures }
|
|
126
|
+
: undefined;
|
|
127
|
+
const understanding = await runSourceItemUnderstandingStage({
|
|
128
|
+
sourceItems,
|
|
129
|
+
modelRuntimeConfig,
|
|
130
|
+
artifact,
|
|
131
|
+
...(collectionInputRefs ? { inputRefs: collectionInputRefs } : {}),
|
|
132
|
+
...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
|
|
133
|
+
});
|
|
134
|
+
const selected = await runSignalSelectionAndRankingStages({
|
|
135
|
+
sourceItems,
|
|
136
|
+
annotations: understanding.annotations,
|
|
137
|
+
artifact,
|
|
138
|
+
modelRuntimeConfig,
|
|
139
|
+
...(collectionInputRefs ? { inputRefs: collectionInputRefs } : {}),
|
|
140
|
+
...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
|
|
141
|
+
});
|
|
142
|
+
const selectedBrief = {
|
|
143
|
+
...baseBrief,
|
|
144
|
+
executiveSummary: selected.signals.length === 0
|
|
145
|
+
? "今天是 low-signal day:Selection/Ranking Stages 没有选出足够强的 Source-grounded Signals。"
|
|
146
|
+
: `今天有 ${selected.signals.length} 个 Agent-selected Source-grounded Signals,均保留 Source Item citation 以便回溯。`,
|
|
147
|
+
signals: selected.signals
|
|
148
|
+
};
|
|
149
|
+
const narrativeEvents = [];
|
|
150
|
+
const narrative = await enrichDailyBriefNarrativeWithAgent({
|
|
151
|
+
brief: selectedBrief,
|
|
152
|
+
sourceItems,
|
|
153
|
+
modelRuntimeConfig,
|
|
154
|
+
...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {})
|
|
155
|
+
});
|
|
156
|
+
narrativeEvents.push(...narrative.events);
|
|
157
|
+
const narrativeStage = await recordNarrativeStage({
|
|
158
|
+
artifact,
|
|
159
|
+
date,
|
|
160
|
+
sourceItems,
|
|
161
|
+
brief: narrative.brief,
|
|
162
|
+
...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {})
|
|
163
|
+
});
|
|
164
|
+
const audited = await auditBriefWithSingleRepairAttempt({
|
|
165
|
+
narrativeBrief: narrative.brief,
|
|
166
|
+
sourceItems,
|
|
167
|
+
artifact,
|
|
168
|
+
date,
|
|
169
|
+
modelRuntimeConfig,
|
|
170
|
+
...(options.modelRuntimeEnv ? { modelRuntimeEnv: options.modelRuntimeEnv } : {}),
|
|
171
|
+
...(options.collectionFailures ? { collectionFailures: options.collectionFailures } : {}),
|
|
172
|
+
...(collectionInputRefs ? { collectionInputRefs } : {})
|
|
173
|
+
});
|
|
174
|
+
narrativeEvents.push(...audited.narrativeEvents);
|
|
175
|
+
const writtenArtifact = options.agentRunRoot ? await writeAgentRunArtifact(artifact, date, options.agentRunRoot, dateKey) : undefined;
|
|
176
|
+
const markdown = renderDailyBriefMarkdown(audited.brief);
|
|
177
|
+
const piResult = await renderBriefThroughPiRuntime(markdown);
|
|
178
|
+
const archived = await writeBriefArchive(piResult.markdown, date, options.archiveRoot, dateKey);
|
|
179
|
+
return {
|
|
180
|
+
archivePath: archived.path,
|
|
181
|
+
markdown: piResult.markdown,
|
|
182
|
+
brief: audited.brief,
|
|
183
|
+
sourceItemCount: sourceItems.length,
|
|
184
|
+
piEvents: [...understanding.events, ...selected.events, ...narrativeEvents, ...piResult.events],
|
|
185
|
+
modelRuntimeConfig,
|
|
186
|
+
...((writtenArtifact?.path ?? narrativeStage.artifactPath) ? { agentRunArtifactPath: writtenArtifact?.path ?? narrativeStage.artifactPath } : {})
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
artifact.failure = {
|
|
191
|
+
kind: "execution",
|
|
192
|
+
message: error instanceof Error ? error.message : String(error)
|
|
193
|
+
};
|
|
194
|
+
if (options.agentRunRoot) {
|
|
195
|
+
await writeAgentRunArtifact(artifact, date, options.agentRunRoot, dateKey);
|
|
196
|
+
}
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function recordNarrativeStage(input) {
|
|
201
|
+
const result = await runAgentStage({
|
|
202
|
+
stage: "narrative",
|
|
203
|
+
artifact: input.artifact,
|
|
204
|
+
date: input.date,
|
|
205
|
+
inputRefs: {
|
|
206
|
+
...(input.collectionFailures ? { collectionFailures: input.collectionFailures } : {}),
|
|
207
|
+
sourceItemIds: input.sourceItems.map((item) => item.id),
|
|
208
|
+
signalIds: input.brief.signals.map((signal) => signal.id)
|
|
209
|
+
},
|
|
210
|
+
validationContext: {
|
|
211
|
+
signalIds: input.brief.signals.map((signal) => signal.id)
|
|
212
|
+
},
|
|
213
|
+
execute: async () => ({
|
|
214
|
+
stage: "narrative",
|
|
215
|
+
executiveSummary: input.brief.executiveSummary,
|
|
216
|
+
signalNarratives: input.brief.signals.map((signal) => ({
|
|
217
|
+
signalId: signal.id,
|
|
218
|
+
focusAreas: signal.focusAreas ?? [],
|
|
219
|
+
directions: signal.directions ?? [],
|
|
220
|
+
whatItIs: signal.summary.whatItIs,
|
|
221
|
+
whatItIsNot: signal.summary.whatItIsNot,
|
|
222
|
+
minimalExample: signal.summary.minimalExample,
|
|
223
|
+
whyItMatters: signal.whyItMatters
|
|
224
|
+
}))
|
|
225
|
+
})
|
|
226
|
+
});
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
async function auditBriefWithSingleRepairAttempt(input) {
|
|
230
|
+
const narrativeEvents = [];
|
|
231
|
+
try {
|
|
232
|
+
const audited = await runSourceGroundingAuditStage({
|
|
233
|
+
brief: input.narrativeBrief,
|
|
234
|
+
sourceItems: input.sourceItems,
|
|
235
|
+
artifact: input.artifact,
|
|
236
|
+
modelRuntimeConfig: input.modelRuntimeConfig,
|
|
237
|
+
...(input.collectionInputRefs ? { inputRefs: input.collectionInputRefs } : {}),
|
|
238
|
+
...(input.modelRuntimeEnv ? { modelRuntimeEnv: input.modelRuntimeEnv } : {})
|
|
239
|
+
});
|
|
240
|
+
return { brief: audited.brief, narrativeEvents };
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (!(error instanceof AnalysisFailureError)) {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
const repaired = await runSourceGroundingRepairStage({
|
|
247
|
+
brief: input.narrativeBrief,
|
|
248
|
+
sourceItems: input.sourceItems,
|
|
249
|
+
auditFindings: error.findings,
|
|
250
|
+
artifact: input.artifact,
|
|
251
|
+
modelRuntimeConfig: input.modelRuntimeConfig,
|
|
252
|
+
...(input.collectionInputRefs ? { inputRefs: input.collectionInputRefs } : {}),
|
|
253
|
+
...(input.modelRuntimeEnv ? { modelRuntimeEnv: input.modelRuntimeEnv } : {})
|
|
254
|
+
});
|
|
255
|
+
narrativeEvents.push(...repaired.events);
|
|
256
|
+
const audited = await runSourceGroundingAuditStage({
|
|
257
|
+
brief: repaired.brief,
|
|
258
|
+
sourceItems: input.sourceItems,
|
|
259
|
+
artifact: input.artifact,
|
|
260
|
+
modelRuntimeConfig: input.modelRuntimeConfig,
|
|
261
|
+
...(input.collectionInputRefs ? { inputRefs: input.collectionInputRefs } : {}),
|
|
262
|
+
...(input.modelRuntimeEnv ? { modelRuntimeEnv: input.modelRuntimeEnv } : {})
|
|
263
|
+
});
|
|
264
|
+
return { brief: audited.brief, narrativeEvents };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function collectionFailureRefs(collection) {
|
|
268
|
+
return collection.sources
|
|
269
|
+
.filter((source) => source.status === "failed")
|
|
270
|
+
.map((source) => ({ sourceId: source.sourceId, reason: source.reason ?? "Unknown failure" }));
|
|
271
|
+
}
|
|
272
|
+
function classifyCollectionCoreFailure(collection, storedSourceItemCount) {
|
|
273
|
+
const enabledResults = collection.sources.filter((source) => source.status !== "skipped");
|
|
274
|
+
const failedResults = enabledResults.filter((source) => source.status === "failed");
|
|
275
|
+
if (enabledResults.length === 0 && storedSourceItemCount === 0) {
|
|
276
|
+
return {
|
|
277
|
+
kind: "no-usable-source-items",
|
|
278
|
+
message: "No enabled Sources produced Source Items; Daily Brief will not generate a false low-signal brief."
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
if (enabledResults.length > 0 && failedResults.length === enabledResults.length && storedSourceItemCount === 0) {
|
|
282
|
+
return {
|
|
283
|
+
kind: "no-usable-source-items",
|
|
284
|
+
message: `All enabled Sources failed and no Source Items exist for this date: ${failedResults
|
|
285
|
+
.map((source) => `${source.sourceId}: ${source.reason ?? "Unknown failure"}`)
|
|
286
|
+
.join("; ")}`
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
export async function deliverOnce(options = {}) {
|
|
292
|
+
const date = options.date ?? new Date();
|
|
293
|
+
const archivePath = options.archivePath ?? briefArchivePath(date, options.archiveRoot, options.dateKey);
|
|
294
|
+
try {
|
|
295
|
+
await readFile(archivePath, "utf8");
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return { status: "failed", reason: `Brief Archive entry not found: ${archivePath}` };
|
|
299
|
+
}
|
|
300
|
+
const brief = options.brief ??
|
|
301
|
+
generateDailyBrief({
|
|
302
|
+
date,
|
|
303
|
+
...(options.dateKey ? { dateKey: options.dateKey } : {}),
|
|
304
|
+
sourceItems: await readSourceItems(date, options.sourceItemRoot, options.dateKey)
|
|
305
|
+
});
|
|
306
|
+
return deliverDiscordNotification({
|
|
307
|
+
brief,
|
|
308
|
+
briefPath: archivePath,
|
|
309
|
+
...(options.discordTemplatePath ? { templatePath: options.discordTemplatePath } : {})
|
|
310
|
+
}, {
|
|
311
|
+
...(options.discordWebhookUrl ? { webhookUrl: options.discordWebhookUrl } : {}),
|
|
312
|
+
...(options.discordFetchImpl ? { fetchImpl: options.discordFetchImpl } : {})
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
export async function renderBriefThroughPiRuntime(markdown) {
|
|
316
|
+
const provider = registerFauxProvider();
|
|
317
|
+
provider.setResponses([fauxAssistantMessage(markdown)]);
|
|
318
|
+
try {
|
|
319
|
+
const events = [];
|
|
320
|
+
const model = provider.getModel();
|
|
321
|
+
const agent = new Agent({
|
|
322
|
+
initialState: {
|
|
323
|
+
systemPrompt: "You are the Daily Brief Agent. Return only the already grounded Markdown Daily Brief you are given.",
|
|
324
|
+
model,
|
|
325
|
+
thinkingLevel: "off"
|
|
326
|
+
},
|
|
327
|
+
sessionId: "daily-brief-run-once"
|
|
328
|
+
});
|
|
329
|
+
agent.subscribe((event) => {
|
|
330
|
+
events.push(event.type);
|
|
331
|
+
});
|
|
332
|
+
await agent.prompt([
|
|
333
|
+
"Render this Source-grounded Daily Brief exactly as Markdown.",
|
|
334
|
+
"",
|
|
335
|
+
markdown
|
|
336
|
+
].join("\n"));
|
|
337
|
+
const assistantMessage = [...agent.state.messages].reverse().find((message) => message.role === "assistant");
|
|
338
|
+
const rendered = assistantMessage?.content
|
|
339
|
+
.filter((block) => block.type === "text")
|
|
340
|
+
.map((block) => block.text)
|
|
341
|
+
.join("");
|
|
342
|
+
if (!rendered) {
|
|
343
|
+
throw new Error("Pi Agent Runtime did not return Daily Brief Markdown");
|
|
344
|
+
}
|
|
345
|
+
return { markdown: rendered, events };
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
provider.unregister();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./daily-brief-agent.js";
|
|
2
|
+
export * from "./model-runtime-config.js";
|
|
3
|
+
export * from "./model-stage-runtime.js";
|
|
4
|
+
export * from "./signal-narrative.js";
|
|
5
|
+
export * from "./signal-selection-ranking.js";
|
|
6
|
+
export * from "./source-item-understanding.js";
|
|
7
|
+
export * from "./source-grounding-audit.js";
|
|
8
|
+
export * from "./source-grounding-repair.js";
|
|
9
|
+
export * from "./stage-contracts.js";
|
|
10
|
+
export * from "./stage-runner.js";
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { getOAuthApiKey, loginOpenAICodex } from "@earendil-works/pi-ai/oauth";
|
|
3
|
+
import { envNameFromCredentialRef, getCredential, isEnvCredentialRef, putCredential, readCredentialStore, readModelConfig, removeCredential, writeCredentialStore } from "../config/index.js";
|
|
4
|
+
import { resolveDailyBriefPaths } from "../config/paths.js";
|
|
5
|
+
export const defaultOAuthHelpers = {
|
|
6
|
+
getOAuthApiKey,
|
|
7
|
+
loginOpenAICodex
|
|
8
|
+
};
|
|
9
|
+
export function readModelRuntimeConfig(env = process.env, options = {}) {
|
|
10
|
+
const paths = resolveDailyBriefPaths(env);
|
|
11
|
+
const configPath = options.configPath ?? paths.configPath;
|
|
12
|
+
const authPath = options.authPath ?? paths.authPath;
|
|
13
|
+
const config = readEnvModelConfig(env) ?? (existsSync(configPath) ? readModelConfig(configPath) : undefined);
|
|
14
|
+
if (!config) {
|
|
15
|
+
return {
|
|
16
|
+
provider: "openai-codex",
|
|
17
|
+
model: "gpt-5.5",
|
|
18
|
+
credentialRef: "openai-codex.default",
|
|
19
|
+
ready: false,
|
|
20
|
+
issues: [`Model config not found: ${configPath}. Run daily-brief setup and daily-brief model configure/login first.`]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const issues = credentialIssues(config.provider, config.credentialRef, env, authPath);
|
|
24
|
+
return {
|
|
25
|
+
provider: config.provider,
|
|
26
|
+
model: config.model,
|
|
27
|
+
...(config.credentialRef ? { credentialRef: config.credentialRef } : {}),
|
|
28
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
29
|
+
ready: issues.length === 0,
|
|
30
|
+
issues
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function resolveModelApiKey(config, env = process.env, options = {}) {
|
|
34
|
+
if (config.provider === "faux") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const credentialRef = config.credentialRef;
|
|
38
|
+
if (!credentialRef) {
|
|
39
|
+
throw new Error(`credentialRef is required for ${config.provider}`);
|
|
40
|
+
}
|
|
41
|
+
if (isEnvCredentialRef(credentialRef)) {
|
|
42
|
+
const envName = envNameFromCredentialRef(credentialRef);
|
|
43
|
+
return env[envName]?.trim();
|
|
44
|
+
}
|
|
45
|
+
const authPath = options.authPath ?? resolveDailyBriefPaths(env).authPath;
|
|
46
|
+
const credential = getCredential(credentialRef, authPath);
|
|
47
|
+
if (!credential) {
|
|
48
|
+
throw new Error(`Credential not found: ${credentialRef}`);
|
|
49
|
+
}
|
|
50
|
+
if (credential.provider !== config.provider) {
|
|
51
|
+
throw new Error(`Credential ${credentialRef} is for ${credential.provider}, not ${config.provider}`);
|
|
52
|
+
}
|
|
53
|
+
if (credential.type === "api-key") {
|
|
54
|
+
return credential.apiKey;
|
|
55
|
+
}
|
|
56
|
+
const helpers = options.oauthHelpers ?? defaultOAuthHelpers;
|
|
57
|
+
const result = await helpers.getOAuthApiKey(credential.provider, {
|
|
58
|
+
[credential.provider]: credential.credentials
|
|
59
|
+
});
|
|
60
|
+
if (!result) {
|
|
61
|
+
throw new Error(`OAuth credential ${credentialRef} is not logged in`);
|
|
62
|
+
}
|
|
63
|
+
persistRefreshedOAuthCredential(credentialRef, credential, result.newCredentials, authPath);
|
|
64
|
+
return result.apiKey;
|
|
65
|
+
}
|
|
66
|
+
export async function loginModelCredential(input) {
|
|
67
|
+
if (input.provider !== "openai-codex") {
|
|
68
|
+
throw new Error(`model login currently supports OAuth provider openai-codex, received ${input.provider}`);
|
|
69
|
+
}
|
|
70
|
+
const helpers = input.oauthHelpers ?? defaultOAuthHelpers;
|
|
71
|
+
const credentials = await helpers.loginOpenAICodex({
|
|
72
|
+
onAuth: (info) => {
|
|
73
|
+
input.io.stdout(`Open authorization URL: ${info.url}`);
|
|
74
|
+
if (info.instructions) {
|
|
75
|
+
input.io.stdout(info.instructions);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
onDeviceCode: (info) => {
|
|
79
|
+
input.io.stdout(`Open verification URL: ${info.verificationUri}`);
|
|
80
|
+
input.io.stdout(`Device code: ${info.userCode}`);
|
|
81
|
+
},
|
|
82
|
+
onPrompt: (prompt) => promptForOAuth(input.io, prompt.message),
|
|
83
|
+
onManualCodeInput: () => promptForOAuth(input.io, "Authorization code:"),
|
|
84
|
+
onProgress: (message) => input.io.stdout(message),
|
|
85
|
+
onSelect: async (prompt) => {
|
|
86
|
+
for (const option of prompt.options) {
|
|
87
|
+
input.io.stdout(`${option.id}: ${option.label}`);
|
|
88
|
+
}
|
|
89
|
+
const selected = await promptForOAuth(input.io, prompt.message);
|
|
90
|
+
return selected || undefined;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
putCredential(input.credentialRef, {
|
|
94
|
+
type: "oauth",
|
|
95
|
+
provider: "openai-codex",
|
|
96
|
+
credentials
|
|
97
|
+
}, input.authPath ?? resolveDailyBriefPaths(input.env ?? process.env).authPath);
|
|
98
|
+
}
|
|
99
|
+
export function logoutModelCredential(ref, env = process.env, authPath) {
|
|
100
|
+
removeCredential(ref, authPath ?? resolveDailyBriefPaths(env).authPath);
|
|
101
|
+
}
|
|
102
|
+
export function statusModelCredentials(env = process.env, options = {}) {
|
|
103
|
+
const authPath = options.authPath ?? resolveDailyBriefPaths(env).authPath;
|
|
104
|
+
return Object.entries(readCredentialStore(authPath).credentials).map(([ref, credential]) => ({
|
|
105
|
+
ref,
|
|
106
|
+
type: credential.type,
|
|
107
|
+
provider: credential.provider,
|
|
108
|
+
secret: "<redacted>"
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
function credentialIssues(provider, credentialRef, env, authPath) {
|
|
112
|
+
if (provider === "faux") {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (!credentialRef) {
|
|
116
|
+
return [`credentialRef is required for ${provider}`];
|
|
117
|
+
}
|
|
118
|
+
if (isEnvCredentialRef(credentialRef)) {
|
|
119
|
+
const envName = envNameFromCredentialRef(credentialRef);
|
|
120
|
+
return env[envName]?.trim() ? [] : [`${envName} is required for credentialRef ${credentialRef}`];
|
|
121
|
+
}
|
|
122
|
+
const credential = getCredential(credentialRef, authPath);
|
|
123
|
+
if (!credential) {
|
|
124
|
+
return [`Credential not found: ${credentialRef}`];
|
|
125
|
+
}
|
|
126
|
+
if (credential.provider !== provider) {
|
|
127
|
+
return [`Credential ${credentialRef} is for ${credential.provider}, not ${provider}`];
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
function readEnvModelConfig(env) {
|
|
132
|
+
const provider = env.DAILY_BRIEF_MODEL_PROVIDER?.trim();
|
|
133
|
+
if (!provider) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const normalizedProvider = normalizeProvider(provider, env);
|
|
137
|
+
return {
|
|
138
|
+
provider: normalizedProvider,
|
|
139
|
+
model: env.DAILY_BRIEF_MODEL?.trim() || defaultModelForProvider(normalizedProvider),
|
|
140
|
+
...(env.DAILY_BRIEF_MODEL_BASE_URL?.trim() ? { baseUrl: env.DAILY_BRIEF_MODEL_BASE_URL.trim() } : {}),
|
|
141
|
+
...readEnvCredentialRef(env, normalizedProvider)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function normalizeProvider(value, env) {
|
|
145
|
+
const provider = value.trim().toLowerCase();
|
|
146
|
+
if (provider === "codex" || provider === "hermes") {
|
|
147
|
+
return "openai-codex";
|
|
148
|
+
}
|
|
149
|
+
if (provider === "faux") {
|
|
150
|
+
if (env.DAILY_BRIEF_ALLOW_FAUX_PROVIDER === "true") {
|
|
151
|
+
return provider;
|
|
152
|
+
}
|
|
153
|
+
throw new Error("DAILY_BRIEF_MODEL_PROVIDER=faux is only allowed when DAILY_BRIEF_ALLOW_FAUX_PROVIDER=true");
|
|
154
|
+
}
|
|
155
|
+
if (provider === "openai-codex" || provider === "openai" || provider === "deepseek" || provider === "openai-compatible") {
|
|
156
|
+
return provider;
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`Unsupported DAILY_BRIEF_MODEL_PROVIDER: ${value}`);
|
|
159
|
+
}
|
|
160
|
+
function defaultModelForProvider(provider) {
|
|
161
|
+
if (provider === "openai-codex") {
|
|
162
|
+
return "gpt-5.5";
|
|
163
|
+
}
|
|
164
|
+
if (provider === "openai") {
|
|
165
|
+
return "gpt-4.1-mini";
|
|
166
|
+
}
|
|
167
|
+
if (provider === "deepseek") {
|
|
168
|
+
return "deepseek-chat";
|
|
169
|
+
}
|
|
170
|
+
if (provider === "openai-compatible") {
|
|
171
|
+
return "openai-compatible-model";
|
|
172
|
+
}
|
|
173
|
+
return "faux-daily-brief-renderer";
|
|
174
|
+
}
|
|
175
|
+
function defaultCredentialRefForProvider(provider) {
|
|
176
|
+
if (provider === "faux") {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
if (provider === "openai") {
|
|
180
|
+
return "env:OPENAI_API_KEY";
|
|
181
|
+
}
|
|
182
|
+
if (provider === "deepseek") {
|
|
183
|
+
return "env:DEEPSEEK_API_KEY";
|
|
184
|
+
}
|
|
185
|
+
if (provider === "openai-compatible") {
|
|
186
|
+
return "env:OPENAI_API_KEY";
|
|
187
|
+
}
|
|
188
|
+
return "openai-codex.default";
|
|
189
|
+
}
|
|
190
|
+
function persistRefreshedOAuthCredential(ref, credential, credentials, authPath) {
|
|
191
|
+
const store = readCredentialStore(authPath);
|
|
192
|
+
const previous = store.credentials[ref];
|
|
193
|
+
store.credentials[ref] = {
|
|
194
|
+
...credential,
|
|
195
|
+
...((previous?.createdAt ?? credential.createdAt) ? { createdAt: previous?.createdAt ?? credential.createdAt } : {}),
|
|
196
|
+
updatedAt: new Date().toISOString(),
|
|
197
|
+
credentials
|
|
198
|
+
};
|
|
199
|
+
writeCredentialStore(store, authPath);
|
|
200
|
+
}
|
|
201
|
+
function readEnvCredentialRef(env, provider) {
|
|
202
|
+
const credentialRef = env.DAILY_BRIEF_MODEL_CREDENTIAL_REF?.trim() ||
|
|
203
|
+
defaultCredentialRefForProvider(provider);
|
|
204
|
+
return credentialRef ? { credentialRef } : {};
|
|
205
|
+
}
|
|
206
|
+
function promptForOAuth(io, message) {
|
|
207
|
+
if (!io.prompt) {
|
|
208
|
+
throw new Error(`OAuth login requires interactive input: ${message}`);
|
|
209
|
+
}
|
|
210
|
+
return io.prompt(message);
|
|
211
|
+
}
|
|
212
|
+
export function toApiKeyCredential(provider, apiKey) {
|
|
213
|
+
if (provider === "faux" || provider === "openai-codex") {
|
|
214
|
+
throw new Error(`${provider} does not accept API key credentials in auth.json`);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
type: "api-key",
|
|
218
|
+
provider,
|
|
219
|
+
apiKey
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fauxAssistantMessage, getModel, registerBuiltInApiProviders, registerFauxProvider } from "@earendil-works/pi-ai";
|
|
2
|
+
import { resolveModelApiKey } from "./model-runtime-config.js";
|
|
3
|
+
export function createStageModelRuntime(input) {
|
|
4
|
+
const { config, env } = input;
|
|
5
|
+
if (config.provider === "faux") {
|
|
6
|
+
if (!input.fauxResponse) {
|
|
7
|
+
throw new Error("fauxResponse is required for faux model runtime");
|
|
8
|
+
}
|
|
9
|
+
const provider = registerFauxProvider({
|
|
10
|
+
models: [{ id: config.model, name: config.model }]
|
|
11
|
+
});
|
|
12
|
+
provider.setResponses([fauxAssistantMessage(input.fauxResponse)]);
|
|
13
|
+
return {
|
|
14
|
+
model: provider.getModel(config.model) ?? provider.getModel(),
|
|
15
|
+
thinkingLevel: "off",
|
|
16
|
+
unregister: provider.unregister
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
registerBuiltInApiProviders();
|
|
20
|
+
if (config.provider === "openai-codex") {
|
|
21
|
+
return {
|
|
22
|
+
model: getModel("openai-codex", config.model),
|
|
23
|
+
getApiKey: (provider) => (provider === "openai-codex" ? resolveModelApiKey(config, env) : undefined),
|
|
24
|
+
thinkingLevel: input.openAICodexThinkingLevel ?? "low"
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (config.provider === "deepseek") {
|
|
28
|
+
return {
|
|
29
|
+
model: getModel("deepseek", config.model),
|
|
30
|
+
getApiKey: (provider) => (provider === "deepseek" ? resolveModelApiKey(config, env) : undefined),
|
|
31
|
+
thinkingLevel: "off"
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (config.provider === "openai-compatible") {
|
|
35
|
+
return {
|
|
36
|
+
model: buildOpenAICompatibleModel(config),
|
|
37
|
+
getApiKey: (provider) => (provider === "openai-compatible" ? resolveModelApiKey(config, env) : undefined),
|
|
38
|
+
thinkingLevel: "off"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
model: getModel("openai", config.model),
|
|
43
|
+
getApiKey: (provider) => (provider === "openai" ? resolveModelApiKey(config, env) : undefined),
|
|
44
|
+
thinkingLevel: "off"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function buildOpenAICompatibleModel(config) {
|
|
48
|
+
if (!config.baseUrl) {
|
|
49
|
+
throw new Error("baseUrl is required for openai-compatible model runtime");
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
id: config.model,
|
|
53
|
+
name: config.model,
|
|
54
|
+
api: "openai-completions",
|
|
55
|
+
provider: "openai-compatible",
|
|
56
|
+
baseUrl: config.baseUrl,
|
|
57
|
+
reasoning: false,
|
|
58
|
+
input: ["text"],
|
|
59
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
60
|
+
contextWindow: 128000,
|
|
61
|
+
maxTokens: 4096
|
|
62
|
+
};
|
|
63
|
+
}
|