@geravant/sinain 1.8.0 → 1.9.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/.env.example +4 -6
- package/cli.js +16 -2
- package/config-shared.js +469 -0
- package/config.js +152 -0
- package/launcher.js +7 -1
- package/onboard.js +345 -0
- package/package.json +8 -2
- package/sense_client/__main__.py +8 -4
- package/sense_client/gate.py +1 -0
- package/sense_client/ocr.py +52 -22
- package/sense_client/sender.py +2 -0
- package/sense_client/vision.py +31 -11
- package/sinain-agent/.env.example +23 -0
- package/sinain-agent/run.sh +7 -12
- package/sinain-core/src/agent/analyzer.ts +25 -2
- package/sinain-core/src/agent/loop.ts +26 -1
- package/sinain-core/src/audio/transcription.ts +20 -5
- package/sinain-core/src/config.ts +3 -2
- package/sinain-core/src/cost/tracker.ts +64 -0
- package/sinain-core/src/escalation/escalator.ts +31 -59
- package/sinain-core/src/index.ts +41 -45
- package/sinain-core/src/overlay/commands.ts +12 -9
- package/sinain-core/src/overlay/ws-handler.ts +27 -3
- package/sinain-core/src/server.ts +41 -0
- package/sinain-core/src/types.ts +33 -1
package/sinain-core/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { TranscriptionService } from "./audio/transcription.js";
|
|
|
10
10
|
import { AgentLoop } from "./agent/loop.js";
|
|
11
11
|
import { TraitEngine, loadTraitRoster } from "./agent/traits.js";
|
|
12
12
|
import { shortAppName } from "./agent/context-window.js";
|
|
13
|
-
import { Escalator
|
|
13
|
+
import { Escalator } from "./escalation/escalator.js";
|
|
14
14
|
import { Recorder } from "./recorder.js";
|
|
15
15
|
import { Tracer } from "./trace/tracer.js";
|
|
16
16
|
import { TraceStore } from "./trace/trace-store.js";
|
|
@@ -18,6 +18,7 @@ import { FeedbackStore } from "./learning/feedback-store.js";
|
|
|
18
18
|
import { SignalCollector } from "./learning/signal-collector.js";
|
|
19
19
|
import { createAppServer } from "./server.js";
|
|
20
20
|
import { Profiler } from "./profiler.js";
|
|
21
|
+
import { CostTracker } from "./cost/tracker.js";
|
|
21
22
|
import type { SenseEvent, EscalationMode, FeedItem } from "./types.js";
|
|
22
23
|
import { isDuplicateTranscript, bigramSimilarity } from "./util/dedup.js";
|
|
23
24
|
import { log, warn, error } from "./log.js";
|
|
@@ -25,41 +26,6 @@ import { initPrivacy, levelFor, applyLevel } from "./privacy/index.js";
|
|
|
25
26
|
|
|
26
27
|
const TAG = "core";
|
|
27
28
|
|
|
28
|
-
/** Build context snapshot for user-initiated spawn tasks. */
|
|
29
|
-
function buildSpawnContext(
|
|
30
|
-
entry: { digest: string; context: { currentApp: string } },
|
|
31
|
-
feedBuffer: FeedBuffer,
|
|
32
|
-
senseBuffer: SenseBuffer,
|
|
33
|
-
): SpawnContext {
|
|
34
|
-
const cutoff = Date.now() - 60_000;
|
|
35
|
-
|
|
36
|
-
// Recent audio: last ~60s of transcripts
|
|
37
|
-
const recentAudio = feedBuffer.queryByTime(cutoff)
|
|
38
|
-
.filter(m => m.channel === "stream" && (m.text.startsWith("[🔊]") || m.text.startsWith("[🎙]")))
|
|
39
|
-
.map(m => m.text)
|
|
40
|
-
.join("\n")
|
|
41
|
-
.slice(0, 2000);
|
|
42
|
-
|
|
43
|
-
// Recent screen: last ~60s of deduped OCR text
|
|
44
|
-
const screenEvents = senseBuffer.queryByTime(cutoff);
|
|
45
|
-
const seenOcr = new Set<string>();
|
|
46
|
-
const screenLines: string[] = [];
|
|
47
|
-
for (const e of screenEvents) {
|
|
48
|
-
if (e.ocr && !seenOcr.has(e.ocr)) {
|
|
49
|
-
seenOcr.add(e.ocr);
|
|
50
|
-
screenLines.push(`[${e.meta.app}] ${e.ocr}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const recentScreen = screenLines.join("\n").slice(0, 3000);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
currentApp: entry.context.currentApp,
|
|
57
|
-
digest: entry.digest,
|
|
58
|
-
recentAudio: recentAudio || undefined,
|
|
59
|
-
recentScreen: recentScreen || undefined,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
29
|
/** Resolve workspace path, expanding leading ~ to HOME. */
|
|
64
30
|
function resolveWorkspace(): string {
|
|
65
31
|
const raw = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
|
|
@@ -93,6 +59,10 @@ async function main() {
|
|
|
93
59
|
// ── Initialize overlay WS handler ──
|
|
94
60
|
const wsHandler = new WsHandler();
|
|
95
61
|
|
|
62
|
+
// ── Initialize cost tracker ──
|
|
63
|
+
const costTracker = new CostTracker((snapshot) => wsHandler.broadcastCost(snapshot, config.costDisplayEnabled));
|
|
64
|
+
costTracker.startPeriodicLog(60_000);
|
|
65
|
+
|
|
96
66
|
// ── Initialize tracing ──
|
|
97
67
|
const tracer = config.traceEnabled ? new Tracer() : null;
|
|
98
68
|
const traceStore = config.traceEnabled ? new TraceStore(config.traceDir) : null;
|
|
@@ -100,9 +70,6 @@ async function main() {
|
|
|
100
70
|
// ── Initialize recorder ──
|
|
101
71
|
const recorder = new Recorder();
|
|
102
72
|
|
|
103
|
-
// ── Spawn context cache — updated every agent tick for user-initiated spawns ──
|
|
104
|
-
let lastSpawnContext: SpawnContext | null = null;
|
|
105
|
-
|
|
106
73
|
// ── Initialize profiler ──
|
|
107
74
|
const profiler = new Profiler();
|
|
108
75
|
|
|
@@ -146,11 +113,35 @@ async function main() {
|
|
|
146
113
|
getRecorderStatus: () => recorder.getStatus(),
|
|
147
114
|
profiler,
|
|
148
115
|
onAnalysis: (entry, contextWindow) => {
|
|
149
|
-
// Handle recorder commands
|
|
150
|
-
recorder.handleCommand(entry.record);
|
|
116
|
+
// Handle recorder commands
|
|
117
|
+
const stopResult = recorder.handleCommand(entry.record);
|
|
118
|
+
|
|
119
|
+
// Dispatch task via subagent spawn
|
|
120
|
+
if (entry.task || stopResult) {
|
|
121
|
+
let task: string;
|
|
122
|
+
let label: string | undefined;
|
|
123
|
+
|
|
124
|
+
if (stopResult && stopResult.segments > 0 && entry.task) {
|
|
125
|
+
// Recording stopped with explicit task instruction
|
|
126
|
+
task = `${entry.task}\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
127
|
+
label = stopResult.title;
|
|
128
|
+
} else if (stopResult && stopResult.segments > 0) {
|
|
129
|
+
// Recording stopped without explicit task — default to cleanup/summarize
|
|
130
|
+
task = `Clean up and summarize this recording transcript:\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
131
|
+
label = stopResult.title;
|
|
132
|
+
} else if (entry.task) {
|
|
133
|
+
// Standalone task without recording
|
|
134
|
+
task = entry.task;
|
|
135
|
+
} else {
|
|
136
|
+
task = "";
|
|
137
|
+
}
|
|
151
138
|
|
|
152
|
-
|
|
153
|
-
|
|
139
|
+
if (task) {
|
|
140
|
+
escalator.dispatchSpawnTask(task, label).catch(err => {
|
|
141
|
+
error(TAG, "spawn task dispatch error:", err);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
154
145
|
|
|
155
146
|
// Escalation continues as normal
|
|
156
147
|
escalator.onAgentAnalysis(entry, contextWindow);
|
|
@@ -184,6 +175,7 @@ async function main() {
|
|
|
184
175
|
return null;
|
|
185
176
|
},
|
|
186
177
|
feedbackStore: feedbackStore ?? undefined,
|
|
178
|
+
costTracker,
|
|
187
179
|
});
|
|
188
180
|
|
|
189
181
|
// ── Wire learning signal collector (needs agentLoop) ──
|
|
@@ -211,6 +203,7 @@ async function main() {
|
|
|
211
203
|
systemAudioPipeline.setProfiler(profiler);
|
|
212
204
|
if (micPipeline) micPipeline.setProfiler(profiler);
|
|
213
205
|
transcription.setProfiler(profiler);
|
|
206
|
+
transcription.setCostTracker(costTracker);
|
|
214
207
|
|
|
215
208
|
// Wire: audio chunks → transcription (both pipelines share the same transcription service)
|
|
216
209
|
systemAudioPipeline.on("chunk", (chunk) => {
|
|
@@ -322,6 +315,7 @@ async function main() {
|
|
|
322
315
|
senseBuffer,
|
|
323
316
|
wsHandler,
|
|
324
317
|
profiler,
|
|
318
|
+
costTracker,
|
|
325
319
|
feedbackStore: feedbackStore ?? undefined,
|
|
326
320
|
isScreenActive: () => screenActive,
|
|
327
321
|
|
|
@@ -447,7 +441,7 @@ async function main() {
|
|
|
447
441
|
|
|
448
442
|
// Spawn background agent task (from HUD Shift+Enter or bare agent POST /spawn)
|
|
449
443
|
onSpawnCommand: (text: string) => {
|
|
450
|
-
escalator.dispatchSpawnTask(text,
|
|
444
|
+
escalator.dispatchSpawnTask(text, "user-command").catch((err) => {
|
|
451
445
|
log("srv", `spawn via HTTP failed: ${err}`);
|
|
452
446
|
});
|
|
453
447
|
},
|
|
@@ -475,7 +469,7 @@ async function main() {
|
|
|
475
469
|
agentLoop.onNewContext(true);
|
|
476
470
|
},
|
|
477
471
|
onSpawnCommand: (text) => {
|
|
478
|
-
escalator.dispatchSpawnTask(text,
|
|
472
|
+
escalator.dispatchSpawnTask(text, "user-command").catch((err) => {
|
|
479
473
|
log("cmd", `spawn command failed: ${err}`);
|
|
480
474
|
wsHandler.broadcast(`\u26a0 Spawn failed: ${String(err).slice(0, 100)}`, "normal");
|
|
481
475
|
});
|
|
@@ -551,12 +545,14 @@ async function main() {
|
|
|
551
545
|
log(TAG, ` agent: ${config.agentConfig.enabled ? "enabled" : "disabled"}`);
|
|
552
546
|
log(TAG, ` escal: ${config.escalationConfig.mode}`);
|
|
553
547
|
log(TAG, ` traits: ${config.traitConfig.enabled ? "enabled" : "disabled"} (${traitRoster.length} traits)`);
|
|
548
|
+
log(TAG, ` cost: display=${config.costDisplayEnabled ? "on" : "off"} (always logged)`);
|
|
554
549
|
|
|
555
550
|
// ── Graceful shutdown ──
|
|
556
551
|
const shutdown = async (signal: string) => {
|
|
557
552
|
log(TAG, `${signal} received, shutting down...`);
|
|
558
553
|
clearInterval(bufferGaugeTimer);
|
|
559
554
|
if (feedbackSummaryTimer) clearInterval(feedbackSummaryTimer);
|
|
555
|
+
costTracker.stop();
|
|
560
556
|
profiler.stop();
|
|
561
557
|
recorder.forceStop(); // Stop any active recording
|
|
562
558
|
agentLoop.stop();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import type { InboundMessage } from "../types.js";
|
|
2
3
|
import type { WsHandler } from "./ws-handler.js";
|
|
3
4
|
import type { AudioPipeline } from "../audio/pipeline.js";
|
|
4
5
|
import type { CoreConfig } from "../types.js";
|
|
5
6
|
import { WebSocket } from "ws";
|
|
7
|
+
import { loadedEnvPath } from "../config.js";
|
|
6
8
|
import { log } from "../log.js";
|
|
7
9
|
|
|
8
10
|
const TAG = "cmd";
|
|
@@ -60,15 +62,6 @@ export function setupCommands(deps: CommandDeps): void {
|
|
|
60
62
|
case "spawn_command": {
|
|
61
63
|
const preview = msg.text.length > 60 ? msg.text.slice(0, 60) + "…" : msg.text;
|
|
62
64
|
log(TAG, `spawn command received: "${preview}"`);
|
|
63
|
-
// Echo spawn command to all overlay clients as a feed item (green in UI)
|
|
64
|
-
wsHandler.broadcastRaw({
|
|
65
|
-
type: "feed",
|
|
66
|
-
text: `⚡ ${msg.text}`,
|
|
67
|
-
priority: "normal",
|
|
68
|
-
ts: Date.now(),
|
|
69
|
-
channel: "agent",
|
|
70
|
-
sender: "spawn",
|
|
71
|
-
} as any);
|
|
72
65
|
if (deps.onSpawnCommand) {
|
|
73
66
|
deps.onSpawnCommand(msg.text);
|
|
74
67
|
} else {
|
|
@@ -151,6 +144,16 @@ function handleCommand(action: string, deps: CommandDeps): void {
|
|
|
151
144
|
log(TAG, `traits toggled ${nowEnabled ? "ON" : "OFF"}`);
|
|
152
145
|
break;
|
|
153
146
|
}
|
|
147
|
+
case "open_settings": {
|
|
148
|
+
const envPath = loadedEnvPath || `${process.env.HOME || process.env.USERPROFILE}/.sinain/.env`;
|
|
149
|
+
const cmd = process.platform === "win32" ? "notepad" : "open";
|
|
150
|
+
const args = process.platform === "win32" ? [envPath] : ["-t", envPath];
|
|
151
|
+
execFile(cmd, args, (err) => {
|
|
152
|
+
if (err) log(TAG, `open_settings failed: ${err.message}`);
|
|
153
|
+
});
|
|
154
|
+
log(TAG, `open_settings: ${envPath}`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
154
157
|
default:
|
|
155
158
|
log(TAG, `unhandled command: ${action}`);
|
|
156
159
|
}
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
FeedMessage,
|
|
8
8
|
StatusMessage,
|
|
9
9
|
SpawnTaskMessage,
|
|
10
|
+
CostMessage,
|
|
11
|
+
CostSnapshot,
|
|
10
12
|
Priority,
|
|
11
13
|
FeedChannel,
|
|
12
14
|
} from "../types.js";
|
|
@@ -39,6 +41,7 @@ export class WsHandler {
|
|
|
39
41
|
};
|
|
40
42
|
private replayBuffer: FeedMessage[] = [];
|
|
41
43
|
private spawnTaskBuffer: Map<string, SpawnTaskMessage> = new Map();
|
|
44
|
+
private latestCostMsg: CostMessage | null = null;
|
|
42
45
|
|
|
43
46
|
constructor() {
|
|
44
47
|
this.startHeartbeat();
|
|
@@ -89,6 +92,16 @@ export class WsHandler {
|
|
|
89
92
|
log(TAG, `replayed ${this.spawnTaskBuffer.size} spawn tasks to new client`);
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
// Replay current cost snapshot (or send zeroed snapshot so client resets)
|
|
96
|
+
this.sendTo(ws, this.latestCostMsg ?? {
|
|
97
|
+
type: "cost" as const,
|
|
98
|
+
totalCost: 0,
|
|
99
|
+
costBySource: {},
|
|
100
|
+
callCount: 0,
|
|
101
|
+
startedAt: Date.now(),
|
|
102
|
+
displayEnabled: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
92
105
|
ws.on("message", (raw) => {
|
|
93
106
|
try {
|
|
94
107
|
const data = JSON.parse(raw.toString()) as InboundMessage;
|
|
@@ -158,6 +171,20 @@ export class WsHandler {
|
|
|
158
171
|
this.broadcastMessage(msg);
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
/** Broadcast cost snapshot to all connected overlays. */
|
|
175
|
+
broadcastCost(snapshot: CostSnapshot, displayEnabled: boolean): void {
|
|
176
|
+
const msg: CostMessage = {
|
|
177
|
+
type: "cost",
|
|
178
|
+
totalCost: snapshot.totalCost,
|
|
179
|
+
costBySource: snapshot.costBySource,
|
|
180
|
+
callCount: snapshot.callCount,
|
|
181
|
+
startedAt: snapshot.startedAt,
|
|
182
|
+
displayEnabled,
|
|
183
|
+
};
|
|
184
|
+
this.latestCostMsg = msg;
|
|
185
|
+
this.broadcastMessage(msg);
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
/** Update internal state and broadcast. */
|
|
162
189
|
updateState(partial: Partial<BridgeState>): void {
|
|
163
190
|
Object.assign(this.state, partial);
|
|
@@ -199,9 +226,6 @@ export class WsHandler {
|
|
|
199
226
|
case "user_command":
|
|
200
227
|
log(TAG, `\u2190 user command: ${msg.text.slice(0, 100)}`);
|
|
201
228
|
break;
|
|
202
|
-
case "spawn_command":
|
|
203
|
-
log(TAG, `\u2190 spawn command: ${msg.text.slice(0, 100)}`);
|
|
204
|
-
break;
|
|
205
229
|
case "profiling":
|
|
206
230
|
if (this.onProfilingCb) this.onProfilingCb(msg);
|
|
207
231
|
return;
|
|
@@ -2,6 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
|
|
|
2
2
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3
3
|
import type { CoreConfig, SenseEvent } from "./types.js";
|
|
4
4
|
import type { Profiler } from "./profiler.js";
|
|
5
|
+
import type { CostTracker } from "./cost/tracker.js";
|
|
5
6
|
import type { FeedbackStore } from "./learning/feedback-store.js";
|
|
6
7
|
import { FeedBuffer } from "./buffers/feed-buffer.js";
|
|
7
8
|
import { SenseBuffer, type SemanticSenseEvent, type TextDelta } from "./buffers/sense-buffer.js";
|
|
@@ -20,6 +21,7 @@ export interface ServerDeps {
|
|
|
20
21
|
senseBuffer: SenseBuffer;
|
|
21
22
|
wsHandler: WsHandler;
|
|
22
23
|
profiler?: Profiler;
|
|
24
|
+
costTracker?: CostTracker;
|
|
23
25
|
onSenseEvent: (event: SenseEvent) => void;
|
|
24
26
|
onSenseDelta?: (data: { app: string; activity: string; changes: TextDelta[]; priority?: string; ts: number }) => void;
|
|
25
27
|
onFeedPost: (text: string, priority: string) => void;
|
|
@@ -65,6 +67,8 @@ function readBody(req: IncomingMessage, maxBytes: number): Promise<string> {
|
|
|
65
67
|
export function createAppServer(deps: ServerDeps) {
|
|
66
68
|
const { config, feedBuffer, senseBuffer, wsHandler } = deps;
|
|
67
69
|
let senseInBytes = 0;
|
|
70
|
+
const seenVisionCostIds = new Set<string>();
|
|
71
|
+
const visionCostCleanup = setInterval(() => seenVisionCostIds.clear(), 60_000);
|
|
68
72
|
|
|
69
73
|
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
70
74
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -91,6 +95,24 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
91
95
|
senseInBytes += Buffer.byteLength(body);
|
|
92
96
|
deps.profiler?.gauge("network.senseInBytes", senseInBytes);
|
|
93
97
|
const data = JSON.parse(body);
|
|
98
|
+
|
|
99
|
+
// Record vision API cost before dedup — the call is already billed.
|
|
100
|
+
// Dedup by cost_id to avoid double-counting on sender retries.
|
|
101
|
+
const vc = data.vision_cost;
|
|
102
|
+
if (vc && typeof vc.cost === "number" && vc.cost > 0) {
|
|
103
|
+
const costId = vc.cost_id as string | undefined;
|
|
104
|
+
if (!costId || !seenVisionCostIds.has(costId)) {
|
|
105
|
+
if (costId) seenVisionCostIds.add(costId);
|
|
106
|
+
deps.costTracker?.record({
|
|
107
|
+
source: "vision",
|
|
108
|
+
model: vc.model || "unknown",
|
|
109
|
+
cost: vc.cost,
|
|
110
|
+
tokensIn: vc.tokens_in || 0,
|
|
111
|
+
tokensOut: vc.tokens_out || 0,
|
|
112
|
+
ts: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
94
116
|
if (!data.type || data.ts === undefined) {
|
|
95
117
|
res.writeHead(400);
|
|
96
118
|
res.end(JSON.stringify({ ok: false, error: "missing type or ts" }));
|
|
@@ -309,6 +331,24 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
309
331
|
return;
|
|
310
332
|
}
|
|
311
333
|
|
|
334
|
+
// ── /setup/status ── (used by overlay onboarding wizard)
|
|
335
|
+
if (req.method === "GET" && url.pathname === "/setup/status") {
|
|
336
|
+
const health = deps.getHealthPayload();
|
|
337
|
+
res.end(JSON.stringify({
|
|
338
|
+
ok: true,
|
|
339
|
+
setup: {
|
|
340
|
+
openrouterKey: !!config.transcriptionConfig.openrouterApiKey,
|
|
341
|
+
gatewayConfigured: !!config.openclawConfig.gatewayToken,
|
|
342
|
+
gatewayConnected: !!(health as Record<string, any>)?.escalation?.gatewayConnected,
|
|
343
|
+
audioActive: (health as Record<string, any>)?.audio?.state === "active" || (health as Record<string, any>)?.audioPipeline?.state === "running",
|
|
344
|
+
screenActive: deps.isScreenActive(),
|
|
345
|
+
transcriptionBackend: config.transcriptionConfig.backend,
|
|
346
|
+
escalationMode: config.escalationConfig.mode,
|
|
347
|
+
},
|
|
348
|
+
}));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
312
352
|
// ── /user/command ──
|
|
313
353
|
if (req.method === "POST" && url.pathname === "/user/command") {
|
|
314
354
|
const body = await readBody(req, 4096);
|
|
@@ -504,6 +544,7 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
504
544
|
});
|
|
505
545
|
},
|
|
506
546
|
async destroy(): Promise<void> {
|
|
547
|
+
clearInterval(visionCostCleanup);
|
|
507
548
|
wsHandler.destroy();
|
|
508
549
|
wss.close();
|
|
509
550
|
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
|
package/sinain-core/src/types.ts
CHANGED
|
@@ -78,7 +78,36 @@ export interface SpawnCommandMessage {
|
|
|
78
78
|
text: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/** Cost update broadcast to overlay. */
|
|
82
|
+
export interface CostMessage {
|
|
83
|
+
type: "cost";
|
|
84
|
+
totalCost: number;
|
|
85
|
+
costBySource: Record<string, number>;
|
|
86
|
+
callCount: number;
|
|
87
|
+
startedAt: number;
|
|
88
|
+
displayEnabled: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Entry recorded by CostTracker for each LLM call. */
|
|
92
|
+
export interface CostEntry {
|
|
93
|
+
source: "analyzer" | "transcription" | "vision";
|
|
94
|
+
model: string;
|
|
95
|
+
cost: number;
|
|
96
|
+
tokensIn: number;
|
|
97
|
+
tokensOut: number;
|
|
98
|
+
ts: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Snapshot of accumulated cost state. */
|
|
102
|
+
export interface CostSnapshot {
|
|
103
|
+
totalCost: number;
|
|
104
|
+
costBySource: Record<string, number>;
|
|
105
|
+
costByModel: Record<string, number>;
|
|
106
|
+
callCount: number;
|
|
107
|
+
startedAt: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type OutboundMessage = FeedMessage | StatusMessage | PingMessage | SpawnTaskMessage | CostMessage;
|
|
82
111
|
export type InboundMessage = UserMessage | CommandMessage | PongMessage | ProfilingMessage | UserCommandMessage | SpawnCommandMessage;
|
|
83
112
|
|
|
84
113
|
/** Abstraction for user commands (text now, voice later). */
|
|
@@ -253,6 +282,8 @@ export interface AgentResult {
|
|
|
253
282
|
voice?: string;
|
|
254
283
|
voice_stat?: number;
|
|
255
284
|
voice_confidence?: number;
|
|
285
|
+
/** Actual USD cost returned by OpenRouter (undefined if not available). */
|
|
286
|
+
cost?: number;
|
|
256
287
|
}
|
|
257
288
|
|
|
258
289
|
export interface AgentEntry extends AgentResult {
|
|
@@ -442,6 +473,7 @@ export interface CoreConfig {
|
|
|
442
473
|
openclawConfig: OpenClawConfig;
|
|
443
474
|
situationMdPath: string;
|
|
444
475
|
traceEnabled: boolean;
|
|
476
|
+
costDisplayEnabled: boolean;
|
|
445
477
|
traceDir: string;
|
|
446
478
|
learningConfig: LearningConfig;
|
|
447
479
|
traitConfig: TraitConfig;
|