Skip to content

Commit 7c03392

Browse files
committed
feat: add runtime config to traces
1 parent a696af5 commit 7c03392

9 files changed

Lines changed: 293 additions & 4 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
PDF_PASSTHROUGH_MAX_BYTES,
3232
} from './pdf-tools.js';
3333
import * as trace from '../trace/recorder.js';
34+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
3435
import { tracesToMarkdown } from './trace-export.js';
3536
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError, captchaTypesMatch, captchaWebsiteUrl } from './captcha-solver.js';
3637
import { isCapsolverEnabled, normalizeCapsolverApiKey } from './capsolver-config.js';
@@ -724,6 +725,34 @@ export class Agent extends LoopDetector {
724725
return this.conversationIds.get(tabId) || null;
725726
}
726727

728+
_runtimeTraceConfig(provider, { tabId = null, mode = null } = {}) {
729+
let extensionVersion = '';
730+
let promptTier = 'full';
731+
try { extensionVersion = chrome.runtime.getManifest().version || ''; } catch {}
732+
try { promptTier = provider?.promptTier || 'full'; } catch {}
733+
const effectiveMode = mode
734+
|| (tabId != null ? this._effectiveRunMode(tabId) : 'ask');
735+
return normalizeRuntimeTraceConfig({
736+
extension_version: extensionVersion,
737+
browser_target: 'chrome',
738+
mode: effectiveMode,
739+
prompt_tier: promptTier,
740+
screenshot_redaction: this.screenshotRedaction === true,
741+
strict_secret_mode: this.strictSecretMode === true,
742+
plan_before_act_mode: this._normalizePlanBeforeActMode(this.planBeforeActMode),
743+
auto_screenshot: this.autoScreenshot,
744+
use_site_adapters: this.useSiteAdapters === true,
745+
web_mcp_enabled: this.webMcpEnabled === true,
746+
api_mutations_allowed: tabId != null && this.apiAllowedTabs.has(tabId),
747+
user_memory_enabled: this.userMemoryEnabled === true,
748+
selection_grounded: tabId != null && this.selectionGroundingScopes.has(tabId),
749+
image_detail: this.imageDetail,
750+
max_agent_steps: this.maxSteps,
751+
max_image_dimension: this.maxImageDimension,
752+
max_screenshots_per_turn: this.maxScreenshotsPerTurn,
753+
});
754+
}
755+
727756
_cloudGenerationOptions(provider, options = {}, { tabId = null, conversationId = null, generationName = 'main' } = {}) {
728757
if (String(provider?.config?.providerName || '').toLowerCase() !== 'webbrain-cloud') return options;
729758
const effectiveConversationId = conversationId || (tabId != null ? this.conversationIds.get(tabId) : null);
@@ -732,6 +761,7 @@ export class Agent extends LoopDetector {
732761
...options,
733762
webbrainSessionId: String(effectiveConversationId),
734763
webbrainGenerationName: String(generationName || 'main'),
764+
webbrainRuntimeConfig: this._runtimeTraceConfig(provider, { tabId }),
735765
};
736766
}
737767

@@ -6749,6 +6779,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
67496779
providerId: provider?.name,
67506780
providerClass: provider?.constructor?.name,
67516781
webbrainVersion: chrome.runtime.getManifest().version || '',
6782+
runtimeConfig: this._runtimeTraceConfig(provider, { tabId, mode }),
67526783
userMessage: typeof userMessage === 'string' ? userMessage : JSON.stringify(userMessage).slice(0, 2000),
67536784
tabUrl,
67546785
tabTitle,
@@ -13563,6 +13594,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1356313594
mode: 'act',
1356413595
model: this.providerManager?.getActive?.()?.model || '',
1356513596
providerId: this.providerManager?.activeProviderId || '',
13597+
runtimeConfig: this._runtimeTraceConfig(this.providerManager?.getActive?.(), {
13598+
tabId,
13599+
mode: 'act',
13600+
}),
1356613601
});
1356713602
let traceStatus = 'workflow_stopped';
1356813603
let finalContent = '';

src/chrome/src/providers/openai.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
shouldUseOpenAIResponsesApi,
66
supportsOpenAIAskStreaming,
77
} from './provider-compatibility.js';
8+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
89

