@schlessera/brain-ui-server 0.18.0 → 0.19.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/activity/digest.d.ts +29 -0
- package/dist/activity/digest.d.ts.map +1 -0
- package/dist/activity/digest.js +61 -0
- package/dist/activity/digest.js.map +1 -0
- package/dist/activity/notify.d.ts +62 -0
- package/dist/activity/notify.d.ts.map +1 -0
- package/dist/activity/notify.js +171 -0
- package/dist/activity/notify.js.map +1 -0
- package/dist/activity/push-sender.d.ts +63 -0
- package/dist/activity/push-sender.d.ts.map +1 -0
- package/dist/activity/push-sender.js +131 -0
- package/dist/activity/push-sender.js.map +1 -0
- package/dist/activity/query.d.ts +16 -0
- package/dist/activity/query.d.ts.map +1 -0
- package/dist/activity/query.js +128 -0
- package/dist/activity/query.js.map +1 -0
- package/dist/activity/recorder.d.ts +48 -0
- package/dist/activity/recorder.d.ts.map +1 -0
- package/dist/activity/recorder.js +205 -0
- package/dist/activity/recorder.js.map +1 -0
- package/dist/activity/runtime.d.ts +31 -0
- package/dist/activity/runtime.d.ts.map +1 -0
- package/dist/activity/runtime.js +109 -0
- package/dist/activity/runtime.js.map +1 -0
- package/dist/activity/span-sink.d.ts +22 -0
- package/dist/activity/span-sink.d.ts.map +1 -0
- package/dist/activity/span-sink.js +138 -0
- package/dist/activity/span-sink.js.map +1 -0
- package/dist/activity/store.d.ts +207 -0
- package/dist/activity/store.d.ts.map +1 -0
- package/dist/activity/store.js +440 -0
- package/dist/activity/store.js.map +1 -0
- package/dist/activity/stream.d.ts +45 -0
- package/dist/activity/stream.d.ts.map +1 -0
- package/dist/activity/stream.js +312 -0
- package/dist/activity/stream.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +16 -0
- package/dist/app.js.map +1 -1
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +20 -5
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +32 -6
- package/dist/db/client.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/activity.d.ts +20 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +300 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/push.d.ts +6 -0
- package/dist/routes/push.d.ts.map +1 -0
- package/dist/routes/push.js +73 -0
- package/dist/routes/push.js.map +1 -0
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +20 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/ws/bridge.d.ts +2 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +27 -2
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +9 -1
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +11 -0
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +16 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +2 -0
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +21 -1
- package/dist/ws/run-session.js.map +1 -1
- package/migrations/007_activity.sql +114 -0
- package/migrations/008_push_subscriptions.sql +24 -0
- package/package.json +5 -3
- package/src/activity/digest.ts +92 -0
- package/src/activity/notify.ts +285 -0
- package/src/activity/push-sender.ts +215 -0
- package/src/activity/query.ts +160 -0
- package/src/activity/recorder.ts +261 -0
- package/src/activity/runtime.ts +141 -0
- package/src/activity/span-sink.ts +152 -0
- package/src/activity/store.ts +774 -0
- package/src/activity/stream.ts +366 -0
- package/src/app.ts +17 -0
- package/src/cron/scheduler.ts +28 -8
- package/src/db/client.ts +31 -6
- package/src/index.ts +22 -0
- package/src/routes/activity.ts +363 -0
- package/src/routes/push.ts +90 -0
- package/src/routes/sessions.ts +24 -4
- package/src/ws/bridge.ts +29 -2
- package/src/ws/connection.ts +9 -1
- package/src/ws/dispatch.ts +13 -0
- package/src/ws/host.ts +18 -0
- package/src/ws/run-session.ts +28 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { isFailureOutcome } from "@schlessera/brain-ui-sdk/protocol";
|
|
2
|
+
import { latestActivityDigest } from "./digest.js";
|
|
3
|
+
import { rowToRunRollup } from "./store.js";
|
|
4
|
+
export function runActivityQuery(db, store, query, notifier) {
|
|
5
|
+
const limit = Math.min(Math.max(query.limit ?? 20, 1), 100);
|
|
6
|
+
const hoursBack = Math.min(Math.max(query.hoursBack ?? 24, 1), 24 * 90);
|
|
7
|
+
const since = Date.now() - hoursBack * 60 * 60 * 1000;
|
|
8
|
+
switch (query.scope) {
|
|
9
|
+
case "running": {
|
|
10
|
+
return {
|
|
11
|
+
running: store.openRootSpans().map(liveSummary),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
case "recent": {
|
|
15
|
+
const rows = db
|
|
16
|
+
.query("SELECT * FROM activity_run_rollups WHERE started_at >= ? ORDER BY started_at DESC LIMIT ?")
|
|
17
|
+
.all(since, limit).map(rowToRunRollup);
|
|
18
|
+
return {
|
|
19
|
+
windowHours: hoursBack,
|
|
20
|
+
running: store.openRootSpans().map(liveSummary),
|
|
21
|
+
finished: rows.map((r) => ({
|
|
22
|
+
runId: r.runId,
|
|
23
|
+
origin: r.origin,
|
|
24
|
+
name: r.name,
|
|
25
|
+
jobName: r.jobName,
|
|
26
|
+
sessionId: r.sessionId,
|
|
27
|
+
startedAt: iso(r.startedAt),
|
|
28
|
+
outcome: r.outcome,
|
|
29
|
+
durationMs: r.durationMs,
|
|
30
|
+
costUsd: r.costUsd,
|
|
31
|
+
failureReason: r.failureReason,
|
|
32
|
+
})),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
case "run": {
|
|
36
|
+
if (!query.runId)
|
|
37
|
+
return { error: "scope 'run' needs runId" };
|
|
38
|
+
const snapshot = store.snapshotRun(query.runId);
|
|
39
|
+
if (snapshot) {
|
|
40
|
+
return {
|
|
41
|
+
runId: query.runId,
|
|
42
|
+
spans: snapshot.spans.map((s) => ({
|
|
43
|
+
spanId: s.spanId,
|
|
44
|
+
parent: s.parentSpanId,
|
|
45
|
+
name: s.name,
|
|
46
|
+
kind: s.kind,
|
|
47
|
+
startedAt: iso(s.startedAt),
|
|
48
|
+
endedAt: s.endedAt ? iso(s.endedAt) : null,
|
|
49
|
+
outcome: s.outcome,
|
|
50
|
+
reason: s.outcomeReason,
|
|
51
|
+
costUsd: s.usage.costUsd,
|
|
52
|
+
tokens: s.usage.inputTokens !== undefined || s.usage.outputTokens !== undefined
|
|
53
|
+
? { in: s.usage.inputTokens, out: s.usage.outputTokens }
|
|
54
|
+
: undefined,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const row = db
|
|
59
|
+
.query("SELECT * FROM activity_run_rollups WHERE run_id = ?")
|
|
60
|
+
.get(query.runId);
|
|
61
|
+
if (!row)
|
|
62
|
+
return { error: `unknown run ${query.runId}` };
|
|
63
|
+
const rollup = rowToRunRollup(row);
|
|
64
|
+
return {
|
|
65
|
+
runId: query.runId,
|
|
66
|
+
detailPruned: true,
|
|
67
|
+
rollup: {
|
|
68
|
+
name: rollup.name,
|
|
69
|
+
jobName: rollup.jobName,
|
|
70
|
+
outcome: rollup.outcome,
|
|
71
|
+
startedAt: iso(rollup.startedAt),
|
|
72
|
+
durationMs: rollup.durationMs,
|
|
73
|
+
costUsd: rollup.costUsd,
|
|
74
|
+
failureReason: rollup.failureReason,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case "rollups": {
|
|
79
|
+
const rows = db
|
|
80
|
+
.query("SELECT * FROM activity_run_rollups WHERE started_at >= ?")
|
|
81
|
+
.all(since).map(rowToRunRollup);
|
|
82
|
+
const digest = latestActivityDigest(db);
|
|
83
|
+
return {
|
|
84
|
+
windowHours: hoursBack,
|
|
85
|
+
runs: rows.length,
|
|
86
|
+
failures: rows.filter((r) => isFailureOutcome(r.outcome)).length,
|
|
87
|
+
costUsd: round(rows.reduce((a, r) => a + (r.costUsd ?? 0), 0)),
|
|
88
|
+
inputTokens: rows.reduce((a, r) => a + (r.inputTokens ?? 0), 0),
|
|
89
|
+
outputTokens: rows.reduce((a, r) => a + (r.outputTokens ?? 0), 0),
|
|
90
|
+
...(digest ? { lastDigestAt: iso(digest.generatedAt) } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
case "inbox": {
|
|
94
|
+
// The unacknowledged notification inbox — the same list the UI shows.
|
|
95
|
+
if (!notifier)
|
|
96
|
+
return { error: "inbox unavailable" };
|
|
97
|
+
return {
|
|
98
|
+
intents: notifier.inbox(limit).map((i) => ({
|
|
99
|
+
kind: i.kind,
|
|
100
|
+
title: i.title,
|
|
101
|
+
status: i.status,
|
|
102
|
+
createdAt: iso(i.createdAt),
|
|
103
|
+
runId: i.runId,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
return { error: `unknown scope` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function liveSummary(span) {
|
|
112
|
+
return {
|
|
113
|
+
runId: span.runId,
|
|
114
|
+
origin: span.origin,
|
|
115
|
+
name: span.name,
|
|
116
|
+
jobName: span.jobName,
|
|
117
|
+
sessionId: span.sessionId,
|
|
118
|
+
startedAt: iso(span.startedAt),
|
|
119
|
+
elapsedMs: Date.now() - span.startedAt,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function iso(ms) {
|
|
123
|
+
return new Date(ms).toISOString();
|
|
124
|
+
}
|
|
125
|
+
function round(n) {
|
|
126
|
+
return Math.round(n * 10000) / 10000;
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.js","sourceRoot":"","sources":["../../src/activity/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn span derivation: the host-side half of activity recording.
|
|
3
|
+
*
|
|
4
|
+
* The host already sees everything a baseline trace needs — the frames it
|
|
5
|
+
* relays (tool starts/results, the terminal result), its own approval flow
|
|
6
|
+
* (wait time, denials), and the turn lifecycle it owns (timeout, cancel).
|
|
7
|
+
* A `TurnRecorder` turns that stream into spans, so EVERY backend gets a
|
|
8
|
+
* full activity tree with zero backend work; the bridge's activity side
|
|
9
|
+
* channel enriches with what only the backend sees (subagent lifecycle,
|
|
10
|
+
* usage, transcripts).
|
|
11
|
+
*
|
|
12
|
+
* Terminal-write precedence (the write-once race): enrichment arrives on the
|
|
13
|
+
* same ordered call path as frames, and the recorder buffers the result
|
|
14
|
+
* frame's outcome/usage rather than closing the root span on sight. The ONE
|
|
15
|
+
* terminal write for the root happens in `finish()` — after the backend's
|
|
16
|
+
* startTurn resolved, when everything that could refine the outcome has
|
|
17
|
+
* drained. Frame-derived outcome is the fallback, never a competing writer.
|
|
18
|
+
*
|
|
19
|
+
* Nothing here may fail the observed turn: every public method catches and
|
|
20
|
+
* drops its own errors.
|
|
21
|
+
*/
|
|
22
|
+
import type { BackendActivityEvent, ServerMessage } from "@schlessera/brain-ui-sdk/server";
|
|
23
|
+
import type { Logger } from "@opentelemetry/api-logs";
|
|
24
|
+
import type { ActivityStore } from "./store.js";
|
|
25
|
+
export interface TurnRecorderDeps {
|
|
26
|
+
store: ActivityStore;
|
|
27
|
+
/** Called after any committed write so the live stream can pump. */
|
|
28
|
+
onWrite?: () => void;
|
|
29
|
+
log?: Logger;
|
|
30
|
+
}
|
|
31
|
+
export interface TurnRecorder {
|
|
32
|
+
/** The run id (one run per turn). */
|
|
33
|
+
readonly runId: string;
|
|
34
|
+
observeFrame(msg: ServerMessage): void;
|
|
35
|
+
observeActivity(event: BackendActivityEvent): void;
|
|
36
|
+
/** The user's approval decision arrived for a gated tool call. */
|
|
37
|
+
onApprovalDecision(toolUseId: string, allowed: boolean): void;
|
|
38
|
+
/**
|
|
39
|
+
* Close the turn: merge the buffered result enrichment into the root's
|
|
40
|
+
* single terminal write, cascade-cancel anything left open, roll up.
|
|
41
|
+
*/
|
|
42
|
+
finish(disposition: "success" | "error" | "cancelled" | "timeout"): void;
|
|
43
|
+
}
|
|
44
|
+
export declare function createTurnRecorder(deps: TurnRecorderDeps, turn: {
|
|
45
|
+
turnId: string;
|
|
46
|
+
sessionId: string | null;
|
|
47
|
+
}): TurnRecorder;
|
|
48
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../src/activity/recorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,oBAAoB,EACpB,aAAa,EAEd,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAA0B,MAAM,YAAY,CAAC;AAExE,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,aAAa,CAAC;IACrB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,qCAAqC;IACrC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;IACvC,eAAe,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACnD,kEAAkE;IAClE,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC9D;;;OAGG;IACH,MAAM,CAAC,WAAW,EAAE,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,IAAI,CAAC;CAC1E;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACjD,YAAY,CA0Md"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export function createTurnRecorder(deps, turn) {
|
|
2
|
+
const { store, onWrite, log } = deps;
|
|
3
|
+
const runId = turn.turnId;
|
|
4
|
+
const rootSpanId = `${runId}:turn`;
|
|
5
|
+
let sessionId = turn.sessionId ?? undefined;
|
|
6
|
+
let rootStarted = false;
|
|
7
|
+
let finished = false;
|
|
8
|
+
/** Buffered terminal enrichment from the result frame. */
|
|
9
|
+
let resultOutcome = null;
|
|
10
|
+
let resultUsage;
|
|
11
|
+
let resultAttrs;
|
|
12
|
+
const guard = (fn) => {
|
|
13
|
+
try {
|
|
14
|
+
fn();
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
log?.emit({
|
|
18
|
+
severityText: "WARN",
|
|
19
|
+
body: "activity recording failed; dropping",
|
|
20
|
+
attributes: { error: err instanceof Error ? err.message : String(err) },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function ensureRoot() {
|
|
25
|
+
if (rootStarted)
|
|
26
|
+
return;
|
|
27
|
+
rootStarted = true;
|
|
28
|
+
store.startSpan({
|
|
29
|
+
spanId: rootSpanId,
|
|
30
|
+
runId,
|
|
31
|
+
name: "invoke_agent",
|
|
32
|
+
kind: "turn",
|
|
33
|
+
origin: "session",
|
|
34
|
+
sessionId,
|
|
35
|
+
attrs: { "gen_ai.operation.name": "invoke_agent" },
|
|
36
|
+
});
|
|
37
|
+
onWrite?.();
|
|
38
|
+
}
|
|
39
|
+
function usageFromTurnUsage(usage) {
|
|
40
|
+
const models = usage.perModel ? Object.keys(usage.perModel) : [];
|
|
41
|
+
return {
|
|
42
|
+
inputTokens: usage.inputTokens,
|
|
43
|
+
outputTokens: usage.outputTokens,
|
|
44
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
45
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
46
|
+
model: models.length === 1 ? models[0] : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
runId,
|
|
51
|
+
observeFrame(msg) {
|
|
52
|
+
guard(() => {
|
|
53
|
+
if (finished)
|
|
54
|
+
return;
|
|
55
|
+
switch (msg.type) {
|
|
56
|
+
case "session_info": {
|
|
57
|
+
sessionId = msg.sessionId;
|
|
58
|
+
ensureRoot();
|
|
59
|
+
// The root may have been opened before the session had a name.
|
|
60
|
+
store.patchSpan(rootSpanId, { attrs: { "session.id": msg.sessionId } });
|
|
61
|
+
onWrite?.();
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "tool_use_start": {
|
|
65
|
+
ensureRoot();
|
|
66
|
+
const isSubagent = msg.toolName === "Agent";
|
|
67
|
+
store.startSpan({
|
|
68
|
+
spanId: msg.toolUseId,
|
|
69
|
+
runId,
|
|
70
|
+
parentSpanId: msg.parentToolUseId ?? rootSpanId,
|
|
71
|
+
name: `execute_tool ${msg.toolName}`,
|
|
72
|
+
kind: isSubagent ? "subagent" : "tool",
|
|
73
|
+
origin: "session",
|
|
74
|
+
sessionId,
|
|
75
|
+
attrs: {
|
|
76
|
+
"gen_ai.operation.name": isSubagent ? "invoke_agent" : "execute_tool",
|
|
77
|
+
"gen_ai.tool.name": msg.toolName,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
onWrite?.();
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "tool_approval_request": {
|
|
84
|
+
// The span exists (tool_use_start preceded it). Nothing to write:
|
|
85
|
+
// startedAt already marks the wait's beginning; the grant stamps
|
|
86
|
+
// the wait/execution boundary.
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
case "tool_result": {
|
|
90
|
+
// endSpan is write-once: a span already closed (a denial landed
|
|
91
|
+
// the outcome first) rejects this as a no-op, and an unknown
|
|
92
|
+
// span id is equally a no-op — no bookkeeping set needed.
|
|
93
|
+
const outcome = msg.isError ? "error" : "success";
|
|
94
|
+
store.endSpan(msg.toolUseId, {
|
|
95
|
+
outcome,
|
|
96
|
+
reason: outcome === "error" ? clip(msg.output, 500) : undefined,
|
|
97
|
+
});
|
|
98
|
+
onWrite?.();
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
case "result": {
|
|
102
|
+
// Buffered — the single terminal write happens in finish().
|
|
103
|
+
resultOutcome =
|
|
104
|
+
msg.outcome === "success"
|
|
105
|
+
? "success"
|
|
106
|
+
: msg.outcome === "cancelled"
|
|
107
|
+
? "cancelled"
|
|
108
|
+
: msg.outcomeDetail === "max_turns" || msg.outcomeDetail === "max_budget"
|
|
109
|
+
? "timeout"
|
|
110
|
+
: "error";
|
|
111
|
+
if (msg.usage)
|
|
112
|
+
resultUsage = usageFromTurnUsage(msg.usage);
|
|
113
|
+
if (typeof msg.costUsd === "number") {
|
|
114
|
+
resultUsage = { ...(resultUsage ?? {}), costUsd: msg.costUsd };
|
|
115
|
+
}
|
|
116
|
+
resultAttrs = {
|
|
117
|
+
...(msg.outcomeDetail ? { "outcome.detail": msg.outcomeDetail } : {}),
|
|
118
|
+
...(msg.usage?.perModel
|
|
119
|
+
? { "gen_ai.usage.per_model": msg.usage.perModel }
|
|
120
|
+
: {}),
|
|
121
|
+
};
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
default:
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
observeActivity(event) {
|
|
130
|
+
guard(() => {
|
|
131
|
+
if (finished)
|
|
132
|
+
return;
|
|
133
|
+
switch (event.kind) {
|
|
134
|
+
case "subagent_started": {
|
|
135
|
+
store.patchSpan(event.toolUseId, {
|
|
136
|
+
attrs: {
|
|
137
|
+
"subagent.type": event.subagentType,
|
|
138
|
+
"subagent.description": event.description,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
onWrite?.();
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "subagent_status": {
|
|
145
|
+
const attrs = {
|
|
146
|
+
...(event.usage ? { "subagent.total_tokens": event.usage.totalTokens } : {}),
|
|
147
|
+
...(event.summary ? { "subagent.summary": event.summary } : {}),
|
|
148
|
+
};
|
|
149
|
+
if (Object.keys(attrs).length > 0) {
|
|
150
|
+
store.patchSpan(event.toolUseId, { attrs });
|
|
151
|
+
onWrite?.();
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case "subagent_transcript": {
|
|
156
|
+
store.appendEvent(event.toolUseId, `transcript_${event.role}`, event.text);
|
|
157
|
+
onWrite?.();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
onApprovalDecision(toolUseId, allowed) {
|
|
164
|
+
guard(() => {
|
|
165
|
+
if (allowed) {
|
|
166
|
+
// Everything before this moment was approval wait, not execution.
|
|
167
|
+
store.patchSpan(toolUseId, { waitUntil: Date.now() });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// The denied outcome lands now — write-once makes the backend's
|
|
171
|
+
// later error tool_result a no-op on this span.
|
|
172
|
+
store.endSpan(toolUseId, { outcome: "denied", reason: "user declined" });
|
|
173
|
+
}
|
|
174
|
+
onWrite?.();
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
finish(disposition) {
|
|
178
|
+
guard(() => {
|
|
179
|
+
if (finished)
|
|
180
|
+
return;
|
|
181
|
+
finished = true;
|
|
182
|
+
ensureRoot();
|
|
183
|
+
// Merged precedence: the buffered result enrichment refines the
|
|
184
|
+
// host's own disposition; the host wins only where the backend said
|
|
185
|
+
// nothing. Cancellation/timeout are host-owned facts and always win.
|
|
186
|
+
const outcome = disposition === "cancelled" || disposition === "timeout"
|
|
187
|
+
? disposition
|
|
188
|
+
: (resultOutcome ?? disposition);
|
|
189
|
+
store.endSpan(rootSpanId, {
|
|
190
|
+
outcome,
|
|
191
|
+
usage: resultUsage,
|
|
192
|
+
attrs: resultAttrs,
|
|
193
|
+
});
|
|
194
|
+
// Anything still open died with the turn.
|
|
195
|
+
store.cascadeClose(runId, "cancelled", `turn ${outcome}`);
|
|
196
|
+
store.rollupRun(runId);
|
|
197
|
+
onWrite?.();
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function clip(text, max) {
|
|
203
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../../src/activity/recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The activity runtime: everything the activity record needs to live inside
|
|
3
|
+
* one app instance — store, live stream, notifier, push sender, the boot
|
|
4
|
+
* orphan sweep, and the always-on lifecycle tick (stale sweep, notification
|
|
5
|
+
* detection, push delivery, hourly prune). `createApp` constructs one and
|
|
6
|
+
* wires its pieces into the host, routes, and bridge; `close()` releases the
|
|
7
|
+
* interval and subscriptions.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here may block boot or fail the observed work: the sweep and every
|
|
10
|
+
* tick pass log and continue on failure.
|
|
11
|
+
*/
|
|
12
|
+
import type { Database } from "bun:sqlite";
|
|
13
|
+
import type { Logger } from "@opentelemetry/api-logs";
|
|
14
|
+
import type { ActivityQuery, ActivityQueryResult } from "@schlessera/brain-ui-sdk/server";
|
|
15
|
+
import { type ActivityStore } from "./store.js";
|
|
16
|
+
import { type ActivityStream } from "./stream.js";
|
|
17
|
+
import { type ActivityNotifier } from "./notify.js";
|
|
18
|
+
import { type PushSender } from "./push-sender.js";
|
|
19
|
+
export interface ActivityRuntime {
|
|
20
|
+
store: ActivityStore;
|
|
21
|
+
stream: ActivityStream;
|
|
22
|
+
notifier: ActivityNotifier;
|
|
23
|
+
pushSender: PushSender;
|
|
24
|
+
/** The agent-facing read seam (bridge.queryActivity). */
|
|
25
|
+
query(query: ActivityQuery): ActivityQueryResult;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
export declare function createActivityRuntime(db: Database, deps: {
|
|
29
|
+
log: Logger;
|
|
30
|
+
}): ActivityRuntime;
|
|
31
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/activity/runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAE1F,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAoB,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAerE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,UAAU,EAAE,UAAU,CAAC;IACvB,yDAAyD;IACzD,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,mBAAmB,CAAC;IACjD,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,qBAAqB,CACnC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GACpB,eAAe,CA8FjB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { createActivityStore } from "./store.js";
|
|
2
|
+
import { createActivityStream } from "./stream.js";
|
|
3
|
+
import { createActivityNotifier } from "./notify.js";
|
|
4
|
+
import { createPushSender } from "./push-sender.js";
|
|
5
|
+
import { digestRetentionFloor } from "./digest.js";
|
|
6
|
+
import { runActivityQuery } from "./query.js";
|
|
7
|
+
// Activity lifecycle cadence. The stale threshold must comfortably exceed
|
|
8
|
+
// the cron wrapper's heartbeat interval (~30s) so a live writer is never
|
|
9
|
+
// swept; the tick doubles as the always-on low-frequency sweep the
|
|
10
|
+
// notification layer extends.
|
|
11
|
+
const ACTIVITY_TICK_MS = 20_000;
|
|
12
|
+
const ACTIVITY_STALE_AFTER_MS = 2 * 60 * 1000;
|
|
13
|
+
const ACTIVITY_PRUNE_INTERVAL_MS = 60 * 60 * 1000;
|
|
14
|
+
const ACTIVITY_HARD_CEILING_MS = 90 * 24 * 60 * 60 * 1000;
|
|
15
|
+
/** Acknowledged notification intents are kept this long, then deleted. */
|
|
16
|
+
const INTENT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
17
|
+
export function createActivityRuntime(db, deps) {
|
|
18
|
+
const { log } = deps;
|
|
19
|
+
const store = createActivityStore(db);
|
|
20
|
+
const stream = createActivityStream(store, log);
|
|
21
|
+
// Boot sweep: close this server's orphans from a previous life (interrupted).
|
|
22
|
+
try {
|
|
23
|
+
const orphans = store.sweepOwnOrphans();
|
|
24
|
+
if (orphans > 0) {
|
|
25
|
+
log.emit({
|
|
26
|
+
severityText: "WARN",
|
|
27
|
+
body: "closed orphaned activity spans from a previous process",
|
|
28
|
+
attributes: { count: orphans },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
log.emit({
|
|
34
|
+
severityText: "WARN",
|
|
35
|
+
body: "boot activity sweep failed",
|
|
36
|
+
attributes: { error: err instanceof Error ? err.message : String(err) },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const notifier = createActivityNotifier({
|
|
40
|
+
db,
|
|
41
|
+
store,
|
|
42
|
+
isWatched: (scope) => stream.isWatched(scope),
|
|
43
|
+
log,
|
|
44
|
+
});
|
|
45
|
+
const pushSender = createPushSender(db, { log });
|
|
46
|
+
let delivering = false;
|
|
47
|
+
let lastPrune = 0;
|
|
48
|
+
const tick = setInterval(() => {
|
|
49
|
+
try {
|
|
50
|
+
const stale = store.sweepStale(ACTIVITY_STALE_AFTER_MS);
|
|
51
|
+
if (stale > 0) {
|
|
52
|
+
log.emit({
|
|
53
|
+
severityText: "WARN",
|
|
54
|
+
body: "closed stale activity spans (writer went silent)",
|
|
55
|
+
attributes: { count: stale },
|
|
56
|
+
});
|
|
57
|
+
stream.pump();
|
|
58
|
+
}
|
|
59
|
+
notifier.tick();
|
|
60
|
+
// Async delivery, reentrancy-guarded: a slow push service must not
|
|
61
|
+
// stack passes; the next tick simply retries what stayed pending.
|
|
62
|
+
if (!delivering) {
|
|
63
|
+
delivering = true;
|
|
64
|
+
void pushSender
|
|
65
|
+
.deliverPending(notifier)
|
|
66
|
+
.catch((err) => log.emit({
|
|
67
|
+
severityText: "WARN",
|
|
68
|
+
body: "push delivery pass failed",
|
|
69
|
+
attributes: { error: err instanceof Error ? err.message : String(err) },
|
|
70
|
+
}))
|
|
71
|
+
.finally(() => {
|
|
72
|
+
delivering = false;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (Date.now() - lastPrune > ACTIVITY_PRUNE_INTERVAL_MS) {
|
|
76
|
+
lastPrune = Date.now();
|
|
77
|
+
store.prune({
|
|
78
|
+
// Full detail survives until the digest has covered it; the hard
|
|
79
|
+
// ceiling bounds growth even if the digest job silently dies.
|
|
80
|
+
digestFloorAt: digestRetentionFloor(db),
|
|
81
|
+
hardCeilingMs: ACTIVITY_HARD_CEILING_MS,
|
|
82
|
+
});
|
|
83
|
+
notifier.pruneAcknowledged(INTENT_RETENTION_MS);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
log.emit({
|
|
88
|
+
severityText: "WARN",
|
|
89
|
+
body: "activity tick failed",
|
|
90
|
+
attributes: { error: err instanceof Error ? err.message : String(err) },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}, ACTIVITY_TICK_MS);
|
|
94
|
+
// A Bun interval would otherwise keep a closed test app's process alive.
|
|
95
|
+
if (typeof tick === "object" && "unref" in tick)
|
|
96
|
+
tick.unref();
|
|
97
|
+
return {
|
|
98
|
+
store,
|
|
99
|
+
stream,
|
|
100
|
+
notifier,
|
|
101
|
+
pushSender,
|
|
102
|
+
query: (query) => runActivityQuery(db, store, query, notifier),
|
|
103
|
+
close() {
|
|
104
|
+
clearInterval(tick);
|
|
105
|
+
stream.close();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/activity/runtime.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ActivityStore } from "./store.js";
|
|
2
|
+
export interface SpanSinkContext {
|
|
3
|
+
/** The run every ingested span belongs to. */
|
|
4
|
+
runId: string;
|
|
5
|
+
/** Parent for lines that name none — the cron root span. */
|
|
6
|
+
rootSpanId: string;
|
|
7
|
+
jobName?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface SpanSinkResult {
|
|
10
|
+
/** Byte offset consumed so far — pass back on the next incremental call. */
|
|
11
|
+
offset: number;
|
|
12
|
+
ingested: number;
|
|
13
|
+
skipped: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Ingest new complete lines from a span-sink file. Incremental: pass the
|
|
17
|
+
* previous call's `offset`; a partial trailing line is left for next time.
|
|
18
|
+
* A missing file is an empty result, not an error — the child may simply
|
|
19
|
+
* never have emitted.
|
|
20
|
+
*/
|
|
21
|
+
export declare function ingestSpanSink(store: ActivityStore, path: string, context: SpanSinkContext, offset?: number): SpanSinkResult;
|
|
22
|
+
//# sourceMappingURL=span-sink.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"span-sink.d.ts","sourceRoot":"","sources":["../../src/activity/span-sink.ts"],"names":[],"mappings":"AAsBA,OAAO,EAAiB,KAAK,aAAa,EAAoC,MAAM,YAAY,CAAC;AAEjG,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,aAAa,EACpB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,eAAe,EACxB,MAAM,SAAI,GACT,cAAc,CAuBhB"}
|