@kernel.chat/kbot 4.0.0 → 4.1.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.
Files changed (59) hide show
  1. package/README.md +6 -0
  2. package/dist/cache-warmth.d.ts +25 -0
  3. package/dist/cache-warmth.js +131 -0
  4. package/dist/futures/debate/index.d.ts +7 -0
  5. package/dist/futures/debate/index.js +6 -0
  6. package/dist/futures/debate/runner.d.ts +34 -0
  7. package/dist/futures/debate/runner.js +140 -0
  8. package/dist/futures/debate/synthesis.d.ts +25 -0
  9. package/dist/futures/debate/synthesis.js +81 -0
  10. package/dist/futures/debate/types.d.ts +72 -0
  11. package/dist/futures/debate/types.js +12 -0
  12. package/dist/futures/forecast/index.d.ts +5 -0
  13. package/dist/futures/forecast/index.js +5 -0
  14. package/dist/futures/forecast/projection.d.ts +31 -0
  15. package/dist/futures/forecast/projection.js +177 -0
  16. package/dist/futures/forecast/synthesize.d.ts +19 -0
  17. package/dist/futures/forecast/synthesize.js +89 -0
  18. package/dist/futures/forecast/types.d.ts +59 -0
  19. package/dist/futures/forecast/types.js +15 -0
  20. package/dist/futures/harness/critic-evaluator.d.ts +39 -0
  21. package/dist/futures/harness/critic-evaluator.js +131 -0
  22. package/dist/futures/harness/evolution-loop.d.ts +41 -0
  23. package/dist/futures/harness/evolution-loop.js +168 -0
  24. package/dist/futures/harness/index.d.ts +16 -0
  25. package/dist/futures/harness/index.js +13 -0
  26. package/dist/futures/harness/meta-evolution.d.ts +32 -0
  27. package/dist/futures/harness/meta-evolution.js +52 -0
  28. package/dist/futures/harness/noop-evolution.d.ts +23 -0
  29. package/dist/futures/harness/noop-evolution.js +29 -0
  30. package/dist/futures/harness/persistence.d.ts +30 -0
  31. package/dist/futures/harness/persistence.js +99 -0
  32. package/dist/futures/harness/types.d.ts +147 -0
  33. package/dist/futures/harness/types.js +18 -0
  34. package/dist/futures/index.d.ts +16 -0
  35. package/dist/futures/index.js +22 -0
  36. package/dist/futures/latent-state/envelope.d.ts +39 -0
  37. package/dist/futures/latent-state/envelope.js +178 -0
  38. package/dist/futures/latent-state/index.d.ts +5 -0
  39. package/dist/futures/latent-state/index.js +3 -0
  40. package/dist/futures/latent-state/types.d.ts +47 -0
  41. package/dist/futures/latent-state/types.js +13 -0
  42. package/dist/futures/persona/check.d.ts +45 -0
  43. package/dist/futures/persona/check.js +205 -0
  44. package/dist/futures/persona/index.d.ts +5 -0
  45. package/dist/futures/persona/index.js +5 -0
  46. package/dist/futures/persona/registry.d.ts +22 -0
  47. package/dist/futures/persona/registry.js +124 -0
  48. package/dist/futures/persona/types.d.ts +68 -0
  49. package/dist/futures/persona/types.js +28 -0
  50. package/dist/futures/skill-graph/graph.d.ts +31 -0
  51. package/dist/futures/skill-graph/graph.js +151 -0
  52. package/dist/futures/skill-graph/index.d.ts +13 -0
  53. package/dist/futures/skill-graph/index.js +10 -0
  54. package/dist/futures/skill-graph/synthesis.d.ts +20 -0
  55. package/dist/futures/skill-graph/synthesis.js +83 -0
  56. package/dist/futures/skill-graph/types.d.ts +53 -0
  57. package/dist/futures/skill-graph/types.js +19 -0
  58. package/dist/streaming.js +18 -0
  59. package/package.json +1 -1
