ambral 0.6.0 → 0.7.0

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.
Files changed (2) hide show
  1. package/dist/ambral.js +138 -47
  2. package/package.json +1 -1
package/dist/ambral.js CHANGED
@@ -47075,7 +47075,7 @@ var runWith2 = (command, config) => {
47075
47075
  // package.json
47076
47076
  var package_default = {
47077
47077
  name: "ambral",
47078
- version: "0.6.0",
47078
+ version: "0.7.0",
47079
47079
  license: "UNLICENSED",
47080
47080
  type: "module",
47081
47081
  description: "The `ambral` command: signs in through the browser, then calls the platform API as that person.",
@@ -54014,6 +54014,13 @@ var bodyText2 = (chunks2) => {
54014
54014
  return chunks2.map((chunk) => streaming.decode(chunk, { stream: true })).join("") + streaming.decode();
54015
54015
  };
54016
54016
  var isSuccess8 = (status3) => status3 >= 200 && status3 < 300;
54017
+ var EXCERPT_LENGTH = 200;
54018
+ var excerpt = (state2) => $match(state2, {
54019
+ Body: ({ chunks: chunks2 }) => bodyText2(chunks2).slice(0, EXCERPT_LENGTH),
54020
+ Events: () => {
54021
+ return;
54022
+ }
54023
+ });
54017
54024
  var tokensOf2 = (usage) => usage == null ? undefined : tokensOf(usage);
54018
54025
  var readBody = (state2) => {
54019
54026
  const text3 = bodyText2(state2.chunks);
@@ -54745,6 +54752,7 @@ __export(exports_Inference, {
54745
54752
  requestOf: () => requestOf,
54746
54753
  produce: () => produce,
54747
54754
  paidByOf: () => paidByOf,
54755
+ isTransient: () => isTransient,
54748
54756
  completionOf: () => completionOf,
54749
54757
  Unavailable: () => Unavailable2,
54750
54758
  Request: () => Request4,
@@ -54845,21 +54853,28 @@ var completionOf = (model, request3, observed, startedAt, wait) => {
54845
54853
  call: modelCall(request3, model.base.upstreamModelId, observed, startedAt)
54846
54854
  });
54847
54855
  };
54856
+ var TRANSIENT_STATUSES = new Set([408, 502, 504]);
54857
+ var isTransient = (error2) => error2.reason === "Rejected" && error2.rejection !== undefined && error2.rejection.retryAfter === undefined && TRANSIENT_STATUSES.has(error2.rejection.status);
54858
+ var TRANSIENT_RETRY = exports_Schedule.max([
54859
+ exports_Schedule.exponential("5 seconds", 3).pipe(exports_Schedule.jittered, exports_Schedule.modifyDelay(({ duration: duration2 }) => exports_Effect.succeed(exports_Duration.millis(Math.round(exports_Duration.toMillis(duration2)))))),
54860
+ exports_Schedule.during("2 minutes")
54861
+ ]);
54862
+ var TRANSIENT_RETRIES = 3;
54848
54863
  var produce = exports_Effect.fn("Inference.produce")(function* (model, prompt, org) {
54849
54864
  yield* exports_Effect.annotateCurrentSpan({ model: model.id, messages: prompt.messages.length, org });
54850
54865
  const inference = yield* Inference;
54851
54866
  const request3 = requestOf(model, prompt);
54852
- const body = yield* encodeRequest(request3).pipe(exports_Effect.orDie);
54853
54867
  const unavailable3 = (cause) => new ProduceError({ model: model.id, reason: "Unavailable", cause });
54854
- const startedAt = yield* now3;
54855
- return yield* exports_Effect.uninterruptibleMask((restore) => exports_Effect.gen(function* () {
54856
- const response = yield* restore(inference.send({
54857
- upstream: model.base.upstream,
54858
- endpoint: "/chat/completions",
54859
- body,
54860
- requireParameters: true,
54861
- org
54862
- })).pipe(exports_Effect.mapError((error2) => exports_Match.valueTags(error2, {
54868
+ const send = encodeRequest(request3).pipe(exports_Effect.orDie, exports_Effect.flatMap((body) => inference.send({
54869
+ upstream: model.base.upstream,
54870
+ endpoint: "/chat/completions",
54871
+ body,
54872
+ requireParameters: true,
54873
+ org
54874
+ })));
54875
+ const attempt2 = exports_Effect.uninterruptibleMask((restore) => exports_Effect.gen(function* () {
54876
+ const startedAt = yield* now3;
54877
+ const response = yield* restore(send).pipe(exports_Effect.mapError((error2) => exports_Match.valueTags(error2, {
54863
54878
  InferenceUnavailable: unavailable3,
54864
54879
  InferenceKeyRequired: (cause) => new ProduceError({ model: model.id, reason: "KeyRequired", cause }),
54865
54880
  InferenceOutOfCredit: (cause) => new ProduceError({ model: model.id, reason: "OutOfCredit", cause })
@@ -54879,9 +54894,20 @@ var produce = exports_Effect.fn("Inference.produce")(function* (model, prompt, o
54879
54894
  };
54880
54895
  yield* chargeCall(org, call);
54881
54896
  yield* streamed;
54882
- const completion = yield* exports_Effect.fromResult(completionOf(model, request3, observed, startedAt, retryAfter(response.headers, finishedAt)));
54883
- return { ...completion, charged: exports_Option.getOrUndefined(lineOf(call)) };
54897
+ const completion = completionOf(model, request3, observed, startedAt, retryAfter(response.headers, finishedAt));
54898
+ if (exports_Result.isFailure(completion) && completion.failure.reason === "Rejected") {
54899
+ yield* exports_Effect.logWarning("A provider rejected a model call", {
54900
+ model: model.id,
54901
+ status: observed.status,
54902
+ transient: isTransient(completion.failure),
54903
+ durationMs: observed.durationMs,
54904
+ headers: response.headers,
54905
+ body: excerpt(yield* exports_Ref.get(read3))
54906
+ });
54907
+ }
54908
+ return { ...yield* exports_Effect.fromResult(completion), charged: exports_Option.getOrUndefined(lineOf(call)) };
54884
54909
  }));
54910
+ return yield* attempt2.pipe(exports_Effect.retry({ schedule: TRANSIENT_RETRY, times: TRANSIENT_RETRIES, while: isTransient }));
54885
54911
  });
54886
54912
 
54887
54913
  // ../../node_modules/.bun/effect@4.0.3/node_modules/effect/dist/persistence/KeyValueStore.js
@@ -57608,41 +57634,39 @@ var fill = (template, fields3) => {
57608
57634
  };
57609
57635
  var textOf2 = (content) => exports_Predicate.isString(content) || content == null ? content ?? "" : content.map((part) => part.type === "text" ? part.text : "[image]").join(`
57610
57636
  `);
57611
- var callLine = (call) => `→ ${call.name}(${call.type === "custom" ? call.arguments : canonicalArguments(call.arguments)})`;
57612
- var cuttable = (text3) => ({ text: text3, whole: false });
57637
+ var cuttable = (text3) => ({ open: "", text: text3, close: "" });
57638
+ var callPiece = (call, separator) => ({
57639
+ open: `${separator}→ ${call.name}(`,
57640
+ text: call.type === "custom" ? call.arguments : canonicalArguments(call.arguments),
57641
+ close: ")"
57642
+ });
57613
57643
  var turnPieces = (turn) => {
57614
57644
  const text3 = textOf2(turn.content);
57615
- const calls = (turn.tool_calls ?? []).map(callLine);
57645
+ const calls = turn.tool_calls ?? [];
57616
57646
  if (text3 === "" && calls.length === 0)
57617
57647
  return [cuttable("(empty reply)")];
57618
57648
  return [
57619
57649
  ...text3 === "" ? [] : [cuttable(text3)],
57620
- ...calls.map((line, i) => ({ text: i === 0 && text3 === "" ? line : `
57621
- ${line}`, whole: true }))
57650
+ ...calls.map((call, i) => callPiece(call, i === 0 && text3 === "" ? "" : `
57651
+ `))
57622
57652
  ];
57623
57653
  };
57624
57654
  var plural = (count2, noun) => `${count2} ${noun}${count2 === 1 ? "" : "s"}`;
57625
57655
  var capped = (pieces, maxChars) => {
57626
- const text3 = pieces.map((piece) => piece.text).join("");
57627
- if (maxChars === null || text3.length <= maxChars)
57628
- return text3;
57629
- const head7 = [];
57630
- let length2 = 0;
57631
- for (const piece of pieces) {
57632
- const room = maxChars - length2;
57633
- if (piece.text.length <= room) {
57634
- head7.push(piece.text);
57635
- length2 += piece.text.length;
57636
- continue;
57637
- }
57638
- if (!piece.whole) {
57639
- const end3 = /[\uD800-\uDBFF]/.test(piece.text[room - 1] ?? "") ? room - 1 : room;
57640
- head7.push(piece.text.slice(0, end3));
57641
- length2 += end3;
57642
- }
57643
- break;
57644
- }
57645
- return `${head7.join("")}[… ${plural(text3.length - length2, "character")} elided]`;
57656
+ const whole = pieces.map(({ open: open3, text: text3, close: close3 }) => `${open3}${text3}${close3}`).join("");
57657
+ if (maxChars === null || whole.length <= maxChars)
57658
+ return whole;
57659
+ let room = maxChars;
57660
+ return pieces.map(({ open: open3, text: text3, close: close3 }) => {
57661
+ room = Math.max(0, room - open3.length - close3.length);
57662
+ if (text3.length <= room) {
57663
+ room -= text3.length;
57664
+ return `${open3}${text3}${close3}`;
57665
+ }
57666
+ const end3 = /[\uD800-\uDBFF]/.test(text3[room - 1] ?? "") ? room - 1 : room;
57667
+ room = 0;
57668
+ return `${open3}${text3.slice(0, end3)}[… ${plural(text3.length - end3, "character")} elided]${close3}`;
57669
+ }).join("");
57646
57670
  };
57647
57671
  var turnText = (turn, maxChars) => capped(turnPieces(turn), maxChars);
57648
57672
  var render = (messages, conversation2 = messages, maxChars = null) => {
@@ -58663,6 +58687,7 @@ __export(exports_Run, {
58663
58687
  // ../core/src/Trace/TraceReading.ts
58664
58688
  var exports_TraceReading = {};
58665
58689
  __export(exports_TraceReading, {
58690
+ resends: () => resends,
58666
58691
  raw: () => raw3,
58667
58692
  previewOf: () => previewOf,
58668
58693
  list: () => list8,
@@ -58719,6 +58744,20 @@ var latestLeaf = (nodes) => {
58719
58744
  const leaves2 = leaves(nodes);
58720
58745
  return leaves2.findLast((leaf) => nodes[leaf]?.sampled === true) ?? leaves2.at(-1);
58721
58746
  };
58747
+ var replyKey = (node) => node.message.role === "assistant" && node.parent != null ? `${node.parent}:${hash3(exports_Struct.omit(node.message, ["reasoning_content", "provider_state"]))}` : undefined;
58748
+ var resends = (nodes) => {
58749
+ const replies = new Map;
58750
+ nodes.forEach((node, id2) => {
58751
+ const key3 = replyKey(node);
58752
+ if (node.sampled === true && key3 !== undefined && !replies.has(key3))
58753
+ replies.set(key3, NodeId.make(id2));
58754
+ });
58755
+ return new Map(nodes.flatMap((node, id2) => {
58756
+ const key3 = replyKey(node);
58757
+ const reply = node.sampled === true || key3 === undefined ? undefined : replies.get(key3);
58758
+ return reply === undefined ? [] : [[NodeId.make(id2), reply]];
58759
+ }));
58760
+ };
58722
58761
  var latestBranch = (nodes) => {
58723
58762
  const leaf = latestLeaf(nodes);
58724
58763
  return leaf === undefined ? [] : walk(nodes, leaf);
@@ -58760,15 +58799,20 @@ var Conversation = exports_Schema.Struct({
58760
58799
  }).annotate({ identifier: "TraceConversation" });
58761
58800
  var conversationOf = (nodes, calls2, leaf, tasksets) => {
58762
58801
  const placed = new Map;
58802
+ const copies = new Map;
58803
+ for (const [copy5, reply] of resends(nodes))
58804
+ copies.set(reply, [...copies.get(reply) ?? [], copy5]);
58763
58805
  for (const { call, prompt } of calls2) {
58764
58806
  const at = call.node ?? prompt;
58765
58807
  if (at == null)
58766
58808
  continue;
58767
- const already = placed.get(at);
58768
- if (already === undefined)
58769
- placed.set(at, [call]);
58770
- else
58771
- already.push(call);
58809
+ for (const node of [at, ...call.node == null ? [] : copies.get(call.node) ?? []]) {
58810
+ const already = placed.get(node);
58811
+ if (already === undefined)
58812
+ placed.set(node, [call]);
58813
+ else
58814
+ already.push(call);
58815
+ }
58772
58816
  }
58773
58817
  const path = walk(nodes, leaf);
58774
58818
  const onPath = new Set(path.map(({ id: id2 }) => id2));
@@ -59391,6 +59435,11 @@ var load = exports_Effect.fn("Taskset.load")(function* (org, workload, member2,
59391
59435
  for (const call of trace3.calls ?? [])
59392
59436
  if (call.node != null && !replies.has(call.node))
59393
59437
  replies.set(call.node, call);
59438
+ for (const [copy5, reply] of resends(trace3.nodes ?? [])) {
59439
+ const call = replies.get(reply);
59440
+ if (call !== undefined)
59441
+ replies.set(copy5, call);
59442
+ }
59394
59443
  const path = walk(trace3.nodes ?? [], member2.leafNodeId).map(({ id: id2, node }) => ({
59395
59444
  message: node.message,
59396
59445
  call: replies.get(id2) ?? null
@@ -61759,12 +61808,13 @@ var listed8 = (workload) => ({
61759
61808
  return `No traces recorded for ${workload} yet. A session is stored once it has gone quiet for a ` + `little while; see how to connect an agent with \`ambral workloads get ${workload}\`.`;
61760
61809
  }
61761
61810
  const rows = table2([
61762
- ["ID", "RECORDED (UTC)", "SESSION", "STEPS", "TOOLS", "FIRST USER MESSAGE"],
61811
+ ["ID", "RECORDED (UTC)", "SESSION", "STEPS", "BRANCHES", "TOOLS", "FIRST USER MESSAGE"],
61763
61812
  ...traces.map((trace3) => [
61764
61813
  trace3.id,
61765
61814
  minuteOf(trace3.recordedAt),
61766
61815
  trace3.session ?? GENERATED,
61767
61816
  String(trace3.steps),
61817
+ String(trace3.branches),
61768
61818
  trace3.tools.join(", "),
61769
61819
  trace3.firstUserMessage === undefined ? "" : quoted(trace3.firstUserMessage)
61770
61820
  ])
@@ -62855,7 +62905,7 @@ required:
62855
62905
  `;
62856
62906
 
62857
62907
  // skill/references/score.md
62858
- var score_default = "# Designing a scoring config\n\nHow a taskset's scoring config turns replies into scores, which preset fits which kind of session, how to write\ncriteria, and a checklist to think through each time you configure one. The examples (`support-agent.yaml`,\n`research-agent.yaml`, `coding-agent.yaml`, `retrieval.yaml`, `tool-safety.yaml`, all linked from\nSKILL.md) show each of these choices in a complete config. Each judge's `model` there reads `<judge>`, which stands\nfor the model you pass to `--judge`, one from `ambral models list`.\n\n## 1. How a score is built\n\n- **Points.** `points` says where in each trace a reply is asked for: at every assistant turn (`all`), at the last\n (`last`), or at the first and last and a sample between them (`sample`). At each point, each subject gives its\n reply there.\n- **A reply's score.** The `taskset` scorers score each reply. `fromBaseline` compares it with the baseline's, the\n reply the recorded trace holds at that point (did it make the same tool calls, does it say the same thing);\n criteria grade it on its own terms (see \"Writing criteria\"). A reply's score is the weighted mean of the scorers\n that count toward it (each `rewards` entry, judge and criteria group, by its `weight`); `metrics` are recorded\n beside it without counting, unless `required` names one. A scorer that doesn't apply at a point records nothing\n there and doesn't count.\n- **`required` for what must never fail.** A scorer, a group or a criterion (`<group>/<criterion>`) named in\n `required` that scores below 1 makes its reply score 0; a criterion answered n/a doesn't. So a required criterion\n is yes or no: a middle choice scores below 1, and would fail its reply too.\n- **The trace's score** (`episode`). With no `episode.rewards`, a trace scores the mean of its replies' scores.\n `episode.rewards.replies` combines its replies' scores by `mode` (`mean`, `min`, or `all-pass`: 1 only when every\n scored reply scores 1); `episode.rewards.trajectory` is one judge call over the whole session. Once\n `episode.rewards` names either, the trace scores the mean of those alone, so reply scorers count toward it only\n through `replies`.\n- **Production's score.** Production is scored by criteria groups alone: every other scorer, `fromBaseline` and the\n trajectory judge, compares a reply with the baseline's, which production's is. So where the config has other\n scorers that count, production's per-trace score is the mean of its criteria alone, while a candidate's includes the\n rest too. Compare production with a candidate on the criteria rows of a run's summary, not the per-trace row,\n unless criteria groups are all that count.\n- **What `--check` warns about.** A scorer that is recorded but counts toward nothing: when `episode.rewards` names a\n scorer but not `replies`, each reply scorer and each `required` entry, which can't reach the trace's score; and\n otherwise a reward, judge or criteria group of weight 0 that `required` doesn't name. They are warnings, not\n mistakes: the config still saves.\n\n## 2. The presets\n\nEach preset grades a different way. Pick by how a good session of this kind looks, then add criteria (see \"Writing\ncriteria\").\n\n- **`strict`**: a workflow whose tool calls must be the baseline's (look the order up, then refund it). At each point\n where either calls a tool, the reply must make the same calls with the same arguments, in any order within the\n reply (`match_ordered` checks the order too); a point where both answer in text isn't scored. A trace passes only\n when every scored reply does (its replies are scored `all-pass`), so a criterion added to it fails the trace\n wherever it scores below 1, a middle choice included. No judge calls, so it costs only the\n subjects' replies.\n- **`balanced`**: comparing turn by turn with the baseline, where a reply may differ in wording or approach and\n still be right. A judge decides at each scored point whether the reply is equivalent, or a sensible next step: one\n judge call per point.\n- **`trajectory`**: long, open-ended tasks (multi-step retrieval or research) where many paths are valid and their\n order varies from run to run, so comparing each turn with the baseline would mark good runs wrong. One judge call\n per trace and subject asks whether the subject would have handled the whole session. It is made only once every\n point of the trace has its reply, so a point that fails fails the trial without it; a trace scored at more points\n than `maxPoints` is skipped, and has no score from it. The judge reads the whole recorded session, however few\n points are scored, so `context.maxChars` is what bounds its prompt: a session too long for the judge fails its call,\n and so the trial. Know its limit: at each scored point the subject is given the\n recorded session up to there and writes one reply, and the judge sees those replies beside the baseline's and\n judges them together. The subject never runs a session of its own: each reply starts from the recorded history,\n never from its own earlier replies.\n- **`criteria`**: what a good reply must do, in your own words, whatever the baseline did. One judge call per\n criterion per scored point by default. On its own it suits evals where the baseline isn't a bar to match: it judges\n nothing against the baseline's replies; the recorded session is where each reply starts and what the judge reads\n it against.\n\nCriteria count toward each reply's score under `strict`, `balanced` and `criteria`, since a trace there scores its\nreplies. Under `trajectory` the trace scores the judge's verdict alone, so criteria are only recorded beside it.\nFor them to count, also set `episode.rewards.replies` (`mode: mean`, or `all-pass` when every scored reply must keep\nevery rule): the trace then scores the mean of the verdict and its replies' score.\n\nThe judges that compare with the baseline (`balanced`'s, and `trajectory`'s) take `guidance`: your words, added to\nthe judge's prompt, on how to weigh a reply that differs from the baseline's for this kind of agent. Their prompts\nalready treat the baseline as one acceptable reply, not the only one; say what a difference that matters looks like\nhere (a step that loses the thread, an answer from no source), and what's fine (another order, another source).\nSay what counts against a reply, not what it must repeat: a judge told an equivalent reply has \"the same dates and\namounts\" fails a good reply that leaves out an amount the baseline's mentioned. Say instead that leaving out a detail\nthe baseline's gave is fine, and contradicting one (a different date, amount or outcome) isn't.\n\nIf you're unsure which fits, try both: write each to a file, start a run of the same subjects under each with\n`ambral runs start <workload>/<name> --subjects … --scoring <file>`, and compare whether each ranks the subjects the\nway you and the user would. The absolute scores aren't comparable between them.\n\n## 3. Writing criteria\n\n**Criteria**, under `criteria`, grade each reply on its own terms against statements you write, never against the\nbaseline's. Use them for what a good session of this kind must do, and for checks like \"the reply never calls\n`delete_account`\", \"`refund_order` is only called after `lookup_order` appears in the session\" or \"the reply\nincludes an order number\": a judge model grades each. The `criteria` preset starts with one group of two general\nones to replace.\n\n- **One narrow statement per criterion.** Write one statement a good reply meets (\"Before issuing a refund, the\n assistant asks the user to confirm.\"), as its `text`, not a list. Each criterion gets its own answer and its own\n score, so a failure points at one thing. Write several criteria rather than one broad one.\n- **Write for the judge, who reads them as written.** The judge is shown each criterion's `name`, its `text`, and its\n choice labels worst to best, each with its meaning if it has one. So give it a name that says what it checks\n (`confirms_before_refund`, not `c1`), text that stands on its own without the rest of the file, and labels that mean\n something even without `meanings`.\n- **Choices, worst to best.** Every criterion answers on one scale of choices. At each reply the judge picks\n exactly one of them, or n/a where its `notApplicable` says it doesn't apply, and the choice it picks is the\n criterion's score there. A\n choice's score is its place in the list, worst 0, best 1, those between evenly spaced, and isn't written in the\n config:\n\n | `choices` | Scores |\n | ------------------------- | --------------------------------------- |\n | `[no, yes]` (the default) | no 0, yes 1 |\n | `[no, partly, yes]` | no 0, partly 0.5, yes 1 |\n | `[bad, poor, fair, good]` | bad 0, poor 0.33, fair 0.67, good 1 |\n | any, answered n/a | no score: left out of the group's score |\n\n List more than two, worst first, for partial credit. Say what earns each choice in `meanings` (`partly: \"It\n mentions the refund, but doesn't wait for a yes.\"`): the judge is shown each beside its choice, and a choice whose\n meaning you leave out is left to the judge to read.\n- **Group criteria that share a judge and a view.** A group (each entry under `criteria`) holds criteria graded by one\n `model`, at the same `turns`, shown the same `context`. Its score is the weighted mean of its criteria's scores\n (each criterion's `weight`, default 1), counted as a reward under the group's name with the group's `weight`. Each\n criterion's score is also recorded on its own, as `<group>/<criterion>`, not counted toward the reply's score. A group\n whose criteria were all n/a has no score there.\n- **Give a group the smallest window that holds what it checks.** The window is the group's `context.messages`: `0`\n shows the judge the reply alone, its text and tool calls; a number N (default 20) also shows the system prompt and the\n session's latest N messages, the opening request kept in view; `null` shows the whole session. A judge grades best\n when it sees little beyond what it checks, so write criteria about the reply and the steps just before it, and give a\n group the smallest window that holds them: `0` for what the reply itself does or says, a small N for recent steps.\n Split criteria that need different windows into groups of their own. Keep `null` for a criterion that truly depends on\n the whole session: a session too long for the judge fails its call, and a failed call fails the trial.\n- **What N counts.** `context.messages` never shows what came after the reply, and at `0` a `fromBaseline` judge also\n sees the baseline's reply. N counts every message, each tool call and each tool result, so 10 messages may be only two\n to four steps of an agent that calls tools, and the user's latest request may sit well back in a tool loop: only the\n system prompt and the opening request are always shown.\n- **Tools are shown by their calls.** The judge sees each tool call and its result, but never the tools'\n definitions, so a criterion can't rest on what a tool's description says. `fromBaseline`'s judges read the same\n `context` setting.\n- **Long messages are cut.** Every judge cuts each recorded message and the reply at 8000 characters by default\n (`context.maxChars`), keeping its start; the system prompt is never cut. A tool call is shown whole or not at all, so\n one that would pass the limit is dropped, with any after it. Set `maxChars: null` to show every message whole, or a\n higher number when what matters sits deep in a long one, such as a criterion about the calls a reply makes after\n long text.\n- **`notApplicable`: when a criterion doesn't apply, or what it needs isn't shown.** Keep `text` to the statement a\n good reply meets, and say when it doesn't apply in the criterion's `notApplicable` (\"No refund is asked about.\" for\n \"asks to confirm before refunding\"). The judge is shown it beside the choices, and may then answer n/a, which\n records no score and is left out of the group's score. Write one for a criterion only some replies are about,\n rather than one that passes or fails everywhere. Where the window leaves something out, the judge sees a gap and is\n told not to count what it can't see against the reply. So a criterion that needs evidence fails falsely when the\n evidence fell outside (a source it can't see looks like no source), and one that asks whether something was\n requested passes too easily. Say so in its `notApplicable` (\"The request the deletion acts on isn't shown.\"), and\n size the window so what it needs usually is shown.\n- **No `notApplicable` for what applies to every reply** (its tone, never calling a tool it mustn't): without one, the\n judge must pick a choice at every reply it grades, so it can't pass over a reply as n/a. `n/a` is never one of a\n criterion's own choices.\n- **n/a in a run's summary.** A criterion that is n/a at every point of a run shows no row in its summary; if that\n happens where you expected it to apply, reword its `notApplicable`, or check its group's `context` shows the judge\n what it needs.\n- **`perCall`: criteria per judge call.** By default (`1`) each criterion is asked in a call of its own, one small\n decision each, which judges most reliably; a reply's calls run in parallel. A larger number asks that many in one\n call, and `null` all of the group's: fewer calls, and so cheaper, for a large group of quick checks, or for criteria\n the judge should weigh side by side. Try a draft both ways and keep `null` only if the scores agree. A judge call that\n fails fails its trial, so more calls are also more that can fail.\n- **Narrow where it is checked**, with the group's `turns`, which picks points by the recorded turn there:\n `final: true` grades only the reply at the trace's last turn, and `kinds: [tool_call]` only where the recorded agent\n called a tool. A group makes its calls at every reply it grades, so narrowing `turns` cuts calls and cost.\n- **Try them before you save them.** Start a run with `production` as the subject and the draft as `--scoring <file>`:\n it grades the baseline's replies, the ones you read when you picked the traces, and calls only the judges. Read each\n criterion's score: one that fails replies you and the user would call good is misfiring. Find where with\n `ambral runs verdicts <run> --check <group>/<criterion> --below 1`, every reply it scored short of its best choice,\n or `--verdict <choice>` for one choice, which gives the judge's reason for each, then\n read one of those traces with `ambral runs trace <run> <tr_…> --json`: each check's `call` is the judge's whole call,\n the prompt it was sent (exactly what it was shown) and its answer. Tell the causes apart before changing anything:\n - **The reply really breaks it.** The reason is right, and you and the user agree: the criterion works, and the\n baseline is wrong there. Keep the criterion; don't pick that trace as an example of a good session.\n - **The criterion is worded wrong.** The judge applied the words as written, but they don't say what you meant\n (too broad, or a case you didn't think of): reword it, say when it doesn't apply in its `notApplicable`, or write\n `meanings` for its choices.\n - **The judge wasn't shown enough.** What the criterion needs (the request, the tool result it rests on) isn't in the\n prompt, or is cut short with `[… N characters elided]`: raise the group's `context.messages` or `maxChars`, move the\n criterion to a group with a larger window, or name what it needs in its `notApplicable`, so the judge answers n/a\n when that isn't shown.\n - **The judge misread.** The prompt holds what it needed, the criterion says what you meant, and the reason still\n gets the reply wrong: make the criterion more explicit, ask it in a call of its own (`perCall: 1`), or try another\n judge model. A judge whose reply isn't an answer at all fails its trial rather than giving a verdict: it is under\n \"Failed trials\" in `ambral runs show <run>`, as `VerifierError: Failed: <group>`, and on that subject's line in\n `ambral runs trace`. Its reply isn't shown there, so run it again, and if it keeps failing, try another judge model\n or fewer criteria per call.\n\n Then run the draft again and read the same traces' verdicts.\n\n## 4. Checklist for configuring\n\nQuestions to think through each time you create or change a config, not steps to follow in order:\n\n- **What does a good session of this kind do?** Agree on it with the user first: it decides the preset and the\n criteria. (See \"The presets\" and \"Writing criteria\".)\n- **Is the baseline a bar to match?** If it isn't, grade with criteria only, the `criteria` preset. (See \"The\n presets\".)\n- **Is the session judged turn by turn or as a whole?** If as a whole (`trajectory`), do rules hold at every reply? Then\n add them as criteria and set `episode.rewards.replies`, or they won't count. (See \"How a score is built\" and \"The\n presets\".)\n- **Does each judge that compares with the baseline say which differences matter?** Give it `guidance`: what a\n different but fine reply looks like for this kind of agent, and what a difference that counts against it is, never\n that a reply must repeat all the baseline's said. (See \"The presets\".)\n- **Is each criterion one narrow statement about the reply and the steps just before it, and does its\n `notApplicable` say when it doesn't apply?** (See \"Writing criteria\": one narrow statement, `notApplicable`.)\n- **Does each group have the smallest window that holds what it checks?** (See \"Writing criteria\": the window, what\n N counts, long messages.)\n- **What must never fail?** Name it in `required`. (See \"How a score is built\".)\n- **Which points does each scorer grade, and what does that cost?** `points` picks where replies are asked for, and\n each group's `turns` where it grades; every judge makes its calls at every reply it grades, and a criteria group one\n per criterion by default. (See \"How a score is built\": points; \"The presets\"; \"Writing criteria\": `perCall`, narrow\n where it is checked.)\n- **Was the draft tried against `production`, and were the judge's reasons read?** (See \"Writing criteria\": try them\n before you save them.)\n- **Does `--check` pass with no warnings you didn't mean?** (See \"How a score is built\": what `--check` warns\n about.)\n";
62908
+ var score_default = "# Designing a scoring config\n\nHow a taskset's scoring config turns replies into scores, which preset fits which kind of session, how to write\ncriteria, and a checklist to think through each time you configure one. The examples (`support-agent.yaml`,\n`research-agent.yaml`, `coding-agent.yaml`, `retrieval.yaml`, `tool-safety.yaml`, all linked from\nSKILL.md) show each of these choices in a complete config. Each judge's `model` there reads `<judge>`, which stands\nfor the model you pass to `--judge`, one from `ambral models list`.\n\n## 1. How a score is built\n\n- **Points.** `points` says where in each trace a reply is asked for: at every assistant turn (`all`), at the last\n (`last`), or at the first and last and a sample between them (`sample`). At each point, each subject gives its\n reply there.\n- **A reply's score.** The `taskset` scorers score each reply. `fromBaseline` compares it with the baseline's, the\n reply the recorded trace holds at that point (did it make the same tool calls, does it say the same thing);\n criteria grade it on its own terms (see \"Writing criteria\"). A reply's score is the weighted mean of the scorers\n that count toward it (each `rewards` entry, judge and criteria group, by its `weight`); `metrics` are recorded\n beside it without counting, unless `required` names one. A scorer that doesn't apply at a point records nothing\n there and doesn't count.\n- **`required` for what must never fail.** A scorer, a group or a criterion (`<group>/<criterion>`) named in\n `required` that scores below 1 makes its reply score 0; a criterion answered n/a doesn't. So a required criterion\n is yes or no: a middle choice scores below 1, and would fail its reply too.\n- **The trace's score** (`episode`). With no `episode.rewards`, a trace scores the mean of its replies' scores.\n `episode.rewards.replies` combines its replies' scores by `mode` (`mean`, `min`, or `all-pass`: 1 only when every\n scored reply scores 1); `episode.rewards.trajectory` is one judge call over the whole session. Once\n `episode.rewards` names either, the trace scores the mean of those alone, so reply scorers count toward it only\n through `replies`.\n- **Production's score.** Production is scored by criteria groups alone: every other scorer, `fromBaseline` and the\n trajectory judge, compares a reply with the baseline's, which production's is. So where the config has other\n scorers that count, production's per-trace score is the mean of its criteria alone, while a candidate's includes the\n rest too. Compare production with a candidate on the criteria rows of a run's summary, not the per-trace row,\n unless criteria groups are all that count.\n- **What `--check` warns about.** A scorer that is recorded but counts toward nothing: when `episode.rewards` names a\n scorer but not `replies`, each reply scorer and each `required` entry, which can't reach the trace's score; and\n otherwise a reward, judge or criteria group of weight 0 that `required` doesn't name. They are warnings, not\n mistakes: the config still saves.\n\n## 2. The presets\n\nEach preset grades a different way. Pick by how a good session of this kind looks, then add criteria (see \"Writing\ncriteria\").\n\n- **`strict`**: a workflow whose tool calls must be the baseline's (look the order up, then refund it). At each point\n where either calls a tool, the reply must make the same calls with the same arguments, in any order within the\n reply (`match_ordered` checks the order too); a point where both answer in text isn't scored. A trace passes only\n when every scored reply does (its replies are scored `all-pass`), so a criterion added to it fails the trace\n wherever it scores below 1, a middle choice included. No judge calls, so it costs only the\n subjects' replies.\n- **`balanced`**: comparing turn by turn with the baseline, where a reply may differ in wording or approach and\n still be right. A judge decides at each scored point whether the reply is equivalent, or a sensible next step: one\n judge call per point.\n- **`trajectory`**: long, open-ended tasks (multi-step retrieval or research) where many paths are valid and their\n order varies from run to run, so comparing each turn with the baseline would mark good runs wrong. One judge call\n per trace and subject asks whether the subject would have handled the whole session. It is made only once every\n point of the trace has its reply, so a point that fails fails the trial without it; a trace scored at more points\n than `maxPoints` is skipped, and has no score from it. The judge reads the whole recorded session, however few\n points are scored, so `context.maxChars` is what bounds its prompt: a session too long for the judge fails its call,\n and so the trial. Know its limit: at each scored point the subject is given the\n recorded session up to there and writes one reply, and the judge sees those replies beside the baseline's and\n judges them together. The subject never runs a session of its own: each reply starts from the recorded history,\n never from its own earlier replies.\n- **`criteria`**: what a good reply must do, in your own words, whatever the baseline did. One judge call per\n criterion per scored point by default. On its own it suits evals where the baseline isn't a bar to match: it judges\n nothing against the baseline's replies; the recorded session is where each reply starts and what the judge reads\n it against.\n\nCriteria count toward each reply's score under `strict`, `balanced` and `criteria`, since a trace there scores its\nreplies. Under `trajectory` the trace scores the judge's verdict alone, so criteria are only recorded beside it.\nFor them to count, also set `episode.rewards.replies` (`mode: mean`, or `all-pass` when every scored reply must keep\nevery rule): the trace then scores the mean of the verdict and its replies' score.\n\nThe judges that compare with the baseline (`balanced`'s, and `trajectory`'s) take `guidance`: your words, added to\nthe judge's prompt, on how to weigh a reply that differs from the baseline's for this kind of agent. Their prompts\nalready treat the baseline as one acceptable reply, not the only one; say what a difference that matters looks like\nhere (a step that loses the thread, an answer from no source), and what's fine (another order, another source).\nSay what counts against a reply, not what it must repeat: a judge told an equivalent reply has \"the same dates and\namounts\" fails a good reply that leaves out an amount the baseline's mentioned. Say instead that leaving out a detail\nthe baseline's gave is fine, and contradicting one (a different date, amount or outcome) isn't.\n\nIf you're unsure which fits, try both: write each to a file, start a run of the same subjects under each with\n`ambral runs start <workload>/<name> --subjects … --scoring <file>`, and compare whether each ranks the subjects the\nway you and the user would. The absolute scores aren't comparable between them.\n\n## 3. Writing criteria\n\n**Criteria**, under `criteria`, grade each reply on its own terms against statements you write, never against the\nbaseline's. Use them for what a good session of this kind must do, and for checks like \"the reply never calls\n`delete_account`\", \"`refund_order` is only called after `lookup_order` appears in the session\" or \"the reply\nincludes an order number\": a judge model grades each. The `criteria` preset starts with one group of two general\nones to replace.\n\n- **One narrow statement per criterion.** Write one statement a good reply meets (\"Before issuing a refund, the\n assistant asks the user to confirm.\"), as its `text`, not a list. Each criterion gets its own answer and its own\n score, so a failure points at one thing. Write several criteria rather than one broad one.\n- **Write for the judge, who reads them as written.** The judge is shown each criterion's `name`, its `text`, and its\n choice labels worst to best, each with its meaning if it has one. So give it a name that says what it checks\n (`confirms_before_refund`, not `c1`), text that stands on its own without the rest of the file, and labels that mean\n something even without `meanings`.\n- **Choices, worst to best.** Every criterion answers on one scale of choices. At each reply the judge picks\n exactly one of them, or n/a where its `notApplicable` says it doesn't apply, and the choice it picks is the\n criterion's score there. A\n choice's score is its place in the list, worst 0, best 1, those between evenly spaced, and isn't written in the\n config:\n\n | `choices` | Scores |\n | ------------------------- | --------------------------------------- |\n | `[no, yes]` (the default) | no 0, yes 1 |\n | `[no, partly, yes]` | no 0, partly 0.5, yes 1 |\n | `[bad, poor, fair, good]` | bad 0, poor 0.33, fair 0.67, good 1 |\n | any, answered n/a | no score: left out of the group's score |\n\n List more than two, worst first, for partial credit. Say what earns each choice in `meanings` (`partly: \"It\n mentions the refund, but doesn't wait for a yes.\"`): the judge is shown each beside its choice, and a choice whose\n meaning you leave out is left to the judge to read.\n- **Group criteria that share a judge and a view.** A group (each entry under `criteria`) holds criteria graded by one\n `model`, at the same `turns`, shown the same `context`. Its score is the weighted mean of its criteria's scores\n (each criterion's `weight`, default 1), counted as a reward under the group's name with the group's `weight`. Each\n criterion's score is also recorded on its own, as `<group>/<criterion>`, not counted toward the reply's score. A group\n whose criteria were all n/a has no score there.\n- **Give a group the smallest window that holds what it checks.** The window is the group's `context.messages`: `0`\n shows the judge the reply alone, its text and tool calls; a number N (default 20) also shows the system prompt and the\n session's latest N messages, the opening request kept in view; `null` shows the whole session. A judge grades best\n when it sees little beyond what it checks, so write criteria about the reply and the steps just before it, and give a\n group the smallest window that holds them: `0` for what the reply itself does or says, a small N for recent steps.\n Split criteria that need different windows into groups of their own. Keep `null` for a criterion that truly depends on\n the whole session: a session too long for the judge fails its call, and a failed call fails the trial.\n- **What N counts.** `context.messages` never shows what came after the reply, and at `0` a `fromBaseline` judge also\n sees the baseline's reply. N counts every message, each tool call and each tool result, so 10 messages may be only two\n to four steps of an agent that calls tools, and the user's latest request may sit well back in a tool loop: only the\n system prompt and the opening request are always shown.\n- **Tools are shown by their calls.** The judge sees each tool call and its result, but never the tools'\n definitions, so a criterion can't rest on what a tool's description says. `fromBaseline`'s judges read the same\n `context` setting.\n- **Long messages are cut.** Every judge cuts each recorded message and the reply at 8000 characters by default\n (`context.maxChars`), keeping its start; the system prompt is never cut. A tool call is always shown by name, but\n its arguments are cut like text once a message passes the limit, so the judge sees that a write happened without\n seeing all of what it wrote. Set `maxChars: null` to show every message whole, or a higher number when what matters\n sits deep in a long one, such as a criterion about what a large write holds, or the calls a reply makes after long\n text.\n- **`notApplicable`: when a criterion doesn't apply, or what it needs isn't shown.** Keep `text` to the statement a\n good reply meets, and say when it doesn't apply in the criterion's `notApplicable` (\"No refund is asked about.\" for\n \"asks to confirm before refunding\"). The judge is shown it beside the choices, and may then answer n/a, which\n records no score and is left out of the group's score. Write one for a criterion only some replies are about,\n rather than one that passes or fails everywhere. Where the window leaves something out, the judge sees a gap and is\n told not to count what it can't see against the reply. So a criterion that needs evidence fails falsely when the\n evidence fell outside (a source it can't see looks like no source), and one that asks whether something was\n requested passes too easily. Say so in its `notApplicable` (\"The request the deletion acts on isn't shown.\"), and\n size the window so what it needs usually is shown.\n- **No `notApplicable` for what applies to every reply** (its tone, never calling a tool it mustn't): without one, the\n judge must pick a choice at every reply it grades, so it can't pass over a reply as n/a. `n/a` is never one of a\n criterion's own choices.\n- **n/a in a run's summary.** A criterion that is n/a at every point of a run shows no row in its summary; if that\n happens where you expected it to apply, reword its `notApplicable`, or check its group's `context` shows the judge\n what it needs.\n- **`perCall`: criteria per judge call.** By default (`1`) each criterion is asked in a call of its own, one small\n decision each, which judges most reliably; a reply's calls run in parallel. A larger number asks that many in one\n call, and `null` all of the group's: fewer calls, and so cheaper, for a large group of quick checks, or for criteria\n the judge should weigh side by side. Try a draft both ways and keep `null` only if the scores agree. A judge call that\n fails fails its trial, so more calls are also more that can fail.\n- **Narrow where it is checked**, with the group's `turns`, which picks points by the recorded turn there:\n `final: true` grades only the reply at the trace's last turn, and `kinds: [tool_call]` only where the recorded agent\n called a tool. A group makes its calls at every reply it grades, so narrowing `turns` cuts calls and cost.\n- **Try them before you save them.** Start a run with `production` as the subject and the draft as `--scoring <file>`:\n it grades the baseline's replies, the ones you read when you picked the traces, and calls only the judges. Read each\n criterion's score: one that fails replies you and the user would call good is misfiring. Find where with\n `ambral runs verdicts <run> --check <group>/<criterion> --below 1`, every reply it scored short of its best choice,\n or `--verdict <choice>` for one choice, which gives the judge's reason for each, then\n read one of those traces with `ambral runs trace <run> <tr_…> --json`: each check's `call` is the judge's whole call,\n the prompt it was sent (exactly what it was shown) and its answer. Tell the causes apart before changing anything:\n - **The reply really breaks it.** The reason is right, and you and the user agree: the criterion works, and the\n baseline is wrong there. Keep the criterion; don't pick that trace as an example of a good session.\n - **The criterion is worded wrong.** The judge applied the words as written, but they don't say what you meant\n (too broad, or a case you didn't think of): reword it, say when it doesn't apply in its `notApplicable`, or write\n `meanings` for its choices.\n - **The judge wasn't shown enough.** What the criterion needs (the request, the tool result it rests on) isn't in the\n prompt, or is cut short with `[… N characters elided]`: raise the group's `context.messages` or `maxChars`, move the\n criterion to a group with a larger window, or name what it needs in its `notApplicable`, so the judge answers n/a\n when that isn't shown.\n - **The judge misread.** The prompt holds what it needed, the criterion says what you meant, and the reason still\n gets the reply wrong: make the criterion more explicit, ask it in a call of its own (`perCall: 1`), or try another\n judge model. A judge whose reply isn't an answer at all fails its trial rather than giving a verdict: it is under\n \"Failed trials\" in `ambral runs show <run>`, as `VerifierError: Failed: <group>`, and on that subject's line in\n `ambral runs trace`. Its reply isn't shown there, so run it again, and if it keeps failing, try another judge model\n or fewer criteria per call.\n\n Then run the draft again and read the same traces' verdicts.\n\n## 4. Checklist for configuring\n\nQuestions to think through each time you create or change a config, not steps to follow in order:\n\n- **What does a good session of this kind do?** Agree on it with the user first: it decides the preset and the\n criteria. (See \"The presets\" and \"Writing criteria\".)\n- **Is the baseline a bar to match?** If it isn't, grade with criteria only, the `criteria` preset. (See \"The\n presets\".)\n- **Is the session judged turn by turn or as a whole?** If as a whole (`trajectory`), do rules hold at every reply? Then\n add them as criteria and set `episode.rewards.replies`, or they won't count. (See \"How a score is built\" and \"The\n presets\".)\n- **Does each judge that compares with the baseline say which differences matter?** Give it `guidance`: what a\n different but fine reply looks like for this kind of agent, and what a difference that counts against it is, never\n that a reply must repeat all the baseline's said. (See \"The presets\".)\n- **Is each criterion one narrow statement about the reply and the steps just before it, and does its\n `notApplicable` say when it doesn't apply?** (See \"Writing criteria\": one narrow statement, `notApplicable`.)\n- **Does each group have the smallest window that holds what it checks?** (See \"Writing criteria\": the window, what\n N counts, long messages.)\n- **What must never fail?** Name it in `required`. (See \"How a score is built\".)\n- **Which points does each scorer grade, and what does that cost?** `points` picks where replies are asked for, and\n each group's `turns` where it grades; every judge makes its calls at every reply it grades, and a criteria group one\n per criterion by default. (See \"How a score is built\": points; \"The presets\"; \"Writing criteria\": `perCall`, narrow\n where it is checked.)\n- **Was the draft tried against `production`, and were the judge's reasons read?** (See \"Writing criteria\": try them\n before you save them.)\n- **Does `--check` pass with no warnings you didn't mean?** (See \"How a score is built\": what `--check` warns\n about.)\n";
62859
62909
 
62860
62910
  // skill/references/setup.md
62861
62911
  var setup_default = `# Connecting an agent
@@ -62914,14 +62964,54 @@ Gateway. Do the steps in this order.
62914
62964
  - **SESSION** shows your session id. If it reads \`(generated)\`, \`x-session-id\` isn't reaching the Gateway, so
62915
62965
  each request became its own trace. Fix step 4 and try again.
62916
62966
  - **STEPS** counts the agent's replies, so a session of a few steps has more than one.
62967
+ - **BRANCHES** is 1 for a session the agent only adds to, one request after another. More means the agent sent
62968
+ a history that differs from what it was answered. BRANCHES close to STEPS, on trace after trace, is the agent
62969
+ dropping the model's reasoning.
62917
62970
 
62918
62971
  Then read the trace whole with \`ambral traces get tr_…\`, using the id the list printed. Under each reply it
62919
62972
  shows the model that served it, why it finished, its tokens in and out, and its latency. A call that failed
62920
62973
  shows its error where it happened. Check that each reply was served by the model you expect, and that no call
62921
62974
  failed.
62922
62975
 
62976
+ Don't call the connection healthy while a session has more than one branch until you've found why, with "When a
62977
+ session has more than one branch" below, and either fixed it or confirmed with the user that it's expected. A
62978
+ session can look healthy by every other check here and still fork at every reply.
62979
+
62923
62980
  If your session isn't there after listing again, go through "When no trace appears" below.
62924
62981
 
62982
+ ## When a session has more than one branch
62983
+
62984
+ Read the branches \`ambral traces get tr_…\` prints, and find where each splits off the latest one.
62985
+
62986
+ - **The agent drops the model's reasoning.** This is the usual cause, and the one to rule out first. The sign: the
62987
+ branches split at a reply. The same reply, with the same text and the same tool calls, appears twice under the same
62988
+ message: once at the end of a short branch, with the call that produced it, and once on the branch that goes on.
62989
+ With a reasoning model there is one such split at nearly every reply, so a session of ten replies has around ten
62990
+ branches. When the reasoning is text, the short branch's copy shows it as \`thinking:\` and the other copy doesn't;
62991
+ OpenAI's models return theirs encrypted, so neither shows it, and the two copies look the same.
62992
+
62993
+ What happened: each reply the model reasoned for comes back with that reasoning in \`reasoning_details\` (and as
62994
+ text in \`reasoning\`, when there is text). The agent sent the reply back on its next request without them, so it
62995
+ isn't the reply the model gave, and the Gateway records it as a separate message. Many agent frameworks and
62996
+ hand-written loops do this: they rebuild the assistant message from \`role\`, \`content\` and \`tool_calls\`, or a
62997
+ typed client drops fields that aren't OpenAI's.
62998
+
62999
+ Tell the user this plainly, as a problem to fix, not a detail of how traces look:
63000
+ - the model loses its earlier reasoning on every turn, which can lower the quality of multi-step and tool-calling
63001
+ work. Some providers may refuse such a request outright: Anthropic's models while using tools, Gemini 3, and
63002
+ DeepSeek's thinking mode;
63003
+ - every session is recorded as many branches, one per reply.
63004
+
63005
+ The fix is in their code: find where the agent appends the model's reply to the conversation it sends next, and
63006
+ append the assistant message as it came back, \`reasoning_details\` and \`reasoning\` included, rather than a copy
63007
+ of some of its fields. Then run another session and check that it is 1 branch.
63008
+ - **The agent rewrites its history on purpose:** it retries or regenerates a reply, summarizes or trims older
63009
+ messages when the conversation grows long, or edits a message before sending it again. Each rewrite starts a
63010
+ branch where it changes the history, and that is expected. Confirm with the user that the agent does this.
63011
+ - **A call failed, and the agent sent something else next.** A call that failed before a reply is recorded at the
63012
+ prompt it was sent with, and shows its error there. When the agent's next request isn't that same prompt again,
63013
+ the failed one is left as a branch of its own.
63014
+
62925
63015
  ## When no trace appears
62926
63016
 
62927
63017
  Go through these in order:
@@ -63012,8 +63102,9 @@ linked to.
63012
63102
 
63013
63103
  ## What to read when
63014
63104
 
63015
- - **Connecting an agent, or a session's trace not appearing:** read [references/setup.md](references/setup.md). It
63016
- goes step by step from picking the workload to checking the first trace, then what to check when none appears.
63105
+ - **Connecting an agent, a session's trace not appearing, or a session recorded as more than one branch:** read
63106
+ [references/setup.md](references/setup.md). It goes step by step from picking the workload to checking the first
63107
+ trace, then what to check when none appears, and when a session forks.
63017
63108
  - **Building an eval, and running and reading one:** read [references/evals.md](references/evals.md): understanding
63018
63109
  the traffic with the user, picking traces, creating tasksets and when to make a new one, setting and saving a
63019
63110
  scoring config, then running it, comparing subjects including \`production\`, and reading the results: the summary,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ambral",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "The `ambral` command: signs in through the browser, then calls the platform API as that person.",