cbrowser 20.1.1 → 20.1.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/analysis/persona-comparison.d.ts.map +1 -1
- package/dist/analysis/persona-comparison.js +3 -2
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +39 -4
- package/dist/cli-args.js.map +1 -1
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +73 -1
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +231 -84
- package/dist/cognitive/index.js.map +1 -1
- package/dist/cognitive/journey-dialog.d.ts +7 -0
- package/dist/cognitive/journey-dialog.d.ts.map +1 -1
- package/dist/cognitive/journey-dialog.js +14 -1
- package/dist/cognitive/journey-dialog.js.map +1 -1
- package/dist/cognitive/journey-trace.d.ts +2 -0
- package/dist/cognitive/journey-trace.d.ts.map +1 -1
- package/dist/cognitive/journey-trace.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,8 +3,27 @@
|
|
|
3
3
|
* Copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com
|
|
4
4
|
* Learn more at https://cbrowser.ai - MIT License
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Cognitive Journey API Module
|
|
8
|
+
*
|
|
9
|
+
* Provides cognitive user simulation with two modes:
|
|
10
|
+
*
|
|
11
|
+
* ## Mode 1: Claude Code Session (Recommended when inside Claude Code)
|
|
12
|
+
* When running via MCP inside Claude Code, use the step-by-step tools:
|
|
13
|
+
* - `cognitive_journey_init` - Initialize journey with persona
|
|
14
|
+
* - `cognitive_journey_update_state` - Track cognitive state after each action
|
|
15
|
+
* No API key needed - Claude Code itself is the reasoning engine.
|
|
16
|
+
*
|
|
17
|
+
* ## Mode 2: Standalone (API Key Required)
|
|
18
|
+
* When running standalone (CLI, CI/CD, scripts), uses Anthropic API directly:
|
|
19
|
+
* - `npx cbrowser cognitive-journey --persona X --start URL --goal "..."
|
|
20
|
+
* Requires: `npx cbrowser config set-api-key YOUR_KEY`
|
|
21
|
+
*
|
|
22
|
+
* @module cognitive
|
|
23
|
+
*/
|
|
24
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
6
25
|
import { type AutoCaptureSetting } from "../recording/auto-capture.js";
|
|
7
|
-
import type { CognitiveState, CognitiveJourneyResult, CognitiveTraits } from "../types.js";
|
|
26
|
+
import type { CognitiveState, CognitiveJourneyResult, Persona, CognitiveTraits } from "../types.js";
|
|
8
27
|
import type { DecisionProvider } from "./journey-trace.js";
|
|
9
28
|
/**
|
|
10
29
|
* Get the Anthropic API key from environment or config file.
|
|
@@ -39,6 +58,8 @@ export declare function isCognitiveAvailable(): boolean;
|
|
|
39
58
|
export interface CognitiveJourneyOptions {
|
|
40
59
|
/** Persona name or description */
|
|
41
60
|
persona: string;
|
|
61
|
+
/** A label for this run (the CLI's first word). Names its trace and capture files and is returned on the result. */
|
|
62
|
+
name?: string;
|
|
42
63
|
/** Goal statement for the journey */
|
|
43
64
|
goal: string;
|
|
44
65
|
/** Starting URL */
|
|
@@ -177,6 +198,8 @@ export interface CognitiveStep {
|
|
|
177
198
|
* not anything the model said.
|
|
178
199
|
*/
|
|
179
200
|
replyReadable: boolean;
|
|
201
|
+
/** Why the reply could not be read, when replyReadable is false. */
|
|
202
|
+
replyProblem?: string;
|
|
180
203
|
state: CognitiveState;
|
|
181
204
|
/** Step-aligned screenshot. Captured only on initial page load + scroll>100. */
|
|
182
205
|
screenshotPath?: string;
|
|
@@ -193,6 +216,55 @@ export interface CognitiveStep {
|
|
|
193
216
|
url?: string;
|
|
194
217
|
}
|
|
195
218
|
export declare function runCognitiveJourney(options: CognitiveJourneyOptions): Promise<CognitiveJourneyResult>;
|
|
219
|
+
/**
|
|
220
|
+
* The journey's average confusion and peak frustration, over the state each
|
|
221
|
+
* step logged plus the final state for the peak. Both read only the final
|
|
222
|
+
* state before 20.1.3: the "average" added the final confusion once per
|
|
223
|
+
* friction point and divided by their count, and the "max" was the last
|
|
224
|
+
* frustration (2026-10-23). With no step logged, both are the final state's.
|
|
225
|
+
*/
|
|
226
|
+
export declare function journeyEmotionSummary(steps: ReadonlyArray<{
|
|
227
|
+
confusion: number;
|
|
228
|
+
frustration: number;
|
|
229
|
+
}>, final: {
|
|
230
|
+
confusionLevel: number;
|
|
231
|
+
frustrationLevel: number;
|
|
232
|
+
}): {
|
|
233
|
+
avgConfusionLevel: number;
|
|
234
|
+
maxFrustrationLevel: number;
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* The persona a journey runs as.
|
|
238
|
+
*
|
|
239
|
+
* A known name resolves to its own record: built-in, custom and account
|
|
240
|
+
* personas through getPersona, and the accessibility and emotional ones
|
|
241
|
+
* (cognitive-adhd, anxious-user, ...) through getAnyPersona. Only getPersona
|
|
242
|
+
* was asked before 20.1.3, so a journey for any of those 15 personas, which is
|
|
243
|
+
* what empathy_audit starts, ran on traits generated from the name: 14 to 24 of
|
|
244
|
+
* their 25 traits differed from the persona's own (2026-10-23). A name nothing
|
|
245
|
+
* knows is still read as a description of the person, as `--persona` says. An
|
|
246
|
+
* agent persona is refused, because a journey simulates a person.
|
|
247
|
+
*/
|
|
248
|
+
export declare function resolveJourneyPersona(name: string, customTraits?: Partial<CognitiveTraits>): Persona;
|
|
249
|
+
/**
|
|
250
|
+
* The live per-step decision: one messages.create call, its text blocks read
|
|
251
|
+
* together, and one more ask for the JSON alone when the reply does not hold
|
|
252
|
+
* a JSON object. describeLastReply says why the last reply was unreadable.
|
|
253
|
+
* Exported for tests, which pass a fake client.
|
|
254
|
+
*/
|
|
255
|
+
export declare function createLiveDecisionProvider(anthropic: Pick<Anthropic, "messages"> | null, model: string): DecisionProvider;
|
|
256
|
+
/** A journey or persona name as a file-name part: letters, digits, - and _ only. */
|
|
257
|
+
export declare function journeySlug(name: string): string;
|
|
258
|
+
/**
|
|
259
|
+
* The JSON object in a model reply, or why there is none. Tried in order: the
|
|
260
|
+
* reply with any code fences removed, each balanced {...} in it (string-aware,
|
|
261
|
+
* so a reply with prose after its JSON still reads), then the old greedy
|
|
262
|
+
* first-"{"-to-last-"}" match; the first that parses to an object wins.
|
|
263
|
+
*/
|
|
264
|
+
export declare function readReplyJson(reply: string): {
|
|
265
|
+
value?: Record<string, unknown>;
|
|
266
|
+
problem?: string;
|
|
267
|
+
};
|
|
196
268
|
export { createInitialEmotionalState, createEmotionalConfig, applyEmotionalTrigger, decayEmotions, calculateAbandonmentModifier, calculateExplorationTendency, calculateDecisionSpeedModifier, describeEmotionalState, shouldConsiderAbandonment, } from "./emotions.js";
|
|
197
269
|
export { GoalDecomposer } from "./goal-decomposer.js";
|
|
198
270
|
export type { ParsedGoal, GoalPlan, SubGoal, Strategy, PlannedAction, GoalResult, ExecutionOptions, } from "./goal-types.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cognitive/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cognitive/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cognitive/index.js
CHANGED
|
@@ -26,7 +26,8 @@ import { existsSync, readFileSync } from "fs";
|
|
|
26
26
|
import { join } from "path";
|
|
27
27
|
import { CBrowser } from "../browser.js";
|
|
28
28
|
import { finishAutoCapture, resolveCaptureOptions, startAutoCapture } from "../recording/auto-capture.js";
|
|
29
|
-
import { getPersona, getCognitiveProfile, createCognitivePersona, } from "../personas.js";
|
|
29
|
+
import { getPersona, getAnyPersona, getCognitiveProfile, createCognitivePersona, } from "../personas.js";
|
|
30
|
+
import { isAgentPersonaObject } from "../agent-personas.js";
|
|
30
31
|
import { createInitialEmotionalState, createEmotionalConfig, applyEmotionalTrigger, decayEmotionsWithTraits, deriveSimplifiedState, calculateAbandonmentModifier, describeEmotionalState, shouldConsiderAbandonment, } from "./emotions.js";
|
|
31
32
|
import { calculateFatigueIncrement, calculateFittsMovementTime, shouldSwitchToSystem2, canReturnToSystem1, calculateTypingTime, calculateScanWidthMultiplier, calculateGazeMouseLag, calculatePeripheralVision, createHabituationState, updateHabituationState, classifyUIPattern, } from "../types.js";
|
|
32
33
|
import { loadConfigFile, getDataDir } from "../config.js";
|
|
@@ -693,77 +694,17 @@ export async function runCognitiveJourney(options) {
|
|
|
693
694
|
// Decision-provider seam (v18.68.0): the per-step LLM call sits behind a
|
|
694
695
|
// DecisionProvider so a recorded trace (or an injected fake) can stand in
|
|
695
696
|
// for the live API. The default path is the same anthropic.messages.create
|
|
696
|
-
// call as before
|
|
697
|
+
// call as before, reading every text block (20.1.2). The post-journey
|
|
697
698
|
// goal evaluation goes through the same provider (FINAL_EVAL_STEP), so a
|
|
698
699
|
// replay reproduces the recorded verdict with no model call.
|
|
699
|
-
const liveLLMProvider =
|
|
700
|
-
decide: async (ctx) => {
|
|
701
|
-
if (!anthropic)
|
|
702
|
-
throw new Error("Anthropic API key not configured.");
|
|
703
|
-
const response = await anthropic.messages.create({
|
|
704
|
-
model,
|
|
705
|
-
max_tokens: 2000,
|
|
706
|
-
...(ctx.systemPrompt ? { system: ctx.systemPrompt } : {}),
|
|
707
|
-
messages: ctx.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
708
|
-
});
|
|
709
|
-
return response.content[0].type === "text" ? response.content[0].text : "";
|
|
710
|
-
},
|
|
711
|
-
};
|
|
700
|
+
const liveLLMProvider = createLiveDecisionProvider(anthropic, model);
|
|
712
701
|
const decisionProvider = options.decisionProvider ??
|
|
713
702
|
(options.replayTrace ? new TraceReplayProvider(options.replayTrace) : liveLLMProvider);
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
personaObj = createCognitivePersona(options.persona, options.persona, options.customTraits || {});
|
|
720
|
-
}
|
|
721
|
-
else if (options.customTraits) {
|
|
722
|
-
// v16.7.1: full default trait set (was only 7, causing trait dropout).
|
|
723
|
-
// Said 25 until 2026-08-01; the model has 26.
|
|
724
|
-
const defaultTraits = {
|
|
725
|
-
// Required traits (7)
|
|
726
|
-
patience: 0.5,
|
|
727
|
-
riskTolerance: 0.5,
|
|
728
|
-
comprehension: 0.5,
|
|
729
|
-
persistence: 0.5,
|
|
730
|
-
curiosity: 0.5,
|
|
731
|
-
workingMemory: 0.5,
|
|
732
|
-
readingTendency: 0.5,
|
|
733
|
-
// Optional traits - Tier 1: Core
|
|
734
|
-
resilience: 0.5,
|
|
735
|
-
selfEfficacy: 0.5,
|
|
736
|
-
satisficing: 0.5,
|
|
737
|
-
trustCalibration: 0.5,
|
|
738
|
-
interruptRecovery: 0.5,
|
|
739
|
-
// Optional traits - Tier 2+
|
|
740
|
-
informationForaging: 0.5,
|
|
741
|
-
changeBlindness: 0.3,
|
|
742
|
-
anchoringBias: 0.5,
|
|
743
|
-
timeHorizon: 0.5,
|
|
744
|
-
attributionStyle: 0.5,
|
|
745
|
-
metacognitivePlanning: 0.5,
|
|
746
|
-
proceduralFluency: 0.5,
|
|
747
|
-
transferLearning: 0.5,
|
|
748
|
-
authoritySensitivity: 0.5,
|
|
749
|
-
emotionalContagion: 0.5,
|
|
750
|
-
fearOfMissingOut: 0.5,
|
|
751
|
-
socialProofSensitivity: 0.5,
|
|
752
|
-
mentalModelFlexibility: 0.5,
|
|
753
|
-
siteFamiliarity: 0.5,
|
|
754
|
-
};
|
|
755
|
-
personaObj = {
|
|
756
|
-
...existingPersona,
|
|
757
|
-
cognitiveTraits: {
|
|
758
|
-
...defaultTraits,
|
|
759
|
-
...(existingPersona.cognitiveTraits || {}),
|
|
760
|
-
...options.customTraits,
|
|
761
|
-
},
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
personaObj = existingPersona;
|
|
766
|
-
}
|
|
703
|
+
const nameSlug = options.name ? journeySlug(options.name) : undefined;
|
|
704
|
+
const traceWriter = options.trace
|
|
705
|
+
? new JourneyTraceWriter(nameSlug ? { journeyId: `journey-${nameSlug}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` } : undefined)
|
|
706
|
+
: null;
|
|
707
|
+
const personaObj = resolveJourneyPersona(options.persona, options.customTraits);
|
|
767
708
|
const profile = getCognitiveProfile(personaObj);
|
|
768
709
|
const traits = profile.traits;
|
|
769
710
|
// Build the InputTraits subset used by human-input.ts. CognitiveTraits has
|
|
@@ -990,7 +931,7 @@ export async function runCognitiveJourney(options) {
|
|
|
990
931
|
// Auto-capture (v18.70.0): recording starts once the journey's start URL is
|
|
991
932
|
// loaded, so frame 0 is the page the persona actually opens on, and stops
|
|
992
933
|
// just before the browser closes. Capture never fails the journey.
|
|
993
|
-
const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${options.persona
|
|
934
|
+
const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${nameSlug ?? journeySlug(options.persona)}-${Date.now()}`);
|
|
994
935
|
// Apply geolocation at runtime as well (ensures permission is granted after context exists)
|
|
995
936
|
if (effectiveLocation.geolocation) {
|
|
996
937
|
await browser.setGeolocationRuntime(effectiveLocation.geolocation);
|
|
@@ -1206,7 +1147,13 @@ Right now it's ${temporalState.description}.`;
|
|
|
1206
1147
|
const removedClickables = [...prevClickableTexts].filter(t => t && !currentTexts.has(t)).slice(0, 8);
|
|
1207
1148
|
lastActionOutcome.newClickables = newClickables;
|
|
1208
1149
|
lastActionOutcome.removedClickables = removedClickables;
|
|
1209
|
-
|
|
1150
|
+
// After a navigation the scroll position belongs to the new page, so the
|
|
1151
|
+
// difference is not a scroll: every link followed from partway down a
|
|
1152
|
+
// page was reported as "Scrolled up Npx" (2026-10-23). An in-page anchor
|
|
1153
|
+
// keeps its scroll.
|
|
1154
|
+
const pageOf = (u) => (u ?? "").split("#")[0];
|
|
1155
|
+
const navigated = lastActionOutcome.urlChanged && pageOf(lastActionOutcome.fromUrl) !== pageOf(lastActionOutcome.toUrl);
|
|
1156
|
+
lastActionOutcome.scrollDelta = navigated ? undefined : viewportCtx.scroll.y - prevScrollY;
|
|
1210
1157
|
}
|
|
1211
1158
|
prevClickableTexts = new Set(availableElements.map(e => e.text));
|
|
1212
1159
|
prevScrollY = viewportCtx.scroll.y;
|
|
@@ -1326,6 +1273,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1326
1273
|
// action); say so rather than let it pass as an "evaluate" step.
|
|
1327
1274
|
const parsed = parseCognitiveResponse(assistantMessage);
|
|
1328
1275
|
const replyReadable = Object.keys(parsed).length > 0;
|
|
1276
|
+
const replyProblem = replyReadable
|
|
1277
|
+
? undefined
|
|
1278
|
+
: decisionProvider.describeLastReply?.() ?? readReplyJson(assistantMessage).problem;
|
|
1329
1279
|
// Capture URL BEFORE we apply any action. We use the previous URL +
|
|
1330
1280
|
// the post-action URL to gate goal progress on real evidence — Claude's
|
|
1331
1281
|
// self-reported progress is taken as a *signal*, but only allowed to
|
|
@@ -1549,6 +1499,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1549
1499
|
pageTitle: pageTitle,
|
|
1550
1500
|
phase: parsed.phase || "evaluate",
|
|
1551
1501
|
replyReadable,
|
|
1502
|
+
...(replyProblem ? { replyProblem } : {}),
|
|
1552
1503
|
action: parsed.action || null,
|
|
1553
1504
|
actionTarget: parsed.actionTarget || null,
|
|
1554
1505
|
focusedElement: parsed.focusedElement || null,
|
|
@@ -1586,6 +1537,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1586
1537
|
action: parsed.action,
|
|
1587
1538
|
actionTarget: parsed.actionTarget || undefined,
|
|
1588
1539
|
replyReadable,
|
|
1540
|
+
...(replyProblem ? { replyProblem } : {}),
|
|
1589
1541
|
state: { ...state },
|
|
1590
1542
|
screenshotPath,
|
|
1591
1543
|
attentionHeatmapBase64,
|
|
@@ -1781,6 +1733,11 @@ Right now it's ${temporalState.description}.`;
|
|
|
1781
1733
|
target: parsed.actionTarget,
|
|
1782
1734
|
success: result.success,
|
|
1783
1735
|
});
|
|
1736
|
+
// A failed action in fast mode counts toward shouldSwitchToSystem2's
|
|
1737
|
+
// three-error rule. Nothing counted them, so the rule never fired
|
|
1738
|
+
// (2026-10-23).
|
|
1739
|
+
if (!result.success && state.cognitiveMode?.system === 1)
|
|
1740
|
+
state.cognitiveMode.system1Errors++;
|
|
1784
1741
|
// Track page visits if URL changed
|
|
1785
1742
|
const urlChanged = !!result.newUrl && result.newUrl !== _urlBeforeAction;
|
|
1786
1743
|
// Record the outcome for the next step's prompt. The diff fields
|
|
@@ -1829,6 +1786,8 @@ Right now it's ${temporalState.description}.`;
|
|
|
1829
1786
|
error: error instanceof Error ? error.message : String(error),
|
|
1830
1787
|
context: `Step ${step}: ${parsed.action}`,
|
|
1831
1788
|
});
|
|
1789
|
+
if (state.cognitiveMode?.system === 1)
|
|
1790
|
+
state.cognitiveMode.system1Errors++;
|
|
1832
1791
|
lastActionOutcome = {
|
|
1833
1792
|
action: parsed.action,
|
|
1834
1793
|
target: parsed.actionTarget,
|
|
@@ -1926,7 +1885,7 @@ Answer with JSON:
|
|
|
1926
1885
|
});
|
|
1927
1886
|
}
|
|
1928
1887
|
try {
|
|
1929
|
-
const evalJson =
|
|
1888
|
+
const evalJson = (readReplyJson(evalText).value ?? {});
|
|
1930
1889
|
if (evalJson.achieved) {
|
|
1931
1890
|
goalAchieved = true;
|
|
1932
1891
|
goalEvidence = `[FINAL EVAL] ${evalJson.evidence || finalPageText.substring(0, 200)}`;
|
|
@@ -1955,11 +1914,10 @@ Answer with JSON:
|
|
|
1955
1914
|
// Close browser
|
|
1956
1915
|
await browser.close();
|
|
1957
1916
|
// Calculate summary stats
|
|
1958
|
-
const
|
|
1959
|
-
? frictionPoints.reduce((sum, _fp) => sum + (state.confusionLevel), 0) / frictionPoints.length
|
|
1960
|
-
: state.confusionLevel;
|
|
1917
|
+
const emotion = journeyEmotionSummary(journeyLog, state);
|
|
1961
1918
|
// Build result
|
|
1962
1919
|
return {
|
|
1920
|
+
...(options.name ? { name: options.name } : {}),
|
|
1963
1921
|
persona: personaObj.name,
|
|
1964
1922
|
goal: options.goal,
|
|
1965
1923
|
goalAchieved,
|
|
@@ -1975,8 +1933,8 @@ Answer with JSON:
|
|
|
1975
1933
|
fullMonologue,
|
|
1976
1934
|
finalState: state,
|
|
1977
1935
|
summary: {
|
|
1978
|
-
avgConfusionLevel:
|
|
1979
|
-
maxFrustrationLevel:
|
|
1936
|
+
avgConfusionLevel: emotion.avgConfusionLevel,
|
|
1937
|
+
maxFrustrationLevel: emotion.maxFrustrationLevel,
|
|
1980
1938
|
backtrackCount: state.memory.backtrackCount,
|
|
1981
1939
|
timeInConfusion: frictionPoints.length * 2, // Estimate
|
|
1982
1940
|
// Decision fatigue metrics (v9.9.0)
|
|
@@ -2230,18 +2188,207 @@ STRATEGY:
|
|
|
2230
2188
|
|
|
2231
2189
|
Respond in JSON format.`;
|
|
2232
2190
|
}
|
|
2191
|
+
/**
|
|
2192
|
+
* The journey's average confusion and peak frustration, over the state each
|
|
2193
|
+
* step logged plus the final state for the peak. Both read only the final
|
|
2194
|
+
* state before 20.1.3: the "average" added the final confusion once per
|
|
2195
|
+
* friction point and divided by their count, and the "max" was the last
|
|
2196
|
+
* frustration (2026-10-23). With no step logged, both are the final state's.
|
|
2197
|
+
*/
|
|
2198
|
+
export function journeyEmotionSummary(steps, final) {
|
|
2199
|
+
if (steps.length === 0)
|
|
2200
|
+
return { avgConfusionLevel: final.confusionLevel, maxFrustrationLevel: final.frustrationLevel };
|
|
2201
|
+
let confusion = 0;
|
|
2202
|
+
let peak = final.frustrationLevel;
|
|
2203
|
+
for (const s of steps) {
|
|
2204
|
+
confusion += s.confusion;
|
|
2205
|
+
if (s.frustration > peak)
|
|
2206
|
+
peak = s.frustration;
|
|
2207
|
+
}
|
|
2208
|
+
return { avgConfusionLevel: confusion / steps.length, maxFrustrationLevel: peak };
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* The persona a journey runs as.
|
|
2212
|
+
*
|
|
2213
|
+
* A known name resolves to its own record: built-in, custom and account
|
|
2214
|
+
* personas through getPersona, and the accessibility and emotional ones
|
|
2215
|
+
* (cognitive-adhd, anxious-user, ...) through getAnyPersona. Only getPersona
|
|
2216
|
+
* was asked before 20.1.3, so a journey for any of those 15 personas, which is
|
|
2217
|
+
* what empathy_audit starts, ran on traits generated from the name: 14 to 24 of
|
|
2218
|
+
* their 25 traits differed from the persona's own (2026-10-23). A name nothing
|
|
2219
|
+
* knows is still read as a description of the person, as `--persona` says. An
|
|
2220
|
+
* agent persona is refused, because a journey simulates a person.
|
|
2221
|
+
*/
|
|
2222
|
+
export function resolveJourneyPersona(name, customTraits) {
|
|
2223
|
+
const known = getPersona(name) ?? getAnyPersona(name);
|
|
2224
|
+
if (known && isAgentPersonaObject(known)) {
|
|
2225
|
+
throw new Error(`"${name}" is an AI agent persona, and a cognitive journey simulates a person. Pick a human persona (cbrowser persona list).`);
|
|
2226
|
+
}
|
|
2227
|
+
const existingPersona = known;
|
|
2228
|
+
let personaObj;
|
|
2229
|
+
if (!existingPersona) {
|
|
2230
|
+
personaObj = createCognitivePersona(name, name, customTraits || {});
|
|
2231
|
+
}
|
|
2232
|
+
else if (customTraits) {
|
|
2233
|
+
// v16.7.1: full default trait set (was only 7, causing trait dropout).
|
|
2234
|
+
// Said 25 until 2026-08-01; the model has 26.
|
|
2235
|
+
const defaultTraits = {
|
|
2236
|
+
// Required traits (7)
|
|
2237
|
+
patience: 0.5,
|
|
2238
|
+
riskTolerance: 0.5,
|
|
2239
|
+
comprehension: 0.5,
|
|
2240
|
+
persistence: 0.5,
|
|
2241
|
+
curiosity: 0.5,
|
|
2242
|
+
workingMemory: 0.5,
|
|
2243
|
+
readingTendency: 0.5,
|
|
2244
|
+
// Optional traits - Tier 1: Core
|
|
2245
|
+
resilience: 0.5,
|
|
2246
|
+
selfEfficacy: 0.5,
|
|
2247
|
+
satisficing: 0.5,
|
|
2248
|
+
trustCalibration: 0.5,
|
|
2249
|
+
interruptRecovery: 0.5,
|
|
2250
|
+
// Optional traits - Tier 2+
|
|
2251
|
+
informationForaging: 0.5,
|
|
2252
|
+
changeBlindness: 0.3,
|
|
2253
|
+
anchoringBias: 0.5,
|
|
2254
|
+
timeHorizon: 0.5,
|
|
2255
|
+
attributionStyle: 0.5,
|
|
2256
|
+
metacognitivePlanning: 0.5,
|
|
2257
|
+
proceduralFluency: 0.5,
|
|
2258
|
+
transferLearning: 0.5,
|
|
2259
|
+
authoritySensitivity: 0.5,
|
|
2260
|
+
emotionalContagion: 0.5,
|
|
2261
|
+
fearOfMissingOut: 0.5,
|
|
2262
|
+
socialProofSensitivity: 0.5,
|
|
2263
|
+
mentalModelFlexibility: 0.5,
|
|
2264
|
+
siteFamiliarity: 0.5,
|
|
2265
|
+
};
|
|
2266
|
+
personaObj = {
|
|
2267
|
+
...existingPersona,
|
|
2268
|
+
cognitiveTraits: {
|
|
2269
|
+
...defaultTraits,
|
|
2270
|
+
...(existingPersona.cognitiveTraits || {}),
|
|
2271
|
+
...customTraits,
|
|
2272
|
+
},
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
else {
|
|
2276
|
+
personaObj = existingPersona;
|
|
2277
|
+
}
|
|
2278
|
+
return personaObj;
|
|
2279
|
+
}
|
|
2280
|
+
/**
|
|
2281
|
+
* The live per-step decision: one messages.create call, its text blocks read
|
|
2282
|
+
* together, and one more ask for the JSON alone when the reply does not hold
|
|
2283
|
+
* a JSON object. describeLastReply says why the last reply was unreadable.
|
|
2284
|
+
* Exported for tests, which pass a fake client.
|
|
2285
|
+
*/
|
|
2286
|
+
export function createLiveDecisionProvider(anthropic, model) {
|
|
2287
|
+
// Why the last live reply could not be read; the step reports it as
|
|
2288
|
+
// replyProblem. Undefined after a readable reply.
|
|
2289
|
+
let lastLiveProblem;
|
|
2290
|
+
return {
|
|
2291
|
+
decide: async (ctx) => {
|
|
2292
|
+
if (!anthropic)
|
|
2293
|
+
throw new Error("Anthropic API key not configured.");
|
|
2294
|
+
const ask = (messages, maxTokens = 2000) => anthropic.messages.create({
|
|
2295
|
+
model,
|
|
2296
|
+
max_tokens: maxTokens,
|
|
2297
|
+
...(ctx.systemPrompt ? { system: ctx.systemPrompt } : {}),
|
|
2298
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
2299
|
+
});
|
|
2300
|
+
const textOf = (r) => r.content.map((b) => (b.type === "text" ? b.text : "")).join("");
|
|
2301
|
+
const whyUnreadable = (r, text) => text.trim()
|
|
2302
|
+
? readReplyJson(text).problem
|
|
2303
|
+
: `the model returned no text (stop_reason ${r.stop_reason ?? "none"}; content blocks: ${r.content.map((b) => b.type).join(", ") || "none"})`;
|
|
2304
|
+
lastLiveProblem = undefined;
|
|
2305
|
+
const first = await ask(ctx.messages);
|
|
2306
|
+
const firstText = textOf(first);
|
|
2307
|
+
if (readReplyJson(firstText).value)
|
|
2308
|
+
return firstText;
|
|
2309
|
+
// Asked once more, for the JSON alone. Inside the provider, so a trace
|
|
2310
|
+
// records one decision per step and still replays.
|
|
2311
|
+
// Thinking counts against max_tokens, so a reply cut off there may hold
|
|
2312
|
+
// no text at all; the second ask gets room for both.
|
|
2313
|
+
const firstProblem = whyUnreadable(first, firstText);
|
|
2314
|
+
const retry = await ask([
|
|
2315
|
+
...ctx.messages,
|
|
2316
|
+
{ role: "assistant", content: firstText.trim() || "(no text)" },
|
|
2317
|
+
{ role: "user", content: "Your reply could not be read as the JSON object the response format asks for. Reply again with only that JSON object, nothing before or after it." },
|
|
2318
|
+
], first.stop_reason === "max_tokens" ? 8000 : 2000);
|
|
2319
|
+
const retryText = textOf(retry);
|
|
2320
|
+
if (readReplyJson(retryText).value)
|
|
2321
|
+
return retryText;
|
|
2322
|
+
lastLiveProblem = `${firstProblem}; asked once more, and ${whyUnreadable(retry, retryText)}`;
|
|
2323
|
+
return retryText;
|
|
2324
|
+
},
|
|
2325
|
+
describeLastReply: () => lastLiveProblem,
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
/** A journey or persona name as a file-name part: letters, digits, - and _ only. */
|
|
2329
|
+
export function journeySlug(name) {
|
|
2330
|
+
return name.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "journey";
|
|
2331
|
+
}
|
|
2233
2332
|
function parseCognitiveResponse(response) {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2333
|
+
return (readReplyJson(response).value ?? {});
|
|
2334
|
+
}
|
|
2335
|
+
/**
|
|
2336
|
+
* The JSON object in a model reply, or why there is none. Tried in order: the
|
|
2337
|
+
* reply with any code fences removed, each balanced {...} in it (string-aware,
|
|
2338
|
+
* so a reply with prose after its JSON still reads), then the old greedy
|
|
2339
|
+
* first-"{"-to-last-"}" match; the first that parses to an object wins.
|
|
2340
|
+
*/
|
|
2341
|
+
export function readReplyJson(reply) {
|
|
2342
|
+
const text = reply.trim();
|
|
2343
|
+
if (!text)
|
|
2344
|
+
return { problem: "the model returned no text" };
|
|
2345
|
+
const unfenced = text.replace(/```(?:json)?/gi, "").trim();
|
|
2346
|
+
const candidates = [unfenced];
|
|
2347
|
+
for (let i = 0; i < unfenced.length; i++) {
|
|
2348
|
+
if (unfenced[i] !== "{")
|
|
2349
|
+
continue;
|
|
2350
|
+
let depth = 0;
|
|
2351
|
+
let inString = false;
|
|
2352
|
+
let escaped = false;
|
|
2353
|
+
for (let j = i; j < unfenced.length; j++) {
|
|
2354
|
+
const c = unfenced[j];
|
|
2355
|
+
if (inString) {
|
|
2356
|
+
if (escaped)
|
|
2357
|
+
escaped = false;
|
|
2358
|
+
else if (c === "\\")
|
|
2359
|
+
escaped = true;
|
|
2360
|
+
else if (c === '"')
|
|
2361
|
+
inString = false;
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
if (c === '"')
|
|
2365
|
+
inString = true;
|
|
2366
|
+
else if (c === "{")
|
|
2367
|
+
depth++;
|
|
2368
|
+
else if (c === "}" && --depth === 0) {
|
|
2369
|
+
candidates.push(unfenced.slice(i, j + 1));
|
|
2370
|
+
i = j;
|
|
2371
|
+
break;
|
|
2372
|
+
}
|
|
2239
2373
|
}
|
|
2240
2374
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2375
|
+
const greedy = unfenced.match(/\{[\s\S]*\}/);
|
|
2376
|
+
if (greedy)
|
|
2377
|
+
candidates.push(greedy[0]);
|
|
2378
|
+
let firstError = "";
|
|
2379
|
+
for (const candidate of candidates) {
|
|
2380
|
+
try {
|
|
2381
|
+
const value = JSON.parse(candidate);
|
|
2382
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
2383
|
+
return { value };
|
|
2384
|
+
}
|
|
2385
|
+
catch (e) {
|
|
2386
|
+
if (!firstError)
|
|
2387
|
+
firstError = e.message;
|
|
2388
|
+
}
|
|
2243
2389
|
}
|
|
2244
|
-
|
|
2390
|
+
const start = text.slice(0, 80).replace(/\s+/g, " ");
|
|
2391
|
+
return { problem: `the reply was not a JSON object (${firstError || "no {...} in it"}); it began: ${start}${text.length > 80 ? "..." : ""}` };
|
|
2245
2392
|
}
|
|
2246
2393
|
function checkAbandonmentTriggers(state, thresholds) {
|
|
2247
2394
|
if (state.patienceRemaining < thresholds.patienceMin) {
|