package/README.md CHANGED
@@ -36,6 +36,12 @@ Most terminal AI agents lock you into one provider, one model, one way of workin
36
36
  - **Programmatic SDK** — use kbot as a library in your own apps.
37
37
  - **MCP server built in** — plug kbot into Claude Code, Cursor, VS Code, Zed, or Neovim as a tool provider.
38
38
 
39
+ ## Benchmarks
40
+
41
+ Methodology-explicit comparison vs other CLI agents → [BENCHMARKS.md](./BENCHMARKS.md). TL;DR: kbot beats Aider (4.4×) and OpenCode (5.7×) on cold start; loses to Claude Code, Codex, and jcode on raw boot but wins on cost-per-task (BYOK + Ollama fallback), vertical depth (Ableton/security/computer-use/channels), and offline availability (~70% of representative tasks).
42
+
43
+ Using jcode? Wire kbot in as an MCP backend → [templates/jcode-integration.md](./templates/jcode-integration.md).
44
+
39
45
  ## Use with Claude Code / Cursor / Zed
40
46
 
41
47
  kbot is designed to compound with your existing AI editor, not replace it. One command wires everything up — MCP server config + a Claude Code skill that pre-authorizes the integration so safety filters don't refuse legitimate kbot calls.
@@ -0,0 +1,25 @@
1
+ /** Anthropic prompt cache TTL — 5 minutes */
2
+ export declare const CACHE_TTL_MS: number;
3
+ /** Hash a system prompt to a short stable key */
4
+ export declare function hashPrompt(text: string): string;
5
+ /** Reset in-memory cache (test hook) */
6
+ export declare function _resetCacheWarmthCache(): void;
7
+ /** Record a successful API call's timestamp */
8
+ export declare function recordCacheCall(model: string, promptHash: string, now?: number): void;
9
+ export interface CacheWarmthCheck {
10
+ warm: boolean;
11
+ ageMs?: number;
12
+ estimatedExtraCostUSD?: number;
13
+ message?: string;
14
+ }
15
+ /**
16
+ * Check whether the prompt cache is still warm for (model, promptHash).
17
+ * Returns warm=true if no prior call OR within TTL. Returns warm=false
18
+ * with a chalk.yellow message when cold AND we haven't warned for this
19
+ * specific cold-event yet.
20
+ *
21
+ * @param costPerMTokInput USD per million input tokens (from auth.ts)
22
+ * @param promptTokenEstimate rough token count (e.g. text.length / 4)
23
+ */
24
+ export declare function checkCacheWarmth(model: string, promptHash: string, costPerMTokInput: number, promptTokenEstimate: number, now?: number): CacheWarmthCheck;
25
+ //# sourceMappingURL=cache-warmth.d.ts.map
@@ -0,0 +1,131 @@
1
+ // kbot Cache Warmth — Anthropic prompt cache TTL warning
2
+ //
3
+ // Anthropic's prompt cache has a 5-minute TTL. If the next API call lands
4
+ // after the cache expired, the user pays full input-token price instead
5
+ // of the cached price. This module tracks per-(model, prompt-hash) call
6
+ // timestamps and warns once per cold event.
7
+ //
8
+ // State persists at ~/.kbot/cache-warmth.json (atomic tmp+rename writes).
9
+ import { createHash } from 'node:crypto';
10
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join, dirname } from 'node:path';
13
+ import chalk from 'chalk';
14
+ /** Anthropic prompt cache TTL — 5 minutes */
15
+ export const CACHE_TTL_MS = 5 * 60 * 1000;
16
+ /** State file location — overridable via KBOT_CACHE_WARMTH_PATH (test hook) */
17
+ function statePath() {
18
+ return process.env.KBOT_CACHE_WARMTH_PATH || join(homedir(), '.kbot', 'cache-warmth.json');
19
+ }
20
+ /** Hash a system prompt to a short stable key */
21
+ export function hashPrompt(text) {
22
+ return createHash('md5').update(text).digest('hex').slice(0, 16);
23
+ }
24
+ let cached;
25
+ function emptyState() {
26
+ return { lastCall: {}, warnedColdEvents: {} };
27
+ }
28
+ function loadState() {
29
+ if (cached)
30
+ return cached;
31
+ try {
32
+ const path = statePath();
33
+ if (!existsSync(path)) {
34
+ cached = emptyState();
35
+ return cached;
36
+ }
37
+ const raw = readFileSync(path, 'utf8');
38
+ const parsed = JSON.parse(raw);
39
+ cached = {
40
+ lastCall: parsed.lastCall || {},
41
+ warnedColdEvents: parsed.warnedColdEvents || {},
42
+ };
43
+ return cached;
44
+ }
45
+ catch {
46
+ cached = emptyState();
47
+ return cached;
48
+ }
49
+ }
50
+ function saveState(state) {
51
+ try {
52
+ const path = statePath();
53
+ const dir = dirname(path);
54
+ if (!existsSync(dir))
55
+ mkdirSync(dir, { recursive: true });
56
+ const tmp = `${path}.${process.pid}.tmp`;
57
+ writeFileSync(tmp, JSON.stringify(state), 'utf8');
58
+ renameSync(tmp, path);
59
+ }
60
+ catch {
61
+ // Non-fatal — state is best-effort
62
+ }
63
+ }
64
+ /** Reset in-memory cache (test hook) */
65
+ export function _resetCacheWarmthCache() {
66
+ cached = undefined;
67
+ }
68
+ /** Build the composite key */
69
+ function key(model, promptHash) {
70
+ return `${model}::${promptHash}`;
71
+ }
72
+ /** Record a successful API call's timestamp */
73
+ export function recordCacheCall(model, promptHash, now = Date.now()) {
74
+ const state = loadState();
75
+ state.lastCall[key(model, promptHash)] = now;
76
+ saveState(state);
77
+ }
78
+ /** Format ms as "Nm Ss" */
79
+ function formatAge(ms) {
80
+ const totalSec = Math.floor(ms / 1000);
81
+ const m = Math.floor(totalSec / 60);
82
+ const s = totalSec % 60;
83
+ return `${m}m ${s}s`;
84
+ }
85
+ /**
86
+ * Check whether the prompt cache is still warm for (model, promptHash).
87
+ * Returns warm=true if no prior call OR within TTL. Returns warm=false
88
+ * with a chalk.yellow message when cold AND we haven't warned for this
89
+ * specific cold-event yet.
90
+ *
91
+ * @param costPerMTokInput USD per million input tokens (from auth.ts)
92
+ * @param promptTokenEstimate rough token count (e.g. text.length / 4)
93
+ */
94
+ export function checkCacheWarmth(model, promptHash, costPerMTokInput, promptTokenEstimate, now = Date.now()) {
95
+ if (process.env.KBOT_CACHE_WARMTH_WARN === 'off') {
96
+ return { warm: true };
97
+ }
98
+ const state = loadState();
99
+ const k = key(model, promptHash);
100
+ const last = state.lastCall[k];
101
+ // First call ever for this (model, prompt) — cache wasn't expected to exist
102
+ if (!last)
103
+ return { warm: true };
104
+ const ageMs = now - last;
105
+ if (ageMs <= CACHE_TTL_MS) {
106
+ return { warm: true, ageMs };
107
+ }
108
+ // Cold — but only warn once per cold-event (keyed on the prior lastCall ts)
109
+ const warned = state.warnedColdEvents[k] || [];
110
+ if (warned.includes(last)) {
111
+ return { warm: false, ageMs };
112
+ }
113
+ // Cost estimate: cached reads are ~10% of full input price; the cold
114
+ // call pays roughly 90% extra vs. the warm path it would have hit.
115
+ // We report the full input cost as the "extra" — a conservative upper
116
+ // bound that matches what the user actually pays for these tokens.
117
+ const extraUSD = (costPerMTokInput * promptTokenEstimate) / 1_000_000;
118
+ // Persist that we've warned so subsequent calls in the same cold-event
119
+ // (e.g. a tool loop) don't re-warn until a fresh warm window opens.
120
+ warned.push(last);
121
+ // Keep the list bounded
122
+ if (warned.length > 32)
123
+ warned.splice(0, warned.length - 32);
124
+ state.warnedColdEvents[k] = warned;
125
+ saveState(state);
126
+ const message = chalk.yellow(`[kbot] Anthropic prompt cache likely cold — last call was ${formatAge(ageMs)} ago (TTL is 5m). ` +
127
+ `This call will pay full input price (~$${extraUSD.toFixed(2)} more). ` +
128
+ `Run kbot doctor cache for tips.`);
129
+ return { warm: false, ageMs, estimatedExtraCostUSD: extraUSD, message };
130
+ }
131
+ //# sourceMappingURL=cache-warmth.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Public surface for the debate module.
3
+ */
4
+ export type { DebateInput, AsymmetricRoles, DebateRound, Verdict, LLMClient, DebateOpts, TrainingExample, } from './types.js';
5
+ export { runDebate, formatPrompt, parseVerdict } from './runner.js';
6
+ export { synthesizeTrainingData, writeJsonl, loadJsonl, defaultJsonlPath, } from './synthesis.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public surface for the debate module.
3
+ */
4
+ export { runDebate, formatPrompt, parseVerdict } from './runner.js';
5
+ export { synthesizeTrainingData, writeJsonl, loadJsonl, defaultJsonlPath, } from './synthesis.js';
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Debate runner — orchestrates a 4-round asymmetric debate.
3
+ *
4
+ * Algorithm:
5
+ * 1. allow-advocate opens
6
+ * 2. block-advocate rebuts
7
+ * 3. allow-advocate counter-rebuts (if maxRounds >= 3)
8
+ * 4. block-advocate final (if maxRounds >= 4)
9
+ * 5. judge synthesizes a Verdict
10
+ *
11
+ * The LLM client is injected via opts.client. This module never imports
12
+ * a provider SDK. Tests use a deterministic stub.
13
+ */
14
+ import type { AsymmetricRoles, DebateInput, DebateOpts, DebateRound, Verdict } from './types.js';
15
+ /**
16
+ * Compose the role-specific prompt with the debate history.
17
+ * Exported so tests and synthesis can verify prompt shape.
18
+ */
19
+ export declare function formatPrompt(input: DebateInput, role: AsymmetricRoles, history: DebateRound[]): string;
20
+ /**
21
+ * Parse the judge's free-form text into a structured verdict body.
22
+ * Falls back to undecided/0 when fields are missing or malformed.
23
+ */
24
+ export declare function parseVerdict(judgeText: string): {
25
+ label: Verdict['label'];
26
+ confidence: number;
27
+ rationale: string;
28
+ };
29
+ /**
30
+ * Run the debate end-to-end and return a Verdict.
31
+ * Order is fixed: allow → block → allow → block → judge.
32
+ */
33
+ export declare function runDebate(input: DebateInput, opts: DebateOpts): Promise<Verdict>;
34
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Debate runner — orchestrates a 4-round asymmetric debate.
3
+ *
4
+ * Algorithm:
5
+ * 1. allow-advocate opens
6
+ * 2. block-advocate rebuts
7
+ * 3. allow-advocate counter-rebuts (if maxRounds >= 3)
8
+ * 4. block-advocate final (if maxRounds >= 4)
9
+ * 5. judge synthesizes a Verdict
10
+ *
11
+ * The LLM client is injected via opts.client. This module never imports
12
+ * a provider SDK. Tests use a deterministic stub.
13
+ */
14
+ const DEFAULT_MAX_ROUNDS = 4;
15
+ /**
16
+ * Compose the role-specific prompt with the debate history.
17
+ * Exported so tests and synthesis can verify prompt shape.
18
+ */
19
+ export function formatPrompt(input, role, history) {
20
+ const ctx = input.context ? `\nContext:\n${input.context}\n` : '';
21
+ const cat = input.categoryHint ? `\nCategory hint: ${input.categoryHint}\n` : '';
22
+ const transcript = history.length === 0
23
+ ? '(no prior arguments)'
24
+ : history
25
+ .map((r, i) => `[Round ${i + 1} — ${r.role}]\n${r.argument}`)
26
+ .join('\n\n');
27
+ if (role === 'allow-advocate') {
28
+ return [
29
+ `You are the ALLOW-ADVOCATE in an asymmetric debate.`,
30
+ `Argue why the candidate input below is SAFE TO ALLOW.`,
31
+ `Be specific, cite reasons grounded in the candidate's content.`,
32
+ ctx,
33
+ cat,
34
+ `Candidate:\n${input.candidate}`,
35
+ ``,
36
+ `Debate so far:\n${transcript}`,
37
+ ``,
38
+ `Write your argument. Do not include a verdict — that is the judge's role.`,
39
+ ].join('\n');
40
+ }
41
+ if (role === 'block-advocate') {
42
+ return [
43
+ `You are the BLOCK-ADVOCATE in an asymmetric debate.`,
44
+ `Argue why the candidate input below MUST BE BLOCKED.`,
45
+ `Be specific, cite concrete harms or policy violations.`,
46
+ ctx,
47
+ cat,
48
+ `Candidate:\n${input.candidate}`,
49
+ ``,
50
+ `Debate so far:\n${transcript}`,
51
+ ``,
52
+ `Write your argument. Do not include a verdict — that is the judge's role.`,
53
+ ].join('\n');
54
+ }
55
+ // judge
56
+ return [
57
+ `You are the JUDGE in an asymmetric debate.`,
58
+ `Read both advocates' arguments and produce a verdict.`,
59
+ `Output exactly two lines, then a brief rationale:`,
60
+ `LABEL: allow | block | undecided`,
61
+ `CONFIDENCE: 0.00 - 1.00`,
62
+ `RATIONALE: <one or two sentences>`,
63
+ ctx,
64
+ cat,
65
+ `Candidate:\n${input.candidate}`,
66
+ ``,
67
+ `Full transcript:\n${transcript}`,
68
+ ].join('\n');
69
+ }
70
+ /**
71
+ * Parse the judge's free-form text into a structured verdict body.
72
+ * Falls back to undecided/0 when fields are missing or malformed.
73
+ */
74
+ export function parseVerdict(judgeText) {
75
+ const labelMatch = judgeText.match(/LABEL:\s*(allow|block|undecided)/i);
76
+ const confMatch = judgeText.match(/CONFIDENCE:\s*([0-9]*\.?[0-9]+)/i);
77
+ const ratMatch = judgeText.match(/RATIONALE:\s*([\s\S]*?)(?:\n\n|$)/i);
78
+ const rawLabel = labelMatch?.[1]?.toLowerCase();
79
+ const label = rawLabel === 'allow' || rawLabel === 'block' || rawLabel === 'undecided'
80
+ ? rawLabel
81
+ : 'undecided';
82
+ let confidence = 0;
83
+ if (confMatch?.[1]) {
84
+ const n = Number(confMatch[1]);
85
+ if (Number.isFinite(n)) {
86
+ confidence = Math.max(0, Math.min(1, n));
87
+ }
88
+ }
89
+ if (label === 'undecided' && !labelMatch) {
90
+ // unparseable — keep confidence at 0 regardless of what we found
91
+ confidence = 0;
92
+ }
93
+ const rationale = ratMatch?.[1]?.trim() || (labelMatch ? '' : 'unparseable judge output');
94
+ return { label, confidence, rationale };
95
+ }
96
+ function nowIso() {
97
+ return new Date().toISOString();
98
+ }
99
+ /**
100
+ * Run the debate end-to-end and return a Verdict.
101
+ * Order is fixed: allow → block → allow → block → judge.
102
+ */
103
+ export async function runDebate(input, opts) {
104
+ const maxRounds = opts.maxRounds ?? DEFAULT_MAX_ROUNDS;
105
+ if (maxRounds < 2) {
106
+ throw new Error(`runDebate: maxRounds must be >= 2 (got ${maxRounds})`);
107
+ }
108
+ const rounds = [];
109
+ // Round 1 — allow opens
110
+ const r1Prompt = formatPrompt(input, 'allow-advocate', rounds);
111
+ const r1 = await opts.client.respond(r1Prompt, 'allow-advocate');
112
+ rounds.push({ role: 'allow-advocate', argument: r1, ts: nowIso() });
113
+ // Round 2 — block rebuts
114
+ const r2Prompt = formatPrompt(input, 'block-advocate', rounds);
115
+ const r2 = await opts.client.respond(r2Prompt, 'block-advocate');
116
+ rounds.push({ role: 'block-advocate', argument: r2, ts: nowIso() });
117
+ // Round 3 — allow counter-rebuts
118
+ if (maxRounds >= 3) {
119
+ const r3Prompt = formatPrompt(input, 'allow-advocate', rounds);
120
+ const r3 = await opts.client.respond(r3Prompt, 'allow-advocate');
121
+ rounds.push({ role: 'allow-advocate', argument: r3, ts: nowIso() });
122
+ }
123
+ // Round 4 — block final
124
+ if (maxRounds >= 4) {
125
+ const r4Prompt = formatPrompt(input, 'block-advocate', rounds);
126
+ const r4 = await opts.client.respond(r4Prompt, 'block-advocate');
127
+ rounds.push({ role: 'block-advocate', argument: r4, ts: nowIso() });
128
+ }
129
+ // Judge — synthesizes
130
+ const judgePrompt = formatPrompt(input, 'judge', rounds);
131
+ const judgeText = await opts.client.respond(judgePrompt, 'judge');
132
+ const parsed = parseVerdict(judgeText);
133
+ return {
134
+ label: parsed.label,
135
+ confidence: parsed.confidence,
136
+ rationale: parsed.rationale,
137
+ rounds,
138
+ };
139
+ }
140
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Synthesis — fan a list of debate inputs through the runner and
3
+ * persist the verdicts as JSONL training data for the critic.
4
+ */
5
+ import type { DebateInput, DebateOpts, TrainingExample } from './types.js';
6
+ /**
7
+ * Default JSONL path: ~/.kbot/futures/debate/<YYYY-MM-DD>.jsonl
8
+ */
9
+ export declare function defaultJsonlPath(date?: Date): string;
10
+ /**
11
+ * Run a debate per input and return the resulting training examples.
12
+ * Failures are surfaced — the caller decides whether to retry or skip.
13
+ */
14
+ export declare function synthesizeTrainingData(inputs: DebateInput[], opts: DebateOpts): Promise<TrainingExample[]>;
15
+ /**
16
+ * Atomic JSONL write. Creates parent dirs, writes to a tmp file,
17
+ * then renames into place to avoid partial-write corruption.
18
+ */
19
+ export declare function writeJsonl(examples: TrainingExample[], filePath: string): void;
20
+ /**
21
+ * Read JSONL and parse line-by-line. Malformed lines are skipped
22
+ * (callers can audit by counting input vs output).
23
+ */
24
+ export declare function loadJsonl(filePath: string): TrainingExample[];
25
+ //# sourceMappingURL=synthesis.d.ts.map
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Synthesis — fan a list of debate inputs through the runner and
3
+ * persist the verdicts as JSONL training data for the critic.
4
+ */
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as os from 'node:os';
8
+ import { runDebate } from './runner.js';
9
+ /**
10
+ * Default JSONL path: ~/.kbot/futures/debate/<YYYY-MM-DD>.jsonl
11
+ */
12
+ export function defaultJsonlPath(date = new Date()) {
13
+ const yyyy = date.getUTCFullYear();
14
+ const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
15
+ const dd = String(date.getUTCDate()).padStart(2, '0');
16
+ return path.join(os.homedir(), '.kbot', 'futures', 'debate', `${yyyy}-${mm}-${dd}.jsonl`);
17
+ }
18
+ /**
19
+ * Run a debate per input and return the resulting training examples.
20
+ * Failures are surfaced — the caller decides whether to retry or skip.
21
+ */
22
+ export async function synthesizeTrainingData(inputs, opts) {
23
+ const examples = [];
24
+ for (const input of inputs) {
25
+ const verdict = await runDebate(input, opts);
26
+ examples.push({
27
+ input,
28
+ label: verdict.label,
29
+ confidence: verdict.confidence,
30
+ rationale: verdict.rationale,
31
+ rounds: verdict.rounds,
32
+ });
33
+ }
34
+ return examples;
35
+ }
36
+ /**
37
+ * Atomic JSONL write. Creates parent dirs, writes to a tmp file,
38
+ * then renames into place to avoid partial-write corruption.
39
+ */
40
+ export function writeJsonl(examples, filePath) {
41
+ const dir = path.dirname(filePath);
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ const body = examples.map((ex) => JSON.stringify(ex)).join('\n') + (examples.length ? '\n' : '');
44
+ const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
45
+ fs.writeFileSync(tmp, body, 'utf8');
46
+ fs.renameSync(tmp, filePath);
47
+ }
48
+ /**
49
+ * Read JSONL and parse line-by-line. Malformed lines are skipped
50
+ * (callers can audit by counting input vs output).
51
+ */
52
+ export function loadJsonl(filePath) {
53
+ if (!fs.existsSync(filePath))
54
+ return [];
55
+ const raw = fs.readFileSync(filePath, 'utf8');
56
+ const out = [];
57
+ for (const line of raw.split('\n')) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed)
60
+ continue;
61
+ try {
62
+ const obj = JSON.parse(trimmed);
63
+ // minimal shape check — skip if required fields are missing
64
+ if (obj &&
65
+ typeof obj === 'object' &&
66
+ obj.input &&
67
+ typeof obj.input.candidate === 'string' &&
68
+ Array.isArray(obj.rounds) &&
69
+ typeof obj.label === 'string' &&
70
+ typeof obj.confidence === 'number' &&
71
+ typeof obj.rationale === 'string') {
72
+ out.push(obj);
73
+ }
74
+ }
75
+ catch {
76
+ // skip malformed line
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+ //# sourceMappingURL=synthesis.js.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Asymmetric debate types — BARRED-style guardrail synthesis.
3
+ *
4
+ * Two LLMs argue opposite sides of a candidate input
5
+ * ("safe to allow" vs "must block"); a third LLM judges.
6
+ * Output is JSONL training data for the critic.
7
+ *
8
+ * Source: "BARRED: Custom Policy Guardrails via Asymmetric Debate"
9
+ * (Plurai, arXiv 2604.25203). See V5_FUTURES_PLAN.md.
10
+ */
11
+ /**
12
+ * The thing under debate. `candidate` is the input being evaluated;
13
+ * `context` is optional surrounding context (system state, prior turns);
14
+ * `categoryHint` lets the caller tag the example for downstream filtering.
15
+ */
16
+ export interface DebateInput {
17
+ candidate: string;
18
+ context?: string;
19
+ categoryHint?: string;
20
+ }
21
+ /**
22
+ * Three asymmetric roles: two advocates (one for allow, one for block),
23
+ * and a single judge that synthesizes the verdict.
24
+ */
25
+ export type AsymmetricRoles = 'allow-advocate' | 'block-advocate' | 'judge';
26
+ /**
27
+ * One round of the debate. Multiple rounds form the transcript.
28
+ */
29
+ export interface DebateRound {
30
+ role: AsymmetricRoles;
31
+ argument: string;
32
+ ts: string;
33
+ }
34
+ /**
35
+ * Final verdict from the judge, plus the full transcript that produced it.
36
+ * `confidence` is in [0, 1]; `undecided` is used when the judge output
37
+ * is unparseable or the judge explicitly declines.
38
+ */
39
+ export interface Verdict {
40
+ label: 'allow' | 'block' | 'undecided';
41
+ confidence: number;
42
+ rationale: string;
43
+ rounds: DebateRound[];
44
+ }
45
+ /**
46
+ * Injectable LLM client. The runner only ever calls `respond`;
47
+ * tests pass a deterministic stub. Production wiring lives elsewhere
48
+ * so this module never imports a provider SDK.
49
+ */
50
+ export interface LLMClient {
51
+ respond(prompt: string, role: AsymmetricRoles): Promise<string>;
52
+ }
53
+ /**
54
+ * Runner options. `maxRounds` defaults to 4 (allow→block→allow→block);
55
+ * `seed` is forwarded to the client for callers that support it.
56
+ */
57
+ export interface DebateOpts {
58
+ maxRounds?: number;
59
+ client: LLMClient;
60
+ seed?: number;
61
+ }
62
+ /**
63
+ * Persisted training example. One per debate run.
64
+ */
65
+ export interface TrainingExample {
66
+ input: DebateInput;
67
+ label: Verdict['label'];
68
+ confidence: number;
69
+ rationale: string;
70
+ rounds: DebateRound[];
71
+ }
72
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Asymmetric debate types — BARRED-style guardrail synthesis.
3
+ *
4
+ * Two LLMs argue opposite sides of a candidate input
5
+ * ("safe to allow" vs "must block"); a third LLM judges.
6
+ * Output is JSONL training data for the critic.
7
+ *
8
+ * Source: "BARRED: Custom Policy Guardrails via Asymmetric Debate"
9
+ * (Plurai, arXiv 2604.25203). See V5_FUTURES_PLAN.md.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,5 @@
1
+ export type { Signal, Trend, Horizon, Forecast } from './types.js';
2
+ export { HORIZON_MS } from './types.js';
3
+ export { linearProjection, exponentialProjection, flatProjection, bestProjection, clampHorizon, signalHistory, } from './projection.js';
4
+ export { synthesizeForecasts, formatForecast, narrative } from './synthesize.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,5 @@
1
+ // futures/forecast — public surface.
2
+ export { HORIZON_MS } from './types.js';
3
+ export { linearProjection, exponentialProjection, flatProjection, bestProjection, clampHorizon, signalHistory, } from './projection.js';
4
+ export { synthesizeForecasts, formatForecast, narrative } from './synthesize.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,31 @@
1
+ import { type Forecast, type Horizon, type Signal } from './types.js';
2
+ /**
3
+ * Linear projection: value ~ a + b*t. Bounds = point ± 2 * residual stddev.
4
+ * Falls back to flat for fewer than 2 distinct timestamps.
5
+ */
6
+ export declare function linearProjection(signal: Signal, horizon: Horizon): Forecast;
7
+ /**
8
+ * Exponential projection: log-transform, fit linear, exp back.
9
+ * Drops non-positive values (log undefined). Falls back to flat if too few
10
+ * positive points remain.
11
+ */
12
+ export declare function exponentialProjection(signal: Signal, horizon: Horizon): Forecast;
13
+ /** Public flat projection (callers may prefer to force this). */
14
+ export declare function flatProjection(signal: Signal, horizon: Horizon): Forecast;
15
+ /**
16
+ * Pick the model with the best r². If both linear and exponential fit
17
+ * poorly (r² < 0.4 in both), return flat — low-variance signals shouldn't
18
+ * generate over-confident projections.
19
+ */
20
+ export declare function bestProjection(signal: Signal, horizon: Horizon): Forecast;
21
+ /**
22
+ * Guard against projecting absurdly far past available history.
23
+ * Returns true if the horizon is acceptable given the timespan covered.
24
+ * Rule: horizon must not exceed history span * 3.
25
+ *
26
+ * `history` is the timespan in ms from earliest to latest observation.
27
+ */
28
+ export declare function clampHorizon(h: Horizon, history: number): boolean;
29
+ /** Helper for tests/synthesize: compute the timespan of a Signal in ms. */
30
+ export declare function signalHistory(signal: Signal): number;
31
+ //# sourceMappingURL=projection.d.ts.map