@m8i-51/shoal 0.1.15 → 0.1.16
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/framework/diary.ts +96 -0
- package/package.json +1 -1
- package/server/index.ts +38 -0
- package/web/dist/assets/index-BHrkJsFb.js +85 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/index-riAs4l9D.js +0 -85
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { createLLMClient } from "./llm-client.js";
|
|
4
|
+
import type { Finding } from "./types.js";
|
|
5
|
+
|
|
6
|
+
function loadFindings(runId: string): Finding[] {
|
|
7
|
+
const dir = path.join(process.cwd(), "findings", runId);
|
|
8
|
+
if (!fs.existsSync(dir)) return [];
|
|
9
|
+
const out: Finding[] = [];
|
|
10
|
+
for (const file of fs.readdirSync(dir)) {
|
|
11
|
+
if (!file.endsWith(".json")) continue;
|
|
12
|
+
try {
|
|
13
|
+
out.push(JSON.parse(fs.readFileSync(path.join(dir, file), "utf-8")));
|
|
14
|
+
} catch { /* skip */ }
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractEvents(lines: string[]): string[] {
|
|
20
|
+
const events: string[] = [];
|
|
21
|
+
let navCount = 0;
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (/^\[(explorer|browser|regression)\] .+ (start|done|cancelled)/.test(line)) {
|
|
24
|
+
events.push(line.trim());
|
|
25
|
+
} else if (/→ \[findings\] saved:/.test(line)) {
|
|
26
|
+
events.push(line.trim());
|
|
27
|
+
} else if (/→ navigate\(/.test(line) && navCount < 25) {
|
|
28
|
+
navCount++;
|
|
29
|
+
const t = line.trim();
|
|
30
|
+
events.push(t.length > 100 ? t.slice(0, 100) + "…" : t);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return events;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function generateDiary(runId: string, logLines: string[]): Promise<string> {
|
|
37
|
+
const findings = loadFindings(runId);
|
|
38
|
+
const events = extractEvents(logLines);
|
|
39
|
+
|
|
40
|
+
const findingsSummary = findings.length > 0
|
|
41
|
+
? findings.map((f) => `- [${f.category}] ${f.title}`).join("\n")
|
|
42
|
+
: "(発見なし)";
|
|
43
|
+
|
|
44
|
+
const eventsText = events.length > 0
|
|
45
|
+
? events.join("\n")
|
|
46
|
+
: "(イベントログなし)";
|
|
47
|
+
|
|
48
|
+
const { client, defaultModel } = createLLMClient();
|
|
49
|
+
|
|
50
|
+
const msg = await client.createMessage({
|
|
51
|
+
model: defaultModel,
|
|
52
|
+
max_tokens: 1500,
|
|
53
|
+
system: `あなたは AI エージェント群の探索を、読み手の心を動かす「探索日誌」として記録する書記役です。
|
|
54
|
+
エンジニアだけでなく、プロダクトオーナーやデザイナーにも伝わる、物語体の日本語で書いてください。
|
|
55
|
+
技術的なログを人間味あふれる冒険譚に変換するのがあなたの仕事です。`,
|
|
56
|
+
tools: [],
|
|
57
|
+
messages: [
|
|
58
|
+
{
|
|
59
|
+
role: "user",
|
|
60
|
+
content: `以下の探索ログをもとに、shoal エージェント群の「探索日誌」を Markdown 形式で作成してください。
|
|
61
|
+
|
|
62
|
+
## 発見された問題(${findings.length}件)
|
|
63
|
+
${findingsSummary}
|
|
64
|
+
|
|
65
|
+
## 主要イベントログ
|
|
66
|
+
${eventsText}
|
|
67
|
+
|
|
68
|
+
## 作成ルール
|
|
69
|
+
- タイトルは \`# 探索日誌 — ${runId}\`
|
|
70
|
+
- explorer エージェントを「地図製作者」、browser を「現地調査員」、regression を「検証係」として擬人化する
|
|
71
|
+
- 各エージェントの動きを旅人の行動として物語る(「〇〇は△△のページへと足を踏み入れた」など)
|
|
72
|
+
- 発見した問題を「驚き」や「発見」として自然に物語に組み込む
|
|
73
|
+
- 全体で 400〜700 字程度のコンパクトな物語にまとめる
|
|
74
|
+
- 最後に「## 今回の旅のまとめ」セクションを箇条書きで追加する
|
|
75
|
+
- Markdown のみで出力する(説明文は不要)`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const text = msg.content
|
|
81
|
+
.filter((b) => b.type === "text")
|
|
82
|
+
.map((b) => (b as { type: "text"; text: string }).text)
|
|
83
|
+
.join("");
|
|
84
|
+
|
|
85
|
+
const diaryPath = path.join(process.cwd(), "logs", `diary_${runId}.md`);
|
|
86
|
+
fs.mkdirSync(path.dirname(diaryPath), { recursive: true });
|
|
87
|
+
fs.writeFileSync(diaryPath, text, "utf-8");
|
|
88
|
+
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function getDiaryPath(runId: string): string | null {
|
|
93
|
+
if (!/^run_\d+$/.test(runId)) return null;
|
|
94
|
+
const p = path.join(process.cwd(), "logs", `diary_${runId}.md`);
|
|
95
|
+
return fs.existsSync(p) ? p : null;
|
|
96
|
+
}
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
|
7
7
|
import { listRuns, getReportPath } from "./runs.js";
|
|
8
8
|
import { activeSessions, spawnRun, cancelSession } from "./runner.js";
|
|
9
9
|
import { loadSchedule, saveSchedule, startScheduler, type ScheduleConfig } from "./scheduler.js";
|
|
10
|
+
import { generateDiary, getDiaryPath } from "../framework/diary.js";
|
|
10
11
|
|
|
11
12
|
function specFilePath(baseUrl: string): string {
|
|
12
13
|
try {
|
|
@@ -90,6 +91,43 @@ app.get("/api/runs", (_req, res) => {
|
|
|
90
91
|
res.json(enriched);
|
|
91
92
|
});
|
|
92
93
|
|
|
94
|
+
// ----------------------------------------------------------------
|
|
95
|
+
// API: diary for a run
|
|
96
|
+
// ----------------------------------------------------------------
|
|
97
|
+
app.get("/api/runs/:runId/diary", (req, res) => {
|
|
98
|
+
const { runId } = req.params;
|
|
99
|
+
if (!isValidRunId(runId)) { res.status(400).json({ error: "invalid run id" }); return; }
|
|
100
|
+
const p = getDiaryPath(runId);
|
|
101
|
+
if (!p) { res.status(404).json({ error: "diary not found" }); return; }
|
|
102
|
+
res.json({ content: readFileSync(p, "utf-8") });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
app.post("/api/runs/:runId/diary", async (req, res) => {
|
|
106
|
+
const { runId } = req.params;
|
|
107
|
+
if (!isValidRunId(runId)) { res.status(400).json({ error: "invalid run id" }); return; }
|
|
108
|
+
|
|
109
|
+
const session = activeSessions.get(runId);
|
|
110
|
+
let lines: string[];
|
|
111
|
+
if (session) {
|
|
112
|
+
lines = session.lines;
|
|
113
|
+
} else {
|
|
114
|
+
const logFilePath = safeLogPath(`log_${runId}.txt`);
|
|
115
|
+
if (!logFilePath || !existsSync(logFilePath)) {
|
|
116
|
+
res.status(404).json({ error: "no log found" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
lines = readFileSync(logFilePath, "utf-8").split("\n").filter((l) => l !== "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const content = await generateDiary(runId, lines);
|
|
124
|
+
res.json({ content });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.error("[diary] generation failed:", err);
|
|
127
|
+
res.status(500).json({ error: "diary generation failed" });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
93
131
|
// ----------------------------------------------------------------
|
|
94
132
|
// API: serve HTML report for a run
|
|
95
133
|
// ----------------------------------------------------------------
|