910
const OPENAI_RESPONSES_MIN_MAX_OUTPUT_TOKENS = 16;
1011
const KIMI_CURRENT_TOOL_REASONING_MODELS = new Set([
@@ -235,11 +236,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
235236
const sessionId = String(options.webbrainSessionId || '').trim();
236237
if (sessionId) body.session_id = sessionId.slice(0, 200);
237238
const generationName = String(options.webbrainGenerationName || '').trim().toLowerCase();
238-
if (generationName) {
239+
const runtimeConfig = normalizeRuntimeTraceConfig(options.webbrainRuntimeConfig);
240+
if (generationName || runtimeConfig) {
239241
const trace = body.trace && typeof body.trace === 'object' && !Array.isArray(body.trace)
240242
? body.trace
241243
: {};
242-
body.trace = { ...trace, generation_name: generationName.slice(0, 64) };
244+
body.trace = {
245+
...trace,
246+
...(generationName ? { generation_name: generationName.slice(0, 64) } : {}),
247+
...(runtimeConfig ? { runtime_config: runtimeConfig } : {}),
248+
};
243249
}
244250
}
245251

src/chrome/src/trace/recorder.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { normalizeRuntimeTraceConfig } from './runtime-config.js';
2+
13
/**
24
* Trace recorder — writes per-run traces (LLM requests/responses, tool calls,
35
* screenshots) into IndexedDB for later inspection and cross-model comparison.
@@ -130,6 +132,7 @@ export async function startRun(meta = {}) {
130132
providerId: meta.providerId || '',
131133
providerClass: meta.providerClass || '',
132134
webbrainVersion: meta.webbrainVersion || '',
135+
runtimeConfig: normalizeRuntimeTraceConfig(meta.runtimeConfig),
133136
userMessage: meta.userMessage || '',
134137
tabUrl: meta.tabUrl || '',
135138
tabTitle: meta.tabTitle || '',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const STRING_ENUMS = Object.freeze({
2+
browser_target: new Set(['chrome', 'firefox']),
3+
mode: new Set(['ask', 'act', 'dev']),
4+
prompt_tier: new Set(['compact', 'mid', 'full']),
5+
plan_before_act_mode: new Set(['off', 'try', 'strict']),
6+
auto_screenshot: new Set(['off', 'navigation', 'state_change', 'every_step']),
7+
image_detail: new Set(['auto', 'low', 'high']),
8+
});
9+
10+
const BOOLEAN_FIELDS = Object.freeze([
11+
'screenshot_redaction',
12+
'strict_secret_mode',
13+
'use_site_adapters',
14+
'web_mcp_enabled',
15+
'api_mutations_allowed',
16+
'user_memory_enabled',
17+
'selection_grounded',
18+
]);
19+
20+
const INTEGER_RANGES = Object.freeze({
21+
max_agent_steps: [0, 10_000],
22+
max_image_dimension: [256, 16_384],
23+
max_screenshots_per_turn: [0, 1_000],
24+
});
25+
26+
function safeVersion(value) {
27+
const version = String(value || '').trim();
28+
return /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/.test(version) ? version : '';
29+
}
30+
31+
/**
32+
* Runtime trace metadata crosses both a provider boundary and an export
33+
* boundary. Keep it to a small, versioned allowlist of booleans, bounded
34+
* integers, and enums so a caller can never smuggle credentials or profile
35+
* contents into a trace by passing an arbitrary settings object.
36+
*/
37+
export function normalizeRuntimeTraceConfig(value) {
38+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
39+
40+
const normalized = { schema_version: 1 };
41+
const extensionVersion = safeVersion(value.extension_version);
42+
if (extensionVersion) normalized.extension_version = extensionVersion;
43+
44+
for (const [field, allowed] of Object.entries(STRING_ENUMS)) {
45+
const candidate = String(value[field] || '').trim().toLowerCase();
46+
if (allowed.has(candidate)) normalized[field] = candidate;
47+
}
48+
for (const field of BOOLEAN_FIELDS) {
49+
if (typeof value[field] === 'boolean') normalized[field] = value[field];
50+
}
51+
for (const [field, [min, max]] of Object.entries(INTEGER_RANGES)) {
52+
const candidate = Number(value[field]);
53+
if (Number.isInteger(candidate) && candidate >= min && candidate <= max) {
54+
normalized[field] = candidate;
55+
}
56+
}
57+
58+
return normalized;
59+
}

src/firefox/src/agent/agent.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
PDF_PASSTHROUGH_MAX_BYTES,
3434
} from './pdf-tools.js';
3535
import * as trace from '../trace/recorder.js';
36+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
3637
import { tracesToMarkdown } from './trace-export.js';
3738
import { solveCaptcha, detectCaptcha, injectToken, captchaParamError, captchaTypesMatch, captchaWebsiteUrl } from './captcha-solver.js';
3839
import { isCapsolverEnabled, normalizeCapsolverApiKey } from './capsolver-config.js';
@@ -823,6 +824,34 @@ export class Agent extends LoopDetector {
823824
return this.conversationIds.get(tabId) || null;
824825
}
825826

827+
_runtimeTraceConfig(provider, { tabId = null, mode = null } = {}) {
828+
let extensionVersion = '';
829+
let promptTier = 'full';
830+
try { extensionVersion = browser.runtime.getManifest().version || ''; } catch {}
831+
try { promptTier = provider?.promptTier || 'full'; } catch {}
832+
const effectiveMode = mode
833+
|| (tabId != null ? this._effectiveRunMode(tabId) : 'ask');
834+
return normalizeRuntimeTraceConfig({
835+
extension_version: extensionVersion,
836+
browser_target: 'firefox',
837+
mode: effectiveMode,
838+
prompt_tier: promptTier,
839+
screenshot_redaction: this.screenshotRedaction === true,
840+
strict_secret_mode: this.strictSecretMode === true,
841+
plan_before_act_mode: this._normalizePlanBeforeActMode(this.planBeforeActMode),
842+
auto_screenshot: this.autoScreenshot,
843+
use_site_adapters: this.useSiteAdapters === true,
844+
web_mcp_enabled: this.webMcpEnabled === true,
845+
api_mutations_allowed: tabId != null && this.apiAllowedTabs.has(tabId),
846+
user_memory_enabled: this.userMemoryEnabled === true,
847+
selection_grounded: tabId != null && this.selectionGroundingScopes.has(tabId),
848+
image_detail: this.imageDetail,
849+
max_agent_steps: this.maxSteps,
850+
max_image_dimension: this.maxImageDimension,
851+
max_screenshots_per_turn: this.maxScreenshotsPerTurn,
852+
});
853+
}
854+
826855
_cloudGenerationOptions(provider, options = {}, { tabId = null, conversationId = null, generationName = 'main' } = {}) {
827856
if (String(provider?.config?.providerName || '').toLowerCase() !== 'webbrain-cloud') return options;
828857
const effectiveConversationId = conversationId || (tabId != null ? this.conversationIds.get(tabId) : null);
@@ -831,6 +860,7 @@ export class Agent extends LoopDetector {
831860
...options,
832861
webbrainSessionId: String(effectiveConversationId),
833862
webbrainGenerationName: String(generationName || 'main'),
863+
webbrainRuntimeConfig: this._runtimeTraceConfig(provider, { tabId }),
834864
};
835865
}
836866

@@ -5693,6 +5723,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
56935723
providerId: provider?.name,
56945724
providerClass: provider?.constructor?.name,
56955725
webbrainVersion: browser.runtime.getManifest().version || '',
5726+
runtimeConfig: this._runtimeTraceConfig(provider, { tabId, mode }),
56965727
userMessage: typeof userMessage === 'string' ? userMessage : JSON.stringify(userMessage).slice(0, 2000),
56975728
tabUrl,
56985729
tabTitle,
@@ -11833,6 +11864,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
1183311864
mode: 'act',
1183411865
model: this.providerManager?.getActive?.()?.model || '',
1183511866
providerId: this.providerManager?.activeProviderId || '',
11867+
runtimeConfig: this._runtimeTraceConfig(this.providerManager?.getActive?.(), {
11868+
tabId,
11869+
mode: 'act',
11870+
}),
1183611871
});
1183711872
let traceStatus = 'workflow_stopped';
1183811873
let finalContent = '';

