@elench/testkit 0.1.100 → 0.1.102
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 -3
- package/lib/cli/args.mjs +0 -19
- package/lib/cli/assistant/app.mjs +6 -0
- package/lib/cli/assistant/command-observer.mjs +75 -44
- package/lib/cli/assistant/command-results.mjs +29 -2
- package/lib/cli/assistant/context-pack.mjs +21 -1
- package/lib/cli/assistant/providers/claude.mjs +42 -7
- package/lib/cli/assistant/providers/codex.mjs +87 -9
- package/lib/cli/assistant/providers/events.mjs +71 -0
- package/lib/cli/assistant/providers/index.mjs +5 -4
- package/lib/cli/assistant/providers/shared.mjs +40 -21
- package/lib/cli/assistant/session.mjs +46 -8
- package/lib/cli/assistant/settings.mjs +29 -6
- package/lib/cli/assistant/state.mjs +181 -6
- package/lib/cli/assistant/transcript-text.mjs +35 -0
- package/lib/cli/assistant/view-model.mjs +11 -0
- package/lib/cli/command-flags.mjs +0 -3
- package/lib/cli/entrypoint.mjs +0 -2
- package/lib/cli/operations/run/operation.mjs +0 -3
- package/lib/runner/live-run.mjs +5 -1
- package/lib/runner/orchestrator.mjs +26 -26
- package/lib/runner/planning.mjs +0 -75
- package/lib/runner/provenance.mjs +20 -0
- package/lib/runner/reporting.mjs +14 -9
- package/lib/runner/run-finalization.mjs +5 -2
- package/lib/runner/run-guards.mjs +0 -1
- package/lib/runner/scheduler/estimates.mjs +61 -0
- package/lib/runner/scheduler/identity.mjs +31 -0
- package/lib/runner/scheduler/index.mjs +126 -0
- package/lib/runner/scheduler/observations.mjs +27 -0
- package/lib/runner/selection.mjs +1 -2
- package/lib/runner/worker-loop.mjs +3 -4
- package/lib/timing/index.mjs +33 -33
- package/node_modules/@elench/next-analysis/package.json +1 -1
- package/node_modules/@elench/testkit-bridge/package.json +2 -2
- package/node_modules/@elench/testkit-protocol/package.json +1 -1
- package/node_modules/@elench/ts-analysis/package.json +1 -1
- package/package.json +14 -8
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import readline from "readline";
|
|
3
|
+
import {
|
|
4
|
+
providerError,
|
|
5
|
+
providerEvent,
|
|
6
|
+
providerSessionEnd,
|
|
7
|
+
providerStatus,
|
|
8
|
+
providerToolStart,
|
|
9
|
+
} from "./events.mjs";
|
|
3
10
|
|
|
4
|
-
export function createHostedSessionRunner({
|
|
11
|
+
export function createHostedSessionRunner({
|
|
12
|
+
provider,
|
|
13
|
+
child,
|
|
14
|
+
onEvent,
|
|
15
|
+
onRawLine,
|
|
16
|
+
parsePayload,
|
|
17
|
+
readFinalText,
|
|
18
|
+
shouldIgnoreStatus,
|
|
19
|
+
} = {}) {
|
|
5
20
|
let cancelled = false;
|
|
6
21
|
let settled = false;
|
|
7
22
|
let assistantText = "";
|
|
23
|
+
let finalText = null;
|
|
8
24
|
let lastErrorMessage = null;
|
|
9
25
|
|
|
10
26
|
const emit = (event) => {
|
|
11
|
-
if (event
|
|
27
|
+
if (!event) return;
|
|
28
|
+
if (event.type === "assistant-delta") {
|
|
12
29
|
assistantText += event.text || "";
|
|
13
30
|
}
|
|
14
|
-
if (event
|
|
15
|
-
|
|
31
|
+
if (event.type === "assistant-final") {
|
|
32
|
+
finalText = event.text || finalText;
|
|
33
|
+
}
|
|
34
|
+
if (event.type === "error") {
|
|
35
|
+
lastErrorMessage = event.text || lastErrorMessage;
|
|
16
36
|
}
|
|
17
37
|
if (typeof onEvent === "function" && event) onEvent({ provider, ...event });
|
|
18
38
|
};
|
|
19
39
|
|
|
20
|
-
emit(
|
|
40
|
+
emit(providerEvent("session-start"));
|
|
21
41
|
|
|
22
42
|
const stdoutReader = readline.createInterface({ input: child.stdout });
|
|
23
43
|
stdoutReader.on("line", (line) => {
|
|
44
|
+
onRawLine?.({ provider, stream: "stdout", line });
|
|
24
45
|
const parsed = tryParseJson(line);
|
|
25
46
|
if (parsed == null) {
|
|
26
|
-
emit(
|
|
47
|
+
emit(providerStatus(line));
|
|
27
48
|
return;
|
|
28
49
|
}
|
|
29
50
|
const events = parsePayload ? parsePayload(parsed) : [];
|
|
@@ -33,27 +54,31 @@ export function createHostedSessionRunner({ provider, child, onEvent, parsePaylo
|
|
|
33
54
|
|
|
34
55
|
const stderrReader = readline.createInterface({ input: child.stderr });
|
|
35
56
|
stderrReader.on("line", (line) => {
|
|
57
|
+
onRawLine?.({ provider, stream: "stderr", line });
|
|
36
58
|
if (shouldIgnoreStatus?.(line)) return;
|
|
37
|
-
emit({
|
|
59
|
+
emit(providerStatus(line, { stream: "stderr" }));
|
|
38
60
|
});
|
|
39
61
|
|
|
40
62
|
const completion = (async () => {
|
|
41
63
|
const result = await child;
|
|
42
|
-
const
|
|
64
|
+
const fileFinalText = readFinalText ? readFinalText(result) : null;
|
|
65
|
+
const resolvedFinalText = fileFinalText || finalText || assistantText.trim() || null;
|
|
43
66
|
if ((result.exitCode ?? 0) !== 0) {
|
|
44
67
|
const message = lastErrorMessage || result.stderr || `${provider} exited with code ${result.exitCode ?? 1}`;
|
|
45
|
-
emit(
|
|
68
|
+
emit(providerError(message));
|
|
46
69
|
throw new Error(message);
|
|
47
70
|
}
|
|
48
|
-
if (
|
|
49
|
-
|
|
71
|
+
if (resolvedFinalText && resolvedFinalText !== finalText) {
|
|
72
|
+
emit(providerEvent("assistant-final", { text: resolvedFinalText }));
|
|
73
|
+
}
|
|
74
|
+
emit(providerSessionEnd({ exitCode: result.exitCode ?? 0 }));
|
|
50
75
|
settled = true;
|
|
51
76
|
return {
|
|
52
77
|
provider,
|
|
53
78
|
exitCode: result.exitCode ?? 0,
|
|
54
79
|
stdout: result.stdout || "",
|
|
55
80
|
stderr: result.stderr || "",
|
|
56
|
-
finalText:
|
|
81
|
+
finalText: resolvedFinalText || result.stdout || "",
|
|
57
82
|
cancelled,
|
|
58
83
|
};
|
|
59
84
|
})();
|
|
@@ -84,21 +109,15 @@ export function tryParseJson(line) {
|
|
|
84
109
|
|
|
85
110
|
export function buildToolEvent(name, detail = null) {
|
|
86
111
|
if (!name) return null;
|
|
87
|
-
return {
|
|
88
|
-
type: "tool",
|
|
89
|
-
name: String(name),
|
|
90
|
-
...(detail ? { detail: String(detail) } : {}),
|
|
91
|
-
};
|
|
112
|
+
return providerToolStart(name, detail ? { detail: String(detail) } : {});
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
export function buildStatusEvent(message) {
|
|
95
|
-
|
|
96
|
-
return { type: "status", message: String(message) };
|
|
116
|
+
return providerStatus(message);
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
export function buildErrorEvent(message) {
|
|
100
|
-
|
|
101
|
-
return { type: "error", message: String(message) };
|
|
120
|
+
return providerError(message);
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
export function extractTextFragments(payload, fragments = [], depth = 0) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
1
3
|
import { startProviderSession, resolvePreferredProvider } from "./providers/index.mjs";
|
|
2
4
|
import { buildAssistantPrompt } from "./prompt-builder.mjs";
|
|
3
5
|
import { createAssistantCommandObserver } from "./command-observer.mjs";
|
|
@@ -14,6 +16,7 @@ export async function runAssistantConversationTurn({
|
|
|
14
16
|
commandLog,
|
|
15
17
|
onStatus,
|
|
16
18
|
onToolEvent,
|
|
19
|
+
onProviderEvent,
|
|
17
20
|
onResolvedProvider,
|
|
18
21
|
onPrompt,
|
|
19
22
|
} = {}) {
|
|
@@ -36,6 +39,12 @@ export async function runAssistantConversationTurn({
|
|
|
36
39
|
const runtimeSettings = settings || { provider };
|
|
37
40
|
const resolvedProvider = resolvePreferredProvider(runtimeSettings.provider || provider, env);
|
|
38
41
|
const providerEnv = commandLog?.providerEnv?.(env) || env;
|
|
42
|
+
const tracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
43
|
+
? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-events.jsonl")
|
|
44
|
+
: null;
|
|
45
|
+
const rawTracePath = shouldTraceProviderEvents(env, providerEnv)
|
|
46
|
+
? path.join(commandLog?.contextDir || path.join(productDir, ".testkit", "assistant"), "provider-raw.jsonl")
|
|
47
|
+
: null;
|
|
39
48
|
onResolvedProvider?.(resolvedProvider);
|
|
40
49
|
onPrompt?.({
|
|
41
50
|
prompt,
|
|
@@ -44,6 +53,7 @@ export async function runAssistantConversationTurn({
|
|
|
44
53
|
effort: runtimeSettings.effort || null,
|
|
45
54
|
});
|
|
46
55
|
onStatus?.(`Thinking with ${resolvedProvider}...`);
|
|
56
|
+
onProviderEvent?.({ type: "status", provider: resolvedProvider, text: `Thinking with ${resolvedProvider}...` });
|
|
47
57
|
|
|
48
58
|
observer.start();
|
|
49
59
|
try {
|
|
@@ -56,25 +66,53 @@ export async function runAssistantConversationTurn({
|
|
|
56
66
|
prompt,
|
|
57
67
|
purpose: "assistant",
|
|
58
68
|
env: providerEnv,
|
|
69
|
+
onRawLine(line) {
|
|
70
|
+
appendProviderTrace(rawTracePath, line);
|
|
71
|
+
},
|
|
59
72
|
onEvent(event) {
|
|
60
|
-
|
|
73
|
+
appendProviderTrace(tracePath, event);
|
|
74
|
+
onProviderEvent?.(event);
|
|
75
|
+
if (event.type === "status" || event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end") {
|
|
76
|
+
onStatus?.(formatProviderEvent(event));
|
|
77
|
+
}
|
|
61
78
|
},
|
|
62
79
|
});
|
|
63
|
-
|
|
80
|
+
await session.completion;
|
|
64
81
|
observer.scan();
|
|
65
82
|
commandLog?.refresh?.();
|
|
66
|
-
return
|
|
67
|
-
role: "assistant",
|
|
68
|
-
text: result.finalText || "",
|
|
69
|
-
}];
|
|
83
|
+
return { provider: resolvedProvider };
|
|
70
84
|
} finally {
|
|
71
85
|
observer.stop();
|
|
72
86
|
}
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
function formatProviderEvent(event) {
|
|
76
|
-
if (event.type === "tool") {
|
|
90
|
+
if (event.type === "tool-start") {
|
|
77
91
|
return `${event.provider}: ${event.name}${event.detail ? ` (${event.detail})` : ""}`;
|
|
78
92
|
}
|
|
79
|
-
|
|
93
|
+
if (event.type === "tool-update") {
|
|
94
|
+
return `${event.provider}: ${event.name || "tool"}${event.text ? ` (${event.text})` : ""}`;
|
|
95
|
+
}
|
|
96
|
+
if (event.type === "tool-end") {
|
|
97
|
+
return `${event.provider}: ${event.name || "tool"} ${event.status || "done"}`;
|
|
98
|
+
}
|
|
99
|
+
return `${event.provider}: ${event.text || "working"}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function shouldTraceProviderEvents(...envs) {
|
|
103
|
+
return envs.some((entry) => String(entry?.TESTKIT_PROVIDER_TRACE || "").trim() === "1");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function appendProviderTrace(tracePath, event) {
|
|
107
|
+
if (!tracePath || !event) return;
|
|
108
|
+
try {
|
|
109
|
+
fs.mkdirSync(path.dirname(tracePath), { recursive: true });
|
|
110
|
+
fs.appendFileSync(
|
|
111
|
+
tracePath,
|
|
112
|
+
`${JSON.stringify({ timestamp: new Date().toISOString(), ...event })}\n`,
|
|
113
|
+
"utf8"
|
|
114
|
+
);
|
|
115
|
+
} catch {
|
|
116
|
+
// Provider tracing must never interfere with the assistant turn.
|
|
117
|
+
}
|
|
80
118
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { HOSTED_ASSISTANT_PROVIDERS } from "./providers/index.mjs";
|
|
3
4
|
|
|
4
|
-
export const ASSISTANT_PROVIDERS = ["auto",
|
|
5
|
+
export const ASSISTANT_PROVIDERS = ["auto", ...HOSTED_ASSISTANT_PROVIDERS];
|
|
5
6
|
export const ASSISTANT_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
6
7
|
|
|
7
8
|
export const DEFAULT_ASSISTANT_SETTINGS = Object.freeze({
|
|
@@ -19,7 +20,11 @@ export function loadAssistantSettings(productDir) {
|
|
|
19
20
|
const filePath = assistantSettingsPath(productDir);
|
|
20
21
|
try {
|
|
21
22
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
22
|
-
|
|
23
|
+
const normalized = normalizeAssistantSettings(parsed);
|
|
24
|
+
if (JSON.stringify(parsed) !== JSON.stringify(normalized)) {
|
|
25
|
+
writeAssistantSettingsFile(filePath, normalized);
|
|
26
|
+
}
|
|
27
|
+
return normalized;
|
|
23
28
|
} catch {
|
|
24
29
|
return { ...DEFAULT_ASSISTANT_SETTINGS };
|
|
25
30
|
}
|
|
@@ -27,8 +32,7 @@ export function loadAssistantSettings(productDir) {
|
|
|
27
32
|
|
|
28
33
|
export function saveAssistantSettings(productDir, settings) {
|
|
29
34
|
const filePath = assistantSettingsPath(productDir);
|
|
30
|
-
|
|
31
|
-
fs.writeFileSync(filePath, `${JSON.stringify(normalizeAssistantSettings(settings), null, 2)}\n`);
|
|
35
|
+
writeAssistantSettingsFile(filePath, normalizeAssistantSettings(settings));
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export function resetAssistantSettings(productDir) {
|
|
@@ -52,8 +56,9 @@ export function mergeAssistantSettings(...settingsObjects) {
|
|
|
52
56
|
|
|
53
57
|
export function normalizeAssistantSettings(value = {}) {
|
|
54
58
|
const provider = normalizeProvider(value.provider);
|
|
55
|
-
const
|
|
56
|
-
const
|
|
59
|
+
const extracted = extractEffortFromModel(value.model);
|
|
60
|
+
const effort = normalizeEffort(value.effort || extracted.effort);
|
|
61
|
+
const model = extracted.model;
|
|
57
62
|
const providerArgs = Array.isArray(value.providerArgs)
|
|
58
63
|
? value.providerArgs.map((entry) => normalizeOptionalString(entry)).filter(Boolean)
|
|
59
64
|
: [];
|
|
@@ -89,6 +94,19 @@ export function normalizeOptionalString(value) {
|
|
|
89
94
|
return stringValue || null;
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
export function extractEffortFromModel(value) {
|
|
98
|
+
const model = normalizeOptionalString(value);
|
|
99
|
+
if (!model) return { model: null, effort: null };
|
|
100
|
+
const tokens = model.split(/\s+/);
|
|
101
|
+
if (tokens.length < 2) return { model, effort: null };
|
|
102
|
+
const trailingEffort = tokens.at(-1);
|
|
103
|
+
if (!ASSISTANT_EFFORTS.includes(trailingEffort)) return { model, effort: null };
|
|
104
|
+
return {
|
|
105
|
+
model: tokens.slice(0, -1).join(" ").trim() || null,
|
|
106
|
+
effort: trailingEffort,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
function dropNullishSettings(settings) {
|
|
93
111
|
const result = {};
|
|
94
112
|
for (const [key, value] of Object.entries(settings)) {
|
|
@@ -96,3 +114,8 @@ function dropNullishSettings(settings) {
|
|
|
96
114
|
}
|
|
97
115
|
return result;
|
|
98
116
|
}
|
|
117
|
+
|
|
118
|
+
function writeAssistantSettingsFile(filePath, settings) {
|
|
119
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
120
|
+
fs.writeFileSync(filePath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
121
|
+
}
|
|
@@ -99,11 +99,25 @@ export function createAssistantState({
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function appendMessage(message) {
|
|
102
|
-
|
|
102
|
+
const entry = {
|
|
103
103
|
id: `msg-${messages.length + 1}`,
|
|
104
104
|
...message,
|
|
105
|
-
}
|
|
105
|
+
};
|
|
106
|
+
messages.push(entry);
|
|
107
|
+
notify();
|
|
108
|
+
return entry.id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function updateMessage(id, updater) {
|
|
112
|
+
const index = messages.findIndex((message) => message.id === id);
|
|
113
|
+
if (index < 0) return false;
|
|
114
|
+
const next = updater(messages[index]);
|
|
115
|
+
messages[index] = {
|
|
116
|
+
...messages[index],
|
|
117
|
+
...next,
|
|
118
|
+
};
|
|
106
119
|
notify();
|
|
120
|
+
return true;
|
|
107
121
|
}
|
|
108
122
|
|
|
109
123
|
function setBusy(nextBusy, status = null) {
|
|
@@ -354,10 +368,11 @@ export function createAssistantState({
|
|
|
354
368
|
|
|
355
369
|
try {
|
|
356
370
|
setBusy(true, `Thinking with ${settings.provider === "auto" ? "provider" : settings.provider}...`);
|
|
357
|
-
const
|
|
371
|
+
const providerTurn = createProviderTurnState();
|
|
372
|
+
await runAssistantConversationTurn({
|
|
358
373
|
productDir,
|
|
359
374
|
runState,
|
|
360
|
-
transcript: messages
|
|
375
|
+
transcript: buildConversationTranscript(messages),
|
|
361
376
|
userMessage: trimmed,
|
|
362
377
|
settings,
|
|
363
378
|
env,
|
|
@@ -379,11 +394,20 @@ export function createAssistantState({
|
|
|
379
394
|
});
|
|
380
395
|
notify();
|
|
381
396
|
},
|
|
397
|
+
onProviderEvent(event) {
|
|
398
|
+
handleProviderEvent(providerTurn, event, {
|
|
399
|
+
appendMessage,
|
|
400
|
+
updateMessage,
|
|
401
|
+
setStatus(status) {
|
|
402
|
+
activeStatus = status;
|
|
403
|
+
notify();
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
},
|
|
382
407
|
onToolEvent(event) {
|
|
383
408
|
handleAssistantToolEvent(state, event, appendMessage);
|
|
384
409
|
},
|
|
385
410
|
});
|
|
386
|
-
for (const message of emitted) appendMessage(message);
|
|
387
411
|
} catch (error) {
|
|
388
412
|
appendMessage({
|
|
389
413
|
role: "system",
|
|
@@ -470,7 +494,13 @@ async function executeSlashCommand({
|
|
|
470
494
|
}
|
|
471
495
|
if (slash.type === "model") {
|
|
472
496
|
state.setModel(slash.model, { custom: slash.custom });
|
|
473
|
-
|
|
497
|
+
const snapshot = state.getSnapshot();
|
|
498
|
+
appendMessage({
|
|
499
|
+
role: "assistant",
|
|
500
|
+
text: slash.model
|
|
501
|
+
? `Model set to ${snapshot.model}${snapshot.effort ? ` with ${snapshot.effort} effort` : ""}.`
|
|
502
|
+
: "Model reset to provider default.",
|
|
503
|
+
});
|
|
474
504
|
return;
|
|
475
505
|
}
|
|
476
506
|
if (slash.type === "model-list") {
|
|
@@ -602,6 +632,145 @@ function handleAssistantToolEvent(state, event, appendMessage) {
|
|
|
602
632
|
}
|
|
603
633
|
}
|
|
604
634
|
|
|
635
|
+
function createProviderTurnState() {
|
|
636
|
+
return {
|
|
637
|
+
assistantMessageId: null,
|
|
638
|
+
lastActivityText: null,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function handleProviderEvent(turn, event, { appendMessage, updateMessage, setStatus } = {}) {
|
|
643
|
+
if (!event) return;
|
|
644
|
+
if (event.type === "assistant-delta") {
|
|
645
|
+
appendAssistantDelta(turn, event, { appendMessage, updateMessage });
|
|
646
|
+
setStatus?.(`${event.provider || "provider"} responding`);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (event.type === "assistant-final") {
|
|
650
|
+
finalizeAssistantMessage(turn, event, { appendMessage, updateMessage });
|
|
651
|
+
setStatus?.(`${event.provider || "provider"} complete`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (event.type === "session-start") {
|
|
655
|
+
appendProviderActivity(turn, {
|
|
656
|
+
role: "provider-activity",
|
|
657
|
+
title: formatProviderName(event.provider),
|
|
658
|
+
text: "Session started",
|
|
659
|
+
data: event,
|
|
660
|
+
}, { appendMessage, setStatus });
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (event.type === "session-end") {
|
|
664
|
+
appendProviderActivity(turn, {
|
|
665
|
+
role: "provider-activity",
|
|
666
|
+
title: formatProviderName(event.provider),
|
|
667
|
+
text: Number.isInteger(event.exitCode) ? `Session ended with exit code ${event.exitCode}` : "Session ended",
|
|
668
|
+
data: event,
|
|
669
|
+
}, { appendMessage, setStatus });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (event.type === "status") {
|
|
673
|
+
appendProviderActivity(turn, {
|
|
674
|
+
role: "provider-activity",
|
|
675
|
+
title: formatProviderName(event.provider),
|
|
676
|
+
text: event.text || "Working",
|
|
677
|
+
data: event,
|
|
678
|
+
}, { appendMessage, setStatus });
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (event.type === "tool-start" || event.type === "tool-update" || event.type === "tool-end") {
|
|
682
|
+
appendProviderActivity(turn, {
|
|
683
|
+
role: "provider-tool",
|
|
684
|
+
title: formatProviderToolTitle(event),
|
|
685
|
+
text: formatProviderToolText(event),
|
|
686
|
+
data: event,
|
|
687
|
+
}, { appendMessage, setStatus });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (event.type === "error") {
|
|
691
|
+
appendMessage({
|
|
692
|
+
role: "provider-error",
|
|
693
|
+
title: formatProviderName(event.provider),
|
|
694
|
+
text: event.text || "Provider error",
|
|
695
|
+
data: event,
|
|
696
|
+
});
|
|
697
|
+
setStatus?.(`${event.provider || "provider"} error`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function appendAssistantDelta(turn, event, { appendMessage, updateMessage }) {
|
|
702
|
+
if (!turn.assistantMessageId) {
|
|
703
|
+
turn.assistantMessageId = appendMessage({
|
|
704
|
+
role: "assistant",
|
|
705
|
+
status: "streaming",
|
|
706
|
+
provider: event.provider || null,
|
|
707
|
+
text: "",
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
updateMessage(turn.assistantMessageId, (message) => ({
|
|
711
|
+
text: `${message.text || ""}${event.text || ""}`,
|
|
712
|
+
status: "streaming",
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function finalizeAssistantMessage(turn, event, { appendMessage, updateMessage }) {
|
|
717
|
+
const finalText = event.text || "";
|
|
718
|
+
if (!turn.assistantMessageId) {
|
|
719
|
+
turn.assistantMessageId = appendMessage({
|
|
720
|
+
role: "assistant",
|
|
721
|
+
provider: event.provider || null,
|
|
722
|
+
text: finalText,
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
updateMessage(turn.assistantMessageId, (message) => ({
|
|
727
|
+
text: finalText || message.text || "",
|
|
728
|
+
status: null,
|
|
729
|
+
}));
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function appendProviderActivity(turn, message, { appendMessage, setStatus }) {
|
|
733
|
+
const text = String(message.text || "").trim();
|
|
734
|
+
if (!text) return;
|
|
735
|
+
const signature = `${message.role}:${message.title || ""}:${text}`;
|
|
736
|
+
if (turn.lastActivityText === signature) return;
|
|
737
|
+
turn.lastActivityText = signature;
|
|
738
|
+
appendMessage(message);
|
|
739
|
+
setStatus?.(message.title ? `${message.title}: ${text}` : text);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function formatProviderName(provider) {
|
|
743
|
+
return provider ? String(provider) : "provider";
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function formatProviderToolTitle(event) {
|
|
747
|
+
const provider = formatProviderName(event.provider);
|
|
748
|
+
const name = event.name || "tool";
|
|
749
|
+
if (event.type === "tool-end") return `${provider} finished ${name}`;
|
|
750
|
+
if (event.type === "tool-update") return `${provider} updated ${name}`;
|
|
751
|
+
return `${provider} started ${name}`;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function formatProviderToolText(event) {
|
|
755
|
+
const lines = [];
|
|
756
|
+
if (event.detail) lines.push(String(event.detail));
|
|
757
|
+
if (event.text) lines.push(String(event.text));
|
|
758
|
+
if (event.input) lines.push(formatProviderData("input", event.input));
|
|
759
|
+
if (event.output) lines.push(formatProviderData("output", event.output));
|
|
760
|
+
if (event.status) lines.push(`status: ${event.status}`);
|
|
761
|
+
return lines.filter(Boolean).join("\n") || event.name || "Provider tool activity";
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function formatProviderData(label, value) {
|
|
765
|
+
if (value == null) return null;
|
|
766
|
+
if (typeof value === "string") return `${label}: ${value}`;
|
|
767
|
+
try {
|
|
768
|
+
return `${label}: ${JSON.stringify(value)}`;
|
|
769
|
+
} catch {
|
|
770
|
+
return `${label}: ${String(value)}`;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
605
774
|
function formatSettings(snapshot) {
|
|
606
775
|
const rows = [
|
|
607
776
|
["Provider", snapshot.provider || "auto"],
|
|
@@ -648,6 +817,12 @@ function serializeRunSession(session) {
|
|
|
648
817
|
};
|
|
649
818
|
}
|
|
650
819
|
|
|
820
|
+
function buildConversationTranscript(messages) {
|
|
821
|
+
return (messages || [])
|
|
822
|
+
.filter((entry) => !["provider-activity", "provider-tool", "provider-error"].includes(entry.role))
|
|
823
|
+
.map((entry) => ({ role: entry.role, text: entry.text }));
|
|
824
|
+
}
|
|
825
|
+
|
|
651
826
|
function formatObservedCommandTitle(command) {
|
|
652
827
|
const kind = command?.kind || "command";
|
|
653
828
|
return `testkit ${kind}`;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import stripAnsi from "strip-ansi";
|
|
2
|
+
import { buildAssistantViewModel } from "./view-model.mjs";
|
|
3
|
+
import { renderMarkdownToAnsi } from "./markdown-block.mjs";
|
|
4
|
+
|
|
5
|
+
export function renderAssistantSnapshotText(snapshot, { cwd = process.cwd(), ansi = false } = {}) {
|
|
6
|
+
const view = buildAssistantViewModel(snapshot || {}, { cwd });
|
|
7
|
+
const lines = [view.title, view.welcome.rows.find(([label]) => label === "Provider")?.[1] || ""]
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
for (const block of view.blocks || []) {
|
|
10
|
+
lines.push("");
|
|
11
|
+
lines.push(...renderBlockLines(block, { ansi }));
|
|
12
|
+
}
|
|
13
|
+
lines.push("");
|
|
14
|
+
lines.push(view.statusLine);
|
|
15
|
+
const text = `${lines.join("\n").trimEnd()}\n`;
|
|
16
|
+
return ansi ? text : stripAnsi(text);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function renderBlockLines(block, { ansi = false } = {}) {
|
|
20
|
+
const marker = block.marker || "";
|
|
21
|
+
const title = block.title ? ` ${block.title}` : "";
|
|
22
|
+
const text = String(block.text || "").trimEnd();
|
|
23
|
+
if (block.format === "markdown") {
|
|
24
|
+
const rendered = text ? renderMarkdownToAnsi(text) : "";
|
|
25
|
+
const normalized = ansi ? rendered : stripAnsi(rendered);
|
|
26
|
+
return prefixLines(`${marker}${title}`, normalized);
|
|
27
|
+
}
|
|
28
|
+
return prefixLines(`${marker}${title}`, text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function prefixLines(prefix, text) {
|
|
32
|
+
const lines = String(text || "").split(/\r?\n/);
|
|
33
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0] === "")) return [prefix];
|
|
34
|
+
return lines.map((line, index) => (index === 0 ? `${prefix}${prefix && line ? " " : ""}${line}` : ` ${line}`));
|
|
35
|
+
}
|
|
@@ -65,6 +65,17 @@ export function buildTranscriptBlocks(messages) {
|
|
|
65
65
|
exitCode: message.data?.exitCode ?? null,
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
if (role === "provider-activity" || role === "provider-tool" || role === "provider-error") {
|
|
69
|
+
return {
|
|
70
|
+
id: message.id,
|
|
71
|
+
kind: role,
|
|
72
|
+
format: "plain",
|
|
73
|
+
marker: role === "provider-error" ? "!" : "◌",
|
|
74
|
+
title: message.title || null,
|
|
75
|
+
text: message.text || "",
|
|
76
|
+
status: message.status || null,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
68
79
|
if (role === "user") {
|
|
69
80
|
return {
|
|
70
81
|
id: message.id,
|
|
@@ -32,9 +32,6 @@ export const runFlags = {
|
|
|
32
32
|
"file-timeout-seconds": Flags.string({
|
|
33
33
|
description: "Per-file wall-clock timeout in seconds",
|
|
34
34
|
}),
|
|
35
|
-
shard: Flags.string({
|
|
36
|
-
description: "Run only shard i of n at suite granularity",
|
|
37
|
-
}),
|
|
38
35
|
seed: Flags.string({
|
|
39
36
|
description: "Deterministic seed for scenario suites",
|
|
40
37
|
}),
|
package/lib/cli/entrypoint.mjs
CHANGED
|
@@ -23,7 +23,6 @@ export function normalizeCliArgs(argv) {
|
|
|
23
23
|
"--file",
|
|
24
24
|
"--workers",
|
|
25
25
|
"--file-timeout-seconds",
|
|
26
|
-
"--shard",
|
|
27
26
|
"--seed",
|
|
28
27
|
"--input",
|
|
29
28
|
"--output",
|
|
@@ -54,7 +53,6 @@ export function normalizeCliArgs(argv) {
|
|
|
54
53
|
"-f",
|
|
55
54
|
"--workers",
|
|
56
55
|
"--file-timeout-seconds",
|
|
57
|
-
"--shard",
|
|
58
56
|
"--seed",
|
|
59
57
|
"--write-status",
|
|
60
58
|
"--allow-partial-status",
|
|
@@ -2,7 +2,6 @@ import * as runner from "../../../runner/index.mjs";
|
|
|
2
2
|
import { loadManagedConfigs } from "../../../app/configs.mjs";
|
|
3
3
|
import {
|
|
4
4
|
parseFileTimeoutOption,
|
|
5
|
-
parseShardOption,
|
|
6
5
|
parseSuiteOption,
|
|
7
6
|
parseTypeOption,
|
|
8
7
|
parseWorkersOption,
|
|
@@ -25,7 +24,6 @@ export async function buildRunRequest(flags, positionalType = null, cwd = proces
|
|
|
25
24
|
flags["file-timeout-seconds"] == null
|
|
26
25
|
? null
|
|
27
26
|
: parseFileTimeoutOption(flags["file-timeout-seconds"]);
|
|
28
|
-
const shard = parseShardOption(flags.shard);
|
|
29
27
|
const typeValues = parseTypeOption(flags.type, positionalType);
|
|
30
28
|
const suiteSelectors = parseSuiteOption(flags.suite);
|
|
31
29
|
const rawFileNames = Array.isArray(flags.file) ? flags.file : [flags.file].filter(Boolean);
|
|
@@ -44,7 +42,6 @@ export async function buildRunRequest(flags, positionalType = null, cwd = proces
|
|
|
44
42
|
fileNames,
|
|
45
43
|
workers,
|
|
46
44
|
fileTimeoutSeconds,
|
|
47
|
-
shard,
|
|
48
45
|
scenarioSeed: flags.seed || null,
|
|
49
46
|
serviceFilter: flags.service || null,
|
|
50
47
|
writeStatus: flags["write-status"],
|
package/lib/runner/live-run.mjs
CHANGED
|
@@ -4,12 +4,14 @@ import { writeLiveRunArtifact } from "./artifacts.mjs";
|
|
|
4
4
|
|
|
5
5
|
export function createLiveSnapshotWriter({
|
|
6
6
|
productDir,
|
|
7
|
+
runId,
|
|
7
8
|
configs,
|
|
8
9
|
trackers,
|
|
9
10
|
startedAt,
|
|
10
11
|
execution,
|
|
11
12
|
workerState,
|
|
12
13
|
selection,
|
|
14
|
+
provenance = null,
|
|
13
15
|
metadata,
|
|
14
16
|
logRegistry,
|
|
15
17
|
setupRegistry,
|
|
@@ -21,6 +23,7 @@ export function createLiveSnapshotWriter({
|
|
|
21
23
|
productDir,
|
|
22
24
|
buildLiveRunArtifact({
|
|
23
25
|
productDir,
|
|
26
|
+
runId,
|
|
24
27
|
results: partialResults,
|
|
25
28
|
startedAt,
|
|
26
29
|
updatedAt: now,
|
|
@@ -31,9 +34,10 @@ export function createLiveSnapshotWriter({
|
|
|
31
34
|
typeValues: selection.typeValues,
|
|
32
35
|
suiteSelectors: selection.suiteSelectors,
|
|
33
36
|
fileNames: selection.fileNames,
|
|
34
|
-
shard: selection.shard,
|
|
35
37
|
serviceFilter: selection.serviceFilter,
|
|
36
38
|
scenarioSeed: selection.scenarioSeed,
|
|
39
|
+
planning: selection.planning || null,
|
|
40
|
+
provenance,
|
|
37
41
|
metadata,
|
|
38
42
|
summarizeDbBackend,
|
|
39
43
|
serviceLogs: logRegistry.listServiceLogs(),
|