fraim-framework 2.0.124 → 2.0.127
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/bin/fraim.js +1 -1
- package/dist/src/ai-hub/catalog.js +280 -44
- package/dist/src/ai-hub/desktop-main.js +2 -2
- package/dist/src/ai-hub/hosts.js +384 -10
- package/dist/src/ai-hub/server.js +255 -9
- package/dist/src/cli/commands/add-ide.js +4 -3
- package/dist/src/cli/commands/first-run.js +61 -0
- package/dist/src/cli/commands/hub.js +4 -4
- package/dist/src/cli/commands/init-project.js +4 -4
- package/dist/src/cli/commands/setup.js +4 -3
- package/dist/src/cli/commands/sync.js +21 -2
- package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/mcp/ide-formats.js +29 -1
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
- package/dist/src/cli/setup/ide-detector.js +32 -1
- package/dist/src/cli/setup/ide-global-integration.js +5 -1
- package/dist/src/cli/setup/ide-invocation-surfaces.js +70 -17
- package/dist/src/cli/setup/mcp-config-generator.js +12 -1
- package/dist/src/cli/utils/agent-adapters.js +12 -2
- package/dist/src/cli/utils/project-bootstrap.js +4 -3
- package/dist/src/core/quality-evidence.js +81 -8
- package/dist/src/core/utils/git-utils.js +32 -7
- package/dist/src/core/utils/job-aliases.js +47 -0
- package/dist/src/core/utils/workflow-parser.js +3 -5
- package/dist/src/first-run/install-state.js +68 -0
- package/dist/src/first-run/server.js +153 -0
- package/dist/src/first-run/session-service.js +302 -0
- package/dist/src/first-run/types.js +40 -0
- package/dist/src/local-mcp-server/agent-token-prices.js +114 -0
- package/dist/src/local-mcp-server/codex-token-adapter.js +232 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +21 -8
- package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
- package/dist/src/local-mcp-server/stdio-server.js +70 -17
- package/dist/src/local-mcp-server/token-adapter-registry.js +64 -0
- package/dist/src/local-mcp-server/usage-collector.js +25 -0
- package/index.js +83 -83
- package/package.json +7 -1
- package/public/ai-hub/index.html +149 -102
- package/public/ai-hub/script.js +1154 -271
- package/public/ai-hub/styles.css +753 -450
- package/public/first-run/index.html +221 -0
- package/public/first-run/script.js +361 -0
- package/dist/src/cli/services/device-flow-service.js +0 -83
- package/dist/src/local-mcp-server/prometheus-scraper.js +0 -152
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -1,12 +1,209 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createHubEvent = exports.createHubMessage = exports.FakeHostRuntime = exports.CliHostRuntime = void 0;
|
|
3
|
+
exports.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = void 0;
|
|
4
|
+
exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
|
|
5
|
+
exports.parseUsageSignal = parseUsageSignal;
|
|
6
|
+
exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
|
|
4
7
|
exports.detectEmployees = detectEmployees;
|
|
5
8
|
exports.buildStartPlan = buildStartPlan;
|
|
6
9
|
exports.buildContinuePlan = buildContinuePlan;
|
|
7
10
|
exports.parseHostLine = parseHostLine;
|
|
8
11
|
const crypto_1 = require("crypto");
|
|
9
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
// Parse a single line of host stdout looking for a seekMentoring tool-use
|
|
14
|
+
// signal. Returns null if the line does not contain one. Supports both
|
|
15
|
+
// hosts FRAIM ships against today:
|
|
16
|
+
// - Claude Code: tool_use blocks inside message.content with name
|
|
17
|
+
// 'mcp__fraim__seekMentoring' (MCP names are prefixed in Claude's
|
|
18
|
+
// stream-json output).
|
|
19
|
+
// - Codex: top-level events of type 'item.started' / 'item.completed'
|
|
20
|
+
// whose item.type === 'mcp_tool_call' and item.tool === 'seekMentoring'
|
|
21
|
+
// (Codex separates the MCP server name from the tool name).
|
|
22
|
+
function parseSeekMentoringSignal(line) {
|
|
23
|
+
if (!line.includes('seekMentoring'))
|
|
24
|
+
return null;
|
|
25
|
+
let parsed;
|
|
26
|
+
try {
|
|
27
|
+
parsed = JSON.parse(line);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
33
|
+
return null;
|
|
34
|
+
const obj = parsed;
|
|
35
|
+
// Codex shape: { type: 'item.started' | 'item.completed', item: { type: 'mcp_tool_call', tool: 'seekMentoring', arguments: {...} } }.
|
|
36
|
+
// Match both started + completed; the call carries the same arguments
|
|
37
|
+
// either way, so applying the signal at the earliest is fine.
|
|
38
|
+
if ((obj.type === 'item.started' || obj.type === 'item.completed') &&
|
|
39
|
+
typeof obj.item === 'object' && obj.item !== null) {
|
|
40
|
+
const item = obj.item;
|
|
41
|
+
const itemType = item.type;
|
|
42
|
+
const tool = typeof item.tool === 'string' ? item.tool : null;
|
|
43
|
+
if (itemType === 'mcp_tool_call' &&
|
|
44
|
+
(tool === 'seekMentoring' || tool === 'mcp__fraim__seekMentoring')) {
|
|
45
|
+
const args = item.arguments;
|
|
46
|
+
const sig = extractSignalFromArgs(args);
|
|
47
|
+
if (sig)
|
|
48
|
+
return sig;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Claude Code shape: tool_use blocks live inside parsed.message.content.
|
|
52
|
+
// The name is MCP-prefixed (e.g. 'mcp__fraim__seekMentoring').
|
|
53
|
+
const candidates = [obj];
|
|
54
|
+
if (Array.isArray(obj.content))
|
|
55
|
+
candidates.push(...obj.content);
|
|
56
|
+
if (typeof obj.message === 'object' && obj.message !== null) {
|
|
57
|
+
const msg = obj.message;
|
|
58
|
+
if (Array.isArray(msg.content))
|
|
59
|
+
candidates.push(...msg.content);
|
|
60
|
+
}
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
63
|
+
continue;
|
|
64
|
+
const c = candidate;
|
|
65
|
+
const isToolUse = c.type === 'tool_use' || c.type === 'function_call';
|
|
66
|
+
const nameField = typeof c.name === 'string' ? c.name : (typeof c.tool_name === 'string' ? c.tool_name : '');
|
|
67
|
+
const isSeekMentoring = nameField === 'seekMentoring' ||
|
|
68
|
+
nameField === 'mcp__fraim__seekMentoring' ||
|
|
69
|
+
nameField.endsWith('seekMentoring');
|
|
70
|
+
if (!isToolUse || !isSeekMentoring)
|
|
71
|
+
continue;
|
|
72
|
+
const input = (c.input || c.arguments || c.parameters);
|
|
73
|
+
const sig = extractSignalFromArgs(input);
|
|
74
|
+
if (sig)
|
|
75
|
+
return sig;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// Issue #347 — extract per-turn usage from the host's JSON stream.
|
|
80
|
+
// Codex: `{"type":"turn.completed","usage":{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens}}`.
|
|
81
|
+
// Claude Code: `{"type":"result", ..., "usage":{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}, "total_cost_usd": ...}`.
|
|
82
|
+
function parseUsageSignal(line) {
|
|
83
|
+
if (!line.includes('usage'))
|
|
84
|
+
return null;
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(line);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
93
|
+
return null;
|
|
94
|
+
const obj = parsed;
|
|
95
|
+
// Codex turn.completed shape. Codex's `input_tokens` is cumulative
|
|
96
|
+
// and INCLUDES the cached portion, so normalize by subtracting.
|
|
97
|
+
if (obj.type === 'turn.completed' && typeof obj.usage === 'object' && obj.usage !== null) {
|
|
98
|
+
const u = obj.usage;
|
|
99
|
+
const totalInput = numberOrNull(u.input_tokens);
|
|
100
|
+
const cachedInput = numberOrNull(u.cached_input_tokens) ?? 0;
|
|
101
|
+
const outputTokens = numberOrNull(u.output_tokens);
|
|
102
|
+
if (totalInput === null && outputTokens === null)
|
|
103
|
+
return null;
|
|
104
|
+
const nonCached = Math.max(0, (totalInput ?? 0) - cachedInput);
|
|
105
|
+
return {
|
|
106
|
+
nonCachedInputTokens: nonCached,
|
|
107
|
+
cachedInputTokens: cachedInput,
|
|
108
|
+
cacheCreationTokens: 0, // Codex does not bill cache creation separately
|
|
109
|
+
outputTokens: outputTokens ?? 0,
|
|
110
|
+
reasoningTokens: numberOrNull(u.reasoning_output_tokens) ?? undefined,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Claude Code result shape. Anthropic semantics: input_tokens,
|
|
114
|
+
// cache_read_input_tokens, and cache_creation_input_tokens are
|
|
115
|
+
// mutually exclusive — pass each through to its own bucket.
|
|
116
|
+
if (obj.type === 'result' && typeof obj.usage === 'object' && obj.usage !== null) {
|
|
117
|
+
const u = obj.usage;
|
|
118
|
+
const inputTokens = numberOrNull(u.input_tokens);
|
|
119
|
+
const outputTokens = numberOrNull(u.output_tokens);
|
|
120
|
+
if (inputTokens === null && outputTokens === null)
|
|
121
|
+
return null;
|
|
122
|
+
const costUsd = numberOrNull(obj.total_cost_usd);
|
|
123
|
+
return {
|
|
124
|
+
nonCachedInputTokens: inputTokens ?? 0,
|
|
125
|
+
cachedInputTokens: numberOrNull(u.cache_read_input_tokens) ?? 0,
|
|
126
|
+
cacheCreationTokens: numberOrNull(u.cache_creation_input_tokens) ?? 0,
|
|
127
|
+
outputTokens: outputTokens ?? 0,
|
|
128
|
+
costUsd: costUsd ?? undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
// Issue #347 — extract agent identity from the fraim_connect tool call.
|
|
134
|
+
// Both hosts pass `agent: { name, model }` in the call's arguments.
|
|
135
|
+
function parseAgentIdentitySignal(line) {
|
|
136
|
+
if (!line.includes('fraim_connect'))
|
|
137
|
+
return null;
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(line);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
146
|
+
return null;
|
|
147
|
+
const obj = parsed;
|
|
148
|
+
// Codex shape.
|
|
149
|
+
if ((obj.type === 'item.started' || obj.type === 'item.completed') && typeof obj.item === 'object' && obj.item !== null) {
|
|
150
|
+
const item = obj.item;
|
|
151
|
+
if (item.type === 'mcp_tool_call' && item.tool === 'fraim_connect') {
|
|
152
|
+
return readAgentFromArgs(item.arguments);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Claude Code shape.
|
|
156
|
+
const candidates = [obj];
|
|
157
|
+
if (typeof obj.message === 'object' && obj.message !== null) {
|
|
158
|
+
const msg = obj.message;
|
|
159
|
+
if (Array.isArray(msg.content))
|
|
160
|
+
candidates.push(...msg.content);
|
|
161
|
+
}
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
if (typeof candidate !== 'object' || candidate === null)
|
|
164
|
+
continue;
|
|
165
|
+
const c = candidate;
|
|
166
|
+
if (c.type !== 'tool_use' && c.type !== 'function_call')
|
|
167
|
+
continue;
|
|
168
|
+
const name = typeof c.name === 'string' ? c.name : '';
|
|
169
|
+
if (!name.endsWith('fraim_connect'))
|
|
170
|
+
continue;
|
|
171
|
+
const sig = readAgentFromArgs((c.input || c.arguments));
|
|
172
|
+
if (sig)
|
|
173
|
+
return sig;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
function readAgentFromArgs(args) {
|
|
178
|
+
if (!args || typeof args.agent !== 'object' || args.agent === null)
|
|
179
|
+
return null;
|
|
180
|
+
const agent = args.agent;
|
|
181
|
+
const agentName = typeof agent.name === 'string' ? agent.name : null;
|
|
182
|
+
const agentModel = typeof agent.model === 'string' ? agent.model : null;
|
|
183
|
+
if (!agentName || !agentModel)
|
|
184
|
+
return null;
|
|
185
|
+
return { agentName, agentModel };
|
|
186
|
+
}
|
|
187
|
+
function numberOrNull(v) {
|
|
188
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : null;
|
|
189
|
+
}
|
|
190
|
+
function extractSignalFromArgs(args) {
|
|
191
|
+
if (!args)
|
|
192
|
+
return null;
|
|
193
|
+
const phaseId = typeof args.currentPhase === 'string' ? args.currentPhase : null;
|
|
194
|
+
const rawStatus = typeof args.status === 'string' ? args.status : null;
|
|
195
|
+
if (!phaseId || !rawStatus)
|
|
196
|
+
return null;
|
|
197
|
+
const phaseStatus = ['starting', 'complete', 'incomplete', 'failure'].includes(rawStatus)
|
|
198
|
+
? rawStatus
|
|
199
|
+
: 'starting';
|
|
200
|
+
const findings = args.findings;
|
|
201
|
+
const findingsText = findings && typeof findings.summary === 'string' ? findings.summary : undefined;
|
|
202
|
+
const discriminant = typeof args.runDiscriminant === 'string' ? args.runDiscriminant : undefined;
|
|
203
|
+
const jobName = typeof args.jobName === 'string' ? args.jobName : undefined;
|
|
204
|
+
const jobId = typeof args.jobId === 'string' ? args.jobId : undefined;
|
|
205
|
+
return { phaseId, phaseStatus, findingsText, discriminant, jobName, jobId };
|
|
206
|
+
}
|
|
10
207
|
const EMPLOYEE_LABELS = {
|
|
11
208
|
codex: 'Codex',
|
|
12
209
|
claude: 'Claude Code',
|
|
@@ -83,39 +280,51 @@ function parseHostLine(hostId, line) {
|
|
|
83
280
|
const trimmed = line.trim();
|
|
84
281
|
if (!trimmed)
|
|
85
282
|
return {};
|
|
283
|
+
// Issue #347: scan every line for the three signals the Hub UI cares
|
|
284
|
+
// about — seekMentoring (drives the tracker), turn-level usage
|
|
285
|
+
// (drives the totals tokens/cost), and the agent identity from
|
|
286
|
+
// fraim_connect (used to look up cost when the host doesn't emit it).
|
|
287
|
+
const seekMentoring = parseSeekMentoringSignal(trimmed);
|
|
288
|
+
const usage = parseUsageSignal(trimmed);
|
|
289
|
+
const agentIdentity = parseAgentIdentitySignal(trimmed);
|
|
290
|
+
const withSignal = (event) => {
|
|
291
|
+
if (!seekMentoring && !usage && !agentIdentity)
|
|
292
|
+
return event;
|
|
293
|
+
return { ...event, ...(seekMentoring ? { seekMentoring } : {}), ...(usage ? { usage } : {}), ...(agentIdentity ? { agentIdentity } : {}) };
|
|
294
|
+
};
|
|
86
295
|
if (hostId === 'codex') {
|
|
87
296
|
try {
|
|
88
297
|
const parsed = JSON.parse(trimmed);
|
|
89
298
|
if (parsed.type === 'thread.started' && parsed.thread_id) {
|
|
90
|
-
return { sessionId: parsed.thread_id, raw: trimmed };
|
|
299
|
+
return withSignal({ sessionId: parsed.thread_id, raw: trimmed });
|
|
91
300
|
}
|
|
92
301
|
if (parsed.type === 'item.completed' && parsed.item?.type === 'agent_message' && parsed.item.text) {
|
|
93
|
-
return { message: parsed.item.text, raw: trimmed };
|
|
302
|
+
return withSignal({ message: parsed.item.text, raw: trimmed });
|
|
94
303
|
}
|
|
95
|
-
return { raw: trimmed };
|
|
304
|
+
return withSignal({ raw: trimmed });
|
|
96
305
|
}
|
|
97
306
|
catch {
|
|
98
|
-
return { raw: trimmed };
|
|
307
|
+
return withSignal({ raw: trimmed });
|
|
99
308
|
}
|
|
100
309
|
}
|
|
101
310
|
try {
|
|
102
311
|
const parsed = JSON.parse(trimmed);
|
|
103
312
|
if (parsed.type === 'system' && parsed.session_id) {
|
|
104
|
-
return { sessionId: parsed.session_id, raw: trimmed };
|
|
313
|
+
return withSignal({ sessionId: parsed.session_id, raw: trimmed });
|
|
105
314
|
}
|
|
106
315
|
if (parsed.type === 'assistant') {
|
|
107
316
|
const text = parsed.message?.content?.find((entry) => entry.type === 'text')?.text;
|
|
108
317
|
if (text) {
|
|
109
|
-
return { message: text, sessionId: parsed.session_id, raw: trimmed };
|
|
318
|
+
return withSignal({ message: text, sessionId: parsed.session_id, raw: trimmed });
|
|
110
319
|
}
|
|
111
320
|
}
|
|
112
321
|
if (parsed.type === 'result' && parsed.result) {
|
|
113
|
-
return { message: parsed.result, sessionId: parsed.session_id, raw: trimmed };
|
|
322
|
+
return withSignal({ message: parsed.result, sessionId: parsed.session_id, raw: trimmed });
|
|
114
323
|
}
|
|
115
|
-
return { raw: trimmed };
|
|
324
|
+
return withSignal({ raw: trimmed });
|
|
116
325
|
}
|
|
117
326
|
catch {
|
|
118
|
-
return { raw: trimmed };
|
|
327
|
+
return withSignal({ raw: trimmed });
|
|
119
328
|
}
|
|
120
329
|
}
|
|
121
330
|
function wireHostProcess(hostId, child, handlers) {
|
|
@@ -225,6 +434,171 @@ class FakeHostRuntime {
|
|
|
225
434
|
}
|
|
226
435
|
}
|
|
227
436
|
exports.FakeHostRuntime = FakeHostRuntime;
|
|
437
|
+
// Issue #347 — test-only host that lets a test inject seekMentoring
|
|
438
|
+
// signals on a runId-by-runId basis. The Hub server treats it like any
|
|
439
|
+
// HostRuntime; the test holds a reference and calls emitPhase() to
|
|
440
|
+
// drive deterministic phase transitions. Outside tests, prefer
|
|
441
|
+
// FakeHostRuntime (smaller surface, no seekMentoring).
|
|
442
|
+
class ScriptedHostRuntime {
|
|
443
|
+
constructor() {
|
|
444
|
+
this.employees = [
|
|
445
|
+
{ id: 'codex', label: 'Codex', available: true, detail: 'Scripted test double.' },
|
|
446
|
+
{ id: 'claude', label: 'Claude Code', available: true, detail: 'Scripted test double.' },
|
|
447
|
+
];
|
|
448
|
+
// Track each active run so the test can emit signals at it. Key is the
|
|
449
|
+
// sessionId we hand back on startRun; mapping sessionId → handlers
|
|
450
|
+
// lets emitPhase() reach the right run's onEvent callback.
|
|
451
|
+
this.handlersBySession = new Map();
|
|
452
|
+
// Tracks runDiscriminant per sessionId so consecutive emitPhase calls
|
|
453
|
+
// can resolve onSuccess routing without each test having to repeat it.
|
|
454
|
+
this.discriminantBySession = new Map();
|
|
455
|
+
}
|
|
456
|
+
detectEmployees() {
|
|
457
|
+
return this.employees;
|
|
458
|
+
}
|
|
459
|
+
startRun(_hostId, _projectPath, _message, handlers) {
|
|
460
|
+
const sessionId = (0, crypto_1.randomUUID)();
|
|
461
|
+
handlers.onEvent({ sessionId, raw: 'scripted-session-start' }, 'system');
|
|
462
|
+
this.handlersBySession.set(sessionId, handlers);
|
|
463
|
+
return this.spawnDouble();
|
|
464
|
+
}
|
|
465
|
+
continueRun(_hostId, _projectPath, sessionId, _message, handlers) {
|
|
466
|
+
this.handlersBySession.set(sessionId, handlers);
|
|
467
|
+
handlers.onEvent({ sessionId, raw: 'scripted-session-resume' }, 'system');
|
|
468
|
+
return this.spawnDouble();
|
|
469
|
+
}
|
|
470
|
+
// Test API — fire a seekMentoring tool-use event for the most recent
|
|
471
|
+
// active run. The phaseId is the raw FSM phase id (e.g.,
|
|
472
|
+
// 'implement-validate'); the parser will turn it into a friendly
|
|
473
|
+
// tracker label downstream.
|
|
474
|
+
emitPhase(runId, phaseId, status, findingsText) {
|
|
475
|
+
const target = this.resolveSession(runId);
|
|
476
|
+
if (!target)
|
|
477
|
+
return;
|
|
478
|
+
target.handlers.onEvent({
|
|
479
|
+
sessionId: target.sessionId,
|
|
480
|
+
raw: `scripted-seekMentoring:${phaseId}:${status}`,
|
|
481
|
+
seekMentoring: { phaseId, phaseStatus: status, findingsText },
|
|
482
|
+
}, 'stdout');
|
|
483
|
+
}
|
|
484
|
+
// Test API — set the runDiscriminant for a run so onSuccess routing
|
|
485
|
+
// resolves correctly (e.g., 'feature' vs 'bug' for feature-implementation).
|
|
486
|
+
emitDiscriminant(runId, discriminant) {
|
|
487
|
+
const target = this.resolveSession(runId);
|
|
488
|
+
if (!target)
|
|
489
|
+
return;
|
|
490
|
+
this.discriminantBySession.set(target.sessionId, discriminant);
|
|
491
|
+
target.handlers.onEvent({
|
|
492
|
+
sessionId: target.sessionId,
|
|
493
|
+
raw: `scripted-discriminant:${discriminant}`,
|
|
494
|
+
seekMentoring: {
|
|
495
|
+
phaseId: '__discriminant__',
|
|
496
|
+
phaseStatus: 'starting',
|
|
497
|
+
discriminant,
|
|
498
|
+
},
|
|
499
|
+
}, 'system');
|
|
500
|
+
}
|
|
501
|
+
// Test API — emit a seekMentoring signal for a job *other* than the
|
|
502
|
+
// one this run is tracking. Used to verify the cross-job pollution
|
|
503
|
+
// filter in applySeekMentoringSignal.
|
|
504
|
+
emitForeignPhase(runId, foreignJobId, phaseId, status) {
|
|
505
|
+
const target = this.resolveSession(runId);
|
|
506
|
+
if (!target)
|
|
507
|
+
return;
|
|
508
|
+
target.handlers.onEvent({
|
|
509
|
+
sessionId: target.sessionId,
|
|
510
|
+
raw: `scripted-foreign-seekMentoring:${foreignJobId}:${phaseId}`,
|
|
511
|
+
seekMentoring: {
|
|
512
|
+
phaseId,
|
|
513
|
+
phaseStatus: status,
|
|
514
|
+
jobId: foreignJobId,
|
|
515
|
+
jobName: foreignJobId,
|
|
516
|
+
},
|
|
517
|
+
}, 'stdout');
|
|
518
|
+
}
|
|
519
|
+
// Test API — emit a per-turn usage signal in the normalized shape
|
|
520
|
+
// the server expects. Mirrors what parseUsageSignal would produce
|
|
521
|
+
// from a real host's stream.
|
|
522
|
+
emitUsage(runId, usage) {
|
|
523
|
+
const target = this.resolveSession(runId);
|
|
524
|
+
if (!target)
|
|
525
|
+
return;
|
|
526
|
+
target.handlers.onEvent({
|
|
527
|
+
sessionId: target.sessionId,
|
|
528
|
+
raw: `scripted-usage:${JSON.stringify(usage)}`,
|
|
529
|
+
usage: {
|
|
530
|
+
nonCachedInputTokens: usage.nonCachedInputTokens ?? 0,
|
|
531
|
+
cachedInputTokens: usage.cachedInputTokens ?? 0,
|
|
532
|
+
cacheCreationTokens: usage.cacheCreationTokens ?? 0,
|
|
533
|
+
outputTokens: usage.outputTokens ?? 0,
|
|
534
|
+
costUsd: usage.costUsd,
|
|
535
|
+
},
|
|
536
|
+
}, 'stdout');
|
|
537
|
+
}
|
|
538
|
+
// Test API — emit the agent-identity signal that fraim_connect would
|
|
539
|
+
// produce, so applyUsageSignal's price-table lookup has identity to
|
|
540
|
+
// work with.
|
|
541
|
+
emitAgentIdentity(runId, agentName, agentModel) {
|
|
542
|
+
const target = this.resolveSession(runId);
|
|
543
|
+
if (!target)
|
|
544
|
+
return;
|
|
545
|
+
target.handlers.onEvent({
|
|
546
|
+
sessionId: target.sessionId,
|
|
547
|
+
raw: `scripted-agent-identity:${agentName}:${agentModel}`,
|
|
548
|
+
agentIdentity: { agentName, agentModel },
|
|
549
|
+
}, 'stdout');
|
|
550
|
+
}
|
|
551
|
+
// Reset between tests (called from beforeEach).
|
|
552
|
+
reset() {
|
|
553
|
+
this.handlersBySession.clear();
|
|
554
|
+
this.discriminantBySession.clear();
|
|
555
|
+
}
|
|
556
|
+
// The Hub server keeps its own sessionId map keyed off run.sessionId —
|
|
557
|
+
// but at the time the test calls emitPhase(runId), we may only have
|
|
558
|
+
// one handler registered (the latest start). Resolve by sessionId
|
|
559
|
+
// first, fall back to the most recent registered handler.
|
|
560
|
+
resolveSession(_runId) {
|
|
561
|
+
if (this.handlersBySession.size === 0)
|
|
562
|
+
return null;
|
|
563
|
+
const entries = [...this.handlersBySession.entries()];
|
|
564
|
+
const [sessionId, handlers] = entries[entries.length - 1];
|
|
565
|
+
return { sessionId, handlers };
|
|
566
|
+
}
|
|
567
|
+
spawnDouble() {
|
|
568
|
+
return {
|
|
569
|
+
stdout: process.stdout,
|
|
570
|
+
stderr: process.stderr,
|
|
571
|
+
stdin: process.stdin,
|
|
572
|
+
kill: () => true,
|
|
573
|
+
on: () => ({}),
|
|
574
|
+
once: () => ({}),
|
|
575
|
+
emit: () => true,
|
|
576
|
+
addListener: () => ({}),
|
|
577
|
+
removeListener: () => ({}),
|
|
578
|
+
removeAllListeners: () => ({}),
|
|
579
|
+
setMaxListeners: () => ({}),
|
|
580
|
+
getMaxListeners: () => 0,
|
|
581
|
+
listeners: () => [],
|
|
582
|
+
rawListeners: () => [],
|
|
583
|
+
listenerCount: () => 0,
|
|
584
|
+
prependListener: () => ({}),
|
|
585
|
+
prependOnceListener: () => ({}),
|
|
586
|
+
eventNames: () => [],
|
|
587
|
+
pid: 0,
|
|
588
|
+
connected: false,
|
|
589
|
+
disconnect: () => undefined,
|
|
590
|
+
exitCode: 0,
|
|
591
|
+
killed: false,
|
|
592
|
+
signalCode: null,
|
|
593
|
+
spawnargs: [],
|
|
594
|
+
spawnfile: '',
|
|
595
|
+
stdio: [],
|
|
596
|
+
unref: () => undefined,
|
|
597
|
+
ref: () => undefined,
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
exports.ScriptedHostRuntime = ScriptedHostRuntime;
|
|
228
602
|
const createHubMessage = (role, text) => ({
|
|
229
603
|
id: (0, crypto_1.randomUUID)(),
|
|
230
604
|
role,
|