dsh-tacit 0.2.2 → 0.3.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/README.md +24 -3
- package/client/client.js +1822 -108
- package/docs/README.md +3 -3
- package/docs/README.zh.md +7 -6
- package/lib/analyze.js +289 -49
- package/lib/index.js +16 -1
- package/lib/pricing-source.js +133 -0
- package/lib/pricing.js +311 -0
- package/lib/routes.js +15 -2
- package/lib/schema.js +187 -1
- package/lib/service.js +542 -103
- package/lib/store.js +164 -3
- package/lib/usage.js +708 -0
- package/package.json +11 -5
package/lib/schema.js
CHANGED
|
@@ -21,6 +21,16 @@ export const COACH_MODELS = ['deepseek-v4-flash', 'deepseek-v4-pro']
|
|
|
21
21
|
*/
|
|
22
22
|
export const COACH_PROVIDER = 'deepseek-official'
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The model-call failure codes Tacit is allowed to put on the wire. The client
|
|
26
|
+
* renders `err.<code>` for whatever the envelope carries, so a raw provider
|
|
27
|
+
* code (`RATE_LIMIT`, `ERROR`, `ABORTED`, …) would surface as a literal
|
|
28
|
+
* `err.ABORTED` banner — `coachErrorCode` (`lib/service.js`) maps anything
|
|
29
|
+
* outside this list onto one of these. Every entry has an `err.*` key in both
|
|
30
|
+
* dictionaries (`client/src/10-i18n.js`, test-enforced).
|
|
31
|
+
*/
|
|
32
|
+
export const COACH_ERROR_CODES = ['no-llm', 'no-api-key', 'rate-limited', 'timeout', 'empty-response', 'call-failed']
|
|
33
|
+
|
|
24
34
|
/**
|
|
25
35
|
* The loader-facing plugin config. Wrapped in `z.preprocess` so a patch row
|
|
26
36
|
* without a `config:` block (`undefined`) resolves to all defaults — a bare
|
|
@@ -59,6 +69,16 @@ export const Config = z.preprocess((v) => v ?? {}, z.object({
|
|
|
59
69
|
directiveTrialTurns: z.number().default(10),
|
|
60
70
|
/** A candidate retires when the messy-turn rate during its trial exceeds the baseline by more than this. */
|
|
61
71
|
directiveWorseBy: z.number().default(0.15),
|
|
72
|
+
/** Bootstrap analyses run at once (1 = one after another; same calls, less waiting). */
|
|
73
|
+
bootstrapConcurrency: z.number().default(1),
|
|
74
|
+
/** Also learn from a clean turn that follows a messy one (what the user included the second time). Automatic, capped. */
|
|
75
|
+
learnFromGood: z.boolean().default(true),
|
|
76
|
+
/** Days of detailed usage-ledger day files kept before they expire (7-365, clamped in mergeConfig). */
|
|
77
|
+
costHistoryDays: z.number().default(30),
|
|
78
|
+
/** Daily USD spend that triggers the warn/exceeded cost UI; 0 disables (clamped in mergeConfig). */
|
|
79
|
+
costWarnDailyUsd: z.number().default(0),
|
|
80
|
+
/** Same as `costWarnDailyUsd`, over a calendar month; 0 disables (clamped in mergeConfig). */
|
|
81
|
+
costWarnMonthlyUsd: z.number().default(0),
|
|
62
82
|
}))
|
|
63
83
|
|
|
64
84
|
/**
|
|
@@ -82,6 +102,11 @@ const configPatchSchema = z.object({
|
|
|
82
102
|
enrichPrompts: z.boolean().optional(),
|
|
83
103
|
directiveTrialTurns: z.number().optional(),
|
|
84
104
|
directiveWorseBy: z.number().optional(),
|
|
105
|
+
bootstrapConcurrency: z.number().optional(),
|
|
106
|
+
learnFromGood: z.boolean().optional(),
|
|
107
|
+
costHistoryDays: z.number().optional(),
|
|
108
|
+
costWarnDailyUsd: z.number().optional(),
|
|
109
|
+
costWarnMonthlyUsd: z.number().optional(),
|
|
85
110
|
})
|
|
86
111
|
|
|
87
112
|
// ── Trajectory projection ──────────────────────────────────────────────────
|
|
@@ -160,6 +185,12 @@ export const reportSchema = z.object({
|
|
|
160
185
|
trigger: z.string().default('manual'),
|
|
161
186
|
/** The user's next message when it triggered the analysis (clipped). */
|
|
162
187
|
followUp: z.string().optional(),
|
|
188
|
+
/** Absolute workspace directory of the conversation, when the harness knew it. */
|
|
189
|
+
cwd: z.string().optional(),
|
|
190
|
+
/** trigger 'good' only: what the clean prompt supplied that the messy one before it lacked. */
|
|
191
|
+
strengths: z.array(z.object({ kind: z.string(), what: z.string() })).optional(),
|
|
192
|
+
/** trigger 'good' only: the one-sentence lesson fed to the distiller. */
|
|
193
|
+
lesson: z.string().optional(),
|
|
163
194
|
})
|
|
164
195
|
|
|
165
196
|
/**
|
|
@@ -178,6 +209,8 @@ export const patternCountersSchema = z.object({
|
|
|
178
209
|
verified: z.number().int().default(0),
|
|
179
210
|
/** Times the next turn's outcome was same/worse than the baseline. */
|
|
180
211
|
unverified: z.number().int().default(0),
|
|
212
|
+
/** Times a clean prompt right after a messy turn showed the user supplying this themselves. */
|
|
213
|
+
resolved: z.number().int().default(0),
|
|
181
214
|
})
|
|
182
215
|
|
|
183
216
|
/** One distilled durable style rule (from rejected-improvement reasons). */
|
|
@@ -219,6 +252,8 @@ const directiveSchema = z.object({
|
|
|
219
252
|
status: z.enum(['candidate', 'active', 'retired']).default('active'),
|
|
220
253
|
trial: directiveTrialSchema.optional(),
|
|
221
254
|
retiredReason: z.string().optional(),
|
|
255
|
+
/** Absolute workspace directory this directive is limited to; absent = every conversation. */
|
|
256
|
+
workspace: z.string().optional(),
|
|
222
257
|
})
|
|
223
258
|
|
|
224
259
|
/** The persistent user-wide mistake profile. */
|
|
@@ -257,7 +292,7 @@ export const statsArgSchema = z.object({
|
|
|
257
292
|
|
|
258
293
|
export const directivesArgSchema = z.discriminatedUnion('action', [
|
|
259
294
|
z.object({ action: z.literal('toggle'), id: z.string().min(1).max(64), enabled: z.boolean() }),
|
|
260
|
-
z.object({ action: z.literal('add'), text: z.string().min(1).max(300) }),
|
|
295
|
+
z.object({ action: z.literal('add'), text: z.string().min(1).max(300), workspace: z.string().max(1000).optional() }),
|
|
261
296
|
z.object({ action: z.literal('remove'), id: z.string().min(1).max(64) }),
|
|
262
297
|
])
|
|
263
298
|
|
|
@@ -272,6 +307,12 @@ export const analyzeArgSchema = z.object({
|
|
|
272
307
|
turn: z.number().int().min(1),
|
|
273
308
|
})
|
|
274
309
|
|
|
310
|
+
/** `/api/tacit/analyze-batch`: one session, up to 50 turns analyzed under a single run. */
|
|
311
|
+
export const analyzeBatchArgSchema = z.object({
|
|
312
|
+
sessionId: z.string().min(1).max(200),
|
|
313
|
+
turns: z.array(z.number().int().min(1)).min(1).max(50),
|
|
314
|
+
})
|
|
315
|
+
|
|
275
316
|
export const improveArgSchema = z.object({
|
|
276
317
|
sessionId: z.string().min(1).max(200),
|
|
277
318
|
draft: z.string().min(1).max(100000),
|
|
@@ -292,3 +333,148 @@ export const appliedArgSchema = z.object({
|
|
|
292
333
|
export const configArgSchema = z.object({
|
|
293
334
|
patch: configPatchSchema,
|
|
294
335
|
})
|
|
336
|
+
|
|
337
|
+
// ── Usage ledger (content-free: no prompts, no responses, no tool args) ────
|
|
338
|
+
|
|
339
|
+
/** Every op a metered model call can be tagged with (Task 1's sink + the distillation/enrichment calls). */
|
|
340
|
+
export const USAGE_OPS = [
|
|
341
|
+
'analysis',
|
|
342
|
+
'analysis-repair',
|
|
343
|
+
'directive-distillation',
|
|
344
|
+
'style-distillation',
|
|
345
|
+
'improve',
|
|
346
|
+
'improve-repair',
|
|
347
|
+
'enrichment',
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
/** Every kind of run the tracker groups attempts into. */
|
|
351
|
+
export const USAGE_RUN_TYPES = [
|
|
352
|
+
'bootstrap',
|
|
353
|
+
'analysis',
|
|
354
|
+
'analysis-batch',
|
|
355
|
+
'improve',
|
|
356
|
+
'directive-distillation',
|
|
357
|
+
'style-distillation',
|
|
358
|
+
'prompt-enrichment',
|
|
359
|
+
]
|
|
360
|
+
|
|
361
|
+
/** Raw token counts, zero-filled so totals can be summed without null checks. */
|
|
362
|
+
export const tokenBucketsSchema = z.object({
|
|
363
|
+
inputTokens: z.number().default(0),
|
|
364
|
+
outputTokens: z.number().default(0),
|
|
365
|
+
cacheReadTokens: z.number().default(0),
|
|
366
|
+
cacheWriteTokens: z.number().default(0),
|
|
367
|
+
reasoningTokens: z.number().default(0),
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* One metered model call. Mirrors the sink record `callCoachModel` hands the
|
|
372
|
+
* tracker (`startedAt`..`usage`) plus the identity fields the tracker itself
|
|
373
|
+
* assigns (`id`, `op`, `sessionId`, `turn`) and the priced result. Never
|
|
374
|
+
* carries prompt/response text, tool args, or API keys.
|
|
375
|
+
*/
|
|
376
|
+
export const usageAttemptSchema = z.object({
|
|
377
|
+
id: z.string(),
|
|
378
|
+
op: z.enum(USAGE_OPS),
|
|
379
|
+
startedAt: z.number(),
|
|
380
|
+
durationMs: z.number().default(0),
|
|
381
|
+
model: z.string().default(''),
|
|
382
|
+
provider: z.string().default(''),
|
|
383
|
+
reasoningEffort: z.string().nullable().default(null),
|
|
384
|
+
finish: z.string().default(''),
|
|
385
|
+
status: z.enum(['ok', 'failed', 'unmetered']),
|
|
386
|
+
code: z.string().default(''),
|
|
387
|
+
sessionId: z.string().default(''),
|
|
388
|
+
turn: z.number().nullable().default(null),
|
|
389
|
+
usage: tokenBucketsSchema.nullable().default(null),
|
|
390
|
+
/** null when no price table matched the route/model (e.g. a proxy provider). */
|
|
391
|
+
priced: z.object({
|
|
392
|
+
source: z.enum(['bundled', 'costMeter']),
|
|
393
|
+
tier: z.string(),
|
|
394
|
+
rates: z.object({ cacheHit: z.number(), cacheMiss: z.number(), output: z.number() }),
|
|
395
|
+
asOf: z.string(),
|
|
396
|
+
usd: z.number(),
|
|
397
|
+
}).nullable().default(null),
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* A precomputed, already-defaulted instance of a nested object schema.
|
|
402
|
+
* zod's `.default(value)` injects `value` verbatim when a field is absent —
|
|
403
|
+
* it does NOT re-run `value` through the schema — so a literal `{}` default
|
|
404
|
+
* on a nested object would skip that object's own field defaults. Passing
|
|
405
|
+
* `schema.parse({})` instead gives the same "all defaults" shape correctly.
|
|
406
|
+
*/
|
|
407
|
+
const emptyTokenBuckets = tokenBucketsSchema.parse({})
|
|
408
|
+
|
|
409
|
+
/** Aggregate counters shared by a run's totals, the lifetime summary, and every summary bucket. */
|
|
410
|
+
export const usageTotalsSchema = z.object({
|
|
411
|
+
attempts: z.number().default(0),
|
|
412
|
+
billedCalls: z.number().default(0),
|
|
413
|
+
unmeteredCalls: z.number().default(0),
|
|
414
|
+
unpricedCalls: z.number().default(0),
|
|
415
|
+
tokens: tokenBucketsSchema.default(emptyTokenBuckets),
|
|
416
|
+
usdKnown: z.number().default(0),
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
const emptyUsageTotals = usageTotalsSchema.parse({})
|
|
420
|
+
|
|
421
|
+
/** One tracker run: a group of attempts sharing a trigger (a single call, an auto-analysis, a bootstrap batch, ...). */
|
|
422
|
+
export const usageRunSchema = z.object({
|
|
423
|
+
runId: z.string(),
|
|
424
|
+
type: z.enum(USAGE_RUN_TYPES),
|
|
425
|
+
trigger: z.string().default(''),
|
|
426
|
+
startedAt: z.number(),
|
|
427
|
+
endedAt: z.number().default(0),
|
|
428
|
+
status: z.enum(['running', 'success', 'partial', 'failed']).default('running'),
|
|
429
|
+
sessionId: z.string().default(''),
|
|
430
|
+
turn: z.number().nullable().default(null),
|
|
431
|
+
workspace: z.string().default(''),
|
|
432
|
+
model: z.string().default(''),
|
|
433
|
+
provider: z.string().default(''),
|
|
434
|
+
results: z.record(z.number()).default({}),
|
|
435
|
+
attempts: z.array(usageAttemptSchema).default([]),
|
|
436
|
+
totals: usageTotalsSchema.default(emptyUsageTotals),
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
/** One day's `usage/YYYY-MM-DD.json` file. */
|
|
440
|
+
export const usageDayFileSchema = z.object({
|
|
441
|
+
version: z.literal(1),
|
|
442
|
+
day: z.string(),
|
|
443
|
+
runs: z.array(usageRunSchema).default([]),
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
const usageDayTotalsSchema = usageTotalsSchema.extend({
|
|
447
|
+
byType: z.record(usageTotalsSchema).default({}),
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
/** `usage/summary.json`: rolling totals kept alongside the day files so reports never have to re-scan every day. */
|
|
451
|
+
export const usageSummarySchema = z.object({
|
|
452
|
+
version: z.literal(1),
|
|
453
|
+
trackingSince: z.number(),
|
|
454
|
+
lifetime: usageTotalsSchema.default(emptyUsageTotals),
|
|
455
|
+
byType: z.record(usageTotalsSchema).default({}),
|
|
456
|
+
byModel: z.record(usageTotalsSchema).default({}),
|
|
457
|
+
days: z.record(usageDayTotalsSchema).default({}),
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Arguments for `/api/tacit/usage`. Declared here (not next to
|
|
462
|
+
* `bootstrapArgSchema`) because `z.enum(USAGE_RUN_TYPES)` needs the run-type
|
|
463
|
+
* list above it. Every field is optional on the wire; `tracker.report()`
|
|
464
|
+
* applies the defaults (`range: '30d'`, `page: 1`, `pageSize: 20`).
|
|
465
|
+
*/
|
|
466
|
+
export const usageArgSchema = z.object({
|
|
467
|
+
range: z.enum(['today', '7d', '30d', 'month', 'all']).optional(),
|
|
468
|
+
type: z.enum(USAGE_RUN_TYPES).optional(),
|
|
469
|
+
status: z.enum(['success', 'partial', 'failed']).optional(),
|
|
470
|
+
model: z.string().max(64).optional(),
|
|
471
|
+
workspace: z.string().max(200).optional(),
|
|
472
|
+
sessionId: z.string().max(200).optional(),
|
|
473
|
+
page: z.number().int().min(1).max(1000).optional(),
|
|
474
|
+
pageSize: z.number().int().min(1).max(100).optional(),
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
/** Arguments for `/api/tacit/usage-run`: one run id, as minted by `beginRun`. */
|
|
478
|
+
export const usageRunArgSchema = z.object({
|
|
479
|
+
runId: z.string().min(1).max(64),
|
|
480
|
+
})
|