wyrm-mcp 7.2.0 → 7.2.2
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/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/tasks.js
CHANGED
|
@@ -1,282 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* MCP Tasks (T037) — capability-gated, SYNCHRONOUS-fallback task lifecycle for
|
|
3
|
-
* the long ops of the tools that REMAIN on MCP (Article V: vendor-neutral,
|
|
4
|
-
* capability-gated; Article VII: admin-gated long ops are off by default).
|
|
5
|
-
*
|
|
6
|
-
* WHY this exists. Two of Wyrm's retained tools have ops that can run long on a
|
|
7
|
-
* large corpus and are the ones a curated client NEEDS on the MCP wire:
|
|
8
|
-
* - wyrm_maintenance / wyrm_reindex / wyrm_vector_setup — the admin upkeep +
|
|
9
|
-
* vector (re)build. This is the fix for the curated-client "can't reindex
|
|
10
|
-
* the last 17% of vectors" wound — these stay on MCP precisely so a
|
|
11
|
-
* resource-constrained client CAN drive a backfill from the agent.
|
|
12
|
-
* - harvest / auto_capture extraction (modes of wyrm_capture) — corpus scans.
|
|
13
|
-
*
|
|
14
|
-
* HOW it composes with the SDK. The MCP Tasks RC's server-side `tools/call`
|
|
15
|
-
* augmentation works like this (server/index.js): when the request carries a
|
|
16
|
-
* `task` param, the SDK REQUIRES the CallTool handler to return a
|
|
17
|
-
* `CreateTaskResult` (`{task:{taskId,status,…}}`) — NOT a CallToolResult — and
|
|
18
|
-
* the actual payload is fetched later via `tasks/result`. That deferred-result
|
|
19
|
-
* machinery is the part the spec flags as not-yet-stable. Wyrm's faithful,
|
|
20
|
-
* "synchronous fallback until the RC stabilizes" implementation runs the op
|
|
21
|
-
* IMMEDIATELY and never truly defers:
|
|
22
|
-
* 1. Detect the client's `tasks` capability (negotiated at initialize) AND the
|
|
23
|
-
* request's `task` augmentation param (`TaskAugmentedRequestParamsSchema`).
|
|
24
|
-
* 2. When BOTH are present: run the op SYNCHRONOUSLY, store the completed
|
|
25
|
-
* CallToolResult, and return a `CreateTaskResult` whose task is already
|
|
26
|
-
* `completed` (or `failed`). The client fetches the payload via
|
|
27
|
-
* `tasks/result` (served from the store). No work is deferred — the op
|
|
28
|
-
* already ran — so a client that never polls still triggered the op.
|
|
29
|
-
* 3. Otherwise (the default for every client today, and any client that omits
|
|
30
|
-
* `task`): run the op inline and return the CallToolResult directly. This
|
|
31
|
-
* is the synchronous fallback and the always-correct path; no data lost.
|
|
32
|
-
*
|
|
33
|
-
* The whole module is deterministic and no-network (Article III): the only
|
|
34
|
-
* non-determinism is the task id + timestamps, confined to the task record and
|
|
35
|
-
* the CreateTaskResult envelope — never the stored CallToolResult bytes.
|
|
36
|
-
*/
|
|
37
|
-
import { randomUUID } from 'node:crypto';
|
|
38
|
-
import { renderErrorResponse } from './render.js';
|
|
39
|
-
/** Default retention for a completed task record (ms). */
|
|
40
|
-
export const DEFAULT_TASK_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
41
|
-
/** Hard cap so a hostile/buggy `task.ttl` can never pin memory (Article VII). */
|
|
42
|
-
export const MAX_TASK_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
43
|
-
/** Suggested client poll cadence advertised on the task record (ms). */
|
|
44
|
-
export const DEFAULT_POLL_INTERVAL_MS = 500;
|
|
45
|
-
/** Hard cap on retained task records — bounds memory; oldest evicted first. */
|
|
46
|
-
export const MAX_RETAINED_TASKS = 256;
|
|
47
|
-
/**
|
|
48
|
-
* True when the connected client advertised the `tasks` capability. Vendor-
|
|
49
|
-
* neutral (Article V): reads the negotiated capability, never a client name.
|
|
50
|
-
*/
|
|
51
|
-
export function clientSupportsTasks(tasksCapability) {
|
|
52
|
-
return tasksCapability != null && typeof tasksCapability === 'object';
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* The `task` augmentation a client attaches to a CallTool request when it wants
|
|
56
|
-
* a task created (`TaskAugmentedRequestParamsSchema.task`). Reads it off the
|
|
57
|
-
* raw request params; returns the requested ttl (clamped) or null when absent.
|
|
58
|
-
* Total on adversarial input — never throws (an invalid ttl degrades to the
|
|
59
|
-
* default, a missing augmentation yields null = synchronous path).
|
|
60
|
-
*/
|
|
61
|
-
export function readTaskAugmentation(params) {
|
|
62
|
-
const task = params && typeof params === 'object' ? params.task : undefined;
|
|
63
|
-
if (task == null || typeof task !== 'object')
|
|
64
|
-
return { requested: false };
|
|
65
|
-
const rawTtl = task.ttl;
|
|
66
|
-
let ttlMs = DEFAULT_TASK_TTL_MS;
|
|
67
|
-
if (typeof rawTtl === 'number' && Number.isFinite(rawTtl) && rawTtl > 0) {
|
|
68
|
-
ttlMs = Math.min(Math.floor(rawTtl), MAX_TASK_TTL_MS);
|
|
69
|
-
}
|
|
70
|
-
return { requested: true, ttlMs };
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* In-memory task store. Single-process (the stdio server is one process); the
|
|
74
|
-
* MCP RC has no persistence requirement and a restarted server starts clean.
|
|
75
|
-
* Bounded by MAX_RETAINED_TASKS with oldest-first eviction.
|
|
76
|
-
*/
|
|
77
|
-
export class TaskStore {
|
|
78
|
-
tasks = new Map();
|
|
79
|
-
/** Create a `working` task record and return its id. */
|
|
80
|
-
create(tool, ttlMs, nowMs = Date.now()) {
|
|
81
|
-
this.evictExpired(nowMs);
|
|
82
|
-
if (this.tasks.size >= MAX_RETAINED_TASKS)
|
|
83
|
-
this.evictOldest();
|
|
84
|
-
const iso = new Date(nowMs).toISOString();
|
|
85
|
-
const record = {
|
|
86
|
-
taskId: randomUUID(),
|
|
87
|
-
status: 'working',
|
|
88
|
-
ttl: ttlMs,
|
|
89
|
-
createdAt: iso,
|
|
90
|
-
lastUpdatedAt: iso,
|
|
91
|
-
pollInterval: DEFAULT_POLL_INTERVAL_MS,
|
|
92
|
-
tool,
|
|
93
|
-
_createdAtMs: nowMs,
|
|
94
|
-
};
|
|
95
|
-
this.tasks.set(record.taskId, record);
|
|
96
|
-
return record;
|
|
97
|
-
}
|
|
98
|
-
/** Mark a task terminal (completed/failed/cancelled) with its result. */
|
|
99
|
-
settle(taskId, status, result, statusMessage, nowMs = Date.now()) {
|
|
100
|
-
const record = this.tasks.get(taskId);
|
|
101
|
-
if (!record)
|
|
102
|
-
return;
|
|
103
|
-
record.status = status;
|
|
104
|
-
record.lastUpdatedAt = new Date(nowMs).toISOString();
|
|
105
|
-
if (result !== undefined)
|
|
106
|
-
record.result = result;
|
|
107
|
-
if (statusMessage !== undefined)
|
|
108
|
-
record.statusMessage = statusMessage;
|
|
109
|
-
}
|
|
110
|
-
/** Read a task record (after evicting any whose ttl has elapsed). */
|
|
111
|
-
get(taskId, nowMs = Date.now()) {
|
|
112
|
-
this.evictExpired(nowMs);
|
|
113
|
-
return this.tasks.get(taskId);
|
|
114
|
-
}
|
|
115
|
-
/** List the live task records, newest first (after eviction). */
|
|
116
|
-
list(nowMs = Date.now()) {
|
|
117
|
-
this.evictExpired(nowMs);
|
|
118
|
-
return [...this.tasks.values()].sort((a, b) => b._createdAtMs - a._createdAtMs);
|
|
119
|
-
}
|
|
120
|
-
/** Cancel a still-working task; no-op if unknown or already terminal. */
|
|
121
|
-
cancel(taskId, nowMs = Date.now()) {
|
|
122
|
-
const record = this.tasks.get(taskId);
|
|
123
|
-
if (!record || record.status !== 'working')
|
|
124
|
-
return false;
|
|
125
|
-
record.status = 'cancelled';
|
|
126
|
-
record.lastUpdatedAt = new Date(nowMs).toISOString();
|
|
127
|
-
return true;
|
|
128
|
-
}
|
|
129
|
-
size() {
|
|
130
|
-
return this.tasks.size;
|
|
131
|
-
}
|
|
132
|
-
/** Drop records whose (createdAt + ttl) is in the past. */
|
|
133
|
-
evictExpired(nowMs) {
|
|
134
|
-
for (const [id, rec] of this.tasks) {
|
|
135
|
-
if (rec.ttl != null && nowMs - rec._createdAtMs > rec.ttl)
|
|
136
|
-
this.tasks.delete(id);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
/** Drop the single oldest record (Map preserves insertion order). */
|
|
140
|
-
evictOldest() {
|
|
141
|
-
const oldest = this.tasks.keys().next();
|
|
142
|
-
if (!oldest.done)
|
|
143
|
-
this.tasks.delete(oldest.value);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/** Project a TaskRecord to the on-wire shape (strips Wyrm internals). */
|
|
147
|
-
export function toWireTask(record) {
|
|
148
|
-
const wire = {
|
|
149
|
-
taskId: record.taskId,
|
|
150
|
-
status: record.status,
|
|
151
|
-
ttl: record.ttl,
|
|
152
|
-
createdAt: record.createdAt,
|
|
153
|
-
lastUpdatedAt: record.lastUpdatedAt,
|
|
154
|
-
};
|
|
155
|
-
if (record.pollInterval !== undefined)
|
|
156
|
-
wire.pollInterval = record.pollInterval;
|
|
157
|
-
if (record.statusMessage !== undefined)
|
|
158
|
-
wire.statusMessage = record.statusMessage;
|
|
159
|
-
return wire;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Resolve whether a given CallTool should run as a task.
|
|
163
|
-
*
|
|
164
|
-
* The decisive signal is the request's `task` augmentation, because the MCP SDK
|
|
165
|
-
* (server/index.js) UNCONDITIONALLY requires the CallTool handler to return a
|
|
166
|
-
* `CreateTaskResult` whenever `params.task` is present — it does NOT consult the
|
|
167
|
-
* negotiated client capability. So a request bearing `task` MUST run the task
|
|
168
|
-
* path or the SDK rejects the response with -32602. The negotiated `tasks`
|
|
169
|
-
* capability is recorded as advisory context (a well-behaved client only sends
|
|
170
|
-
* `task` after negotiating it), not a suppressor — suppressing it here would
|
|
171
|
-
* desync from the SDK and crash the very clients the gate is meant to serve.
|
|
172
|
-
*
|
|
173
|
-
* No `task` augmentation → the synchronous fallback (a normal CallToolResult),
|
|
174
|
-
* the default for every client today.
|
|
175
|
-
*/
|
|
176
|
-
export function resolveTaskGate(tasksCapability, params) {
|
|
177
|
-
const capabilityNegotiated = clientSupportsTasks(tasksCapability);
|
|
178
|
-
const aug = readTaskAugmentation(params);
|
|
179
|
-
if (aug.requested)
|
|
180
|
-
return { asTask: true, ttlMs: aug.ttlMs, capabilityNegotiated };
|
|
181
|
-
return { asTask: false, ttlMs: DEFAULT_TASK_TTL_MS, capabilityNegotiated };
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Run a long op under the task gate. ALWAYS runs the op SYNCHRONOUSLY (no
|
|
185
|
-
* deferral) regardless of the gate — the op having run is the spine.
|
|
186
|
-
*
|
|
187
|
-
* - gate.asTask=false → returns {kind:'inline'} with the CallToolResult; the
|
|
188
|
-
* dispatcher returns it directly. This is the synchronous fallback.
|
|
189
|
-
* - gate.asTask=true → stores the completed CallToolResult and returns
|
|
190
|
-
* {kind:'task'} with a CreateTaskResult whose task is already `completed`
|
|
191
|
-
* (or `failed`). The dispatcher returns the CreateTaskResult on the wire;
|
|
192
|
-
* the client fetches the payload via tasks/result (served from the store).
|
|
193
|
-
*
|
|
194
|
-
* A throwing op records a `failed` task and re-throws (the dispatcher's error
|
|
195
|
-
* boundary owns the wire error shape — we never swallow it).
|
|
196
|
-
*/
|
|
197
|
-
export async function runLongOp(tool, gate, store, run, nowMs = Date.now()) {
|
|
198
|
-
if (!gate.asTask) {
|
|
199
|
-
return { kind: 'inline', result: await run() };
|
|
200
|
-
}
|
|
201
|
-
const record = store.create(tool, gate.ttlMs, nowMs);
|
|
202
|
-
let result;
|
|
203
|
-
try {
|
|
204
|
-
result = await run();
|
|
205
|
-
}
|
|
206
|
-
catch (err) {
|
|
207
|
-
store.settle(record.taskId, 'failed', undefined, String(err));
|
|
208
|
-
throw err;
|
|
209
|
-
}
|
|
210
|
-
store.settle(record.taskId, result?.isError ? 'failed' : 'completed', result);
|
|
211
|
-
// re-read so the wire task reflects the settled status/timestamp
|
|
212
|
-
const settled = store.get(record.taskId, nowMs) ?? record;
|
|
213
|
-
return { kind: 'task', createTask: { task: toWireTask(settled) } };
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Fetch the stored CallToolResult payload for a completed task (the
|
|
217
|
-
* tasks/result backend). Returns undefined for an unknown/expired/unsettled id.
|
|
218
|
-
*/
|
|
219
|
-
export function taskPayload(store, taskId, nowMs = Date.now()) {
|
|
220
|
-
return store.get(taskId, nowMs)?.result;
|
|
221
|
-
}
|
|
222
|
-
// ── Admin gate (Article VII: destructive long ops off by default) ───────────
|
|
223
|
-
//
|
|
224
|
-
// `wyrm_maintenance` is destructiveHint:true and `wyrm_reindex`/
|
|
225
|
-
// `wyrm_vector_setup` rebuild the embedding store — operations a stray fleet
|
|
226
|
-
// agent should not be able to trigger over MCP unless the operator has opted
|
|
227
|
-
// in. The gate is a single off-by-default env flag (`WYRM_ADMIN=1`), evaluated
|
|
228
|
-
// per call so it can be flipped without a restart in tests. The tools STILL
|
|
229
|
-
// remain on MCP (that is the whole point of T037 — the curated client must be
|
|
230
|
-
// able to drive a reindex); the gate just requires the operator's intent.
|
|
231
|
-
/** The admin-gated tools — the destructive/rebuild long ops on the wire. */
|
|
232
|
-
export const ADMIN_GATED_TOOLS = new Set([
|
|
233
|
-
'wyrm_maintenance',
|
|
234
|
-
'wyrm_reindex',
|
|
235
|
-
'wyrm_vector_setup',
|
|
236
|
-
]);
|
|
237
|
-
/**
|
|
238
|
-
* The RESOLVED dispatcher names the MCP-Tasks affordance TARGETS — the long ops
|
|
239
|
-
* of tools that REMAIN on MCP (spec T037): the admin upkeep/vector tools (also
|
|
240
|
-
* WYRM_ADMIN-gated) + the extraction modes of wyrm_capture, which resolve
|
|
241
|
-
* through the noun shim to wyrm_harvest (mode=artifacts) / wyrm_auto_capture
|
|
242
|
-
* (mode=extract). Note: a task augmentation on ANY tool is still honored by the
|
|
243
|
-
* dispatcher (the SDK forces a CreateTaskResult whenever params.task is set) —
|
|
244
|
-
* this set documents where a task is actually USEFUL, not the only place the
|
|
245
|
-
* shape is produced.
|
|
246
|
-
*/
|
|
247
|
-
export const LONG_OP_TOOLS = new Set([
|
|
248
|
-
...ADMIN_GATED_TOOLS,
|
|
249
|
-
'wyrm_harvest',
|
|
250
|
-
'wyrm_auto_capture',
|
|
251
|
-
]);
|
|
252
|
-
/** Machine-readable code for an admin-gate rejection (SEP-1303 family). */
|
|
253
|
-
export const WYRM_ADMIN_REQUIRED_CODE = 'WYRM_ADMIN_REQUIRED';
|
|
254
|
-
/** True when the operator has opted into admin-gated long ops. */
|
|
255
|
-
export function adminGateOpen(env = process.env) {
|
|
256
|
-
const v = env.WYRM_ADMIN;
|
|
257
|
-
return v === '1' || v === 'true' || v === 'yes';
|
|
258
|
-
}
|
|
259
|
-
/** True when `tool` is admin-gated and the operator has NOT opted in. */
|
|
260
|
-
export function adminGateBlocks(tool, env = process.env) {
|
|
261
|
-
return ADMIN_GATED_TOOLS.has(tool) && !adminGateOpen(env);
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* The structured (SEP-1303-shaped) response for an admin-gate rejection.
|
|
265
|
-
* `isError:true` with a non-retryable {error,expected} body so a fleet subagent
|
|
266
|
-
* does not loop — the operator must set WYRM_ADMIN, not the agent.
|
|
267
|
-
*/
|
|
268
|
-
export function adminDeniedResponse(tool) {
|
|
269
|
-
return renderErrorResponse({
|
|
270
|
-
error: {
|
|
271
|
-
code: WYRM_ADMIN_REQUIRED_CODE,
|
|
272
|
-
message: `${tool} is an admin-gated maintenance op (it rebuilds or vacuums the local memory store) ` +
|
|
273
|
-
`and is disabled by default.`,
|
|
274
|
-
retryable: false,
|
|
275
|
-
},
|
|
276
|
-
expected: `Set the WYRM_ADMIN=1 environment variable on the Wyrm MCP server process to enable ${tool}, ` +
|
|
277
|
-
`then call it again. This rejection is deterministic — do NOT retry unchanged. The operator ` +
|
|
278
|
-
`controls this flag, not the agent: it exists so a stray fleet call cannot vacuum/reindex the ` +
|
|
279
|
-
`store. The equivalent local CLI (\`wyrm maintenance\` / \`wyrm index rebuild\`) is always available.`,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
//# sourceMappingURL=tasks.js.map
|
|
1
|
+
import{randomUUID as u}from"node:crypto";import{renderErrorResponse as p}from"./render.js";const l=300*1e3,f=3600*1e3,k=500,_=256;function h(e){return e!=null&&typeof e=="object"}function m(e){const t=e&&typeof e=="object"?e.task:void 0;if(t==null||typeof t!="object")return{requested:!1};const s=t.ttl;let a=l;return typeof s=="number"&&Number.isFinite(s)&&s>0&&(a=Math.min(Math.floor(s),f)),{requested:!0,ttlMs:a}}class w{tasks=new Map;create(t,s,a=Date.now()){this.evictExpired(a),this.tasks.size>=_&&this.evictOldest();const i=new Date(a).toISOString(),n={taskId:u(),status:"working",ttl:s,createdAt:i,lastUpdatedAt:i,pollInterval:k,tool:t,_createdAtMs:a};return this.tasks.set(n.taskId,n),n}settle(t,s,a,i,n=Date.now()){const r=this.tasks.get(t);r&&(r.status=s,r.lastUpdatedAt=new Date(n).toISOString(),a!==void 0&&(r.result=a),i!==void 0&&(r.statusMessage=i))}get(t,s=Date.now()){return this.evictExpired(s),this.tasks.get(t)}list(t=Date.now()){return this.evictExpired(t),[...this.tasks.values()].sort((s,a)=>a._createdAtMs-s._createdAtMs)}cancel(t,s=Date.now()){const a=this.tasks.get(t);return!a||a.status!=="working"?!1:(a.status="cancelled",a.lastUpdatedAt=new Date(s).toISOString(),!0)}size(){return this.tasks.size}evictExpired(t){for(const[s,a]of this.tasks)a.ttl!=null&&t-a._createdAtMs>a.ttl&&this.tasks.delete(s)}evictOldest(){const t=this.tasks.keys().next();t.done||this.tasks.delete(t.value)}}function A(e){const t={taskId:e.taskId,status:e.status,ttl:e.ttl,createdAt:e.createdAt,lastUpdatedAt:e.lastUpdatedAt};return e.pollInterval!==void 0&&(t.pollInterval=e.pollInterval),e.statusMessage!==void 0&&(t.statusMessage=e.statusMessage),t}function I(e,t){const s=h(e),a=m(t);return a.requested?{asTask:!0,ttlMs:a.ttlMs,capabilityNegotiated:s}:{asTask:!1,ttlMs:l,capabilityNegotiated:s}}async function g(e,t,s,a,i=Date.now()){if(!t.asTask)return{kind:"inline",result:await a()};const n=s.create(e,t.ttlMs,i);let r;try{r=await a()}catch(o){throw s.settle(n.taskId,"failed",void 0,String(o)),o}s.settle(n.taskId,r?.isError?"failed":"completed",r);const d=s.get(n.taskId,i)??n;return{kind:"task",createTask:{task:A(d)}}}function D(e,t,s=Date.now()){return e.get(t,s)?.result}const c=new Set(["wyrm_maintenance","wyrm_reindex","wyrm_vector_setup"]),y=new Set([...c,"wyrm_harvest","wyrm_auto_capture"]),T="WYRM_ADMIN_REQUIRED";function x(e=process.env){const t=e.WYRM_ADMIN;return t==="1"||t==="true"||t==="yes"}function S(e,t=process.env){return c.has(e)&&!x(t)}function E(e){return p({error:{code:T,message:`${e} is an admin-gated maintenance op (it rebuilds or vacuums the local memory store) and is disabled by default.`,retryable:!1},expected:`Set the WYRM_ADMIN=1 environment variable on the Wyrm MCP server process to enable ${e}, then call it again. This rejection is deterministic \u2014 do NOT retry unchanged. The operator controls this flag, not the agent: it exists so a stray fleet call cannot vacuum/reindex the store. The equivalent local CLI (\`wyrm maintenance\` / \`wyrm index rebuild\`) is always available.`})}export{c as ADMIN_GATED_TOOLS,k as DEFAULT_POLL_INTERVAL_MS,l as DEFAULT_TASK_TTL_MS,y as LONG_OP_TOOLS,_ as MAX_RETAINED_TASKS,f as MAX_TASK_TTL_MS,w as TaskStore,T as WYRM_ADMIN_REQUIRED_CODE,E as adminDeniedResponse,S as adminGateBlocks,x as adminGateOpen,h as clientSupportsTasks,m as readTaskAugmentation,I as resolveTaskGate,g as runLongOp,D as taskPayload,A as toWireTask};
|
package/dist/token-budget.js
CHANGED
|
@@ -1,143 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Token budget primitive (spec 014).
|
|
3
|
-
*
|
|
4
|
-
* Pure functions over typed items. No DB, no I/O. Two responsibilities:
|
|
5
|
-
*
|
|
6
|
-
* 1. Estimate the token cost of a string. Phase 1 uses a deterministic
|
|
7
|
-
* 4-characters-per-token approximation (round up — slightly
|
|
8
|
-
* over-counts so we stay under budget). A future spec replaces this
|
|
9
|
-
* with `tiktoken` for exact `cl100k_base` counts.
|
|
10
|
-
*
|
|
11
|
-
* 2. Apply a token budget to a ranked list of items: rank-by-score,
|
|
12
|
-
* fill until budget, elide the rest as stubs. Items already shown in
|
|
13
|
-
* the session are demoted to a "seen" elision bucket.
|
|
14
|
-
*
|
|
15
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
16
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
17
|
-
*/
|
|
18
|
-
export function makeEstimator() {
|
|
19
|
-
// Phase 1: 4-chars-per-token approximation. English averages ~4 cpt
|
|
20
|
-
// in cl100k_base; code is closer to 3. Wyrm content is mixed; rounding
|
|
21
|
-
// up keeps us under budget rather than over.
|
|
22
|
-
return {
|
|
23
|
-
source: 'approx-4cpt',
|
|
24
|
-
count(text) {
|
|
25
|
-
if (!text)
|
|
26
|
-
return 0;
|
|
27
|
-
return Math.ceil(text.length / 4);
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Apply a token budget to a list of candidate items.
|
|
33
|
-
*
|
|
34
|
-
* Algorithm:
|
|
35
|
-
* 1. Sort by score desc (stable on tie).
|
|
36
|
-
* 2. For each item:
|
|
37
|
-
* - If key is in `alreadySeen`, elide with reason='seen', count stubCost.
|
|
38
|
-
* - Else if inlineCost fits in remaining budget, render inline.
|
|
39
|
-
* - Else elide with reason='budget', count stubCost.
|
|
40
|
-
* 3. Return both lists + token totals.
|
|
41
|
-
*
|
|
42
|
-
* Stubs are always emitted (operator can deref via wyrm_recall). The
|
|
43
|
-
* `reserved` knob lets callers carve out budget for a stable preamble
|
|
44
|
-
* block before items start consuming.
|
|
45
|
-
*/
|
|
46
|
-
export function applyBudget(items, opts) {
|
|
47
|
-
const seen = opts.alreadySeen ?? new Set();
|
|
48
|
-
const reserved = Math.max(0, opts.reserved ?? 0);
|
|
49
|
-
const usable = Math.max(0, opts.budget - reserved);
|
|
50
|
-
// Stable sort by score desc. JavaScript Array.prototype.sort is stable
|
|
51
|
-
// since ES2019 — relied on here for tie-break determinism.
|
|
52
|
-
const sorted = [...items].sort((a, b) => b.score - a.score);
|
|
53
|
-
const inline = [];
|
|
54
|
-
const elided = [];
|
|
55
|
-
let tokensInline = 0;
|
|
56
|
-
let tokensStubs = 0;
|
|
57
|
-
for (const candidate of sorted) {
|
|
58
|
-
if (seen.has(candidate.key)) {
|
|
59
|
-
elided.push({
|
|
60
|
-
item: candidate.item,
|
|
61
|
-
key: candidate.key,
|
|
62
|
-
reason: 'seen',
|
|
63
|
-
stubCost: candidate.stubCost,
|
|
64
|
-
});
|
|
65
|
-
tokensStubs += candidate.stubCost;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
if (tokensInline + candidate.inlineCost <= usable) {
|
|
69
|
-
inline.push(candidate.item);
|
|
70
|
-
tokensInline += candidate.inlineCost;
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
elided.push({
|
|
74
|
-
item: candidate.item,
|
|
75
|
-
key: candidate.key,
|
|
76
|
-
reason: 'budget',
|
|
77
|
-
stubCost: candidate.stubCost,
|
|
78
|
-
});
|
|
79
|
-
tokensStubs += candidate.stubCost;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
inline,
|
|
84
|
-
elided,
|
|
85
|
-
tokensInline,
|
|
86
|
-
tokensStubs,
|
|
87
|
-
budget: opts.budget,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
// -----------------------------------------------------------------------------
|
|
91
|
-
// Model-tier defaults (D1: auto-detect from MCP clientInfo.name; spec 014)
|
|
92
|
-
// -----------------------------------------------------------------------------
|
|
93
|
-
const FALLBACK_BUDGET = 4096;
|
|
94
|
-
/**
|
|
95
|
-
* Map a known MCP client name to its sensible default context budget.
|
|
96
|
-
* Precedence at call site: call-arg > env > auto > fallback.
|
|
97
|
-
*/
|
|
98
|
-
export function budgetForClient(clientName) {
|
|
99
|
-
if (!clientName)
|
|
100
|
-
return FALLBACK_BUDGET;
|
|
101
|
-
const n = clientName.toLowerCase();
|
|
102
|
-
// Anthropic surfaces
|
|
103
|
-
if (n.includes('mythos'))
|
|
104
|
-
return 4096; // future-proof — same as opus until we know
|
|
105
|
-
if (n.includes('opus'))
|
|
106
|
-
return 4096;
|
|
107
|
-
if (n.includes('sonnet'))
|
|
108
|
-
return 8192;
|
|
109
|
-
if (n.includes('haiku'))
|
|
110
|
-
return 12288;
|
|
111
|
-
if (n === 'claude-code' || n.startsWith('claude-code'))
|
|
112
|
-
return 4096; // assume opus
|
|
113
|
-
if (n === 'claude-desktop' || n.startsWith('claude-desktop'))
|
|
114
|
-
return 4096;
|
|
115
|
-
// Other clients — assume mid-tier
|
|
116
|
-
if (n === 'cursor' || n.includes('cursor'))
|
|
117
|
-
return 8192;
|
|
118
|
-
if (n === 'windsurf' || n.includes('windsurf'))
|
|
119
|
-
return 8192;
|
|
120
|
-
if (n === 'zed' || n.includes('zed'))
|
|
121
|
-
return 8192;
|
|
122
|
-
if (n === 'continue' || n.includes('continue'))
|
|
123
|
-
return 8192;
|
|
124
|
-
if (n === 'copilot' || n.includes('copilot'))
|
|
125
|
-
return 8192;
|
|
126
|
-
return FALLBACK_BUDGET;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Resolve the effective budget for a context_build call.
|
|
130
|
-
*
|
|
131
|
-
* call-arg > WYRM_CONTEXT_TOKEN_BUDGET env > clientName-derived > FALLBACK
|
|
132
|
-
*/
|
|
133
|
-
export function resolveBudget(opts) {
|
|
134
|
-
if (opts.callArg && opts.callArg > 0)
|
|
135
|
-
return opts.callArg;
|
|
136
|
-
if (opts.envValue) {
|
|
137
|
-
const n = parseInt(opts.envValue, 10);
|
|
138
|
-
if (!Number.isNaN(n) && n > 0)
|
|
139
|
-
return n;
|
|
140
|
-
}
|
|
141
|
-
return budgetForClient(opts.clientName);
|
|
142
|
-
}
|
|
143
|
-
//# sourceMappingURL=token-budget.js.map
|
|
1
|
+
function h(){return{source:"approx-4cpt",count(t){return t?Math.ceil(t.length/4):0}}}function g(t,e){const c=e.alreadySeen??new Set,l=Math.max(0,e.reserved??0),d=Math.max(0,e.budget-l),a=[...t].sort((n,f)=>f.score-n.score),i=[],r=[];let u=0,s=0;for(const n of a){if(c.has(n.key)){r.push({item:n.item,key:n.key,reason:"seen",stubCost:n.stubCost}),s+=n.stubCost;continue}u+n.inlineCost<=d?(i.push(n.item),u+=n.inlineCost):(r.push({item:n.item,key:n.key,reason:"budget",stubCost:n.stubCost}),s+=n.stubCost)}return{inline:i,elided:r,tokensInline:u,tokensStubs:s,budget:e.budget}}const o=4096;function b(t){if(!t)return o;const e=t.toLowerCase();return e.includes("mythos")||e.includes("opus")?4096:e.includes("sonnet")?8192:e.includes("haiku")?12288:e==="claude-code"||e.startsWith("claude-code")||e==="claude-desktop"||e.startsWith("claude-desktop")?4096:e==="cursor"||e.includes("cursor")||e==="windsurf"||e.includes("windsurf")||e==="zed"||e.includes("zed")||e==="continue"||e.includes("continue")||e==="copilot"||e.includes("copilot")?8192:o}function k(t){if(t.callArg&&t.callArg>0)return t.callArg;if(t.envValue){const e=parseInt(t.envValue,10);if(!Number.isNaN(e)&&e>0)return e}return b(t.clientName)}export{g as applyBudget,b as budgetForClient,h as makeEstimator,k as resolveBudget};
|
package/dist/tool-analytics.js
CHANGED
|
@@ -1,84 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* Tool Call Analytics — Wyrm watches Wyrm.
|
|
3
|
-
*
|
|
4
|
-
* Every wyrm_* tool call gets logged here: tool name, args summary,
|
|
5
|
-
* success/failure, latency, optional project context. From this stream
|
|
6
|
-
* we derive:
|
|
7
|
-
*
|
|
8
|
-
* - usage histogram (which tools are most-called)
|
|
9
|
-
* - error rate per tool
|
|
10
|
-
* - p50/p95 latency per tool
|
|
11
|
-
* - args-pattern summarisation (what args do callers pass)
|
|
12
|
-
* - recall→action correlation (was a returned memory followed by an action?)
|
|
13
|
-
*
|
|
14
|
-
* Operators see "Claude calls wyrm_recall 80% of the time but the result
|
|
15
|
-
* is unused 60% of the time → ranking is bad → tune retrieval".
|
|
16
|
-
*
|
|
17
|
-
* Privacy: args_summary is truncated and stripped of values longer than
|
|
18
|
-
* 100 chars per field — we keep keys and short values for pattern
|
|
19
|
-
* analysis, never full payloads.
|
|
20
|
-
*
|
|
21
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
22
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
23
|
-
*/
|
|
24
|
-
export class ToolAnalytics {
|
|
25
|
-
db;
|
|
26
|
-
constructor(db) {
|
|
27
|
-
this.db = db;
|
|
28
|
-
}
|
|
29
|
-
/** Truncate args for logging — drop long values, keep keys + short values. */
|
|
30
|
-
static summarizeArgs(args) {
|
|
31
|
-
if (args == null)
|
|
32
|
-
return '';
|
|
33
|
-
if (typeof args !== 'object')
|
|
34
|
-
return String(args).slice(0, 100);
|
|
35
|
-
try {
|
|
36
|
-
const o = args;
|
|
37
|
-
const out = {};
|
|
38
|
-
for (const [k, v] of Object.entries(o)) {
|
|
39
|
-
if (v == null) {
|
|
40
|
-
out[k] = null;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (typeof v === 'string') {
|
|
44
|
-
out[k] = v.length > 100 ? `<${v.length}ch>` : v;
|
|
45
|
-
}
|
|
46
|
-
else if (typeof v === 'number' || typeof v === 'boolean') {
|
|
47
|
-
out[k] = v;
|
|
48
|
-
}
|
|
49
|
-
else if (Array.isArray(v)) {
|
|
50
|
-
out[k] = `<array:${v.length}>`;
|
|
51
|
-
}
|
|
52
|
-
else if (typeof v === 'object') {
|
|
53
|
-
out[k] = `<object>`;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return JSON.stringify(out).slice(0, 500);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return '<unserializable>';
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
/** Log a single tool call. Designed to be fire-and-forget — swallows errors
|
|
63
|
-
* so logging never breaks the tool's own response path. */
|
|
64
|
-
log(input) {
|
|
65
|
-
try {
|
|
66
|
-
this.db.prepare(`
|
|
1
|
+
class _{db;constructor(t){this.db=t}static summarizeArgs(t){if(t==null)return"";if(typeof t!="object")return String(t).slice(0,100);try{const s=t,e={};for(const[a,o]of Object.entries(s)){if(o==null){e[a]=null;continue}typeof o=="string"?e[a]=o.length>100?`<${o.length}ch>`:o:typeof o=="number"||typeof o=="boolean"?e[a]=o:Array.isArray(o)?e[a]=`<array:${o.length}>`:typeof o=="object"&&(e[a]="<object>")}return JSON.stringify(e).slice(0,500)}catch{return"<unserializable>"}}log(t){try{this.db.prepare(`
|
|
67
2
|
INSERT INTO tool_call_log
|
|
68
3
|
(tool_name, project_id, args_summary, success, error_message, latency_ms)
|
|
69
4
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
70
|
-
`).run(
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// Logging must never break the tool path.
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/** Per-tool usage statistics over the last `days` days (default 30). */
|
|
77
|
-
usageStats(opts) {
|
|
78
|
-
const days = opts?.days ?? 30;
|
|
79
|
-
const limit = opts?.limit ?? 100;
|
|
80
|
-
// Get aggregate stats per tool
|
|
81
|
-
const aggregates = this.db.prepare(`
|
|
5
|
+
`).run(t.tool_name,t.project_id??null,t.args!==void 0?_.summarizeArgs(t.args):null,t.success?1:0,t.error_message??null,Math.max(0,Math.round(t.latency_ms)))}catch{}}usageStats(t){const s=t?.days??30,e=t?.limit??100,a=this.db.prepare(`
|
|
82
6
|
SELECT
|
|
83
7
|
tool_name,
|
|
84
8
|
COUNT(*) AS total,
|
|
@@ -90,49 +14,18 @@ export class ToolAnalytics {
|
|
|
90
14
|
GROUP BY tool_name
|
|
91
15
|
ORDER BY total DESC
|
|
92
16
|
LIMIT ?
|
|
93
|
-
`).all(
|
|
94
|
-
// For each tool, compute p50/p95 latency (small enough to do in-process)
|
|
95
|
-
const results = [];
|
|
96
|
-
for (const a of aggregates) {
|
|
97
|
-
const latencies = this.db.prepare(`
|
|
17
|
+
`).all(s,e),o=[];for(const l of a){const c=this.db.prepare(`
|
|
98
18
|
SELECT latency_ms FROM tool_call_log
|
|
99
19
|
WHERE tool_name = ?
|
|
100
20
|
AND called_at >= datetime('now', '-' || ? || ' days')
|
|
101
21
|
ORDER BY latency_ms
|
|
102
|
-
`).all(
|
|
103
|
-
const p = (arr, pct) => {
|
|
104
|
-
if (arr.length === 0)
|
|
105
|
-
return 0;
|
|
106
|
-
const idx = Math.min(arr.length - 1, Math.floor((arr.length - 1) * pct));
|
|
107
|
-
return arr[idx];
|
|
108
|
-
};
|
|
109
|
-
results.push({
|
|
110
|
-
tool_name: a.tool_name,
|
|
111
|
-
total: a.total,
|
|
112
|
-
success_count: a.success_count,
|
|
113
|
-
error_count: a.error_count,
|
|
114
|
-
error_rate: a.total > 0 ? Math.round((a.error_count / a.total) * 1000) / 1000 : 0,
|
|
115
|
-
p50_ms: p(latencies, 0.5),
|
|
116
|
-
p95_ms: p(latencies, 0.95),
|
|
117
|
-
last_called: a.last_called,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
return results;
|
|
121
|
-
}
|
|
122
|
-
/** Recent error sample for a tool — useful when error_rate > 0. */
|
|
123
|
-
recentErrors(toolName, limit = 10) {
|
|
124
|
-
return this.db.prepare(`
|
|
22
|
+
`).all(l.tool_name,s).map(r=>r.latency_ms),n=(r,u)=>{if(r.length===0)return 0;const i=Math.min(r.length-1,Math.floor((r.length-1)*u));return r[i]};o.push({tool_name:l.tool_name,total:l.total,success_count:l.success_count,error_count:l.error_count,error_rate:l.total>0?Math.round(l.error_count/l.total*1e3)/1e3:0,p50_ms:n(c,.5),p95_ms:n(c,.95),last_called:l.last_called})}return o}recentErrors(t,s=10){return this.db.prepare(`
|
|
125
23
|
SELECT called_at, error_message, args_summary
|
|
126
24
|
FROM tool_call_log
|
|
127
25
|
WHERE tool_name = ? AND success = 0
|
|
128
26
|
ORDER BY called_at DESC
|
|
129
27
|
LIMIT ?
|
|
130
|
-
`).all(
|
|
131
|
-
}
|
|
132
|
-
/** Total volume + success rate over the whole window. */
|
|
133
|
-
overview(opts) {
|
|
134
|
-
const days = opts?.days ?? 30;
|
|
135
|
-
const row = this.db.prepare(`
|
|
28
|
+
`).all(t,s)}overview(t){const s=t?.days??30,e=this.db.prepare(`
|
|
136
29
|
SELECT
|
|
137
30
|
COUNT(*) AS total,
|
|
138
31
|
SUM(success) AS success_count,
|
|
@@ -140,22 +33,7 @@ export class ToolAnalytics {
|
|
|
140
33
|
COUNT(DISTINCT tool_name) AS distinct_tools
|
|
141
34
|
FROM tool_call_log
|
|
142
35
|
WHERE called_at >= datetime('now', '-' || ? || ' days')
|
|
143
|
-
`).get(days)
|
|
144
|
-
return {
|
|
145
|
-
days,
|
|
146
|
-
total_calls: row.total || 0,
|
|
147
|
-
success_count: row.success_count || 0,
|
|
148
|
-
error_count: row.error_count || 0,
|
|
149
|
-
success_rate: row.total > 0 ? Math.round((row.success_count / row.total) * 1000) / 1000 : 0,
|
|
150
|
-
distinct_tools: row.distinct_tools || 0,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
/** Prune rows older than N days. Returns count deleted. Run via maintenance. */
|
|
154
|
-
prune(olderThanDays = 90) {
|
|
155
|
-
return this.db.prepare(`
|
|
36
|
+
`).get(s);return{days:s,total_calls:e.total||0,success_count:e.success_count||0,error_count:e.error_count||0,success_rate:e.total>0?Math.round(e.success_count/e.total*1e3)/1e3:0,distinct_tools:e.distinct_tools||0}}prune(t=90){return this.db.prepare(`
|
|
156
37
|
DELETE FROM tool_call_log
|
|
157
38
|
WHERE called_at < datetime('now', '-' || ? || ' days')
|
|
158
|
-
`).run(
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
//# sourceMappingURL=tool-analytics.js.map
|
|
39
|
+
`).run(t).changes}}export{_ as ToolAnalytics};
|