@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.
- package/README.md +6 -0
- package/dist/cache-warmth.d.ts +25 -0
- package/dist/cache-warmth.js +131 -0
- package/dist/futures/debate/index.d.ts +7 -0
- package/dist/futures/debate/index.js +6 -0
- package/dist/futures/debate/runner.d.ts +34 -0
- package/dist/futures/debate/runner.js +140 -0
- package/dist/futures/debate/synthesis.d.ts +25 -0
- package/dist/futures/debate/synthesis.js +81 -0
- package/dist/futures/debate/types.d.ts +72 -0
- package/dist/futures/debate/types.js +12 -0
- package/dist/futures/forecast/index.d.ts +5 -0
- package/dist/futures/forecast/index.js +5 -0
- package/dist/futures/forecast/projection.d.ts +31 -0
- package/dist/futures/forecast/projection.js +177 -0
- package/dist/futures/forecast/synthesize.d.ts +19 -0
- package/dist/futures/forecast/synthesize.js +89 -0
- package/dist/futures/forecast/types.d.ts +59 -0
- package/dist/futures/forecast/types.js +15 -0
- package/dist/futures/harness/critic-evaluator.d.ts +39 -0
- package/dist/futures/harness/critic-evaluator.js +131 -0
- package/dist/futures/harness/evolution-loop.d.ts +41 -0
- package/dist/futures/harness/evolution-loop.js +168 -0
- package/dist/futures/harness/index.d.ts +16 -0
- package/dist/futures/harness/index.js +13 -0
- package/dist/futures/harness/meta-evolution.d.ts +32 -0
- package/dist/futures/harness/meta-evolution.js +52 -0
- package/dist/futures/harness/noop-evolution.d.ts +23 -0
- package/dist/futures/harness/noop-evolution.js +29 -0
- package/dist/futures/harness/persistence.d.ts +30 -0
- package/dist/futures/harness/persistence.js +99 -0
- package/dist/futures/harness/types.d.ts +147 -0
- package/dist/futures/harness/types.js +18 -0
- package/dist/futures/index.d.ts +16 -0
- package/dist/futures/index.js +22 -0
- package/dist/futures/latent-state/envelope.d.ts +39 -0
- package/dist/futures/latent-state/envelope.js +178 -0
- package/dist/futures/latent-state/index.d.ts +5 -0
- package/dist/futures/latent-state/index.js +3 -0
- package/dist/futures/latent-state/types.d.ts +47 -0
- package/dist/futures/latent-state/types.js +13 -0
- package/dist/futures/persona/check.d.ts +45 -0
- package/dist/futures/persona/check.js +205 -0
- package/dist/futures/persona/index.d.ts +5 -0
- package/dist/futures/persona/index.js +5 -0
- package/dist/futures/persona/registry.d.ts +22 -0
- package/dist/futures/persona/registry.js +124 -0
- package/dist/futures/persona/types.d.ts +68 -0
- package/dist/futures/persona/types.js +28 -0
- package/dist/futures/skill-graph/graph.d.ts +31 -0
- package/dist/futures/skill-graph/graph.js +151 -0
- package/dist/futures/skill-graph/index.d.ts +13 -0
- package/dist/futures/skill-graph/index.js +10 -0
- package/dist/futures/skill-graph/synthesis.d.ts +20 -0
- package/dist/futures/skill-graph/synthesis.js +83 -0
- package/dist/futures/skill-graph/types.d.ts +53 -0
- package/dist/futures/skill-graph/types.js +19 -0
- package/dist/streaming.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// futures/persona/registry — example personas. Hand-curated, not auto-generated.
|
|
2
|
+
//
|
|
3
|
+
// These three exist as concrete starting points for integration work; the
|
|
4
|
+
// next session will wire them into permissions.ts. Until then they're
|
|
5
|
+
// import-and-use values for tests and ad-hoc experimentation.
|
|
6
|
+
/**
|
|
7
|
+
* Researcher: read-only research tools. Cannot mutate filesystem, repo, or
|
|
8
|
+
* external state. Web fetches and grep are fine; writes are not.
|
|
9
|
+
*/
|
|
10
|
+
export const RESEARCHER = {
|
|
11
|
+
id: 'researcher',
|
|
12
|
+
description: 'Read-only research and search tools. No filesystem writes, no shell execution.',
|
|
13
|
+
maxBlastRadius: 'read-only',
|
|
14
|
+
scopes: [
|
|
15
|
+
{ toolPattern: 'read_file', blastRadius: 'read-only' },
|
|
16
|
+
{ toolPattern: 'list_directory', blastRadius: 'read-only' },
|
|
17
|
+
{ toolPattern: 'grep', blastRadius: 'read-only' },
|
|
18
|
+
{ toolPattern: 'glob', blastRadius: 'read-only' },
|
|
19
|
+
{ toolPattern: 'web_search', blastRadius: 'read-only' },
|
|
20
|
+
{ toolPattern: 'papers_search', blastRadius: 'read-only' },
|
|
21
|
+
{ toolPattern: 'kbot_search', blastRadius: 'read-only' },
|
|
22
|
+
{ toolPattern: 'arxiv_search', blastRadius: 'read-only' },
|
|
23
|
+
{ toolPattern: /^github_(read_file|repo_info|search|trending|activity|issues)$/, blastRadius: 'read-only' },
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Coder: read-write inside the workspace. Bash allowed but the most common
|
|
28
|
+
* destructive forms are denied via deny-pattern argRules. No force pushes.
|
|
29
|
+
* Rate limit on bash so a runaway loop can't burn 10k commands.
|
|
30
|
+
*/
|
|
31
|
+
export const CODER = {
|
|
32
|
+
id: 'coder',
|
|
33
|
+
description: 'Read-write code tools. Bash allowed but rm -rf and force-push denied. Bash 60/min.',
|
|
34
|
+
maxBlastRadius: 'sandboxed',
|
|
35
|
+
scopes: [
|
|
36
|
+
{ toolPattern: 'read_file', blastRadius: 'read-only' },
|
|
37
|
+
{ toolPattern: 'list_directory', blastRadius: 'read-only' },
|
|
38
|
+
{ toolPattern: 'grep', blastRadius: 'read-only' },
|
|
39
|
+
{ toolPattern: 'glob', blastRadius: 'read-only' },
|
|
40
|
+
{ toolPattern: 'write_file', blastRadius: 'sandboxed' },
|
|
41
|
+
{ toolPattern: 'edit_file', blastRadius: 'sandboxed' },
|
|
42
|
+
{ toolPattern: 'multi_file_write', blastRadius: 'sandboxed' },
|
|
43
|
+
{
|
|
44
|
+
toolPattern: 'bash',
|
|
45
|
+
blastRadius: 'sandboxed',
|
|
46
|
+
argConstraints: {
|
|
47
|
+
// deny rm -rf at /, ~, or anywhere with force; deny sudo; deny curl|sh
|
|
48
|
+
command: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
denyPattern: true,
|
|
51
|
+
pattern: /(\brm\s+-[rRfF]+\s+(\/|~|\$HOME)|sudo\s+|curl\s+[^|]*\|\s*(sh|bash)|:\(\)\s*\{)/,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
rateLimit: { max: 60, windowMs: 60_000 },
|
|
55
|
+
},
|
|
56
|
+
{ toolPattern: /^git_(status|log|diff|branch|commit)$/, blastRadius: 'sandboxed' },
|
|
57
|
+
{
|
|
58
|
+
toolPattern: 'git_push',
|
|
59
|
+
blastRadius: 'sandboxed',
|
|
60
|
+
argConstraints: {
|
|
61
|
+
// forbid --force, --force-with-lease, -f
|
|
62
|
+
args: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
denyPattern: true,
|
|
65
|
+
pattern: /(^|\s)(--force(-with-lease)?|-f)(\s|$)/,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{ toolPattern: /^npm_/, blastRadius: 'sandboxed' },
|
|
70
|
+
{ toolPattern: /^pip_/, blastRadius: 'sandboxed' },
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Computer-use: explicit destructive opt-in. GUI tools that drive the user's
|
|
75
|
+
* physical desktop. Rate-limited so a hung loop can't spam clicks.
|
|
76
|
+
*/
|
|
77
|
+
export const COMPUTER_USE = {
|
|
78
|
+
id: 'computer-use',
|
|
79
|
+
description: 'Desktop control: mouse, keyboard, app launch. Destructive blast radius. 30/min.',
|
|
80
|
+
maxBlastRadius: 'destructive',
|
|
81
|
+
scopes: [
|
|
82
|
+
{
|
|
83
|
+
toolPattern: 'mouse_click',
|
|
84
|
+
blastRadius: 'destructive',
|
|
85
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
toolPattern: 'mouse_move',
|
|
89
|
+
blastRadius: 'destructive',
|
|
90
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
toolPattern: 'mouse_drag',
|
|
94
|
+
blastRadius: 'destructive',
|
|
95
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
toolPattern: 'keyboard_type',
|
|
99
|
+
blastRadius: 'destructive',
|
|
100
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
toolPattern: 'keyboard_shortcut',
|
|
104
|
+
blastRadius: 'destructive',
|
|
105
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
toolPattern: 'app_launch',
|
|
109
|
+
blastRadius: 'destructive',
|
|
110
|
+
rateLimit: { max: 30, windowMs: 60_000 },
|
|
111
|
+
},
|
|
112
|
+
{ toolPattern: 'screenshot', blastRadius: 'read-only' },
|
|
113
|
+
{ toolPattern: 'window_list', blastRadius: 'read-only' },
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Default registry. Add to this when wiring more personas.
|
|
118
|
+
*/
|
|
119
|
+
export const PERSONA_REGISTRY = {
|
|
120
|
+
researcher: RESEARCHER,
|
|
121
|
+
coder: CODER,
|
|
122
|
+
'computer-use': COMPUTER_USE,
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast-radius hierarchy. Ordered from least to most dangerous.
|
|
3
|
+
* A Persona's maxBlastRadius caps the radius any of its scopes may target;
|
|
4
|
+
* mergePersonas() takes the maximum across inputs.
|
|
5
|
+
*/
|
|
6
|
+
export type BlastRadius = 'none' | 'read-only' | 'sandboxed' | 'destructive';
|
|
7
|
+
export declare const BLAST_RADIUS_ORDER: readonly BlastRadius[];
|
|
8
|
+
/**
|
|
9
|
+
* Constraint applied to a single tool argument by name.
|
|
10
|
+
* - `type` — required runtime type
|
|
11
|
+
* - `allowedValues` — enum of permitted literal values (when type === 'enum')
|
|
12
|
+
* - `pattern` — regex test for string args (rejected if it matches a
|
|
13
|
+
* forbidden form; we use it as a *deny* pattern by convention — see check.ts)
|
|
14
|
+
* - `min` / `max` — numeric bounds (inclusive). For strings, applied to length.
|
|
15
|
+
*/
|
|
16
|
+
export interface ArgRule {
|
|
17
|
+
type: 'string' | 'number' | 'boolean' | 'enum';
|
|
18
|
+
allowedValues?: unknown[];
|
|
19
|
+
pattern?: RegExp;
|
|
20
|
+
/** When true, the regex acts as a *deny* pattern (match → reject). Defaults to false (must match). */
|
|
21
|
+
denyPattern?: boolean;
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A single permission scope. Match against a tool name and a payload of args.
|
|
27
|
+
* - `toolPattern` — exact string match or RegExp test
|
|
28
|
+
* - `argConstraints` — keyed by argument name
|
|
29
|
+
* - `rateLimit` — sliding-window counter shared per (persona.id, toolName)
|
|
30
|
+
*/
|
|
31
|
+
export interface Scope {
|
|
32
|
+
toolPattern: string | RegExp;
|
|
33
|
+
argConstraints?: Record<string, ArgRule>;
|
|
34
|
+
rateLimit?: {
|
|
35
|
+
max: number;
|
|
36
|
+
windowMs: number;
|
|
37
|
+
};
|
|
38
|
+
/** Optional radius for this scope; cannot exceed Persona's maxBlastRadius. */
|
|
39
|
+
blastRadius?: BlastRadius;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Named, composable persona. `id` is the lookup key in the registry.
|
|
43
|
+
*/
|
|
44
|
+
export interface Persona {
|
|
45
|
+
id: string;
|
|
46
|
+
description: string;
|
|
47
|
+
scopes: Scope[];
|
|
48
|
+
maxBlastRadius?: BlastRadius;
|
|
49
|
+
}
|
|
50
|
+
export interface PermissionGrant {
|
|
51
|
+
persona: Persona;
|
|
52
|
+
toolName: string;
|
|
53
|
+
args: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
export interface Verdict {
|
|
56
|
+
allowed: boolean;
|
|
57
|
+
reason?: string;
|
|
58
|
+
matchedScope?: Scope;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Thrown by enforce(). Carries the full verdict for upstream logging.
|
|
62
|
+
*/
|
|
63
|
+
export declare class PermissionDeniedError extends Error {
|
|
64
|
+
readonly verdict: Verdict;
|
|
65
|
+
readonly grant: PermissionGrant;
|
|
66
|
+
constructor(grant: PermissionGrant, verdict: Verdict);
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// futures/persona/types — type-checked privilege scoping for tool invocation.
|
|
2
|
+
//
|
|
3
|
+
// A Persona is a named bundle of Scopes. A Scope binds a tool pattern to a
|
|
4
|
+
// set of argument constraints, an optional rate limit, and is bounded by
|
|
5
|
+
// the Persona's max blast radius. canInvoke() resolves a (persona, tool, args)
|
|
6
|
+
// triple to a Verdict. enforce() turns a denied Verdict into a thrown error.
|
|
7
|
+
//
|
|
8
|
+
// Module is standalone — no integration into permissions.ts yet.
|
|
9
|
+
export const BLAST_RADIUS_ORDER = [
|
|
10
|
+
'none',
|
|
11
|
+
'read-only',
|
|
12
|
+
'sandboxed',
|
|
13
|
+
'destructive',
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by enforce(). Carries the full verdict for upstream logging.
|
|
17
|
+
*/
|
|
18
|
+
export class PermissionDeniedError extends Error {
|
|
19
|
+
verdict;
|
|
20
|
+
grant;
|
|
21
|
+
constructor(grant, verdict) {
|
|
22
|
+
super(`permission denied: persona=${grant.persona.id} tool=${grant.toolName} reason=${verdict.reason ?? 'no scope matched'}`);
|
|
23
|
+
this.name = 'PermissionDeniedError';
|
|
24
|
+
this.verdict = verdict;
|
|
25
|
+
this.grant = grant;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph — runtime ops.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions over the SkillGraph data structure. Mutators return new
|
|
5
|
+
* instances rather than mutating in place; the underlying Maps and arrays
|
|
6
|
+
* are copied. Random walks use a seeded LCG for deterministic tests.
|
|
7
|
+
*/
|
|
8
|
+
import type { Edge, GraphPath, Scenario, Skill, SkillGraph } from './types.js';
|
|
9
|
+
export declare function buildGraph(): SkillGraph;
|
|
10
|
+
export declare function addSkill(g: SkillGraph, skill: Skill): SkillGraph;
|
|
11
|
+
export declare function addScenario(g: SkillGraph, scenario: Scenario): SkillGraph;
|
|
12
|
+
export declare function addEdge(g: SkillGraph, edge: Edge): SkillGraph;
|
|
13
|
+
export interface SampleOptions {
|
|
14
|
+
start?: string;
|
|
15
|
+
maxLength?: number;
|
|
16
|
+
seed?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function samplePath(g: SkillGraph, opts?: SampleOptions): GraphPath;
|
|
19
|
+
export declare function findPaths(g: SkillGraph, fromId: string, toId: string, maxDepth?: number): GraphPath[];
|
|
20
|
+
export interface PathLengthStats {
|
|
21
|
+
min: number;
|
|
22
|
+
max: number;
|
|
23
|
+
avg: number;
|
|
24
|
+
p50: number;
|
|
25
|
+
p95: number;
|
|
26
|
+
samples: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function pathLengthDistribution(g: SkillGraph, samples?: number, opts?: {
|
|
29
|
+
seed?: number;
|
|
30
|
+
}): PathLengthStats;
|
|
31
|
+
//# sourceMappingURL=graph.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph — runtime ops.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions over the SkillGraph data structure. Mutators return new
|
|
5
|
+
* instances rather than mutating in place; the underlying Maps and arrays
|
|
6
|
+
* are copied. Random walks use a seeded LCG for deterministic tests.
|
|
7
|
+
*/
|
|
8
|
+
function lcg(seed) {
|
|
9
|
+
let state = seed >>> 0;
|
|
10
|
+
return {
|
|
11
|
+
next() {
|
|
12
|
+
state = (state * 1664525 + 1013904223) >>> 0;
|
|
13
|
+
return state / 0xffffffff;
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function defaultRng() {
|
|
18
|
+
return { next: () => Math.random() };
|
|
19
|
+
}
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Construction
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
+
export function buildGraph() {
|
|
24
|
+
return {
|
|
25
|
+
skills: new Map(),
|
|
26
|
+
scenarios: new Map(),
|
|
27
|
+
edges: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function addSkill(g, skill) {
|
|
31
|
+
const next = new Map(g.skills);
|
|
32
|
+
next.set(skill.id, skill);
|
|
33
|
+
return { ...g, skills: next };
|
|
34
|
+
}
|
|
35
|
+
export function addScenario(g, scenario) {
|
|
36
|
+
const next = new Map(g.scenarios);
|
|
37
|
+
next.set(scenario.id, scenario);
|
|
38
|
+
return { ...g, scenarios: next };
|
|
39
|
+
}
|
|
40
|
+
export function addEdge(g, edge) {
|
|
41
|
+
return { ...g, edges: [...g.edges, edge] };
|
|
42
|
+
}
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// Lookup helpers
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
function getNode(g, id) {
|
|
47
|
+
return g.skills.get(id) ?? g.scenarios.get(id);
|
|
48
|
+
}
|
|
49
|
+
function outgoing(g, fromId) {
|
|
50
|
+
return g.edges.filter((e) => e.from === fromId);
|
|
51
|
+
}
|
|
52
|
+
export function samplePath(g, opts = {}) {
|
|
53
|
+
const rng = opts.seed !== undefined ? lcg(opts.seed) : defaultRng();
|
|
54
|
+
const maxLength = opts.maxLength ?? 6;
|
|
55
|
+
const startId = opts.start ?? pickStartNode(g, rng);
|
|
56
|
+
if (!startId)
|
|
57
|
+
return { nodes: [], edges: [], pathLength: 0 };
|
|
58
|
+
const startNode = getNode(g, startId);
|
|
59
|
+
if (!startNode)
|
|
60
|
+
return { nodes: [], edges: [], pathLength: 0 };
|
|
61
|
+
const nodes = [startNode];
|
|
62
|
+
const edges = [];
|
|
63
|
+
const visited = new Set([startId]);
|
|
64
|
+
let currentId = startId;
|
|
65
|
+
while (nodes.length < maxLength) {
|
|
66
|
+
const candidates = outgoing(g, currentId).filter((e) => !visited.has(e.to));
|
|
67
|
+
if (candidates.length === 0)
|
|
68
|
+
break;
|
|
69
|
+
const edge = weightedPick(candidates, rng);
|
|
70
|
+
const nextNode = getNode(g, edge.to);
|
|
71
|
+
if (!nextNode)
|
|
72
|
+
break;
|
|
73
|
+
edges.push(edge);
|
|
74
|
+
nodes.push(nextNode);
|
|
75
|
+
visited.add(edge.to);
|
|
76
|
+
currentId = edge.to;
|
|
77
|
+
}
|
|
78
|
+
return { nodes, edges, pathLength: nodes.length };
|
|
79
|
+
}
|
|
80
|
+
function pickStartNode(g, rng) {
|
|
81
|
+
const all = [...g.skills.keys(), ...g.scenarios.keys()];
|
|
82
|
+
if (all.length === 0)
|
|
83
|
+
return undefined;
|
|
84
|
+
return all[Math.floor(rng.next() * all.length)];
|
|
85
|
+
}
|
|
86
|
+
function weightedPick(edges, rng) {
|
|
87
|
+
const total = edges.reduce((s, e) => s + (e.weight ?? 1), 0);
|
|
88
|
+
let pick = rng.next() * total;
|
|
89
|
+
for (const edge of edges) {
|
|
90
|
+
pick -= edge.weight ?? 1;
|
|
91
|
+
if (pick <= 0)
|
|
92
|
+
return edge;
|
|
93
|
+
}
|
|
94
|
+
return edges[edges.length - 1];
|
|
95
|
+
}
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
// Path finding — DFS with depth limit
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
export function findPaths(g, fromId, toId, maxDepth = 5) {
|
|
100
|
+
const startNode = getNode(g, fromId);
|
|
101
|
+
if (!startNode)
|
|
102
|
+
return [];
|
|
103
|
+
const results = [];
|
|
104
|
+
const stack = [
|
|
105
|
+
{ id: fromId, nodes: [startNode], edges: [] },
|
|
106
|
+
];
|
|
107
|
+
while (stack.length > 0) {
|
|
108
|
+
const { id, nodes, edges } = stack.pop();
|
|
109
|
+
if (id === toId && nodes.length > 1) {
|
|
110
|
+
results.push({ nodes, edges, pathLength: nodes.length });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (nodes.length >= maxDepth)
|
|
114
|
+
continue;
|
|
115
|
+
const visited = new Set(nodes.map((n) => n.id));
|
|
116
|
+
for (const edge of outgoing(g, id)) {
|
|
117
|
+
if (visited.has(edge.to))
|
|
118
|
+
continue;
|
|
119
|
+
const next = getNode(g, edge.to);
|
|
120
|
+
if (!next)
|
|
121
|
+
continue;
|
|
122
|
+
stack.push({
|
|
123
|
+
id: edge.to,
|
|
124
|
+
nodes: [...nodes, next],
|
|
125
|
+
edges: [...edges, edge],
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return results;
|
|
130
|
+
}
|
|
131
|
+
export function pathLengthDistribution(g, samples = 100, opts = {}) {
|
|
132
|
+
if (samples <= 0) {
|
|
133
|
+
return { min: 0, max: 0, avg: 0, p50: 0, p95: 0, samples: 0 };
|
|
134
|
+
}
|
|
135
|
+
const baseSeed = opts.seed ?? Date.now();
|
|
136
|
+
const lengths = [];
|
|
137
|
+
for (let i = 0; i < samples; i++) {
|
|
138
|
+
const path = samplePath(g, { seed: baseSeed + i });
|
|
139
|
+
lengths.push(path.pathLength);
|
|
140
|
+
}
|
|
141
|
+
lengths.sort((a, b) => a - b);
|
|
142
|
+
const min = lengths[0] ?? 0;
|
|
143
|
+
const max = lengths[lengths.length - 1] ?? 0;
|
|
144
|
+
const sum = lengths.reduce((s, n) => s + n, 0);
|
|
145
|
+
const avg = sum / lengths.length;
|
|
146
|
+
const p50 = lengths[Math.floor(lengths.length * 0.5)] ?? 0;
|
|
147
|
+
const p95Idx = Math.min(lengths.length - 1, Math.floor(lengths.length * 0.95));
|
|
148
|
+
const p95 = lengths[p95Idx] ?? 0;
|
|
149
|
+
return { min, max, avg, p50, p95, samples };
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface for the skill-graph module.
|
|
3
|
+
*
|
|
4
|
+
* Tencent Hunyuan SkillSynth-style graph formalism over kbot's skill set.
|
|
5
|
+
* Path sampling produces synthetic Tasks for the harness/ evolution loop.
|
|
6
|
+
*/
|
|
7
|
+
export type { Edge, GraphPath, Scenario, Skill, SkillGraph, } from './types.js';
|
|
8
|
+
export { isSkill } from './types.js';
|
|
9
|
+
export { buildGraph, addSkill, addScenario, addEdge, samplePath, findPaths, pathLengthDistribution, } from './graph.js';
|
|
10
|
+
export type { SampleOptions, PathLengthStats } from './graph.js';
|
|
11
|
+
export { pathToTask } from './synthesis.js';
|
|
12
|
+
export type { PathToTaskOptions } from './synthesis.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface for the skill-graph module.
|
|
3
|
+
*
|
|
4
|
+
* Tencent Hunyuan SkillSynth-style graph formalism over kbot's skill set.
|
|
5
|
+
* Path sampling produces synthetic Tasks for the harness/ evolution loop.
|
|
6
|
+
*/
|
|
7
|
+
export { isSkill } from './types.js';
|
|
8
|
+
export { buildGraph, addSkill, addScenario, addEdge, samplePath, findPaths, pathLengthDistribution, } from './graph.js';
|
|
9
|
+
export { pathToTask } from './synthesis.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph → synthetic task synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Translates a sampled GraphPath into a Task instance compatible with the
|
|
5
|
+
* harness module's `Task` interface. The acceptance criteria are derived
|
|
6
|
+
* from the path's edges; the instructions describe the workflow in human-
|
|
7
|
+
* readable form.
|
|
8
|
+
*/
|
|
9
|
+
import type { Task } from '../harness/types.js';
|
|
10
|
+
import type { GraphPath } from './types.js';
|
|
11
|
+
export interface PathToTaskOptions {
|
|
12
|
+
/** Optional id prefix; defaults to "synth-". */
|
|
13
|
+
prefix?: string;
|
|
14
|
+
/** Optional context line prepended to instructions. */
|
|
15
|
+
contextHeader?: string;
|
|
16
|
+
/** Extra metadata to merge into Task.meta. */
|
|
17
|
+
meta?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export declare function pathToTask(path: GraphPath, opts?: PathToTaskOptions): Task;
|
|
20
|
+
//# sourceMappingURL=synthesis.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph → synthetic task synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Translates a sampled GraphPath into a Task instance compatible with the
|
|
5
|
+
* harness module's `Task` interface. The acceptance criteria are derived
|
|
6
|
+
* from the path's edges; the instructions describe the workflow in human-
|
|
7
|
+
* readable form.
|
|
8
|
+
*/
|
|
9
|
+
export function pathToTask(path, opts = {}) {
|
|
10
|
+
const prefix = opts.prefix ?? 'synth-';
|
|
11
|
+
const id = `${prefix}${shortHash(path)}`;
|
|
12
|
+
const instructions = buildInstructions(path, opts.contextHeader);
|
|
13
|
+
const acceptance = buildAcceptance(path);
|
|
14
|
+
const meta = {
|
|
15
|
+
source: 'skill-graph',
|
|
16
|
+
pathLength: path.pathLength,
|
|
17
|
+
nodeIds: path.nodes.map((n) => n.id),
|
|
18
|
+
...opts.meta,
|
|
19
|
+
};
|
|
20
|
+
return { id, instructions, acceptance, meta };
|
|
21
|
+
}
|
|
22
|
+
function buildInstructions(path, header) {
|
|
23
|
+
const parts = [];
|
|
24
|
+
if (header)
|
|
25
|
+
parts.push(header);
|
|
26
|
+
if (path.nodes.length === 0) {
|
|
27
|
+
parts.push('No nodes in path; nothing to do.');
|
|
28
|
+
return parts.join('\n\n');
|
|
29
|
+
}
|
|
30
|
+
parts.push(`Workflow with ${path.nodes.length} step(s):`);
|
|
31
|
+
path.nodes.forEach((node, i) => {
|
|
32
|
+
parts.push(` ${i + 1}. ${describeNode(node)}`);
|
|
33
|
+
});
|
|
34
|
+
if (path.edges.length > 0) {
|
|
35
|
+
parts.push('');
|
|
36
|
+
parts.push('Transitions:');
|
|
37
|
+
path.edges.forEach((edge) => {
|
|
38
|
+
parts.push(` - ${edge.from} → ${edge.to} (${edge.kind})`);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return parts.join('\n');
|
|
42
|
+
}
|
|
43
|
+
function buildAcceptance(path) {
|
|
44
|
+
if (path.nodes.length === 0)
|
|
45
|
+
return ['No nodes to verify.'];
|
|
46
|
+
const criteria = [];
|
|
47
|
+
for (const node of path.nodes) {
|
|
48
|
+
if (isSkillNode(node)) {
|
|
49
|
+
const tool = node.toolName ? ` (${node.toolName})` : '';
|
|
50
|
+
criteria.push(`Skill exercised: ${node.description}${tool}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
criteria.push(`Scenario context entered: ${node.description}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const edge of path.edges) {
|
|
57
|
+
if (edge.kind === 'requires') {
|
|
58
|
+
criteria.push(`Prerequisite satisfied: ${edge.from} before ${edge.to}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return criteria;
|
|
62
|
+
}
|
|
63
|
+
function describeNode(node) {
|
|
64
|
+
if (isSkillNode(node)) {
|
|
65
|
+
return node.toolName
|
|
66
|
+
? `${node.description} (tool: ${node.toolName})`
|
|
67
|
+
: node.description;
|
|
68
|
+
}
|
|
69
|
+
return `${node.description}${node.tags?.length ? ` [${node.tags.join(', ')}]` : ''}`;
|
|
70
|
+
}
|
|
71
|
+
function isSkillNode(node) {
|
|
72
|
+
return !('tags' in node);
|
|
73
|
+
}
|
|
74
|
+
function shortHash(path) {
|
|
75
|
+
// Simple deterministic hash over node ids; not crypto.
|
|
76
|
+
const key = path.nodes.map((n) => n.id).join('|');
|
|
77
|
+
let h = 0;
|
|
78
|
+
for (let i = 0; i < key.length; i++) {
|
|
79
|
+
h = (h * 31 + key.charCodeAt(i)) >>> 0;
|
|
80
|
+
}
|
|
81
|
+
return h.toString(36).slice(0, 8);
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=synthesis.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph — explicit graph formalism over skills + scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Maps onto Tencent Hunyuan's "Toward Scalable Terminal Task Synthesis via
|
|
5
|
+
* Skill Graphs" (arXiv:2604.25727). Wraps the Bayesian flat-skill ratings in
|
|
6
|
+
* `packages/skill-router/` with structure: skill nodes, scenario nodes
|
|
7
|
+
* (intermediate workflow contexts), and weighted edges between them.
|
|
8
|
+
*
|
|
9
|
+
* A path through the graph is a candidate workflow. Sampling paths produces
|
|
10
|
+
* synthetic Tasks (compatible with `harness/types.ts`) for evaluation and
|
|
11
|
+
* training.
|
|
12
|
+
*
|
|
13
|
+
* This module is types-only. Runtime lives in graph.ts and synthesis.ts.
|
|
14
|
+
*/
|
|
15
|
+
/** A skill node — typically maps 1:1 to a registered kbot tool. */
|
|
16
|
+
export interface Skill {
|
|
17
|
+
id: string;
|
|
18
|
+
description: string;
|
|
19
|
+
/** Optional name of the underlying tool (from packages/kbot/src/tools/). */
|
|
20
|
+
toolName?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A scenario node — an intermediate workflow context that connects skills.
|
|
24
|
+
* Example: "after-cloning-a-repo", "in-an-ableton-set", "researching-a-paper".
|
|
25
|
+
*/
|
|
26
|
+
export interface Scenario {
|
|
27
|
+
id: string;
|
|
28
|
+
description: string;
|
|
29
|
+
tags?: string[];
|
|
30
|
+
}
|
|
31
|
+
/** A directed edge between two nodes (skill or scenario). */
|
|
32
|
+
export interface Edge {
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
/** Sampling weight; defaults to 1. Higher = more likely on a random walk. */
|
|
36
|
+
weight?: number;
|
|
37
|
+
kind: 'invokes' | 'follows' | 'requires';
|
|
38
|
+
}
|
|
39
|
+
/** Result of a graph traversal: ordered nodes + edges + length. */
|
|
40
|
+
export interface GraphPath {
|
|
41
|
+
nodes: Array<Skill | Scenario>;
|
|
42
|
+
edges: Edge[];
|
|
43
|
+
pathLength: number;
|
|
44
|
+
}
|
|
45
|
+
/** Immutable-ish graph container. Mutators return new instances. */
|
|
46
|
+
export interface SkillGraph {
|
|
47
|
+
skills: Map<string, Skill>;
|
|
48
|
+
scenarios: Map<string, Scenario>;
|
|
49
|
+
edges: Edge[];
|
|
50
|
+
}
|
|
51
|
+
/** Type guard: skill vs scenario. */
|
|
52
|
+
export declare function isSkill(node: Skill | Scenario): node is Skill;
|
|
53
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill graph — explicit graph formalism over skills + scenarios.
|
|
3
|
+
*
|
|
4
|
+
* Maps onto Tencent Hunyuan's "Toward Scalable Terminal Task Synthesis via
|
|
5
|
+
* Skill Graphs" (arXiv:2604.25727). Wraps the Bayesian flat-skill ratings in
|
|
6
|
+
* `packages/skill-router/` with structure: skill nodes, scenario nodes
|
|
7
|
+
* (intermediate workflow contexts), and weighted edges between them.
|
|
8
|
+
*
|
|
9
|
+
* A path through the graph is a candidate workflow. Sampling paths produces
|
|
10
|
+
* synthetic Tasks (compatible with `harness/types.ts`) for evaluation and
|
|
11
|
+
* training.
|
|
12
|
+
*
|
|
13
|
+
* This module is types-only. Runtime lives in graph.ts and synthesis.ts.
|
|
14
|
+
*/
|
|
15
|
+
/** Type guard: skill vs scenario. */
|
|
16
|
+
export function isSkill(node) {
|
|
17
|
+
return 'description' in node && !('tags' in node) && !('id' in node && node.tags !== undefined);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=types.js.map
|
package/dist/streaming.js
CHANGED
|
@@ -67,6 +67,24 @@ export async function streamAnthropicResponse(apiKey, apiUrl, model, system, mes
|
|
|
67
67
|
}
|
|
68
68
|
if (tools && tools.length > 0)
|
|
69
69
|
body.tools = tools;
|
|
70
|
+
// Anthropic prompt-cache TTL warning (jcode borrow). Warn once per (model,
|
|
71
|
+
// prompt-hash) cold event when the cache likely expired since last call.
|
|
72
|
+
if (apiUrl.includes('anthropic') && system && process.env.KBOT_CACHE_WARMTH_WARN !== 'off') {
|
|
73
|
+
try {
|
|
74
|
+
const { hashPrompt, checkCacheWarmth, recordCacheCall } = await import('./cache-warmth.js');
|
|
75
|
+
const promptHash = hashPrompt(system);
|
|
76
|
+
const inputCostPerMTok = model.includes('opus') ? 15 : model.includes('haiku') ? 0.8 : 3;
|
|
77
|
+
const promptTokenEstimate = Math.ceil(system.length / 4);
|
|
78
|
+
const check = checkCacheWarmth(model, promptHash, inputCostPerMTok, promptTokenEstimate);
|
|
79
|
+
if (!check.warm && check.message) {
|
|
80
|
+
console.warn((await import('chalk')).default.yellow(check.message));
|
|
81
|
+
}
|
|
82
|
+
recordCacheCall(model, promptHash);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// never let warning logic break the API call
|
|
86
|
+
}
|
|
87
|
+
}
|
|
70
88
|
let res;
|
|
71
89
|
let lastError;
|
|
72
90
|
for (let attempt = 0; attempt <= MAX_STREAM_RETRIES; attempt++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kernel.chat/kbot",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Open-source terminal AI agent. 100+ specialist skills, 35 specialist agents, 20 providers. Dreams, learns, watches your system. Controls your phone. Fully local, fully sovereign. MIT. v4.0 — evidence-based curation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|