@akalsey/sapience 0.1.3 → 0.2.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/dist/src/dashboard.js +201 -0
- package/dist/src/delivery.js +8 -0
- package/dist/src/events.js +12 -0
- package/dist/src/service.js +36 -0
- package/dist/src/types.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, stat, rename } from "fs/promises";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { resolvePath } from "./utils.js";
|
|
4
|
+
import { loadProfile } from "./calibration.js";
|
|
5
|
+
const SPARK = "▁▂▃▄▅▆▇█";
|
|
6
|
+
function safe(s) {
|
|
7
|
+
return String(s).replace(/\|/g, "\\|");
|
|
8
|
+
}
|
|
9
|
+
const MAX_EVENTS_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
11
|
+
const SKIP_TYPES = new Set(["pass_skipped", "routing_skipped", "check_skipped"]);
|
|
12
|
+
export function parseEvents(raw) {
|
|
13
|
+
const events = [];
|
|
14
|
+
let malformed = 0;
|
|
15
|
+
for (const line of raw.split("\n")) {
|
|
16
|
+
if (!line.trim())
|
|
17
|
+
continue;
|
|
18
|
+
try {
|
|
19
|
+
const ev = JSON.parse(line);
|
|
20
|
+
if (typeof ev.ts === "string" && typeof ev.type === "string")
|
|
21
|
+
events.push(ev);
|
|
22
|
+
else
|
|
23
|
+
malformed++;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
malformed++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { events, malformed };
|
|
30
|
+
}
|
|
31
|
+
export function sparkline(values) {
|
|
32
|
+
return values
|
|
33
|
+
.map(v => SPARK[Math.min(7, Math.max(0, Math.round(v * 7)))])
|
|
34
|
+
.join("");
|
|
35
|
+
}
|
|
36
|
+
export function confidenceTrend(events, domain, actionClass, currentConfidence, now) {
|
|
37
|
+
const cutoff = now.getTime() - 7 * DAY_MS;
|
|
38
|
+
const changes = events
|
|
39
|
+
.filter(e => e.type === "calibration_change" && e.domain === domain && e.action_class === actionClass)
|
|
40
|
+
.filter(e => new Date(e.ts).getTime() >= cutoff)
|
|
41
|
+
.sort((a, b) => (a.ts < b.ts ? -1 : 1));
|
|
42
|
+
if (changes.length === 0)
|
|
43
|
+
return "(no history yet)";
|
|
44
|
+
const series = changes
|
|
45
|
+
.map(e => Number(e.new_confidence))
|
|
46
|
+
.filter(n => !Number.isNaN(n));
|
|
47
|
+
if (series.length === 0)
|
|
48
|
+
return "(no history yet)";
|
|
49
|
+
const baselineRaw = changes[0].old_confidence;
|
|
50
|
+
const baseline = typeof baselineRaw === "number" ? baselineRaw : (series[0] ?? currentConfidence);
|
|
51
|
+
const delta = currentConfidence - baseline;
|
|
52
|
+
const arrow = delta > 0.001 ? "↑" : delta < -0.001 ? "↓" : "→";
|
|
53
|
+
const sign = delta >= 0 ? "+" : "";
|
|
54
|
+
return `${sparkline(series.slice(-10))} ${arrow} ${sign}${delta.toFixed(2)}`;
|
|
55
|
+
}
|
|
56
|
+
function within(e, now, ms) {
|
|
57
|
+
const t = new Date(e.ts).getTime();
|
|
58
|
+
return !Number.isNaN(t) && now.getTime() - t <= ms && t <= now.getTime() + 60_000;
|
|
59
|
+
}
|
|
60
|
+
function fmtTime(ts) {
|
|
61
|
+
return ts.slice(0, 16).replace("T", " ");
|
|
62
|
+
}
|
|
63
|
+
function expectedRuns(activeHours) {
|
|
64
|
+
const [sh, sm] = activeHours.start.split(":").map(Number);
|
|
65
|
+
const [eh, em] = activeHours.end.split(":").map(Number);
|
|
66
|
+
const minutes = ((eh ?? 0) * 60 + (em ?? 0)) - ((sh ?? 0) * 60 + (sm ?? 0));
|
|
67
|
+
if (Number.isNaN(minutes))
|
|
68
|
+
return 0;
|
|
69
|
+
return Math.max(0, Math.floor(minutes / 15) + 1);
|
|
70
|
+
}
|
|
71
|
+
function skipSummary(skips) {
|
|
72
|
+
const byReason = new Map();
|
|
73
|
+
for (const e of skips) {
|
|
74
|
+
const r = String(e.reason ?? "unknown");
|
|
75
|
+
byReason.set(r, (byReason.get(r) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
if (byReason.size === 0)
|
|
78
|
+
return "0";
|
|
79
|
+
return [...byReason.entries()].map(([r, n]) => `${n} ${r}`).join(", ");
|
|
80
|
+
}
|
|
81
|
+
function lastActivity(events) {
|
|
82
|
+
if (events.length === 0)
|
|
83
|
+
return "—";
|
|
84
|
+
return fmtTime(events.map(e => e.ts).sort().at(-1));
|
|
85
|
+
}
|
|
86
|
+
function describeEvent(e) {
|
|
87
|
+
switch (e.type) {
|
|
88
|
+
case "pass_completed":
|
|
89
|
+
return e.nothing_to_report
|
|
90
|
+
? `thinking pass ${safe(e.pass_id)}: nothing to report`
|
|
91
|
+
: `thinking pass ${safe(e.pass_id)}: ${e.observations} obs, ${e.actions} actions, ${e.audits} audits, ${e.questions} questions`;
|
|
92
|
+
case "routing_completed":
|
|
93
|
+
return `routed ${e.items} item(s) from ${e.passes} pass(es)`;
|
|
94
|
+
case "calibration_change":
|
|
95
|
+
return `calibration ${safe(e.domain)}/${safe(e.action_class)}: ${e.old_tier ?? "new"}→${e.new_tier} conf ${e.new_confidence}`;
|
|
96
|
+
case "action_logged":
|
|
97
|
+
return `autonomous action (${safe(e.domain)}/${safe(e.action_class)})`;
|
|
98
|
+
case "digest_delivered":
|
|
99
|
+
return "weekly digest delivered";
|
|
100
|
+
case "signal_detected":
|
|
101
|
+
return `${safe(e.signal_type)} captured (${safe(e.domain)}, ${safe(e.source)})`;
|
|
102
|
+
case "signal_orphaned":
|
|
103
|
+
return `${safe(e.signal_type)} matched no calibration entry (${safe(e.domain)})`;
|
|
104
|
+
case "goal_created":
|
|
105
|
+
return `goal created (${safe(e.goal_id)})`;
|
|
106
|
+
case "status_delivered":
|
|
107
|
+
return `goal status delivered (${safe(e.goal_id)})`;
|
|
108
|
+
default:
|
|
109
|
+
return String(e.type);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function buildDashboard(input) {
|
|
113
|
+
const { events, malformed, profile, goals, activeHours, now } = input;
|
|
114
|
+
const d30 = events.filter(e => within(e, now, 30 * DAY_MS));
|
|
115
|
+
const d7 = d30.filter(e => within(e, now, 7 * DAY_MS));
|
|
116
|
+
const d24 = d30.filter(e => within(e, now, DAY_MS));
|
|
117
|
+
const lines = [];
|
|
118
|
+
lines.push("# Sapience Dashboard", "");
|
|
119
|
+
lines.push(`Generated: ${fmtTime(now.toISOString())} UTC · Period: last 30 days`, "");
|
|
120
|
+
lines.push("## Autonomy progression", "");
|
|
121
|
+
if (profile.length === 0) {
|
|
122
|
+
lines.push("No calibration entries yet.", "");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lines.push("| Domain / class | Tier | Confidence | 7d trend | Confirmed | Corrected |");
|
|
126
|
+
lines.push("| --- | --- | --- | --- | --- | --- |");
|
|
127
|
+
for (const e of [...profile].sort((a, b) => b.confidence - a.confidence)) {
|
|
128
|
+
lines.push(`| ${safe(e.domain)} / ${safe(e.action_class)} | ${e.tier} | ${e.confidence.toFixed(2)} | ` +
|
|
129
|
+
`${confidenceTrend(d7, e.domain, e.action_class, e.confidence, now)} | ` +
|
|
130
|
+
`${e.confirmed_count} | ${e.corrected_count} |`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("");
|
|
133
|
+
}
|
|
134
|
+
const tierChanges = d30.filter(e => e.type === "calibration_change" && e.old_tier && e.new_tier && e.old_tier !== e.new_tier);
|
|
135
|
+
lines.push(`**Tier changes (30d):** ${tierChanges.length === 0 ? "none" : tierChanges
|
|
136
|
+
.map(e => `${e.domain}/${e.action_class} ${e.old_tier}→${e.new_tier} (${String(e.ts).slice(0, 10)})`)
|
|
137
|
+
.join(" · ")}`);
|
|
138
|
+
const acted7 = d7.filter(e => e.type === "action_logged").length;
|
|
139
|
+
const acted30 = d30.filter(e => e.type === "action_logged").length;
|
|
140
|
+
lines.push(`**Autonomous actions:** ${acted7} in last 7d · ${acted30} in last 30d`, "");
|
|
141
|
+
lines.push("## Heartbeat", "");
|
|
142
|
+
const exp = expectedRuns(activeHours);
|
|
143
|
+
const of = (plugin, types) => d24.filter(e => e.plugin === plugin && types.includes(String(e.type)));
|
|
144
|
+
lines.push("| Plugin | Runs (24h) | Expected | Skips (24h) | Last activity |");
|
|
145
|
+
lines.push("| --- | --- | --- | --- | --- |");
|
|
146
|
+
lines.push(`| thinking | ${of("thinking", ["pass_completed"]).length} | ~${exp} | ${skipSummary(of("thinking", ["pass_skipped"]))} | ${lastActivity(events.filter(e => e.plugin === "thinking"))} |`);
|
|
147
|
+
lines.push(`| sapience | ${of("sapience", ["routing_completed"]).length} | ~${exp} | ${skipSummary(of("sapience", ["routing_skipped"]))} | ${lastActivity(events.filter(e => e.plugin === "sapience"))} |`);
|
|
148
|
+
lines.push(`| feedback | ${of("feedback", ["signal_detected", "signal_orphaned"]).length} signals | — | — | ${lastActivity(events.filter(e => e.plugin === "feedback"))} |`);
|
|
149
|
+
lines.push(`| goals | ${of("goals", ["goal_created", "status_delivered", "check_skipped"]).length} | ~${exp} | ${skipSummary(of("goals", ["check_skipped"]))} | ${lastActivity(events.filter(e => e.plugin === "goals"))} |`);
|
|
150
|
+
const active = goals.filter(g => g.status === "active").length;
|
|
151
|
+
const decomposing = goals.filter(g => g.status === "decomposing").length;
|
|
152
|
+
lines.push("", `**Goals:** ${active} active · ${decomposing} decomposing · ${goals.length} total`, "");
|
|
153
|
+
lines.push("## Recent activity", "");
|
|
154
|
+
const notable = d30
|
|
155
|
+
.filter(e => !SKIP_TYPES.has(String(e.type)))
|
|
156
|
+
.sort((a, b) => (a.ts < b.ts ? 1 : -1))
|
|
157
|
+
.slice(0, 15);
|
|
158
|
+
if (notable.length === 0)
|
|
159
|
+
lines.push("No event data yet.");
|
|
160
|
+
for (const e of notable)
|
|
161
|
+
lines.push(`- ${fmtTime(e.ts)} ${describeEvent(e)}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
if (malformed > 0)
|
|
164
|
+
lines.push(`_${malformed} malformed event line(s) skipped._`, "");
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
// Known benign race: a concurrent appendEvent between stat and rename can lose
|
|
168
|
+
// at most one event line. This is acceptable for observability data.
|
|
169
|
+
export async function rotateIfNeeded(eventsPath, now, maxBytes = MAX_EVENTS_BYTES) {
|
|
170
|
+
try {
|
|
171
|
+
const s = await stat(eventsPath);
|
|
172
|
+
if (s.size > maxBytes) {
|
|
173
|
+
const stamp = now.toISOString().slice(0, 19).replace(/[T:]/g, "-");
|
|
174
|
+
await rename(eventsPath, join(dirname(eventsPath), `events-archive-${stamp}.jsonl`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// missing file: nothing to rotate
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export async function generateDashboard(config, now = new Date()) {
|
|
182
|
+
const eventsPath = resolvePath(config.output.eventsPath);
|
|
183
|
+
await rotateIfNeeded(eventsPath, now);
|
|
184
|
+
let raw = "";
|
|
185
|
+
try {
|
|
186
|
+
raw = await readFile(eventsPath, "utf-8");
|
|
187
|
+
}
|
|
188
|
+
catch { /* no events yet */ }
|
|
189
|
+
const { events, malformed } = parseEvents(raw);
|
|
190
|
+
const profile = await loadProfile(config.output.calibrationPath);
|
|
191
|
+
let goals = [];
|
|
192
|
+
try {
|
|
193
|
+
goals = JSON.parse(await readFile(resolvePath(config.output.goalsPath), "utf-8"));
|
|
194
|
+
}
|
|
195
|
+
catch { /* no goals file */ }
|
|
196
|
+
const md = buildDashboard({ events, malformed, profile, goals, activeHours: config.activeHours, now });
|
|
197
|
+
const out = resolvePath(config.output.dashboardPath);
|
|
198
|
+
await mkdir(dirname(out), { recursive: true });
|
|
199
|
+
await writeFile(out + ".tmp", md, "utf-8");
|
|
200
|
+
await rename(out + ".tmp", out);
|
|
201
|
+
}
|
package/dist/src/delivery.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendAction } from "./action-log.js";
|
|
2
|
+
import { appendEvent } from "./events.js";
|
|
2
3
|
export function buildTierPrompt(item) {
|
|
3
4
|
switch (item.tier) {
|
|
4
5
|
case "act":
|
|
@@ -45,6 +46,13 @@ export async function deliverItems(items, api, config) {
|
|
|
45
46
|
for (const item of sorted) {
|
|
46
47
|
if (item.tier === "act") {
|
|
47
48
|
await appendAction(item, "Queued for immediate execution", config.output.actionLogPath);
|
|
49
|
+
await appendEvent(config.output.eventsPath, {
|
|
50
|
+
plugin: "sapience",
|
|
51
|
+
type: "action_logged",
|
|
52
|
+
domain: item.domain,
|
|
53
|
+
action_class: item.action_class,
|
|
54
|
+
confidence: item.confidence,
|
|
55
|
+
});
|
|
48
56
|
}
|
|
49
57
|
await api.session.workflow.enqueueNextTurnInjection({
|
|
50
58
|
sessionTarget: "main",
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "fs/promises";
|
|
2
|
+
import { dirname } from "path";
|
|
3
|
+
export async function appendEvent(eventsPath, event) {
|
|
4
|
+
try {
|
|
5
|
+
const full = { ...event, ts: event.ts ?? new Date().toISOString() };
|
|
6
|
+
await mkdir(dirname(eventsPath), { recursive: true });
|
|
7
|
+
await appendFile(eventsPath, JSON.stringify(full) + "\n", "utf-8");
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// Observability must never break the host plugin.
|
|
11
|
+
}
|
|
12
|
+
}
|
package/dist/src/service.js
CHANGED
|
@@ -10,6 +10,8 @@ import { readUnprocessedPasses, proposalSetToItems } from "./proposal-adapter.js
|
|
|
10
10
|
import { loadProcessedPasses, markPassProcessed, bootstrapProcessedPasses } from "./processed-passes.js";
|
|
11
11
|
import { deliverItems } from "./delivery.js";
|
|
12
12
|
import { isDigestDay, buildDigestPrompt } from "./weekly-digest.js";
|
|
13
|
+
import { appendEvent } from "./events.js";
|
|
14
|
+
import { generateDashboard } from "./dashboard.js";
|
|
13
15
|
function mergeConfig(raw, workspaceDir) {
|
|
14
16
|
return {
|
|
15
17
|
...DEFAULT_CONFIG,
|
|
@@ -29,6 +31,9 @@ function mergeConfig(raw, workspaceDir) {
|
|
|
29
31
|
calibrationPath: resolveDataPath(raw.output?.calibrationPath, workspaceDir, DEFAULT_CONFIG.output.calibrationPath),
|
|
30
32
|
actionLogPath: resolveDataPath(raw.output?.actionLogPath, workspaceDir, DEFAULT_CONFIG.output.actionLogPath),
|
|
31
33
|
processedPassesPath: resolveDataPath(raw.output?.processedPassesPath, workspaceDir, DEFAULT_CONFIG.output.processedPassesPath),
|
|
34
|
+
eventsPath: resolveDataPath(raw.output?.eventsPath, workspaceDir, DEFAULT_CONFIG.output.eventsPath),
|
|
35
|
+
dashboardPath: resolveDataPath(raw.output?.dashboardPath, workspaceDir, DEFAULT_CONFIG.output.dashboardPath),
|
|
36
|
+
goalsPath: resolveDataPath(raw.output?.goalsPath, workspaceDir, DEFAULT_CONFIG.output.goalsPath),
|
|
32
37
|
},
|
|
33
38
|
};
|
|
34
39
|
}
|
|
@@ -50,6 +55,8 @@ export default definePluginEntry({
|
|
|
50
55
|
async execute(_id, _params) {
|
|
51
56
|
try {
|
|
52
57
|
if (!isWithinActiveHours(config)) {
|
|
58
|
+
await appendEvent(config.output.eventsPath, { plugin: "sapience", type: "routing_skipped", reason: "outside_hours" });
|
|
59
|
+
await generateDashboard(config).catch(() => { });
|
|
53
60
|
return { content: [{ type: "text", text: "SILENT_REPLY_TOKEN" }] };
|
|
54
61
|
}
|
|
55
62
|
let processed = await loadProcessedPasses(config.output.processedPassesPath);
|
|
@@ -61,26 +68,55 @@ export default definePluginEntry({
|
|
|
61
68
|
const newPasses = await readUnprocessedPasses(config.proactiveThinking.proposalsPath, processed);
|
|
62
69
|
let updatedProcessed = processed;
|
|
63
70
|
let updatedProfile = profile;
|
|
71
|
+
let totalItems = 0;
|
|
72
|
+
const byTier = {};
|
|
64
73
|
for (const pass of newPasses) {
|
|
65
74
|
const items = proposalSetToItems(pass);
|
|
66
75
|
const routed = items.map(item => routeItem(item, updatedProfile, config));
|
|
67
76
|
await deliverItems(routed, api, config);
|
|
68
77
|
updatedProcessed = await markPassProcessed(pass.pass_id, config.output.processedPassesPath, updatedProcessed);
|
|
69
78
|
for (const item of routed) {
|
|
79
|
+
totalItems++;
|
|
80
|
+
byTier[item.tier] = (byTier[item.tier] ?? 0) + 1;
|
|
70
81
|
const exists = updatedProfile.find(e => e.domain === item.domain && e.action_class === item.action_class);
|
|
71
82
|
if (!exists) {
|
|
72
83
|
updatedProfile = upsertEntry(updatedProfile, item.domain, item.action_class, {
|
|
73
84
|
tier: config.autonomy.defaultTier,
|
|
74
85
|
confidence: 0,
|
|
75
86
|
});
|
|
87
|
+
await appendEvent(config.output.eventsPath, {
|
|
88
|
+
plugin: "sapience",
|
|
89
|
+
type: "calibration_change",
|
|
90
|
+
domain: item.domain,
|
|
91
|
+
action_class: item.action_class,
|
|
92
|
+
old_confidence: null,
|
|
93
|
+
new_confidence: 0,
|
|
94
|
+
old_tier: null,
|
|
95
|
+
new_tier: config.autonomy.defaultTier,
|
|
96
|
+
source: "new_entry",
|
|
97
|
+
});
|
|
76
98
|
}
|
|
77
99
|
}
|
|
78
100
|
}
|
|
79
101
|
await saveProfile(updatedProfile, config.output.calibrationPath);
|
|
102
|
+
if (newPasses.length === 0) {
|
|
103
|
+
await appendEvent(config.output.eventsPath, { plugin: "sapience", type: "routing_skipped", reason: "no_new_passes" });
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
await appendEvent(config.output.eventsPath, {
|
|
107
|
+
plugin: "sapience",
|
|
108
|
+
type: "routing_completed",
|
|
109
|
+
passes: newPasses.length,
|
|
110
|
+
items: totalItems,
|
|
111
|
+
by_tier: byTier,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
80
114
|
if (config.digest.enabled && isDigestDay(config)) {
|
|
81
115
|
const prompt = await buildDigestPrompt(config);
|
|
82
116
|
await api.session.workflow.enqueueNextTurnInjection({ sessionTarget: "main", text: prompt });
|
|
117
|
+
await appendEvent(config.output.eventsPath, { plugin: "sapience", type: "digest_delivered" });
|
|
83
118
|
}
|
|
119
|
+
await generateDashboard(config).catch(() => { });
|
|
84
120
|
return { content: [{ type: "text", text: "SILENT_REPLY_TOKEN" }] };
|
|
85
121
|
}
|
|
86
122
|
catch (err) {
|
package/dist/src/types.js
CHANGED
|
@@ -18,5 +18,8 @@ export const DEFAULT_CONFIG = {
|
|
|
18
18
|
calibrationPath: "sapience/calibration.json",
|
|
19
19
|
actionLogPath: "sapience/action-log.md",
|
|
20
20
|
processedPassesPath: "sapience/processed-passes.json",
|
|
21
|
+
eventsPath: "sapience/events.jsonl",
|
|
22
|
+
dashboardPath: "sapience/dashboard.md",
|
|
23
|
+
goalsPath: "goals/goals.json",
|
|
21
24
|
},
|
|
22
25
|
};
|