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.
@@ -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":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cognitive/index.ts"],"names":[],"mappings":"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"}
@@ -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 — params and text extraction unchanged. The post-journey
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 traceWriter = options.trace ? new JourneyTraceWriter() : null;
715
- // Get or create persona
716
- const existingPersona = getPersona(options.persona);
717
- let personaObj;
718
- if (!existingPersona) {
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.replace(/[^a-zA-Z0-9-_]+/g, "-").toLowerCase()}-${Date.now()}`);
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
- lastActionOutcome.scrollDelta = viewportCtx.scroll.y - prevScrollY;
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 = JSON.parse(evalText.replace(/```json?\n?/g, '').replace(/```/g, '').trim());
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 avgConfusion = frictionPoints.length > 0
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: avgConfusion,
1979
- maxFrustrationLevel: state.frustrationLevel,
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
- try {
2235
- // Extract JSON from response (may have markdown code blocks)
2236
- const jsonMatch = response.match(/\{[\s\S]*\}/);
2237
- if (jsonMatch) {
2238
- return JSON.parse(jsonMatch[0]);
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
- catch {
2242
- // Parsing failed
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
- return {};
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) {