@addai/node 0.30.16-msp.1 → 0.30.16-msp.3
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/dist/capabilities.d.ts +3 -1
- package/dist/capabilities.js +1 -1
- package/dist/model-selector.d.ts +45 -0
- package/dist/model-selector.js +140 -4
- package/dist/session-runner.js +1 -0
- package/package.json +1 -1
package/dist/capabilities.d.ts
CHANGED
|
@@ -83,9 +83,11 @@ interface CapabilitiesShape {
|
|
|
83
83
|
auto_update?: AutoUpdateState;
|
|
84
84
|
/** Present when this build has the Model Selector Protocol, so Studio can
|
|
85
85
|
* say which nodes would ignore an entity's selector setting instead of
|
|
86
|
-
* guessing from a version number. enabled is false under ADDAI_MSP=0.
|
|
86
|
+
* guessing from a version number. enabled is false under ADDAI_MSP=0.
|
|
87
|
+
* jev is true on builds that can use Jev as the selector. */
|
|
87
88
|
model_selector?: {
|
|
88
89
|
enabled: boolean;
|
|
90
|
+
jev?: boolean;
|
|
89
91
|
};
|
|
90
92
|
}
|
|
91
93
|
/** What the heartbeat says about claude's models. `models` keeps its old
|
package/dist/capabilities.js
CHANGED
|
@@ -510,7 +510,7 @@ async function probeCapabilities() {
|
|
|
510
510
|
autostart: (0, autostart_1.status)(),
|
|
511
511
|
container_engine: containerEngine,
|
|
512
512
|
flows: await probeFlows(),
|
|
513
|
-
model_selector: { enabled: process.env.ADDAI_MSP !== '0' },
|
|
513
|
+
model_selector: { enabled: process.env.ADDAI_MSP !== '0', jev: true },
|
|
514
514
|
auto_update: (0, auto_update_1.autoUpdateState)(),
|
|
515
515
|
machine: (0, machine_metrics_1.sampleMachine)(),
|
|
516
516
|
claude: deco('claude', claude),
|
package/dist/model-selector.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface MspConfig {
|
|
|
12
12
|
model: string;
|
|
13
13
|
effort: string;
|
|
14
14
|
timeout_ms: number;
|
|
15
|
+
/** 'jev' needs a credential_id; without one the selector stays on Claude. */
|
|
16
|
+
provider: 'claude' | 'jev';
|
|
17
|
+
/** The Vault item holding the TypeSafe key. The server reads it, never the node. */
|
|
18
|
+
credential_id: string | null;
|
|
15
19
|
};
|
|
16
20
|
tiers: Record<TierName, TierSpec>;
|
|
17
21
|
floor: TierName;
|
|
@@ -34,6 +38,8 @@ export interface MspLast {
|
|
|
34
38
|
model?: string | null;
|
|
35
39
|
effort?: string | null;
|
|
36
40
|
light_streak?: number | null;
|
|
41
|
+
/** When it was decided, added by the server. */
|
|
42
|
+
at?: string | null;
|
|
37
43
|
}
|
|
38
44
|
export interface MspDecision {
|
|
39
45
|
/** null when the turn fell back to the entity's own model. */
|
|
@@ -56,6 +62,13 @@ export interface MspDecision {
|
|
|
56
62
|
selector_error: string | null;
|
|
57
63
|
selector_ms: number | null;
|
|
58
64
|
selector_input_tokens: number | null;
|
|
65
|
+
selector_provider: MspConfig['selector']['provider'];
|
|
66
|
+
/** How sure Jev was of its pick, 0 to 1. Null for Claude, which doesn't say. */
|
|
67
|
+
confidence: number | null;
|
|
68
|
+
/** Jev says this message leans on earlier chat it can't see. */
|
|
69
|
+
continues: boolean;
|
|
70
|
+
/** Jev says it approves or restarts work, so it stays on the chat's tier. */
|
|
71
|
+
go_ahead: boolean;
|
|
59
72
|
max_run: MspConfig['max_run'];
|
|
60
73
|
fan_out: boolean;
|
|
61
74
|
}
|
|
@@ -132,6 +145,35 @@ export type SelectorRunner = (call: SelectorCall) => Promise<SelectorResult>;
|
|
|
132
145
|
/** argv for the bare selector. The message itself goes down stdin. */
|
|
133
146
|
export declare function selectorArgs(call: Pick<SelectorCall, 'model' | 'effort' | 'systemPrompt'>): string[];
|
|
134
147
|
export declare const spawnSelector: SelectorRunner;
|
|
148
|
+
export interface JevCall {
|
|
149
|
+
message: string;
|
|
150
|
+
current: TierName | null;
|
|
151
|
+
}
|
|
152
|
+
export interface JevResult {
|
|
153
|
+
ok: boolean;
|
|
154
|
+
tier: TierName | null;
|
|
155
|
+
confidence: number | null;
|
|
156
|
+
continues: boolean;
|
|
157
|
+
goAhead: boolean;
|
|
158
|
+
ms: number;
|
|
159
|
+
inputTokens: number | null;
|
|
160
|
+
error: string | null;
|
|
161
|
+
}
|
|
162
|
+
export type JevRunner = (call: JevCall) => Promise<JevResult>;
|
|
163
|
+
/** Asks the server, which holds the key. Never rejects. */
|
|
164
|
+
export declare const askJev: (token: string, requestId: string) => JevRunner;
|
|
165
|
+
export declare const JEV_CONFIDENCE_FLOOR = 0.6;
|
|
166
|
+
/**
|
|
167
|
+
* Jev only sees one message, so it mustn't mean a weak model. "yes do that" reads
|
|
168
|
+
* as quick and can be the go-ahead for an hour's work, so a go-ahead can't go
|
|
169
|
+
* lighter than where the chat already is. A follow-up question, or a pick Jev is
|
|
170
|
+
* unsure of, needs less: at least standard. settle() keeps a heavy model for its
|
|
171
|
+
* cache, so only the effort comes down. Holding max effort for "and the fix for
|
|
172
|
+
* number one?" took 3 minutes where medium took 15 s.
|
|
173
|
+
*/
|
|
174
|
+
export declare function steadyJevPick(pick: TierName, confidence: number | null, current: TierName | null, continues?: boolean, goAhead?: boolean): TierName;
|
|
175
|
+
/** The last decision, unless it is too old to be worth staying steady for. */
|
|
176
|
+
export declare function freshLast(last: MspLast | null | undefined, now: number): MspLast | null;
|
|
135
177
|
/**
|
|
136
178
|
* The whole decision for one turn. Never throws: every failure comes back as
|
|
137
179
|
* a fallback decision, and the turn runs.
|
|
@@ -145,6 +187,9 @@ export declare function decide(opts: {
|
|
|
145
187
|
entityModel: string | null;
|
|
146
188
|
entityEffort: string | null;
|
|
147
189
|
run?: SelectorRunner;
|
|
190
|
+
/** Required for an entity on Jev; without it the turn falls back. */
|
|
191
|
+
runJev?: JevRunner;
|
|
192
|
+
now?: number;
|
|
148
193
|
}): Promise<MspDecision | null>;
|
|
149
194
|
/** Is MSP worth asking about for this request at all? Cheap, no I/O. */
|
|
150
195
|
export declare function mspApplies(req: {
|
package/dist/model-selector.js
CHANGED
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
// The selector runs bare, because its whole budget is about a second: no MCP
|
|
23
23
|
// servers, no tools, no slash commands, no session file, its own system prompt
|
|
24
24
|
// in place of Claude Code's, and an empty folder so no CLAUDE.md is found.
|
|
25
|
+
//
|
|
26
|
+
// An entity can use TypeSafe's Jev instead (selector.provider 'jev'). Jev only
|
|
27
|
+
// sorts: it returns a tier and how sure it is, never words, so the line is the
|
|
28
|
+
// plain one and it can't be talked into saying something odd. The key stays on
|
|
29
|
+
// the server: the node asks the entity-msp-select edge function, which reads
|
|
30
|
+
// the entity's chosen Vault key. Only the new message goes, never the chat, and
|
|
31
|
+
// a message with override words isn't sent at all.
|
|
25
32
|
// Plan: addai-entity-studio docs/model-selector-protocol-plan.md.
|
|
26
33
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
27
34
|
if (k2 === undefined) k2 = k;
|
|
@@ -57,7 +64,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
57
64
|
};
|
|
58
65
|
})();
|
|
59
66
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
60
|
-
exports.spawnSelector = exports.TIER_ORDER = void 0;
|
|
67
|
+
exports.JEV_CONFIDENCE_FLOOR = exports.askJev = exports.spawnSelector = exports.TIER_ORDER = void 0;
|
|
61
68
|
exports.parseConfig = parseConfig;
|
|
62
69
|
exports.messageFromPrompt = messageFromPrompt;
|
|
63
70
|
exports.recentLines = recentLines;
|
|
@@ -70,6 +77,8 @@ exports.plainLine = plainLine;
|
|
|
70
77
|
exports.selectorSystemPrompt = selectorSystemPrompt;
|
|
71
78
|
exports.selectorUserPrompt = selectorUserPrompt;
|
|
72
79
|
exports.selectorArgs = selectorArgs;
|
|
80
|
+
exports.steadyJevPick = steadyJevPick;
|
|
81
|
+
exports.freshLast = freshLast;
|
|
73
82
|
exports.decide = decide;
|
|
74
83
|
exports.mspApplies = mspApplies;
|
|
75
84
|
const child_process_1 = require("child_process");
|
|
@@ -77,12 +86,13 @@ const fs = __importStar(require("fs"));
|
|
|
77
86
|
const os = __importStar(require("os"));
|
|
78
87
|
const path = __importStar(require("path"));
|
|
79
88
|
const claude_binary_1 = require("./claude-binary");
|
|
89
|
+
const config_1 = require("./config");
|
|
80
90
|
const win_1 = require("./win");
|
|
81
91
|
exports.TIER_ORDER = ['quick', 'standard', 'deep', 'max'];
|
|
82
92
|
const DEFAULT_CONFIG = {
|
|
83
93
|
enabled: false,
|
|
84
94
|
// Picks take 2-3 s on a node (most of it the CLI starting); 6 s leaves room.
|
|
85
|
-
selector: { model: 'claude-haiku-5-5', effort: 'low', timeout_ms: 6000 },
|
|
95
|
+
selector: { model: 'claude-haiku-5-5', effort: 'low', timeout_ms: 6000, provider: 'claude', credential_id: null },
|
|
86
96
|
tiers: {
|
|
87
97
|
quick: { model: 'claude-haiku-5-5', effort: 'low' },
|
|
88
98
|
standard: { model: 'claude-sonnet-5-5', effort: 'medium' },
|
|
@@ -136,12 +146,15 @@ function parseConfig(raw) {
|
|
|
136
146
|
const sel = r.selector ?? {};
|
|
137
147
|
const selModel = str(sel.model, 80);
|
|
138
148
|
const selEffort = str(sel.effort, 10);
|
|
149
|
+
const credentialId = str(sel.credential_id, 64) || null;
|
|
139
150
|
return {
|
|
140
151
|
enabled: true,
|
|
141
152
|
selector: {
|
|
142
153
|
model: selModel && /^(claude|haiku|sonnet)/i.test(selModel) ? selModel : DEFAULT_CONFIG.selector.model,
|
|
143
154
|
effort: EFFORTS.has(selEffort) ? selEffort : DEFAULT_CONFIG.selector.effort,
|
|
144
155
|
timeout_ms: num(sel.timeout_ms, 500, 15_000, DEFAULT_CONFIG.selector.timeout_ms),
|
|
156
|
+
provider: sel.provider === 'jev' && credentialId ? 'jev' : 'claude',
|
|
157
|
+
credential_id: credentialId,
|
|
145
158
|
},
|
|
146
159
|
tiers,
|
|
147
160
|
floor,
|
|
@@ -303,7 +316,7 @@ function selectorSystemPrompt(cfg, entityName) {
|
|
|
303
316
|
}
|
|
304
317
|
function selectorUserPrompt(opts) {
|
|
305
318
|
return [
|
|
306
|
-
`Current tier: ${opts.current ?? 'none
|
|
319
|
+
`Current tier: ${opts.current ?? 'none (first message of this chat, or it has been quiet for a while)'}`,
|
|
307
320
|
opts.forced ? `The person asked for the ${opts.forced} tier in words. Pick ${opts.forced}.` : '',
|
|
308
321
|
opts.recent.length ? `\nRecent chat, oldest first:\n${opts.recent.join('\n')}` : '',
|
|
309
322
|
'\nNew message:',
|
|
@@ -407,7 +420,73 @@ const spawnSelector = (call) => new Promise((resolve) => {
|
|
|
407
420
|
catch { /* same */ }
|
|
408
421
|
});
|
|
409
422
|
exports.spawnSelector = spawnSelector;
|
|
423
|
+
// The whole trip takes about 0.4 s, or 2 s when the edge function starts cold,
|
|
424
|
+
// and setup takes about 2 s anyway. A little over the function's own 2 s limit
|
|
425
|
+
// on Jev, so its answer arrives rather than being cut off just before it.
|
|
426
|
+
const JEV_TIMEOUT_MS = 3000;
|
|
427
|
+
/** Asks the server, which holds the key. Never rejects. */
|
|
428
|
+
const askJev = (token, requestId) => async (call) => {
|
|
429
|
+
const started = Date.now();
|
|
430
|
+
const failed = (error) => ({ ok: false, tier: null, confidence: null, continues: false, goAhead: false, ms: Date.now() - started, inputTokens: null, error });
|
|
431
|
+
try {
|
|
432
|
+
const res = await fetch(`${config_1.SUPABASE_URL}/functions/v1/entity-msp-select`, {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
headers: { 'Content-Type': 'application/json', apikey: config_1.SUPABASE_ANON_KEY, authorization: `Bearer ${config_1.SUPABASE_ANON_KEY}` },
|
|
435
|
+
body: JSON.stringify({ token, requestId, message: call.message, current: call.current }),
|
|
436
|
+
signal: AbortSignal.timeout(JEV_TIMEOUT_MS),
|
|
437
|
+
});
|
|
438
|
+
if (!res.ok)
|
|
439
|
+
return failed(`http_${res.status}`);
|
|
440
|
+
const out = await res.json();
|
|
441
|
+
if (out.ok !== true || !isTier(out.tier))
|
|
442
|
+
return failed(str(out.error, 80) || 'jev_bad_answer');
|
|
443
|
+
return {
|
|
444
|
+
ok: true,
|
|
445
|
+
tier: out.tier,
|
|
446
|
+
confidence: typeof out.confidence === 'number' ? out.confidence : null,
|
|
447
|
+
continues: out.continues === true,
|
|
448
|
+
goAhead: out.go_ahead === true,
|
|
449
|
+
ms: Date.now() - started,
|
|
450
|
+
inputTokens: typeof out.input_tokens === 'number' ? out.input_tokens : null,
|
|
451
|
+
error: null,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
catch (err) {
|
|
455
|
+
const name = err.name;
|
|
456
|
+
return failed(name === 'TimeoutError' || name === 'AbortError' ? 'timeout' : `fetch: ${err.message}`);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
exports.askJev = askJev;
|
|
460
|
+
// TypeSafe's own guidance for acting on an answer automatically.
|
|
461
|
+
exports.JEV_CONFIDENCE_FLOOR = 0.6;
|
|
462
|
+
/**
|
|
463
|
+
* Jev only sees one message, so it mustn't mean a weak model. "yes do that" reads
|
|
464
|
+
* as quick and can be the go-ahead for an hour's work, so a go-ahead can't go
|
|
465
|
+
* lighter than where the chat already is. A follow-up question, or a pick Jev is
|
|
466
|
+
* unsure of, needs less: at least standard. settle() keeps a heavy model for its
|
|
467
|
+
* cache, so only the effort comes down. Holding max effort for "and the fix for
|
|
468
|
+
* number one?" took 3 minutes where medium took 15 s.
|
|
469
|
+
*/
|
|
470
|
+
function steadyJevPick(pick, confidence, current, continues = false, goAhead = false) {
|
|
471
|
+
const atLeast = (least) => (rank(pick) < rank(least) ? least : pick);
|
|
472
|
+
if (goAhead)
|
|
473
|
+
return atLeast(current && rank(current) > rank('standard') ? current : 'standard');
|
|
474
|
+
if (continues || (confidence !== null && confidence < exports.JEV_CONFIDENCE_FLOOR))
|
|
475
|
+
return atLeast('standard');
|
|
476
|
+
return pick;
|
|
477
|
+
}
|
|
410
478
|
// ── one turn ─────────────────────────────────────────────────────────────────
|
|
479
|
+
// How long the prompt cache lives, and so how long holding a heavy model for it
|
|
480
|
+
// is worth anything. Past this a light message would pay to rewrite the cache on
|
|
481
|
+
// the heavy model for nothing, so the chat starts fresh.
|
|
482
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
483
|
+
/** The last decision, unless it is too old to be worth staying steady for. */
|
|
484
|
+
function freshLast(last, now) {
|
|
485
|
+
if (!last || typeof last !== 'object')
|
|
486
|
+
return null;
|
|
487
|
+
const at = typeof last.at === 'string' ? Date.parse(last.at) : NaN;
|
|
488
|
+
return Number.isFinite(at) && now - at > CACHE_TTL_MS ? null : last;
|
|
489
|
+
}
|
|
411
490
|
/**
|
|
412
491
|
* The whole decision for one turn. Never throws: every failure comes back as
|
|
413
492
|
* a fallback decision, and the turn runs.
|
|
@@ -416,10 +495,11 @@ async function decide(opts) {
|
|
|
416
495
|
const cfg = parseConfig(opts.config);
|
|
417
496
|
if (!cfg)
|
|
418
497
|
return null;
|
|
419
|
-
const last = opts.last
|
|
498
|
+
const last = freshLast(opts.last, opts.now ?? Date.now());
|
|
420
499
|
const current = last && isTier(last.tier) ? last.tier : null;
|
|
421
500
|
const message = messageFromPrompt(opts.prompt);
|
|
422
501
|
const forced = overrideFor(message, cfg.overrides);
|
|
502
|
+
const provider = cfg.selector.provider;
|
|
423
503
|
const fallback = (reason, ms, tokens) => {
|
|
424
504
|
// This chat's last tier if it had one; otherwise exactly what the entity
|
|
425
505
|
// would have run without MSP.
|
|
@@ -439,10 +519,62 @@ async function decide(opts) {
|
|
|
439
519
|
selector_error: reason,
|
|
440
520
|
selector_ms: ms,
|
|
441
521
|
selector_input_tokens: tokens,
|
|
522
|
+
selector_provider: provider,
|
|
523
|
+
confidence: null,
|
|
524
|
+
continues: false,
|
|
525
|
+
go_ahead: false,
|
|
442
526
|
max_run: cfg.max_run,
|
|
443
527
|
fan_out: t ? cfg.tiers[t].fan_out === true : false,
|
|
444
528
|
};
|
|
445
529
|
};
|
|
530
|
+
if (provider === 'jev') {
|
|
531
|
+
// The person said which tier, so the message has no reason to leave the node.
|
|
532
|
+
let jev = null;
|
|
533
|
+
if (!forced) {
|
|
534
|
+
if (!opts.runJev)
|
|
535
|
+
return fallback('jev_unavailable', null, null);
|
|
536
|
+
try {
|
|
537
|
+
jev = await opts.runJev({ message, current });
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
jev = { ok: false, tier: null, confidence: null, continues: false, goAhead: false, ms: 0, inputTokens: null, error: `runner: ${err.message}` };
|
|
541
|
+
}
|
|
542
|
+
if (!jev.ok || !jev.tier)
|
|
543
|
+
return fallback(jev.error ?? 'jev_bad_answer', jev.ms, jev.inputTokens);
|
|
544
|
+
}
|
|
545
|
+
const continues = jev?.continues === true;
|
|
546
|
+
const goAhead = jev?.goAhead === true;
|
|
547
|
+
const picked = forced ?? steadyJevPick(jev.tier, jev.confidence, current, continues, goAhead);
|
|
548
|
+
const s = settle(cfg, last, picked, forced ? 'override' : 'selector');
|
|
549
|
+
const sure = jev?.confidence;
|
|
550
|
+
const decision = {
|
|
551
|
+
tier: s.tier,
|
|
552
|
+
model: s.model,
|
|
553
|
+
effort: s.effort,
|
|
554
|
+
picked: clampTier(picked, cfg.floor, cfg.ceiling),
|
|
555
|
+
source: forced ? 'override' : 'selector',
|
|
556
|
+
why: forced
|
|
557
|
+
? 'asked for in the message'
|
|
558
|
+
: `Jev${typeof sure === 'number' ? `, ${Math.round(sure * 100)}% sure` : ''}${goAhead ? ', a go-ahead' : continues ? ', follows on from earlier' : ''}`,
|
|
559
|
+
line: plainLine(cfg, s.tier),
|
|
560
|
+
light_streak: s.light_streak,
|
|
561
|
+
effort_only: s.effort_only,
|
|
562
|
+
fell_back: false,
|
|
563
|
+
fallback_reason: null,
|
|
564
|
+
selector_error: null,
|
|
565
|
+
selector_ms: jev?.ms ?? null,
|
|
566
|
+
selector_input_tokens: jev?.inputTokens ?? null,
|
|
567
|
+
selector_provider: 'jev',
|
|
568
|
+
confidence: jev?.confidence ?? null,
|
|
569
|
+
continues,
|
|
570
|
+
go_ahead: goAhead,
|
|
571
|
+
max_run: cfg.max_run,
|
|
572
|
+
fan_out: cfg.tiers[s.tier].fan_out === true,
|
|
573
|
+
};
|
|
574
|
+
if (!shouldAnnounce(cfg, last, decision))
|
|
575
|
+
decision.line = null;
|
|
576
|
+
return decision;
|
|
577
|
+
}
|
|
446
578
|
let result;
|
|
447
579
|
try {
|
|
448
580
|
result = await (opts.run ?? exports.spawnSelector)({
|
|
@@ -483,6 +615,10 @@ async function decide(opts) {
|
|
|
483
615
|
selector_error: parsed ? null : (result.error ?? 'unparseable_reply'),
|
|
484
616
|
selector_ms: result.ms,
|
|
485
617
|
selector_input_tokens: result.inputTokens,
|
|
618
|
+
selector_provider: 'claude',
|
|
619
|
+
confidence: null,
|
|
620
|
+
continues: false,
|
|
621
|
+
go_ahead: false,
|
|
486
622
|
max_run: cfg.max_run,
|
|
487
623
|
fan_out: cfg.tiers[s.tier].fan_out === true,
|
|
488
624
|
};
|
package/dist/session-runner.js
CHANGED
|
@@ -1600,6 +1600,7 @@ async function startModelSelection(req) {
|
|
|
1600
1600
|
entityName: setting.entity_name || 'the assistant',
|
|
1601
1601
|
entityModel: req.model,
|
|
1602
1602
|
entityEffort: req.effort,
|
|
1603
|
+
runJev: (0, model_selector_1.askJev)(token(), req.id),
|
|
1603
1604
|
});
|
|
1604
1605
|
}
|
|
1605
1606
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.30.16-msp.
|
|
3
|
+
"version": "0.30.16-msp.3",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|