src/firefox/src/providers/openai.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
shouldUseOpenAIResponsesApi,
66
supportsOpenAIAskStreaming,
77
} from './provider-compatibility.js';
8+
import { normalizeRuntimeTraceConfig } from '../trace/runtime-config.js';
89

910
const OPENAI_RESPONSES_MIN_MAX_OUTPUT_TOKENS = 16;
1011
const KIMI_CURRENT_TOOL_REASONING_MODELS = new Set([
@@ -235,11 +236,16 @@ export class OpenAICompatibleProvider extends BaseLLMProvider {
235236
const sessionId = String(options.webbrainSessionId || '').trim();
236237
if (sessionId) body.session_id = sessionId.slice(0, 200);
237238
const generationName = String(options.webbrainGenerationName || '').trim().toLowerCase();
238-
if (generationName) {
239+
const runtimeConfig = normalizeRuntimeTraceConfig(options.webbrainRuntimeConfig);
240+
if (generationName || runtimeConfig) {
239241
const trace = body.trace && typeof body.trace === 'object' && !Array.isArray(body.trace)
240242
? body.trace
241243
: {};
242-
body.trace = { ...trace, generation_name: generationName.slice(0, 64) };
244+
body.trace = {
245+
...trace,
246+
...(generationName ? { generation_name: generationName.slice(0, 64) } : {}),
247+
...(runtimeConfig ? { runtime_config: runtimeConfig } : {}),
248+
};
243249
}
244250
}
245251

src/firefox/src/trace/recorder.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { normalizeRuntimeTraceConfig } from './runtime-config.js';
2+
13
/**
24
* Trace recorder — writes per-run traces (LLM requests/responses, tool calls,
35
* screenshots) into IndexedDB for later inspection and cross-model comparison.
@@ -114,6 +116,7 @@ export async function startRun(meta) {
114116
providerId: meta.providerId || '',
115117
providerClass: meta.providerClass || '',
116118
webbrainVersion: meta.webbrainVersion || '',
119+
runtimeConfig: normalizeRuntimeTraceConfig(meta.runtimeConfig),
117120
userMessage: meta.userMessage || '',
118121
tabUrl: meta.tabUrl || '',
119122
tabTitle: meta.tabTitle || '',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const STRING_ENUMS = Object.freeze({
2+
browser_target: new Set(['chrome', 'firefox']),
3+
mode: new Set(['ask', 'act', 'dev']),
4+
prompt_tier: new Set(['compact', 'mid', 'full']),
5+
plan_before_act_mode: new Set(['off', 'try', 'strict']),
6+
auto_screenshot: new Set(['off', 'navigation', 'state_change', 'every_step']),
7+
image_detail: new Set(['auto', 'low', 'high']),
8+
});
9+
10+
const BOOLEAN_FIELDS = Object.freeze([
11+
'screenshot_redaction',
12+
'strict_secret_mode',
13+
'use_site_adapters',
14+
'web_mcp_enabled',
15+
'api_mutations_allowed',
16+
'user_memory_enabled',
17+
'selection_grounded',
18+
]);
19+
20+
const INTEGER_RANGES = Object.freeze({
21+
max_agent_steps: [0, 10_000],
22+
max_image_dimension: [256, 16_384],
23+
max_screenshots_per_turn: [0, 1_000],
24+
});
25+
26+
function safeVersion(value) {
27+
const version = String(value || '').trim();
28+
return /^[0-9A-Za-z][0-9A-Za-z.+_-]{0,63}$/.test(version) ? version : '';
29+
}
30+
31+
/**
32+
* Runtime trace metadata crosses both a provider boundary and an export
33+
* boundary. Keep it to a small, versioned allowlist of booleans, bounded
34+
* integers, and enums so a caller can never smuggle credentials or profile
35+
* contents into a trace by passing an arbitrary settings object.
36+
*/
37+
export function normalizeRuntimeTraceConfig(value) {
38+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
39+
40+
const normalized = { schema_version: 1 };
41+
const extensionVersion = safeVersion(value.extension_version);
42+
if (extensionVersion) normalized.extension_version = extensionVersion;
43+
44+
for (const [field, allowed] of Object.entries(STRING_ENUMS)) {
45+
const candidate = String(value[field] || '').trim().toLowerCase();
46+
if (allowed.has(candidate)) normalized[field] = candidate;
47+
}
48+
for (const field of BOOLEAN_FIELDS) {
49+
if (typeof value[field] === 'boolean') normalized[field] = value[field];
50+
}
51+
for (const [field, [min, max]] of Object.entries(INTEGER_RANGES)) {
52+
const candidate = Number(value[field]);
53+
if (Number.isInteger(candidate) && candidate >= min && candidate <= max) {
54+
normalized[field] = candidate;
55+
}
56+
}
57+
58+
return normalized;
59+
}

0 commit comments

Comments
 (0)